feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles

- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD)
  * DEV: Clés TEST Pierre (mode test)
  * REC: Clés TEST Client (mode test)
  * PROD: Clés LIVE Client (mode live)
- Ajout de la gestion des bases de données immeubles/bâtiments
  * Configuration buildings_database pour DEV/REC/PROD
  * Service BuildingService pour enrichissement des adresses
- Optimisations pages et améliorations ergonomie
- Mises à jour des dépendances Composer
- Nettoyage des fichiers obsolètes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-11-09 18:26:27 +01:00
parent 21657a3820
commit 2f5946a184
812 changed files with 142105 additions and 25992 deletions

View File

@@ -0,0 +1,533 @@
<?php
declare(strict_types=1);
namespace App\Services;
use ClientDetector;
use Session;
/**
* EventLogService - Système de logs d'événements JSONL
*
* Enregistre tous les événements métier (authentification, CRUD passages/secteurs/users/entités/opérations)
* dans des fichiers JSONL quotidiens pour statistiques et audit.
*
* Format : Un événement = une ligne JSON
* Stockage : /logs/events/YYYY-MM-DD.jsonl
* Rétention : 15 mois (compression après 30 jours)
*
* @see docs/EVENTS-LOG.md
*/
class EventLogService
{
/** @var string Chemin du dossier de logs d'événements */
private const LOG_DIR = __DIR__ . '/../../logs/events';
/** @var int Permissions du dossier */
private const DIR_PERMISSIONS = 0750;
/** @var int Permissions des fichiers */
private const FILE_PERMISSIONS = 0640;
// ==================== MÉTHODES D'AUTHENTIFICATION ====================
/**
* Log une connexion réussie
*
* @param int $userId ID utilisateur (users.id)
* @param int|null $entityId ID entité
* @param string $username Nom d'utilisateur
*/
public static function logLoginSuccess(int $userId, ?int $entityId, string $username): void
{
self::writeEvent('login_success', [
'user_id' => $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 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);
}
}
}