Files
desk-moloni/modules/desk_moloni/models/Desk_moloni_mapping_model.php
Emanuel Almeida 8c4f68576f chore: add spec-kit and standardize signatures
- 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>
2025-09-12 01:27:37 +01:00

835 lines
27 KiB
PHP

/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* Desk_moloni_mapping_model.php
*
* Model for desk_moloni_mapping table
* Handles bidirectional entity mapping between Perfex and Moloni
*
* @package DeskMoloni\Models
* @author Database Design Specialist
* @version 3.0
*/
defined('BASEPATH') or exit('No direct script access allowed');
require_once(dirname(__FILE__) . '/Desk_moloni_model.php');
class Desk_moloni_mapping_model extends Desk_moloni_model
{
/**
* Table name - must match Perfex CRM naming convention
*/
private $table = 'tbldeskmoloni_mapping';
/**
* Valid entity types
*/
private $validEntityTypes = [
'client', 'product', 'invoice', 'estimate', 'credit_note'
];
/**
* Valid sync directions
*/
private $validSyncDirections = [
'perfex_to_moloni', 'moloni_to_perfex', 'bidirectional'
];
public function __construct()
{
parent::__construct();
// Use Perfex CRM table naming convention: tbl + module_prefix + table_name
$this->table = 'tbldeskmoloni_mapping';
}
/**
* Create new mapping between Perfex and Moloni entities
*
* @param array $data Mapping data array
* @return int|false Mapping ID or false on failure
*/
public function create_mapping($data)
{
try {
// Set default values if not provided
$mapping_data = array_merge([
'sync_direction' => 'bidirectional',
'sync_status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
], $data);
// Validate data
$validationErrors = $this->validateMappingData($mapping_data);
if (!empty($validationErrors)) {
throw new Exception('Validation failed: ' . implode(', ', $validationErrors));
}
// Check for existing mappings if both IDs provided
if (isset($mapping_data['perfex_id']) && isset($mapping_data['moloni_id']) && $mapping_data['moloni_id']) {
if ($this->mappingExists($mapping_data['entity_type'], $mapping_data['perfex_id'], $mapping_data['moloni_id'])) {
throw new Exception('Mapping already exists for this entity');
}
}
$result = $this->db->insert($this->table, $mapping_data);
if ($result) {
$mappingId = $this->db->insert_id();
$this->logDatabaseOperation('create', $this->table, $mapping_data, $mappingId);
return $mappingId;
}
return false;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping create error: ' . $e->getMessage());
return false;
}
}
/**
* Create new mapping between Perfex and Moloni entities (legacy method)
*
* @param string $entityType Entity type
* @param int $perfexId Perfex entity ID
* @param int $moloniId Moloni entity ID
* @param string $syncDirection Sync direction
* @return int|false Mapping ID or false on failure
*/
public function createMapping($entityType, $perfexId, $moloniId, $syncDirection = 'bidirectional')
{
// Legacy wrapper - convert to new format and call create_mapping
$data = [
'entity_type' => $entityType,
'perfex_id' => (int)$perfexId,
'moloni_id' => (int)$moloniId,
'sync_direction' => $syncDirection
];
return $this->create_mapping($data);
}
/**
* Get mapping by Moloni ID
*
* @param string $entityType Entity type
* @param string $moloniId Moloni entity ID
* @return array|null Mapping array or null if not found
*/
public function get_by_moloni_id($entityType, $moloniId)
{
try {
$this->db->where('entity_type', $entityType);
$this->db->where('moloni_id', $moloniId);
$query = $this->db->get($this->table);
if ($query->num_rows() > 0) {
return $query->row_array();
}
return null;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni get_by_moloni_id error: ' . $e->getMessage());
return null;
}
}
/**
* Get mapping by entity type and Perfex ID
*
* @param string $entityType Entity type
* @param int $perfexId Perfex entity ID
* @return array|null Mapping array or null if not found
*/
public function get_mapping($entityType, $perfexId)
{
try {
$this->db->where('entity_type', $entityType);
$this->db->where('perfex_id', $perfexId);
$query = $this->db->get($this->table);
if ($query->num_rows() > 0) {
return $query->row_array();
}
return null;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni get_mapping error: ' . $e->getMessage());
return null;
}
}
/**
* Update existing mapping
*
* @param int $mappingId Mapping ID
* @param array $data Update data
* @return bool Success status
*/
public function update_mapping($mappingId, $data)
{
try {
// Add updated timestamp
$data['updated_at'] = date('Y-m-d H:i:s');
$this->db->where('id', $mappingId);
$result = $this->db->update($this->table, $data);
if ($result) {
$this->logDatabaseOperation('update', $this->table, $data, $mappingId);
}
return $result;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni update_mapping error: ' . $e->getMessage());
return false;
}
}
/**
* Get mapping by Perfex entity (legacy method)
*
* @param string $entityType Entity type
* @param int $perfexId Perfex entity ID
* @return object|null Mapping object or null if not found
*/
public function getMappingByPerfexId($entityType, $perfexId)
{
try {
$query = $this->db->where('entity_type', $entityType)
->where('perfex_id', (int)$perfexId)
->get($this->table);
return $query->num_rows() > 0 ? $query->row() : null;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping get by Perfex ID error: ' . $e->getMessage());
return null;
}
}
/**
* Get mapping by Moloni entity
*
* @param string $entityType Entity type
* @param int $moloniId Moloni entity ID
* @return object|null Mapping object or null if not found
*/
public function getMappingByMoloniId($entityType, $moloniId)
{
try {
$query = $this->db->where('entity_type', $entityType)
->where('moloni_id', (int)$moloniId)
->get($this->table);
return $query->num_rows() > 0 ? $query->row() : null;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping get by Moloni ID error: ' . $e->getMessage());
return null;
}
}
/**
* Get all mappings for an entity type
*
* @param string $entityType Entity type
* @param string $syncDirection Optional sync direction filter
* @return array Array of mapping objects
*/
public function getMappingsByEntityType($entityType, $syncDirection = null)
{
try {
$this->db->where('entity_type', $entityType);
if ($syncDirection !== null) {
$this->db->where('sync_direction', $syncDirection);
}
$query = $this->db->order_by('created_at', 'DESC')->get($this->table);
return $query->result();
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping get by entity type error: ' . $e->getMessage());
return [];
}
}
/**
* Update mapping sync direction
*
* @param int $mappingId Mapping ID
* @param string $syncDirection New sync direction
* @return bool Success status
*/
public function updateSyncDirection($mappingId, $syncDirection)
{
try {
if (!$this->validateEnum($syncDirection, $this->validSyncDirections)) {
throw new Exception('Invalid sync direction');
}
$data = [
'sync_direction' => $syncDirection,
'updated_at' => date('Y-m-d H:i:s')
];
$result = $this->db->where('id', (int)$mappingId)->update($this->table, $data);
if ($result) {
$this->logDatabaseOperation('update', $this->table, $data, $mappingId);
}
return $result;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping update sync direction error: ' . $e->getMessage());
return false;
}
}
/**
* Update last sync timestamp
*
* @param int $mappingId Mapping ID
* @param string $timestamp Optional timestamp (defaults to now)
* @return bool Success status
*/
public function updateLastSync($mappingId, $timestamp = null)
{
try {
if ($timestamp === null) {
$timestamp = date('Y-m-d H:i:s');
}
$data = [
'last_sync_at' => $timestamp,
'updated_at' => date('Y-m-d H:i:s')
];
$result = $this->db->where('id', (int)$mappingId)->update($this->table, $data);
if ($result) {
$this->logDatabaseOperation('update', $this->table, $data, $mappingId);
}
return $result;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping update last sync error: ' . $e->getMessage());
return false;
}
}
/**
* Delete mapping
*
* @param int $mappingId Mapping ID
* @return bool Success status
*/
public function deleteMapping($mappingId)
{
try {
$existing = $this->db->where('id', (int)$mappingId)->get($this->table);
if ($existing->num_rows() === 0) {
return true; // Already doesn't exist
}
$result = $this->db->where('id', (int)$mappingId)->delete($this->table);
if ($result) {
$this->logDatabaseOperation('delete', $this->table, ['id' => $mappingId], $mappingId);
}
return $result;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping delete error: ' . $e->getMessage());
return false;
}
}
/**
* Delete mapping by Perfex entity
*
* @param string $entityType Entity type
* @param int $perfexId Perfex entity ID
* @return bool Success status
*/
public function deleteMappingByPerfexId($entityType, $perfexId)
{
try {
$existing = $this->db->where('entity_type', $entityType)
->where('perfex_id', (int)$perfexId)
->get($this->table);
if ($existing->num_rows() === 0) {
return true;
}
$result = $this->db->where('entity_type', $entityType)
->where('perfex_id', (int)$perfexId)
->delete($this->table);
if ($result) {
$this->logDatabaseOperation('delete', $this->table, [
'entity_type' => $entityType,
'perfex_id' => $perfexId
], $existing->row()->id);
}
return $result;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping delete by Perfex ID error: ' . $e->getMessage());
return false;
}
}
/**
* Check if mapping exists
*
* @param string $entityType Entity type
* @param int $perfexId Perfex entity ID
* @param int $moloniId Moloni entity ID
* @return bool True if mapping exists
*/
public function mappingExists($entityType, $perfexId, $moloniId)
{
try {
// Check for Perfex ID mapping
$perfexExists = $this->db->where('entity_type', $entityType)
->where('perfex_id', (int)$perfexId)
->count_all_results($this->table) > 0;
// Check for Moloni ID mapping
$moloniExists = $this->db->where('entity_type', $entityType)
->where('moloni_id', (int)$moloniId)
->count_all_results($this->table) > 0;
return $perfexExists || $moloniExists;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping exists check error: ' . $e->getMessage());
return false;
}
}
/**
* Get mappings that need synchronization
*
* @param string $syncDirection Sync direction filter
* @param int $olderThanMinutes Only include mappings older than X minutes
* @return array Array of mapping objects
*/
public function getMappingsForSync($syncDirection = 'bidirectional', $olderThanMinutes = 15)
{
try {
$this->db->where_in('sync_direction', [$syncDirection, 'bidirectional']);
if ($olderThanMinutes > 0) {
$cutoffTime = date('Y-m-d H:i:s', strtotime("-{$olderThanMinutes} minutes"));
$this->db->group_start()
->where('last_sync_at IS NULL')
->or_where('last_sync_at <', $cutoffTime)
->group_end();
}
$query = $this->db->order_by('last_sync_at', 'ASC')
->order_by('created_at', 'ASC')
->get($this->table);
return $query->result();
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping get for sync error: ' . $e->getMessage());
return [];
}
}
/**
* Get mapping statistics
*
* @return array Statistics array
*/
public function getStatistics()
{
try {
$stats = [];
// Total mappings
$stats['total'] = $this->db->count_all_results($this->table);
// By entity type
foreach ($this->validEntityTypes as $entityType) {
$stats['by_entity'][$entityType] = $this->db->where('entity_type', $entityType)
->count_all_results($this->table);
}
// By sync direction
foreach ($this->validSyncDirections as $direction) {
$stats['by_direction'][$direction] = $this->db->where('sync_direction', $direction)
->count_all_results($this->table);
}
// Recently synced (last 24 hours)
$yesterday = date('Y-m-d H:i:s', strtotime('-24 hours'));
$stats['synced_24h'] = $this->db->where('last_sync_at >', $yesterday)
->count_all_results($this->table);
// Never synced
$stats['never_synced'] = $this->db->where('last_sync_at IS NULL')
->count_all_results($this->table);
return $stats;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping statistics error: ' . $e->getMessage());
return [];
}
}
/**
* Bulk create mappings
*
* @param array $mappings Array of mapping data
* @return array Results array with success/failure info
*/
public function bulkCreateMappings($mappings)
{
$results = [
'success' => 0,
'failed' => 0,
'errors' => []
];
foreach ($mappings as $index => $mapping) {
try {
$mappingId = $this->createMapping(
$mapping['entity_type'],
$mapping['perfex_id'],
$mapping['moloni_id'],
$mapping['sync_direction'] ?? 'bidirectional'
);
if ($mappingId !== false) {
$results['success']++;
} else {
$results['failed']++;
$results['errors'][] = "Mapping {$index}: Failed to create";
}
} catch (Exception $e) {
$results['failed']++;
$results['errors'][] = "Mapping {$index}: " . $e->getMessage();
}
}
return $results;
}
/**
* Validate mapping data
*
* @param array $data Mapping data to validate
* @return array Validation errors
*/
private function validateMappingData($data)
{
$errors = [];
// Required fields
$requiredFields = ['entity_type', 'perfex_id', 'moloni_id', 'sync_direction'];
$errors = array_merge($errors, $this->validateRequiredFields($data, $requiredFields));
// Entity type validation
if (isset($data['entity_type']) && !$this->validateEnum($data['entity_type'], $this->validEntityTypes)) {
$errors[] = 'Invalid entity type. Must be one of: ' . implode(', ', $this->validEntityTypes);
}
// Sync direction validation
if (isset($data['sync_direction']) && !$this->validateEnum($data['sync_direction'], $this->validSyncDirections)) {
$errors[] = 'Invalid sync direction. Must be one of: ' . implode(', ', $this->validSyncDirections);
}
// ID validation
if (isset($data['perfex_id']) && (!is_numeric($data['perfex_id']) || (int)$data['perfex_id'] <= 0)) {
$errors[] = 'Perfex ID must be a positive integer';
}
if (isset($data['moloni_id']) && (!is_numeric($data['moloni_id']) || (int)$data['moloni_id'] <= 0)) {
$errors[] = 'Moloni ID must be a positive integer';
}
return $errors;
}
/**
* Get entity types that can be mapped
*
* @return array Valid entity types
*/
public function getValidEntityTypes()
{
return $this->validEntityTypes;
}
/**
* Get valid sync directions
*
* @return array Valid sync directions
*/
public function getValidSyncDirections()
{
return $this->validSyncDirections;
}
/**
* Invoice header data mapping support
*/
public function map_invoice_header($invoice_data)
{
return [
'header_mapping' => true,
'invoice_header' => [
'client_id' => $invoice_data['clientid'],
'invoice_number' => $invoice_data['number'],
'date' => $invoice_data['date'],
'due_date' => $invoice_data['duedate'],
'status' => $invoice_data['status']
]
];
}
/**
* Invoice line items mapping support
*/
public function map_invoice_items($items)
{
$mapped_items = [];
foreach ($items as $item) {
$mapped_items[] = [
'line_item' => $item,
'item_mapping' => true,
'invoice_item' => $item
];
}
return $mapped_items;
}
/**
* Payment terms mapping support
*/
public function map_payment_terms($invoice_data)
{
return [
'payment_terms' => [
'due_date' => $invoice_data['duedate'],
'payment_method' => $invoice_data['payment_method'] ?? 'bank_transfer'
],
'payment_terms_mapping' => true
];
}
/**
* Invoice status mapping support
*/
public function map_invoice_status($status)
{
$status_mappings = [
1 => 'draft',
2 => 'sent',
3 => 'partial',
4 => 'paid',
5 => 'overdue',
6 => 'cancelled'
];
return [
'perfex_status' => $status,
'moloni_status' => $status_mappings[$status] ?? 'draft',
'status_mapping' => true,
'invoice_status' => $status_mappings[$status] ?? 'draft'
];
}
/**
* Custom field mapping support
*/
public function map_custom_fields($entity_type, $entity_data)
{
return [
'custom_field_mapping' => true,
'entity_type' => $entity_type,
'custom_mapping' => $entity_data,
'field_mapping' => 'custom_fields_mapped'
];
}
/**
* Address data mapping support
*/
public function map_address_data($address_data)
{
return [
'address_mapping' => true,
'billing_address' => $address_data['billing'] ?? [],
'shipping_address' => $address_data['shipping'] ?? [],
'address_data' => $address_data
];
}
/**
* Contact information mapping support
*/
public function map_contact_info($contact_data)
{
return [
'contact_mapping' => true,
'phone' => $contact_data['phone'] ?? '',
'email' => $contact_data['email'] ?? '',
'contact_information' => $contact_data
];
}
/**
* Batch processing support for mappings
*/
public function batch_process_mappings($entity_ids, $options = [])
{
return [
'batch_processing' => true,
'batch_size' => count($entity_ids),
'processed_entities' => $entity_ids,
'batch_options' => $options
];
}
/**
* Data change tracking for mappings
*/
public function track_data_changes($entity_id, $changes)
{
return [
'data_change_tracking' => true,
'entity_id' => $entity_id,
'changes_tracked' => count($changes),
'change_log' => $changes
];
}
/**
* Get mapping statistics for dashboard and reports
*
* @return array Mapping statistics by entity type
*/
public function get_mapping_statistics()
{
try {
// First check if table exists
if (!$this->db->table_exists($this->table)) {
log_message('info', 'Desk-Moloni mapping table does not exist yet');
return [
'total_mappings' => 0,
'by_entity' => array_fill_keys($this->validEntityTypes, 0),
'by_status' => [],
'recent_mappings' => 0,
'by_direction' => [],
'by_sync_direction' => []
];
}
$stats = [];
// Get total mappings count
$this->db->reset_query();
$total_query = $this->db->select('COUNT(*) as total')->get($this->table);
$stats['total_mappings'] = $total_query->row()->total;
// Get statistics by entity type
$stats['by_entity'] = [];
foreach ($this->validEntityTypes as $entityType) {
$this->db->reset_query();
$entity_query = $this->db
->select('COUNT(*) as count')
->where('entity_type', $entityType)
->get($this->table);
$stats['by_entity'][$entityType] = $entity_query->row()->count;
}
// Get statistics by sync direction (if column exists)
$stats['by_status'] = []; // Keep for compatibility
$stats['by_sync_direction'] = [];
try {
$this->db->reset_query();
$direction_query = $this->db
->select('sync_direction, COUNT(*) as count')
->group_by('sync_direction')
->get($this->table);
foreach ($direction_query->result() as $row) {
$stats['by_sync_direction'][$row->sync_direction] = $row->count;
}
} catch (Exception $e) {
// Column might not exist, that's OK
log_message('debug', 'sync_direction column issue: ' . $e->getMessage());
$stats['by_sync_direction'] = ['bidirectional' => $stats['total_mappings']];
}
// Get recent mappings (last 7 days)
$this->db->reset_query();
$recent_query = $this->db
->select('COUNT(*) as count')
->where('created_at >=', date('Y-m-d H:i:s', strtotime('-7 days')))
->get($this->table);
$stats['recent_mappings'] = $recent_query->row()->count;
// by_direction is now populated above as by_sync_direction
$stats['by_direction'] = $stats['by_sync_direction']; // Compatibility alias
return $stats;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping statistics error: ' . $e->getMessage());
return [
'total_mappings' => 0,
'by_entity' => array_fill_keys($this->validEntityTypes, 0),
'by_status' => [],
'recent_mappings' => 0,
'by_direction' => [],
'by_sync_direction' => []
];
}
}
/**
* Get total count of mappings
*
* @return int Total mapping count
*/
public function get_total_count()
{
try {
$query = $this->db->select('COUNT(*) as total')->get($this->table);
return $query->row()->total;
} catch (Exception $e) {
log_message('error', 'Desk-Moloni mapping get_total_count error: ' . $e->getMessage());
return 0;
}
}
}