$userId, 'entity_id' => $entityId, 'username' => $username ]); } /** * Log une tentative de connexion échouée * * @param string $username Nom d'utilisateur tenté * @param string $reason Raison (invalid_password, user_not_found, account_inactive, blocked_ip) * @param int $attempt Numéro de tentative */ public static function logLoginFailed(string $username, string $reason, int $attempt = 1): void { self::writeEvent('login_failed', [ 'username' => $username, 'reason' => $reason, 'attempt' => $attempt ]); } /** * Log une déconnexion * * @param int $userId ID utilisateur * @param int|null $entityId ID entité * @param int $sessionDuration Durée session en secondes */ public static function logLogout(int $userId, ?int $entityId, int $sessionDuration = 0): void { self::writeEvent('logout', [ 'user_id' => $userId, 'entity_id' => $entityId, 'session_duration' => $sessionDuration ]); } // ==================== MÉTHODES PASSAGES ==================== /** * Log la création d'un passage * * @param int $passageId ID du passage créé * @param int $operationId ID opération * @param int $sectorId ID secteur * @param float $amount Montant * @param string $paymentType Type paiement (cash, stripe, check, etc.) */ public static function logPassageCreated( int $passageId, int $operationId, int $sectorId, float $amount, string $paymentType ): void { self::writeEvent('passage_created', [ 'passage_id' => $passageId, 'operation_id' => $operationId, 'sector_id' => $sectorId, 'amount' => $amount, 'payment_type' => $paymentType ]); } /** * Log la modification d'un passage * * @param int $passageId ID du passage * @param array $changes Tableau des changements ['field' => ['old' => val, 'new' => val]] */ public static function logPassageUpdated(int $passageId, array $changes): void { self::writeEvent('passage_updated', [ 'passage_id' => $passageId, 'changes' => $changes ]); } /** * Log la suppression d'un passage * * @param int $passageId ID du passage * @param int $operationId ID opération * @param bool $softDelete Suppression logique ou physique */ public static function logPassageDeleted(int $passageId, int $operationId, bool $softDelete = true): void { $userId = Session::getUserId(); self::writeEvent('passage_deleted', [ 'passage_id' => $passageId, 'operation_id' => $operationId, 'deleted_by' => $userId, 'soft_delete' => $softDelete ]); } // ==================== MÉTHODES SECTEURS ==================== /** * Log la création d'un secteur * * @param int $sectorId ID du secteur créé * @param int $operationId ID opération * @param string $sectorName Nom du secteur */ public static function logSectorCreated(int $sectorId, int $operationId, string $sectorName): void { self::writeEvent('sector_created', [ 'sector_id' => $sectorId, 'operation_id' => $operationId, 'sector_name' => $sectorName ]); } /** * Log la modification d'un secteur * * @param int $sectorId ID du secteur * @param int $operationId ID opération * @param array $changes Tableau des changements */ public static function logSectorUpdated(int $sectorId, int $operationId, array $changes): void { self::writeEvent('sector_updated', [ 'sector_id' => $sectorId, 'operation_id' => $operationId, 'changes' => $changes ]); } /** * Log la suppression d'un secteur * * @param int $sectorId ID du secteur * @param int $operationId ID opération * @param bool $softDelete Suppression logique ou physique */ public static function logSectorDeleted(int $sectorId, int $operationId, bool $softDelete = true): void { $userId = Session::getUserId(); self::writeEvent('sector_deleted', [ 'sector_id' => $sectorId, 'operation_id' => $operationId, 'deleted_by' => $userId, 'soft_delete' => $softDelete ]); } // ==================== MÉTHODES USERS ==================== /** * Log la création d'un utilisateur * * @param int $newUserId ID utilisateur créé * @param int $entityId ID entité * @param int $roleId ID rôle * @param string $username Nom d'utilisateur */ public static function logUserCreated(int $newUserId, int $entityId, int $roleId, string $username): void { $createdBy = Session::getUserId(); self::writeEvent('user_created', [ 'new_user_id' => $newUserId, 'entity_id' => $entityId, 'created_by' => $createdBy, 'role_id' => $roleId, 'username' => $username ]); } /** * Log la modification d'un utilisateur * * @param int $userId ID utilisateur modifié * @param array $changes Tableau des changements (booléen pour champs chiffrés) */ public static function logUserUpdated(int $userId, array $changes): void { $updatedBy = Session::getUserId(); $entityId = Session::getEntityId(); self::writeEvent('user_updated', [ 'user_id' => $userId, 'entity_id' => $entityId, 'updated_by' => $updatedBy, 'changes' => $changes ]); } /** * Log la suppression d'un utilisateur * * @param int $userId ID utilisateur supprimé * @param bool $softDelete Suppression logique ou physique */ public static function logUserDeleted(int $userId, bool $softDelete = true): void { $deletedBy = Session::getUserId(); $entityId = Session::getEntityId(); self::writeEvent('user_deleted', [ 'user_id' => $userId, 'entity_id' => $entityId, 'deleted_by' => $deletedBy, 'soft_delete' => $softDelete ]); } // ==================== MÉTHODES ENTITÉS ==================== /** * Log la création d'une entité * * @param int $entityId ID entité créée * @param int $entityTypeId Type d'entité * @param string $postalCode Code postal */ public static function logEntityCreated(int $entityId, int $entityTypeId, string $postalCode): void { $createdBy = Session::getUserId() ?? 1; // Super-admin par défaut self::writeEvent('entity_created', [ 'entity_id' => $entityId, 'created_by' => $createdBy, 'entity_type_id' => $entityTypeId, 'postal_code' => $postalCode ]); } /** * Log la modification d'une entité * * @param int $entityId ID entité * @param array $changes Tableau des changements (booléen pour champs chiffrés) */ public static function logEntityUpdated(int $entityId, array $changes): void { $updatedBy = Session::getUserId(); self::writeEvent('entity_updated', [ 'entity_id' => $entityId, 'user_id' => $updatedBy, 'updated_by' => $updatedBy, 'changes' => $changes ]); } /** * Log la suppression d'une entité * * @param int $entityId ID entité * @param string $reason Raison de la suppression */ public static function logEntityDeleted(int $entityId, string $reason = ''): void { $deletedBy = Session::getUserId() ?? 1; self::writeEvent('entity_deleted', [ 'entity_id' => $entityId, 'deleted_by' => $deletedBy, 'soft_delete' => true, 'reason' => $reason ]); } // ==================== MÉTHODES STRIPE ==================== /** * Log la création d'un PaymentIntent * * @param string $paymentIntentId ID Stripe du PaymentIntent * @param int $passageId ID du passage * @param int $amount Montant en centimes * @param string $method Méthode (tap_to_pay, qr_code, web) */ public static function logStripePaymentCreated( string $paymentIntentId, int $passageId, int $amount, string $method ): void { $entityId = Session::getEntityId(); self::writeEvent('stripe_payment_created', [ 'payment_intent_id' => $paymentIntentId, 'passage_id' => $passageId, 'entity_id' => $entityId, 'amount' => $amount, 'method' => $method ]); } /** * Log un paiement Stripe réussi * * @param string $paymentIntentId ID Stripe du PaymentIntent * @param int $passageId ID du passage * @param int $amount Montant en centimes * @param string $method Méthode (tap_to_pay, qr_code, web) */ public static function logStripePaymentSuccess( string $paymentIntentId, int $passageId, int $amount, string $method ): void { $entityId = Session::getEntityId(); self::writeEvent('stripe_payment_success', [ 'payment_intent_id' => $paymentIntentId, 'passage_id' => $passageId, 'entity_id' => $entityId, 'amount' => $amount, 'method' => $method ]); } /** * Log un paiement Stripe échoué * * @param string|null $paymentIntentId ID Stripe (peut être null si création échouée) * @param int|null $passageId ID du passage (peut être null) * @param int|null $amount Montant en centimes (peut être null) * @param string $method Méthode tentée * @param string $errorCode Code d'erreur * @param string $errorMessage Message d'erreur */ public static function logStripePaymentFailed( ?string $paymentIntentId, ?int $passageId, ?int $amount, string $method, string $errorCode, string $errorMessage ): void { $entityId = Session::getEntityId(); $data = [ 'entity_id' => $entityId, 'method' => $method, 'error_code' => $errorCode, 'error_message' => $errorMessage ]; if ($paymentIntentId !== null) { $data['payment_intent_id'] = $paymentIntentId; } if ($passageId !== null) { $data['passage_id'] = $passageId; } if ($amount !== null) { $data['amount'] = $amount; } self::writeEvent('stripe_payment_failed', $data); } /** * Log l'annulation d'un paiement Stripe * * @param string $paymentIntentId ID Stripe du PaymentIntent * @param int|null $passageId ID du passage (peut être null) * @param string $reason Raison (user_cancelled, timeout, error, etc.) */ public static function logStripePaymentCancelled( string $paymentIntentId, ?int $passageId, string $reason ): void { $entityId = Session::getEntityId(); $data = [ 'payment_intent_id' => $paymentIntentId, 'entity_id' => $entityId, 'reason' => $reason ]; if ($passageId !== null) { $data['passage_id'] = $passageId; } self::writeEvent('stripe_payment_cancelled', $data); } /** * Log une erreur du Terminal Tap to Pay * * @param string $errorCode Code d'erreur (cardReadTimedOut, device_not_compatible, etc.) * @param string $errorMessage Message d'erreur * @param array $metadata Métadonnées supplémentaires (device_model, is_simulated, etc.) */ public static function logStripeTerminalError( string $errorCode, string $errorMessage, array $metadata = [] ): void { $entityId = Session::getEntityId(); self::writeEvent('stripe_terminal_error', array_merge([ 'entity_id' => $entityId, 'error_code' => $errorCode, 'error_message' => $errorMessage ], $metadata)); } // ==================== MÉTHODES OPÉRATIONS ==================== /** * Log la création d'une opération * * @param int $operationId ID opération créée * @param string $dateStart Date début (YYYY-MM-DD) * @param string $dateEnd Date fin (YYYY-MM-DD) */ public static function logOperationCreated(int $operationId, string $dateStart, string $dateEnd): void { $entityId = Session::getEntityId(); $createdBy = Session::getUserId(); self::writeEvent('operation_created', [ 'operation_id' => $operationId, 'entity_id' => $entityId, 'created_by' => $createdBy, 'date_start' => $dateStart, 'date_end' => $dateEnd ]); } /** * Log la modification d'une opération * * @param int $operationId ID opération * @param array $changes Tableau des changements */ public static function logOperationUpdated(int $operationId, array $changes): void { $entityId = Session::getEntityId(); $updatedBy = Session::getUserId(); self::writeEvent('operation_updated', [ 'operation_id' => $operationId, 'entity_id' => $entityId, 'updated_by' => $updatedBy, 'changes' => $changes ]); } /** * Log la suppression d'une opération * * @param int $operationId ID opération * @param bool $softDelete Suppression logique ou physique */ public static function logOperationDeleted(int $operationId, bool $softDelete = true): void { $entityId = Session::getEntityId(); $deletedBy = Session::getUserId(); self::writeEvent('operation_deleted', [ 'operation_id' => $operationId, 'entity_id' => $entityId, 'deleted_by' => $deletedBy, 'soft_delete' => $softDelete ]); } // ==================== MÉTHODES PRIVÉES ==================== /** * Méthode centrale d'écriture d'un événement * * @param string $eventName Nom de l'événement * @param array $data Données spécifiques à l'événement */ private static function writeEvent(string $eventName, array $data): void { try { // Enrichir avec timestamp, user_id, entity_id, IP, platform, app_version $event = self::enrichEvent($eventName, $data); // Générer le chemin du fichier quotidien $filename = self::LOG_DIR . '/' . date('Y-m-d') . '.jsonl'; // Créer le dossier si nécessaire self::ensureLogDirectoryExists(); // Encoder en JSON compact (une ligne) $jsonLine = json_encode($event, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; // Écrire en mode append if (file_put_contents($filename, $jsonLine, FILE_APPEND | LOCK_EX) === false) { error_log("EventLogService: Impossible d'écrire dans {$filename}"); return; } // Appliquer les permissions au fichier if (file_exists($filename)) { @chmod($filename, self::FILE_PERMISSIONS); } } catch (\Throwable $e) { // Ne jamais bloquer l'application si le logging échoue error_log("EventLogService: Erreur lors de l'écriture de l'événement {$eventName}: " . $e->getMessage()); } } /** * Enrichit un événement avec les métadonnées communes * * @param string $eventName Nom de l'événement * @param array $data Données de l'événement * @return array Événement enrichi */ private static function enrichEvent(string $eventName, array $data): array { // Récupérer les informations client $clientInfo = ClientDetector::getClientInfo(); // Structure de base $event = [ 'timestamp' => gmdate('Y-m-d\TH:i:s\Z'), // ISO 8601 UTC 'event' => $eventName, ]; // Ajouter user_id si disponible et pas déjà dans $data if (!isset($data['user_id'])) { $userId = Session::getUserId(); if ($userId !== null) { $event['user_id'] = $userId; } } // Ajouter entity_id si disponible et pas déjà dans $data if (!isset($data['entity_id'])) { $entityId = Session::getEntityId(); if ($entityId !== null) { $event['entity_id'] = $entityId; } } // Fusionner avec les données spécifiques $event = array_merge($event, $data); // Ajouter IP $event['ip'] = $clientInfo['ip']; // Ajouter platform $event['platform'] = self::getPlatform($clientInfo); // Ajouter app_version si mobile if ($event['platform'] === 'ios' || $event['platform'] === 'android') { $appVersion = self::getAppVersion($clientInfo); if ($appVersion !== null) { $event['app_version'] = $appVersion; } } return $event; } /** * Détermine la plateforme (ios, android, web) * * @param array $clientInfo Informations client de ClientDetector * @return string Platform (ios|android|web) */ private static function getPlatform(array $clientInfo): string { if ($clientInfo['type'] !== 'mobile') { return 'web'; } $userAgent = $clientInfo['userAgent']; // Détection iOS if (stripos($userAgent, 'iOS') !== false || stripos($userAgent, 'iPhone') !== false || stripos($userAgent, 'iPad') !== false) { return 'ios'; } // Détection Android if (stripos($userAgent, 'Android') !== false) { return 'android'; } // Par défaut mobile générique = web return 'web'; } /** * Extrait la version de l'application depuis le User-Agent * Format attendu: AppName/VersionNumber ou Platform/Version AppName/Version * * @param array $clientInfo Informations client * @return string|null Version de l'app ou null */ private static function getAppVersion(array $clientInfo): ?string { $userAgent = $clientInfo['userAgent']; // Tentative extraction format: GeoSector/3.3.6 if (preg_match('/GeoSector\/([0-9\.]+)/', $userAgent, $matches)) { return $matches[1]; } // Format alternatif: AppName/Version if (preg_match('/([A-Za-z0-9_]+)\/([0-9\.]+)/', $userAgent, $matches)) { return $matches[2]; } return null; } /** * Crée le dossier de logs si nécessaire avec les bonnes permissions */ private static function ensureLogDirectoryExists(): void { if (!is_dir(self::LOG_DIR)) { if (!@mkdir(self::LOG_DIR, self::DIR_PERMISSIONS, true)) { error_log("EventLogService: Impossible de créer le dossier " . self::LOG_DIR); return; } } // Vérifier les permissions if (!is_writable(self::LOG_DIR)) { @chmod(self::LOG_DIR, self::DIR_PERMISSIONS); } } }