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:
@@ -6,9 +6,9 @@ declare(strict_types=1);
|
||||
* Configuration de l'application Geosector
|
||||
*
|
||||
* Ce fichier contient la configuration de l'application Geosector pour les trois environnements :
|
||||
* - Production (app.geosector.fr)
|
||||
* - Production (app3.geosector.fr)
|
||||
* - Recette (rapp.geosector.fr)
|
||||
* - Développement (app.geo.dev)
|
||||
* - Développement (dapp.geosector.fr)
|
||||
*
|
||||
* Il inclut les paramètres de base de données, les informations SMTP,
|
||||
* les clés de chiffrement et les configurations des services externes (Mapbox, Stripe, SMS OVH).
|
||||
@@ -24,6 +24,25 @@ class AppConfig {
|
||||
// Récupération du host directement depuis SERVER_NAME ou HTTP_HOST
|
||||
$this->currentHost = $_SERVER['SERVER_NAME'] ?? $_SERVER['HTTP_HOST'] ?? '';
|
||||
|
||||
// Si on est en CLI (CRON, scripts), tenter de détecter via le marqueur d'environnement
|
||||
if (empty($this->currentHost) && php_sapi_name() === 'cli') {
|
||||
$markerFile = __DIR__ . '/../../.env_marker';
|
||||
if (file_exists($markerFile)) {
|
||||
$envMarker = trim(file_get_contents($markerFile));
|
||||
switch ($envMarker) {
|
||||
case 'production':
|
||||
$this->currentHost = 'app3.geosector.fr';
|
||||
break;
|
||||
case 'recette':
|
||||
$this->currentHost = 'rapp.geosector.fr';
|
||||
break;
|
||||
case 'development':
|
||||
$this->currentHost = 'dapp.geosector.fr';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les autres en-têtes pour une utilisation ultérieure si nécessaire
|
||||
// getallheaders() n'existe pas en CLI, donc on vérifie
|
||||
$this->headers = function_exists('getallheaders') ? getallheaders() : [];
|
||||
@@ -81,10 +100,10 @@ class AppConfig {
|
||||
];
|
||||
|
||||
// Configuration PRODUCTION
|
||||
$this->config['app.geosector.fr'] = array_merge($baseConfig, [
|
||||
$this->config['app3.geosector.fr'] = array_merge($baseConfig, [
|
||||
'env' => 'production',
|
||||
'database' => [
|
||||
'host' => '13.23.33.4', // Container maria4 sur IN4
|
||||
'host' => '13.23.33.4', // Container maria4 sur IN4 (51.159.7.190)
|
||||
'name' => 'pra_geo',
|
||||
'username' => 'pra_geo_user',
|
||||
'password' => 'd2jAAGGWi8fxFrWgXjOA',
|
||||
@@ -93,17 +112,23 @@ class AppConfig {
|
||||
'host' => '13.23.33.4', // Container maria4 sur IN4
|
||||
'name' => 'adresses',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeo.User',
|
||||
'password' => 'd66,AdrGeoPrd.User',
|
||||
],
|
||||
'buildings_database' => [
|
||||
'host' => '13.23.33.4', // Container maria4 sur IN4
|
||||
'name' => 'batiments',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoPrd.User',
|
||||
],
|
||||
// Configuration Stripe PRODUCTION - Clés LIVE du CLIENT
|
||||
'stripe' => [
|
||||
'public_key_test' => 'pk_test_XXXXXX', // Non utilisé en PROD
|
||||
'secret_key_test' => 'sk_test_XXXXXX', // Non utilisé en PROD
|
||||
'public_key_live' => 'CLIENT_PK_LIVE_A_REMPLACER', // ← À REMPLACER avec pk_live_...
|
||||
'secret_key_live' => 'CLIENT_SK_LIVE_A_REMPLACER', // ← À REMPLACER avec sk_live_...
|
||||
'public_key_live' => 'pk_live_51S5oMd1tQE0jBEomdRW82RvqAFjmqN45szbU08t8nDk4yc5QnhAJtPrP1IZJB48fF1pePUqrGsM5vyAhhoaWCT8d00nh51QIsU',
|
||||
'secret_key_live' => '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',
|
||||
'webhook_secret_test' => 'whsec_test_XXXXXXXXXXXX',
|
||||
'webhook_secret_live' => 'whsec_live_XXXXXXXXXXXX',
|
||||
'api_version' => '2024-06-20',
|
||||
'webhook_secret_live' => 'whsec_gFnA6pR92RLdbAS2T6CSC18xsSdNBZHR',
|
||||
'api_version' => '2025-08-27.basil',
|
||||
'application_fee_percent' => 0,
|
||||
'application_fee_minimum' => 0,
|
||||
'mode' => 'live', // ← MODE LIVE pour la production
|
||||
@@ -114,17 +139,17 @@ class AppConfig {
|
||||
$this->config['rapp.geosector.fr'] = array_merge($baseConfig, [
|
||||
'env' => 'recette',
|
||||
'database' => [
|
||||
// Configuration future avec maria3 (à activer après migration)
|
||||
// 'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
// 'name' => 'rca_geo',
|
||||
// 'username' => 'rca_geo_user',
|
||||
// 'password' => 'UPf3C0cQ805LypyM71iW',
|
||||
// Configuration maria3 activée (migration effectuée le 16/10/2025)
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'rca_geo',
|
||||
'username' => 'rca_geo_user',
|
||||
'password' => 'UPf3C0cQ805LypyM71iW',
|
||||
|
||||
// Configuration actuelle - base locale dans rca-geo
|
||||
'host' => 'localhost',
|
||||
'name' => 'geo_app',
|
||||
'username' => 'geo_app_user_rec',
|
||||
'password' => 'UPf3C0cQ805LypyM71iW', // À ajuster si nécessaire
|
||||
// Configuration AVANT migration (base locale dans rca-geo) - DÉSACTIVÉE
|
||||
// 'host' => 'localhost',
|
||||
// 'name' => 'geo_app',
|
||||
// 'username' => 'geo_app_user_rec',
|
||||
// 'password' => 'UPf3C0cQ805LypyM71iW',
|
||||
],
|
||||
'addresses_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
@@ -132,15 +157,21 @@ class AppConfig {
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoRec.User',
|
||||
],
|
||||
'buildings_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'batiments',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoRec.User',
|
||||
],
|
||||
// Configuration Stripe RECETTE - Clés TEST du CLIENT
|
||||
'stripe' => [
|
||||
'public_key_test' => 'pk_test_51S5oMd1tQE0jBEomd1u28D1bUujOcl87ASuGf9xulcz4rY27QfHrLBtQj20MVlWta4AGXsX0YMfeOJFE66AlGlkz00vG30U8Rr',
|
||||
'secret_key_test' => 'sk_test_51S5oMd1tQE0jBEomAhzPBvUcCf0HX9ydK0xq7DagKnidp3JsovbQoVaTj24TKSUPvujQA3PP7IpIS8iWzAd15Rte00TETmbimh',
|
||||
'public_key_test' => 'pk_test_51S5oN00EZ9a0jvy2VSPjAYyCiJWci8lwfuakc0wpStt5YWq8RlQWyliICYIWHwTaejeW8uMSKA6KTfsfUAOvjRi500XPXWRFhJ',
|
||||
'secret_key_test' => 'sk_test_51S5oN00EZ9a0jvy2paTcHY91Alh5QIMJLJZJGJ188jXqte5AkxwymbLoLDiLcCn0uQH41WC75UM03HPDDp04gl7h00wfno08gE',
|
||||
'public_key_live' => 'pk_live_XXXXXX', // Non utilisé en REC
|
||||
'secret_key_live' => 'sk_live_XXXXXX', // Non utilisé en REC
|
||||
'webhook_secret_test' => 'whsec_test_XXXXXXXXXXXX',
|
||||
'webhook_secret_test' => 'whsec_avExshr0MeWTI7wXP8478XVUkrbYG8hs',
|
||||
'webhook_secret_live' => 'whsec_live_XXXXXXXXXXXX',
|
||||
'api_version' => '2024-06-20',
|
||||
'api_version' => '2025-08-27.basil',
|
||||
'application_fee_percent' => 0,
|
||||
'application_fee_minimum' => 0,
|
||||
'mode' => 'test', // ← MODE TEST pour la recette
|
||||
@@ -151,17 +182,17 @@ class AppConfig {
|
||||
$this->config['dapp.geosector.fr'] = array_merge($baseConfig, [
|
||||
'env' => 'development',
|
||||
'database' => [
|
||||
// Configuration future avec maria3 (à activer après migration)
|
||||
// 'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
// 'name' => 'dva_geo',
|
||||
// 'username' => 'dva_geo_user',
|
||||
// 'password' => 'CBq9tKHj6PGPZuTmAHV7',
|
||||
// Configuration maria3 (migration effectuée le 07/10/2025)
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'dva_geo',
|
||||
'username' => 'dva_geo_user',
|
||||
'password' => 'CBq9tKHj6PGPZuTmAHV7',
|
||||
|
||||
// Configuration actuelle - base locale dans dva-geo
|
||||
'host' => 'localhost',
|
||||
'name' => 'geo_app',
|
||||
'username' => 'geo_app_user_dev',
|
||||
'password' => 'CBq9tKHj6PGPZuTmAHV7', // À ajuster si nécessaire
|
||||
// Configuration locale AVANT migration (sauvegarde)
|
||||
// 'host' => 'localhost',
|
||||
// 'name' => 'geo_app',
|
||||
// 'username' => 'geo_app_user_dev',
|
||||
// 'password' => 'CBq9tKHj6PGPZuTmAHV7',
|
||||
],
|
||||
'addresses_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
@@ -169,6 +200,12 @@ class AppConfig {
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoDev.User',
|
||||
],
|
||||
'buildings_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'batiments',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoDev.User',
|
||||
],
|
||||
// Configuration Stripe DÉVELOPPEMENT - Clés TEST de Pierre (plateforme de test existante)
|
||||
'stripe' => [
|
||||
'public_key_test' => 'pk_test_51QwoVN00pblGEgsXkf8qlXmLGEpxDQcG0KLRpjrGLjJHd7AVZ4Iwd6ChgdjO0w0n3vRqwNCEW8KnHUe5eh3uIlkV00k07kCBmd',
|
||||
@@ -177,7 +214,7 @@ class AppConfig {
|
||||
'secret_key_live' => 'sk_live_XXXXXX', // Non utilisé en DEV
|
||||
'webhook_secret_test' => 'whsec_test_XXXXXXXXXXXX',
|
||||
'webhook_secret_live' => 'whsec_live_XXXXXXXXXXXX',
|
||||
'api_version' => '2024-06-20',
|
||||
'api_version' => '2025-08-27.basil',
|
||||
'application_fee_percent' => 0,
|
||||
'application_fee_minimum' => 0,
|
||||
'mode' => 'test', // ← MODE TEST pour le développement
|
||||
@@ -197,13 +234,20 @@ class AppConfig {
|
||||
|
||||
// Si l'hôte n'existe pas dans la configuration, tenter une correction
|
||||
if (!isset($this->config[$this->currentHost])) {
|
||||
// Gestion des cas spéciaux (anciennes URLs)
|
||||
if ($this->currentHost === 'app.geosector.fr') {
|
||||
$this->currentHost = 'app3.geosector.fr';
|
||||
}
|
||||
|
||||
// Essayer de faire correspondre avec l'un des hôtes connus
|
||||
$knownHosts = array_keys($this->config);
|
||||
foreach ($knownHosts as $host) {
|
||||
if (strpos($this->currentHost, str_replace(['app.', 'rapp.', 'dapp.'], '', $host)) !== false) {
|
||||
// Correspondance trouvée, utiliser cette configuration
|
||||
$this->currentHost = $host;
|
||||
break;
|
||||
if (!isset($this->config[$this->currentHost])) {
|
||||
$knownHosts = array_keys($this->config);
|
||||
foreach ($knownHosts as $host) {
|
||||
if (strpos($this->currentHost, str_replace(['app3.', 'rapp.', 'dapp.'], '', $host)) !== false) {
|
||||
// Correspondance trouvée, utiliser cette configuration
|
||||
$this->currentHost = $host;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +275,7 @@ class AppConfig {
|
||||
/**
|
||||
* Retourne l'identifiant de l'application basé sur l'hôte
|
||||
*
|
||||
* @return string L'identifiant de l'application (app.geosector.fr, rapp.geosector.fr, dapp.geosector.fr)
|
||||
* @return string L'identifiant de l'application (app3.geosector.fr, rapp.geosector.fr, dapp.geosector.fr)
|
||||
*/
|
||||
public function getAppIdentifier(): string {
|
||||
return $this->currentHost;
|
||||
@@ -293,7 +337,7 @@ class AppConfig {
|
||||
|
||||
/**
|
||||
* Retourne la configuration de la base de données
|
||||
*
|
||||
*
|
||||
* @return array Configuration de la base de données
|
||||
*/
|
||||
public function getDatabaseConfig(): array {
|
||||
@@ -302,13 +346,22 @@ class AppConfig {
|
||||
|
||||
/**
|
||||
* Retourne la configuration de la base de données des adresses
|
||||
*
|
||||
*
|
||||
* @return array Configuration de la base de données des adresses
|
||||
*/
|
||||
public function getAddressesDatabaseConfig(): array {
|
||||
return $this->getCurrentConfig()['addresses_database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la configuration de la base de données des bâtiments
|
||||
*
|
||||
* @return array Configuration de la base de données des bâtiments
|
||||
*/
|
||||
public function getBuildingsDatabaseConfig(): array {
|
||||
return $this->getCurrentConfig()['buildings_database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la clé de chiffrement
|
||||
*
|
||||
@@ -410,13 +463,23 @@ class AppConfig {
|
||||
|
||||
/**
|
||||
* Retourne l'adresse IP du client
|
||||
*
|
||||
*
|
||||
* @return string L'adresse IP du client
|
||||
*/
|
||||
public function getClientIp(): string {
|
||||
return $this->clientIp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la redirection vers RECETTE est activée
|
||||
*
|
||||
* @return bool True si la redirection est activée
|
||||
*/
|
||||
public function shouldRedirectToRecette(): bool {
|
||||
$value = getenv('REDIRECT_TO_REC');
|
||||
return $value === 'true' || $value === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la configuration des backups
|
||||
*
|
||||
|
||||
@@ -7,13 +7,19 @@ namespace App\Controllers;
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
|
||||
// Les classes sont déjà incluses via require_once, pas besoin de 'use' statements
|
||||
use PDO;
|
||||
use Database;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
|
||||
class ChatController {
|
||||
private \PDO $db;
|
||||
private PDO $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = \Database::getInstance();
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,8 +30,8 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$userId = \Session::getUserId();
|
||||
$entityId = \Session::getEntityId();
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
|
||||
// Vérifier si c'est une synchronisation incrémentale
|
||||
$updatedAfter = $_GET['updated_after'] ?? null;
|
||||
@@ -186,7 +192,7 @@ class ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'sync_timestamp' => $syncTimestamp,
|
||||
'has_changes' => !empty($rooms),
|
||||
@@ -194,11 +200,11 @@ class ChatController {
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la récupération des conversations', [
|
||||
LogService::log('Erreur lors de la récupération des conversations', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -213,9 +219,9 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$entityId = \Session::getEntityId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
$userRole = $this->getUserRole($userId);
|
||||
|
||||
// Récupérer le temp_id s'il est fourni (pour la synchronisation offline)
|
||||
@@ -223,7 +229,7 @@ class ChatController {
|
||||
|
||||
// Validation des données
|
||||
if (!isset($data['type']) || !in_array($data['type'], ['private', 'group', 'broadcast'])) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Type de conversation invalide'
|
||||
], 400);
|
||||
@@ -233,7 +239,7 @@ class ChatController {
|
||||
// Vérification des permissions pour broadcast
|
||||
// Seuls les super admins (role = 9) peuvent créer des broadcasts
|
||||
if ($data['type'] === 'broadcast' && $userRole != 9) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seuls les super administrateurs peuvent créer des annonces'
|
||||
], 403);
|
||||
@@ -242,7 +248,7 @@ class ChatController {
|
||||
|
||||
// Validation des participants
|
||||
if (!isset($data['participants']) || !is_array($data['participants']) || empty($data['participants'])) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Au moins un participant requis'
|
||||
], 400);
|
||||
@@ -251,7 +257,7 @@ class ChatController {
|
||||
|
||||
// Pour une conversation privée, limiter à 2 participants (incluant le créateur)
|
||||
if ($data['type'] === 'private' && count($data['participants']) > 1) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Une conversation privée ne peut avoir que 2 participants'
|
||||
], 400);
|
||||
@@ -272,7 +278,7 @@ class ChatController {
|
||||
if ($tempId !== null) {
|
||||
$existingRoom['temp_id'] = $tempId;
|
||||
}
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'room' => $existingRoom,
|
||||
'existing' => true
|
||||
@@ -351,7 +357,7 @@ class ChatController {
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
\LogService::log('Conversation créée', [
|
||||
LogService::log('Conversation créée', [
|
||||
'level' => 'info',
|
||||
'room_id' => $roomId,
|
||||
'type' => $data['type'],
|
||||
@@ -367,7 +373,7 @@ class ChatController {
|
||||
$room['temp_id'] = $tempId;
|
||||
}
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'room' => $room
|
||||
], 201);
|
||||
@@ -378,20 +384,20 @@ class ChatController {
|
||||
}
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la création de la conversation', [
|
||||
LogService::log('Erreur lors de la création de la conversation', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
} catch (\Exception $e) {
|
||||
\LogService::log('Erreur lors de la création de la conversation', [
|
||||
LogService::log('Erreur lors de la création de la conversation', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 400);
|
||||
@@ -406,8 +412,8 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Récupérer le temp_id s'il est fourni (pour la synchronisation offline)
|
||||
$tempId = $data['temp_id'] ?? null;
|
||||
@@ -423,7 +429,7 @@ class ChatController {
|
||||
$room = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$room) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Conversation non trouvée ou accès non autorisé'
|
||||
], 404);
|
||||
@@ -432,7 +438,7 @@ class ChatController {
|
||||
|
||||
// Vérifier les permissions
|
||||
if ($room['created_by'] != $userId && !$room['is_admin']) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seul le créateur ou un admin peut modifier la conversation'
|
||||
], 403);
|
||||
@@ -460,24 +466,24 @@ class ChatController {
|
||||
$updatedRoom['temp_id'] = $tempId;
|
||||
}
|
||||
|
||||
\LogService::log('Conversation mise à jour', [
|
||||
LogService::log('Conversation mise à jour', [
|
||||
'level' => 'info',
|
||||
'room_id' => $roomId,
|
||||
'updated_by' => $userId
|
||||
]);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'room' => $updatedRoom
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la mise à jour de la conversation', [
|
||||
LogService::log('Erreur lors de la mise à jour de la conversation', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -493,7 +499,7 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$userId = \Session::getUserId();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Vérifier que la room existe et récupérer le créateur
|
||||
$stmt = $this->db->prepare('
|
||||
@@ -506,7 +512,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que la room existe
|
||||
if (!$room) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Conversation non trouvée'
|
||||
], 404);
|
||||
@@ -515,7 +521,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que la room n'est pas déjà supprimée
|
||||
if ($room['is_active'] == 0) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Cette conversation est déjà supprimée'
|
||||
], 400);
|
||||
@@ -524,7 +530,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que l'utilisateur est le créateur
|
||||
if ($room['created_by'] != $userId) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seul le créateur de la conversation peut la supprimer'
|
||||
], 403);
|
||||
@@ -554,13 +560,13 @@ class ChatController {
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
\LogService::log('Conversation supprimée', [
|
||||
LogService::log('Conversation supprimée', [
|
||||
'level' => 'info',
|
||||
'room_id' => $roomId,
|
||||
'deleted_by' => $userId
|
||||
]);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => 'Conversation supprimée avec succès'
|
||||
]);
|
||||
@@ -573,12 +579,12 @@ class ChatController {
|
||||
}
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la suppression de la conversation', [
|
||||
LogService::log('Erreur lors de la suppression de la conversation', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -593,11 +599,11 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$userId = \Session::getUserId();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
if (!$this->isUserInRoom($userId, $roomId)) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé à cette conversation'
|
||||
], 403);
|
||||
@@ -658,7 +664,7 @@ class ChatController {
|
||||
|
||||
// Déchiffrer les noms
|
||||
foreach ($messages as &$message) {
|
||||
$message['sender_name'] = \ApiService::decryptData($message['sender_name']);
|
||||
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
|
||||
$message['is_read'] = (bool)$message['is_read'];
|
||||
$message['is_mine'] = ($message['sender_id'] == $userId);
|
||||
}
|
||||
@@ -675,7 +681,7 @@ class ChatController {
|
||||
// Compter les messages non lus restants (devrait être 0)
|
||||
$unreadCount = $this->getUnreadCount($roomId, $userId);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'messages' => $messages,
|
||||
'has_more' => count($messages) === $limit,
|
||||
@@ -684,12 +690,12 @@ class ChatController {
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la récupération des messages', [
|
||||
LogService::log('Erreur lors de la récupération des messages', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -704,15 +710,15 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Récupérer le temp_id s'il est fourni (pour la synchronisation offline)
|
||||
$tempId = $data['temp_id'] ?? null;
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
if (!$this->isUserInRoom($userId, $roomId)) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé à cette conversation'
|
||||
], 403);
|
||||
@@ -724,7 +730,7 @@ class ChatController {
|
||||
if ($roomInfo && $roomInfo['type'] === 'broadcast') {
|
||||
// Pour les broadcasts, seul le créateur peut écrire
|
||||
if ($roomInfo['created_by'] != $userId) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seul l\'administrateur peut poster dans une annonce'
|
||||
], 403);
|
||||
@@ -733,7 +739,7 @@ class ChatController {
|
||||
} else {
|
||||
// Pour les autres types, vérifier can_write
|
||||
if (!$this->canUserWrite($userId, $roomId)) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Vous n\'avez pas la permission d\'écrire dans cette conversation'
|
||||
], 403);
|
||||
@@ -743,7 +749,7 @@ class ChatController {
|
||||
|
||||
// Validation du contenu
|
||||
if (!isset($data['content']) || empty(trim($data['content']))) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le message ne peut pas être vide'
|
||||
], 400);
|
||||
@@ -754,7 +760,7 @@ class ChatController {
|
||||
|
||||
// Limiter la longueur du message
|
||||
if (mb_strlen($content, 'UTF-8') > 5000) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Message trop long (max 5000 caractères)'
|
||||
], 400);
|
||||
@@ -799,7 +805,7 @@ class ChatController {
|
||||
$msgStmt->execute(['id' => $messageId]);
|
||||
$message = $msgStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
$message['sender_name'] = \ApiService::decryptData($message['sender_name']);
|
||||
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
|
||||
$message['is_mine'] = true;
|
||||
$message['is_read'] = false;
|
||||
$message['read_count'] = 0;
|
||||
@@ -809,25 +815,25 @@ class ChatController {
|
||||
$message['temp_id'] = $tempId;
|
||||
}
|
||||
|
||||
\LogService::log('Message envoyé', [
|
||||
LogService::log('Message envoyé', [
|
||||
'level' => 'debug',
|
||||
'room_id' => $roomId,
|
||||
'message_id' => $messageId,
|
||||
'sender_id' => $userId
|
||||
]);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => $message
|
||||
], 201);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de l\'envoi du message', [
|
||||
LogService::log('Erreur lors de l\'envoi du message', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -842,8 +848,8 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Récupérer le temp_id s'il est fourni (pour la synchronisation offline)
|
||||
$tempId = $data['temp_id'] ?? null;
|
||||
@@ -858,7 +864,7 @@ class ChatController {
|
||||
$message = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$message) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Message non trouvé'
|
||||
], 404);
|
||||
@@ -867,7 +873,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que l'utilisateur est le sender du message
|
||||
if ($message['sender_id'] != $userId) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Vous ne pouvez modifier que vos propres messages'
|
||||
], 403);
|
||||
@@ -876,7 +882,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que le message n'est pas supprimé
|
||||
if ($message['is_deleted']) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Ce message a été supprimé'
|
||||
], 400);
|
||||
@@ -885,7 +891,7 @@ class ChatController {
|
||||
|
||||
// Validation du contenu
|
||||
if (!isset($data['content']) || empty(trim($data['content']))) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le message ne peut pas être vide'
|
||||
], 400);
|
||||
@@ -896,7 +902,7 @@ class ChatController {
|
||||
|
||||
// Limiter la longueur du message
|
||||
if (mb_strlen($content, 'UTF-8') > 5000) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Message trop long (max 5000 caractères)'
|
||||
], 400);
|
||||
@@ -931,7 +937,7 @@ class ChatController {
|
||||
$msgStmt->execute(['id' => $messageId]);
|
||||
$updatedMessage = $msgStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
$updatedMessage['sender_name'] = \ApiService::decryptData($updatedMessage['sender_name']);
|
||||
$updatedMessage['sender_name'] = ApiService::decryptData($updatedMessage['sender_name']);
|
||||
$updatedMessage['is_mine'] = true;
|
||||
|
||||
// Ajouter le temp_id à la réponse si fourni
|
||||
@@ -939,24 +945,24 @@ class ChatController {
|
||||
$updatedMessage['temp_id'] = $tempId;
|
||||
}
|
||||
|
||||
\LogService::log('Message modifié', [
|
||||
LogService::log('Message modifié', [
|
||||
'level' => 'debug',
|
||||
'message_id' => $messageId,
|
||||
'sender_id' => $userId
|
||||
]);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => $updatedMessage
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la modification du message', [
|
||||
LogService::log('Erreur lors de la modification du message', [
|
||||
'level' => 'error',
|
||||
'message_id' => $messageId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -971,12 +977,12 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
if (!$this->isUserInRoom($userId, $roomId)) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé à cette conversation'
|
||||
], 403);
|
||||
@@ -1041,18 +1047,18 @@ class ChatController {
|
||||
]);
|
||||
$result = $countStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'unread_count' => (int)$result['unread_count']
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors du marquage comme lu', [
|
||||
LogService::log('Erreur lors du marquage comme lu', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -1067,8 +1073,8 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$userId = \Session::getUserId();
|
||||
$entityId = \Session::getEntityId();
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
$userRole = $this->getUserRole($userId);
|
||||
|
||||
$sql = '
|
||||
@@ -1122,11 +1128,11 @@ class ChatController {
|
||||
|
||||
foreach ($recipients as &$recipient) {
|
||||
// Déchiffrer le nom
|
||||
$recipient['name'] = \ApiService::decryptData($recipient['name']);
|
||||
$recipient['name'] = ApiService::decryptData($recipient['name']);
|
||||
|
||||
// Déchiffrer le nom de l'entité
|
||||
$entiteName = $recipient['entite_name'] ?
|
||||
\ApiService::decryptData($recipient['entite_name']) :
|
||||
ApiService::decryptData($recipient['entite_name']) :
|
||||
'Sans entité';
|
||||
|
||||
// Créer une copie pour recipients_by_entity
|
||||
@@ -1146,18 +1152,18 @@ class ChatController {
|
||||
$recipientsDecrypted[] = $recipient;
|
||||
}
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'recipients' => $recipientsDecrypted,
|
||||
'recipients_by_entity' => $recipientsByEntity
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la récupération des destinataires', [
|
||||
LogService::log('Erreur lors de la récupération des destinataires', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -1225,7 +1231,7 @@ class ChatController {
|
||||
$participants = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($participants as &$participant) {
|
||||
$participant['name'] = \ApiService::decryptData($participant['name']);
|
||||
$participant['name'] = ApiService::decryptData($participant['name']);
|
||||
}
|
||||
|
||||
return $participants;
|
||||
@@ -1349,7 +1355,7 @@ class ChatController {
|
||||
|
||||
// Déchiffrer les noms et convertir les booléens
|
||||
foreach ($messages as &$message) {
|
||||
$message['sender_name'] = \ApiService::decryptData($message['sender_name']);
|
||||
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
|
||||
$message['is_read'] = (bool)$message['is_read'];
|
||||
$message['is_mine'] = (bool)$message['is_mine'];
|
||||
}
|
||||
@@ -1398,7 +1404,7 @@ class ChatController {
|
||||
|
||||
// Déchiffrer les noms et convertir les booléens
|
||||
foreach ($messages as &$message) {
|
||||
$message['sender_name'] = \ApiService::decryptData($message['sender_name']);
|
||||
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
|
||||
$message['is_read'] = (bool)$message['is_read'];
|
||||
$message['is_mine'] = (bool)$message['is_mine'];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
|
||||
use PDO;
|
||||
@@ -14,8 +15,10 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\FileService;
|
||||
use Exception;
|
||||
|
||||
class EntiteController {
|
||||
@@ -74,13 +77,12 @@ class EntiteController {
|
||||
throw new Exception('Erreur lors de la création de l\'entité');
|
||||
}
|
||||
|
||||
LogService::log('Création d\'une nouvelle entité GeoSector', [
|
||||
'level' => 'info',
|
||||
'entiteId' => $entiteId,
|
||||
'name' => $name,
|
||||
'postalCode' => $postalCode,
|
||||
'cityName' => $cityName
|
||||
]);
|
||||
// Log de création de l'entité
|
||||
EventLogService::logEntityCreated(
|
||||
(int)$entiteId,
|
||||
1, // fk_type toujours à 1 dans cette méthode
|
||||
$postalCode
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $entiteId,
|
||||
@@ -220,12 +222,12 @@ class EntiteController {
|
||||
throw new Exception('Erreur lors de la création de l\'entité');
|
||||
}
|
||||
|
||||
LogService::log('Création d\'une nouvelle entité GeoSector via getOrCreateEntiteByPostalCode', [
|
||||
'level' => 'info',
|
||||
'entiteId' => $entiteId,
|
||||
'name' => $name,
|
||||
'postalCode' => $postalCode
|
||||
]);
|
||||
// Log de création de l'entité
|
||||
EventLogService::logEntityCreated(
|
||||
$entiteId,
|
||||
1, // fk_type toujours à 1 dans cette méthode
|
||||
$postalCode
|
||||
);
|
||||
|
||||
return $entiteId;
|
||||
} catch (Exception $e) {
|
||||
@@ -559,10 +561,8 @@ class EntiteController {
|
||||
$params[] = $data['gps_lng'];
|
||||
}
|
||||
|
||||
if (isset($data['stripe_id'])) {
|
||||
$updateFields[] = 'encrypted_stripe_id = ?';
|
||||
$params[] = ApiService::encryptData($data['stripe_id']);
|
||||
}
|
||||
// Note: stripe_id ne peut plus être modifié ici
|
||||
// Les données Stripe sont gérées via la table stripe_accounts
|
||||
|
||||
if (isset($data['chk_demo'])) {
|
||||
$updateFields[] = 'chk_demo = ?';
|
||||
@@ -629,12 +629,23 @@ class EntiteController {
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Mise à jour d\'une entité GeoSector', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'entiteId' => $entiteId,
|
||||
'isAdmin' => $isAdmin
|
||||
]);
|
||||
// Log de mise à jour de l'entité
|
||||
$changes = [];
|
||||
$encryptedFields = ['name', 'email', 'phone', 'mobile'];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array($key, $encryptedFields)) {
|
||||
// Champs sensibles : booléen uniquement
|
||||
$changes['encrypted_' . $key] = true;
|
||||
} else {
|
||||
// Champs non sensibles : valeur
|
||||
$changes[$key] = ['new' => $value];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logEntityUpdated((int)$entiteId, $changes);
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
@@ -738,7 +749,7 @@ class EntiteController {
|
||||
|
||||
// Créer le dossier de destination
|
||||
require_once __DIR__ . '/../Services/FileService.php';
|
||||
$fileService = new \FileService();
|
||||
$fileService = new FileService();
|
||||
$uploadPath = "/{$entiteId}/logo";
|
||||
$fullPath = $fileService->createDirectory($entiteId, $uploadPath);
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
use Exception;
|
||||
|
||||
class FileController {
|
||||
|
||||
115
api/src/Controllers/HealthController.php
Normal file
115
api/src/Controllers/HealthController.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Database;
|
||||
use Response;
|
||||
use PDO;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* HealthController
|
||||
*
|
||||
* Endpoint de vérification de santé de l'API
|
||||
* Route publique pour permettre le monitoring automatique
|
||||
*/
|
||||
class HealthController
|
||||
{
|
||||
private PDO $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
/**
|
||||
* Vérifie la santé de l'API
|
||||
* GET /api/health
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function check(): void
|
||||
{
|
||||
$checks = [
|
||||
'api' => 'ok',
|
||||
'database' => $this->checkDatabase(),
|
||||
'directories' => $this->checkDirectories()
|
||||
];
|
||||
|
||||
// Déterminer le statut global
|
||||
$status = in_array('error', $checks, true) ? 'error' : 'ok';
|
||||
$httpCode = $status === 'ok' ? 200 : 503;
|
||||
|
||||
Response::json([
|
||||
'status' => $status,
|
||||
'checks' => $checks,
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'environment' => $this->getEnvironment()
|
||||
], $httpCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie la connexion à la base de données
|
||||
*
|
||||
* @return string 'ok' ou 'error'
|
||||
*/
|
||||
private function checkDatabase(): string
|
||||
{
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->query("SELECT 1");
|
||||
return $stmt ? 'ok' : 'error';
|
||||
} catch (Exception $e) {
|
||||
error_log("Health check database error: " . $e->getMessage());
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'accessibilité des dossiers critiques
|
||||
*
|
||||
* @return string 'ok' ou 'error'
|
||||
*/
|
||||
private function checkDirectories(): string
|
||||
{
|
||||
$basePath = __DIR__ . '/../../';
|
||||
$requiredDirs = ['logs', 'uploads'];
|
||||
|
||||
foreach ($requiredDirs as $dir) {
|
||||
$fullPath = $basePath . $dir;
|
||||
|
||||
if (!is_dir($fullPath)) {
|
||||
error_log("Health check: Directory not found: $fullPath");
|
||||
return 'error';
|
||||
}
|
||||
|
||||
if (!is_writable($fullPath)) {
|
||||
error_log("Health check: Directory not writable: $fullPath");
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
/**
|
||||
* Détecte l'environnement actuel
|
||||
*
|
||||
* @return string 'dev', 'recette' ou 'production'
|
||||
*/
|
||||
private function getEnvironment(): string
|
||||
{
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'unknown';
|
||||
|
||||
if (str_contains($host, 'dapp.geosector.fr')) {
|
||||
return 'dev';
|
||||
} elseif (str_contains($host, 'rapp.geosector.fr')) {
|
||||
return 'recette';
|
||||
} elseif (str_contains($host, 'app3.geosector.fr') || str_contains($host, 'app.geosector.fr')) {
|
||||
return 'production';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,12 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\EventLogService;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/EntiteController.php';
|
||||
require_once __DIR__ . '/../Services/Security/SecurityMonitor.php';
|
||||
@@ -55,14 +57,6 @@ class LoginController {
|
||||
// admin accessible uniquement aux fk_role>1 (admins amicale + super-admins)
|
||||
$roleCondition = ($interface === 'user') ? 'AND fk_role IN (1, 2)' : 'AND fk_role>1';
|
||||
|
||||
// Log pour le debug
|
||||
LogService::log('Tentative de connexion GeoSector', [
|
||||
'level' => 'info',
|
||||
'username' => $username,
|
||||
'type' => $interface,
|
||||
'role_condition' => $roleCondition
|
||||
]);
|
||||
|
||||
// Requête optimisée: on récupère l'utilisateur et son entité en une seule fois avec LEFT JOIN
|
||||
$stmt = $this->db->prepare(
|
||||
'SELECT
|
||||
@@ -83,11 +77,8 @@ class LoginController {
|
||||
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||
SecurityMonitor::recordFailedLogin($clientIp, $username, 'user_not_found', $userAgent);
|
||||
|
||||
LogService::log('Tentative de connexion GeoSector échouée : utilisateur non trouvé', [
|
||||
'level' => 'warning',
|
||||
'username' => $username
|
||||
]);
|
||||
|
||||
EventLogService::logLoginFailed($username, 'user_not_found', 1);
|
||||
Response::json(['error' => 'Identifiants invalides'], 401);
|
||||
return;
|
||||
}
|
||||
@@ -100,22 +91,15 @@ class LoginController {
|
||||
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||
SecurityMonitor::recordFailedLogin($clientIp, $username, 'invalid_password', $userAgent);
|
||||
|
||||
LogService::log('Tentative de connexion GeoSector échouée : mot de passe incorrect', [
|
||||
'level' => 'warning',
|
||||
'username' => $username
|
||||
]);
|
||||
|
||||
EventLogService::logLoginFailed($username, 'invalid_password', 1);
|
||||
Response::json(['error' => 'Identifiants invalides'], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier si l'utilisateur a une entité et si elle est active
|
||||
if (!empty($user['fk_entite']) && (!isset($user['entite_chk_active']) || $user['entite_chk_active'] != 1)) {
|
||||
LogService::log('Tentative de connexion GeoSector échouée : entité non active', [
|
||||
'level' => 'warning',
|
||||
'username' => $username,
|
||||
'entite_id' => $user['fk_entite']
|
||||
]);
|
||||
EventLogService::logLoginFailed($username, 'account_inactive', 1);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Votre amicale n\'est pas activée. Veuillez contacter votre administrateur.'
|
||||
@@ -307,16 +291,33 @@ class LoginController {
|
||||
// Récupérer l'ID de l'opération active (première opération retournée)
|
||||
$activeOperationId = $operations[0]['id'];
|
||||
|
||||
// Récupérer ope_user_id pour l'utilisateur connecté et l'opération active
|
||||
$opeUserStmt = $this->db->prepare(
|
||||
'SELECT id FROM ope_users WHERE fk_user = ? AND fk_operation = ?'
|
||||
);
|
||||
$opeUserStmt->execute([$user['id'], $activeOperationId]);
|
||||
$opeUser = $opeUserStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($opeUser) {
|
||||
$userData['ope_user_id'] = $opeUser['id'];
|
||||
} else {
|
||||
$userData['ope_user_id'] = null;
|
||||
}
|
||||
|
||||
// 2. Récupérer les secteurs selon l'interface et le rôle
|
||||
if ($interface === 'user') {
|
||||
// Interface utilisateur : seulement les secteurs affectés à l'utilisateur
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $user['id']]);
|
||||
// Utiliser ope_user_id au lieu de users.id
|
||||
$opeUserId = $userData['ope_user_id'];
|
||||
if ($opeUserId) {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $opeUserId]);
|
||||
}
|
||||
} elseif ($interface === 'admin' && $user['fk_role'] == 2) {
|
||||
// Interface admin avec rôle 2 : tous les secteurs distincts de l'opération
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
@@ -344,11 +345,12 @@ class LoginController {
|
||||
// 3. Récupérer les passages selon l'interface et le rôle
|
||||
if ($interface === 'user' && !empty($sectors)) {
|
||||
// Interface utilisateur : passages de l'utilisateur + passages à finaliser sur ses secteurs
|
||||
$userId = $user['id'];
|
||||
// Utiliser ope_user_id au lieu de users.id
|
||||
$opeUserId = $userData['ope_user_id'];
|
||||
$sectorIds = array_column($sectors, 'id');
|
||||
$sectorIdsString = implode(',', $sectorIds);
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
if (!empty($sectorIdsString) && $opeUserId) {
|
||||
$passagesStmt = $this->db->prepare(
|
||||
"SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
@@ -362,7 +364,7 @@ class LoginController {
|
||||
)
|
||||
ORDER BY passed_at DESC"
|
||||
);
|
||||
$passagesStmt->execute([$activeOperationId, $userId, $userId]);
|
||||
$passagesStmt->execute([$activeOperationId, $opeUserId, $opeUserId]);
|
||||
}
|
||||
} elseif ($interface === 'admin' && $user['fk_role'] == 2) {
|
||||
// Interface admin avec rôle 2 : tous les passages de l'opération
|
||||
@@ -423,13 +425,14 @@ class LoginController {
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
$usersSectorsStmt = $this->db->prepare(
|
||||
"SELECT DISTINCT u.id, u.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
FROM users u
|
||||
JOIN ope_users_sectors us ON u.id = us.fk_user
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
AND u.chk_active = 1
|
||||
"SELECT DISTINCT u.id as user_id, ou.id as ope_user_id, ou.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
FROM users u
|
||||
JOIN ope_users ou ON u.id = ou.fk_user
|
||||
JOIN ope_users_sectors us ON ou.id = us.fk_user AND ou.fk_operation = us.fk_operation
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
AND u.chk_active = 1
|
||||
AND u.id != ?" // Exclure l'utilisateur connecté
|
||||
);
|
||||
$usersSectorsStmt->execute([$activeOperationId, $user['id']]);
|
||||
@@ -458,14 +461,27 @@ class LoginController {
|
||||
|
||||
// 6. Récupérer les membres (users de l'entité du user) si nécessaire
|
||||
if ($interface === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
// Si on a une opération active, on récupère aussi ope_user_id
|
||||
if (isset($activeOperationId)) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT u.id, u.fk_role, u.fk_entite, u.fk_titre, u.encrypted_name, u.first_name, u.sect_name,
|
||||
u.encrypted_user_name, u.encrypted_phone, u.encrypted_mobile, u.encrypted_email,
|
||||
u.date_naissance, u.date_embauche, u.chk_active, ou.id as ope_user_id
|
||||
FROM users u
|
||||
LEFT JOIN ope_users ou ON u.id = ou.fk_user AND ou.fk_operation = ?
|
||||
WHERE u.fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$activeOperationId, $user['fk_entite']]);
|
||||
} else {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
}
|
||||
$membres = $membresStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($membres)) {
|
||||
@@ -474,6 +490,7 @@ class LoginController {
|
||||
foreach ($membres as $membre) {
|
||||
$membreItem = [
|
||||
'id' => $membre['id'],
|
||||
'ope_user_id' => $membre['ope_user_id'] ?? null,
|
||||
'fk_role' => $membre['fk_role'],
|
||||
'fk_entite' => $membre['fk_entite'],
|
||||
'fk_titre' => $membre['fk_titre'],
|
||||
@@ -537,13 +554,15 @@ class LoginController {
|
||||
if ($user['fk_role'] <= 2) {
|
||||
// User normal ou admin avec fk_role=2: uniquement son amicale
|
||||
$amicaleStmt = $this->db->prepare(
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
sa.stripe_account_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass,
|
||||
sa.stripe_location_id
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
WHERE e.id = ? AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute([$user['fk_entite']]);
|
||||
@@ -551,13 +570,15 @@ class LoginController {
|
||||
} else {
|
||||
// Admin avec fk_role>2: toutes les amicales sauf id=1
|
||||
$amicaleStmt = $this->db->prepare(
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
sa.stripe_account_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass,
|
||||
sa.stripe_location_id
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
WHERE e.id != 1 AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute();
|
||||
@@ -872,6 +893,9 @@ class LoginController {
|
||||
// Ajouter les données du chat à la réponse
|
||||
$response['chat'] = $chatData;
|
||||
|
||||
// Log de connexion réussie
|
||||
EventLogService::logLoginSuccess($user['id'], $user['fk_entite'] ?? null, $username);
|
||||
|
||||
// Envoi de la réponse
|
||||
Response::json($response);
|
||||
} catch (PDOException $e) {
|
||||
@@ -918,14 +942,6 @@ class LoginController {
|
||||
// Déterminer le roleCondition selon le mode (même logique que login)
|
||||
$roleCondition = ($mode === 'user') ? 'AND fk_role IN (1, 2)' : 'AND fk_role>1';
|
||||
|
||||
// Log pour le debug
|
||||
LogService::log('Rafraîchissement session GeoSector', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'mode' => $mode,
|
||||
'role_condition' => $roleCondition
|
||||
]);
|
||||
|
||||
// 4. Requête pour récupérer l'utilisateur et son entité (même requête que login)
|
||||
$stmt = $this->db->prepare(
|
||||
'SELECT
|
||||
@@ -1074,15 +1090,32 @@ class LoginController {
|
||||
|
||||
$activeOperationId = $operations[0]['id'];
|
||||
|
||||
// Récupérer ope_user_id pour l'utilisateur connecté et l'opération active
|
||||
$opeUserStmt = $this->db->prepare(
|
||||
'SELECT id FROM ope_users WHERE fk_user = ? AND fk_operation = ?'
|
||||
);
|
||||
$opeUserStmt->execute([$user['id'], $activeOperationId]);
|
||||
$opeUser = $opeUserStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($opeUser) {
|
||||
$userData['ope_user_id'] = $opeUser['id'];
|
||||
} else {
|
||||
$userData['ope_user_id'] = null;
|
||||
}
|
||||
|
||||
// Récupérer les secteurs selon le mode et le rôle
|
||||
if ($mode === 'user') {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $user['id']]);
|
||||
// Utiliser ope_user_id au lieu de users.id
|
||||
$opeUserId = $userData['ope_user_id'];
|
||||
if ($opeUserId) {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $opeUserId]);
|
||||
}
|
||||
} elseif ($mode === 'admin' && $user['fk_role'] == 2) {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT DISTINCT s.id, s.libelle, s.color, s.sector
|
||||
@@ -1106,10 +1139,12 @@ class LoginController {
|
||||
|
||||
// Récupérer les passages selon le mode et le rôle
|
||||
if ($mode === 'user' && !empty($sectors)) {
|
||||
// Utiliser ope_user_id au lieu de users.id
|
||||
$opeUserId = $userData['ope_user_id'];
|
||||
$sectorIds = array_column($sectors, 'id');
|
||||
$sectorIdsString = implode(',', $sectorIds);
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
if (!empty($sectorIdsString) && $opeUserId) {
|
||||
$passagesStmt = $this->db->prepare(
|
||||
"SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
@@ -1123,7 +1158,7 @@ class LoginController {
|
||||
)
|
||||
ORDER BY passed_at DESC"
|
||||
);
|
||||
$passagesStmt->execute([$activeOperationId, $user['id'], $user['id']]);
|
||||
$passagesStmt->execute([$activeOperationId, $opeUserId, $opeUserId]);
|
||||
}
|
||||
} elseif ($mode === 'admin' && $user['fk_role'] == 2) {
|
||||
$passagesStmt = $this->db->prepare(
|
||||
@@ -1177,9 +1212,10 @@ class LoginController {
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
$usersSectorsStmt = $this->db->prepare(
|
||||
"SELECT DISTINCT u.id, u.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
"SELECT DISTINCT u.id as user_id, ou.id as ope_user_id, ou.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
FROM users u
|
||||
JOIN ope_users_sectors us ON u.id = us.fk_user
|
||||
JOIN ope_users ou ON u.id = ou.fk_user
|
||||
JOIN ope_users_sectors us ON ou.id = us.fk_user AND ou.fk_operation = us.fk_operation
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
@@ -1209,20 +1245,34 @@ class LoginController {
|
||||
// Récupérer les membres si nécessaire
|
||||
$membresData = [];
|
||||
if ($mode === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
// Si on a une opération active, on récupère aussi ope_user_id
|
||||
if (isset($activeOperationId)) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT u.id, u.fk_role, u.fk_entite, u.fk_titre, u.encrypted_name, u.first_name, u.sect_name,
|
||||
u.encrypted_user_name, u.encrypted_phone, u.encrypted_mobile, u.encrypted_email,
|
||||
u.date_naissance, u.date_embauche, u.chk_active, ou.id as ope_user_id
|
||||
FROM users u
|
||||
LEFT JOIN ope_users ou ON u.id = ou.fk_user AND ou.fk_operation = ?
|
||||
WHERE u.fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$activeOperationId, $user['fk_entite']]);
|
||||
} else {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
}
|
||||
$membres = $membresStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($membres)) {
|
||||
foreach ($membres as $membre) {
|
||||
$membreItem = [
|
||||
'id' => $membre['id'],
|
||||
'ope_user_id' => $membre['ope_user_id'] ?? null,
|
||||
'fk_role' => $membre['fk_role'],
|
||||
'fk_entite' => $membre['fk_entite'],
|
||||
'fk_titre' => $membre['fk_titre'],
|
||||
@@ -1279,10 +1329,12 @@ class LoginController {
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
sa.stripe_account_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass,
|
||||
sa.stripe_location_id
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
WHERE e.id = ? AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute([$user['fk_entite']]);
|
||||
@@ -1292,10 +1344,12 @@ class LoginController {
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
sa.stripe_account_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass,
|
||||
sa.stripe_location_id
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
WHERE e.id != 1 AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute();
|
||||
@@ -1830,13 +1884,13 @@ class LoginController {
|
||||
}
|
||||
*/
|
||||
|
||||
// 5. Vérification de l'existence du code postal dans la table entites
|
||||
$checkPostalStmt = $this->db->prepare('SELECT id FROM entites WHERE code_postal = ?');
|
||||
$checkPostalStmt->execute([$postalCode]);
|
||||
// 5. Vérification de l'existence du code postal + ville dans la table entites
|
||||
$checkPostalStmt = $this->db->prepare('SELECT id FROM entites WHERE code_postal = ? AND ville = ?');
|
||||
$checkPostalStmt->execute([$postalCode, $cityName]);
|
||||
if ($checkPostalStmt->fetch()) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Une amicale est déjà inscrite à ce code postal'
|
||||
'message' => 'Une amicale est déjà inscrite pour ce code postal et cette ville'
|
||||
], 409);
|
||||
return;
|
||||
}
|
||||
@@ -2073,16 +2127,15 @@ class LoginController {
|
||||
// Méthodes auxiliaires
|
||||
|
||||
public function logout(): void {
|
||||
$userId = Session::getUserId() ?? null;
|
||||
$userEmail = Session::getUserEmail() ?? 'anonyme';
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
|
||||
Session::logout();
|
||||
|
||||
LogService::log('Déconnexion GeoSector réussie', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'email' => $userEmail
|
||||
]);
|
||||
// Log de déconnexion
|
||||
if ($userId) {
|
||||
EventLogService::logLogout($userId, $entityId, 0);
|
||||
}
|
||||
|
||||
// Retourner une réponse standardisée
|
||||
Response::json([
|
||||
@@ -2106,12 +2159,20 @@ class LoginController {
|
||||
// Formater la ville et le code postal pour la recherche
|
||||
$citySearch = urlencode($cityName . ' ' . $postalCode);
|
||||
|
||||
// Créer un contexte avec timeout de 2 secondes
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 2,
|
||||
'ignore_errors' => true
|
||||
]
|
||||
]);
|
||||
|
||||
foreach ($keywords as $keyword) {
|
||||
// Construire l'URL de recherche pour l'API adresse.gouv.fr
|
||||
$searchUrl = "https://api-adresse.data.gouv.fr/search/?q=" . urlencode($keyword) . "+$citySearch&limit=5";
|
||||
|
||||
// Effectuer la requête HTTP
|
||||
$response = @file_get_contents($searchUrl);
|
||||
// Effectuer la requête HTTP avec timeout
|
||||
$response = @file_get_contents($searchUrl, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
LogService::log('Erreur lors de la requête à l\'API adresse.gouv.fr', [
|
||||
@@ -2159,9 +2220,19 @@ class LoginController {
|
||||
}
|
||||
}
|
||||
|
||||
// Si aucune caserne n'a été trouvée avec les mots-clés, utiliser les coordonnées du centre de la ville
|
||||
// Si aucune caserne trouvée, chercher simplement ville + code postal avec timeout
|
||||
$citySearch = urlencode($cityName . ' ' . $postalCode);
|
||||
$cityUrl = "https://api-adresse.data.gouv.fr/search/?q=$citySearch&limit=1";
|
||||
$cityResponse = @file_get_contents($cityUrl);
|
||||
|
||||
// Créer un contexte avec timeout de 2 secondes
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 2,
|
||||
'ignore_errors' => true
|
||||
]
|
||||
]);
|
||||
|
||||
$cityResponse = @file_get_contents($cityUrl, false, $context);
|
||||
|
||||
if ($cityResponse !== false) {
|
||||
$cityData = json_decode($cityResponse, true);
|
||||
@@ -2169,7 +2240,7 @@ class LoginController {
|
||||
if ($cityData && isset($cityData['features'][0]['geometry']['coordinates'])) {
|
||||
$coordinates = $cityData['features'][0]['geometry']['coordinates'];
|
||||
|
||||
LogService::log('Utilisation des coordonnées du centre de la ville', [
|
||||
LogService::log('Coordonnées GPS récupérées pour l\'adresse', [
|
||||
'level' => 'info',
|
||||
'city' => $cityName,
|
||||
'postalCode' => $postalCode
|
||||
@@ -2183,6 +2254,12 @@ class LoginController {
|
||||
}
|
||||
|
||||
// Aucune coordonnée trouvée
|
||||
LogService::log('Aucune coordonnée GPS trouvée (timeout ou adresse invalide)', [
|
||||
'level' => 'warning',
|
||||
'city' => $cityName,
|
||||
'postalCode' => $postalCode
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
544
api/src/Controllers/MigrationController.php
Normal file
544
api/src/Controllers/MigrationController.php
Normal file
@@ -0,0 +1,544 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/MigrationService.php';
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Database;
|
||||
use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use App\Services\LogService;
|
||||
use App\Services\MigrationService;
|
||||
use Exception;
|
||||
|
||||
class MigrationController {
|
||||
private PDO $db;
|
||||
private AppConfig $appConfig;
|
||||
private MigrationService $migrationService;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance();
|
||||
$this->appConfig = AppConfig::getInstance();
|
||||
$this->migrationService = new MigrationService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Teste les connexions aux bases de données source et cible
|
||||
*
|
||||
* GET /api/migrations/test-connections
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testConnections(): void {
|
||||
try {
|
||||
$result = $this->migrationService->testConnections();
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'connections' => $result
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors du test des connexions', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les entités disponibles à migrer depuis la base source
|
||||
*
|
||||
* GET /api/migrations/entities/available
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getAvailableEntities(): void {
|
||||
try {
|
||||
$entities = $this->migrationService->getAvailableEntities();
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'count' => count($entities),
|
||||
'entities' => $entities
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération des entités disponibles', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les détails d'une entité source
|
||||
*
|
||||
* GET /api/migrations/entities/:id
|
||||
*
|
||||
* @param int $id ID de l'entité dans la base source
|
||||
* @return void
|
||||
*/
|
||||
public function getEntityDetails(int $id): void {
|
||||
try {
|
||||
$entity = $this->migrationService->getEntityDetails($id);
|
||||
|
||||
if (!$entity) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Entité non trouvée'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity' => $entity
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération des détails de l\'entité', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre une entité complète ou par étapes
|
||||
*
|
||||
* POST /api/migrations/entity
|
||||
* Body: {
|
||||
* "entity_id": 45,
|
||||
* "steps": ["users", "operations"], // Optionnel
|
||||
* "dry_run": false, // Optionnel
|
||||
* "truncate": false // Optionnel
|
||||
* }
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function migrateEntity(): void {
|
||||
try {
|
||||
$data = Request::getJsonBody();
|
||||
|
||||
// Validation
|
||||
if (!isset($data['entity_id'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le champ entity_id est requis'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$entityId = (int) $data['entity_id'];
|
||||
$steps = $data['steps'] ?? null;
|
||||
$dryRun = $data['dry_run'] ?? false;
|
||||
$truncate = $data['truncate'] ?? false;
|
||||
|
||||
// Vérifier les permissions (admin uniquement)
|
||||
$userRole = Session::get('fk_role');
|
||||
if ($userRole != 3) { // 3 = admin
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès refusé. Cette action nécessite les droits administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Début de migration d\'entité', [
|
||||
'level' => 'info',
|
||||
'entity_id' => $entityId,
|
||||
'steps' => $steps,
|
||||
'dry_run' => $dryRun,
|
||||
'truncate' => $truncate,
|
||||
'user_id' => Session::get('user_id')
|
||||
]);
|
||||
|
||||
// Exécuter la migration
|
||||
$result = $this->migrationService->migrateEntity(
|
||||
$entityId,
|
||||
$steps,
|
||||
$dryRun,
|
||||
$truncate
|
||||
);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $entityId,
|
||||
'entity_name' => $result['entity_name'],
|
||||
'migration_id' => $result['migration_id'],
|
||||
'steps_completed' => $result['steps_completed'],
|
||||
'total_duration_ms' => $result['total_duration_ms'],
|
||||
'summary' => $result['summary']
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la migration d\'entité', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $data['entity_id'] ?? null
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre une étape spécifique pour une entité
|
||||
*
|
||||
* POST /api/migrations/entity/step
|
||||
* Body: {
|
||||
* "entity_id": 45,
|
||||
* "step": "users",
|
||||
* "dry_run": false,
|
||||
* "options": {}
|
||||
* }
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function migrateEntityStep(): void {
|
||||
try {
|
||||
$data = Request::getJsonBody();
|
||||
|
||||
// Validation
|
||||
if (!isset($data['entity_id']) || !isset($data['step'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Les champs entity_id et step sont requis'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$entityId = (int) $data['entity_id'];
|
||||
$step = $data['step'];
|
||||
$dryRun = $data['dry_run'] ?? false;
|
||||
$options = $data['options'] ?? [];
|
||||
|
||||
// Vérifier les permissions (admin uniquement)
|
||||
$userRole = Session::get('fk_role');
|
||||
if ($userRole != 3) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès refusé. Cette action nécessite les droits administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Début de migration d\'étape', [
|
||||
'level' => 'info',
|
||||
'entity_id' => $entityId,
|
||||
'step' => $step,
|
||||
'dry_run' => $dryRun,
|
||||
'options' => $options,
|
||||
'user_id' => Session::get('user_id')
|
||||
]);
|
||||
|
||||
// Exécuter l'étape de migration
|
||||
$result = $this->migrationService->migrateStep(
|
||||
$entityId,
|
||||
$step,
|
||||
$dryRun,
|
||||
$options
|
||||
);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $entityId,
|
||||
'step' => $step,
|
||||
'records_migrated' => $result['records_migrated'],
|
||||
'duration_ms' => $result['duration_ms'],
|
||||
'warnings' => $result['warnings'] ?? [],
|
||||
'details' => $result['details'] ?? []
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la migration d\'étape', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $data['entity_id'] ?? null,
|
||||
'step' => $data['step'] ?? null
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le statut de migration d'une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/status
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function getMigrationStatus(int $id): void {
|
||||
try {
|
||||
$status = $this->migrationService->getMigrationStatus($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'migration_status' => $status
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération du statut de migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les logs de migration d'une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/logs
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function getMigrationLogs(int $id): void {
|
||||
try {
|
||||
$logs = $this->migrationService->getMigrationLogs($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'logs' => $logs
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération des logs de migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un rapport de migration pour une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/report
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function getMigrationReport(int $id): void {
|
||||
try {
|
||||
$report = $this->migrationService->generateMigrationReport($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'report' => $report
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la génération du rapport de migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare les données source vs cible pour une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/compare
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function compareEntityData(int $id): void {
|
||||
try {
|
||||
$comparison = $this->migrationService->compareEntityData($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'comparison' => $comparison
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la comparaison des données', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'intégrité des données migrées pour une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/verify
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function verifyMigration(int $id): void {
|
||||
try {
|
||||
$verification = $this->migrationService->verifyMigration($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'verification' => $verification
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la vérification de la migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Annule la migration d'une entité (rollback)
|
||||
*
|
||||
* DELETE /api/migrations/entity/:id
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function rollbackEntity(int $id): void {
|
||||
try {
|
||||
// Vérifier les permissions (admin uniquement)
|
||||
$userRole = Session::get('fk_role');
|
||||
if ($userRole != 3) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès refusé. Cette action nécessite les droits administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Début de rollback d\'entité', [
|
||||
'level' => 'warning',
|
||||
'entity_id' => $id,
|
||||
'user_id' => Session::get('user_id')
|
||||
]);
|
||||
|
||||
$result = $this->migrationService->rollbackEntity($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'message' => 'Migration annulée avec succès',
|
||||
'deleted_records' => $result['deleted_records']
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors du rollback de la migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une étape spécifique de la migration
|
||||
*
|
||||
* DELETE /api/migrations/entity/:id/step/:step
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @param string $step Nom de l'étape
|
||||
* @return void
|
||||
*/
|
||||
public function rollbackStep(int $id, string $step): void {
|
||||
try {
|
||||
// Vérifier les permissions (admin uniquement)
|
||||
$userRole = Session::get('fk_role');
|
||||
if ($userRole != 3) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès refusé. Cette action nécessite les droits administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Début de rollback d\'étape', [
|
||||
'level' => 'warning',
|
||||
'entity_id' => $id,
|
||||
'step' => $step,
|
||||
'user_id' => Session::get('user_id')
|
||||
]);
|
||||
|
||||
$result = $this->migrationService->rollbackStep($id, $step);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'step' => $step,
|
||||
'message' => 'Étape annulée avec succès',
|
||||
'deleted_records' => $result['deleted_records']
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors du rollback de l\'étape', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id,
|
||||
'step' => $step
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ExportService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/OperationDataService.php';
|
||||
@@ -16,10 +17,11 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ExportService;
|
||||
use ApiService;
|
||||
use OperationDataService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ExportService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\OperationDataService;
|
||||
use Exception;
|
||||
use DateTime;
|
||||
|
||||
@@ -378,34 +380,37 @@ class OperationController {
|
||||
$newSectId = (int)$this->db->lastInsertId();
|
||||
$duplicatedSectors++;
|
||||
|
||||
// Étape 4.3 : Dupliquer les users_sectors en vérifiant que fk_user existe dans ope_users
|
||||
// Étape 4.3 : Dupliquer les users_sectors en convertissant ancien ope_users.id → nouvel ope_users.id
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, fk_user_creat)
|
||||
SELECT ?, ous.fk_user, ?, ?
|
||||
SELECT ?, new_ou.id, ?, ?
|
||||
FROM ope_users_sectors ous
|
||||
INNER JOIN ope_users ou ON ou.fk_user = ous.fk_user AND ou.fk_operation = ?
|
||||
INNER JOIN ope_users old_ou ON old_ou.id = ous.fk_user AND old_ou.fk_operation = ?
|
||||
INNER JOIN ope_users new_ou ON new_ou.fk_user = old_ou.fk_user AND new_ou.fk_operation = ?
|
||||
WHERE ous.fk_operation = ? AND ous.fk_sector = ? AND ous.chk_active = 1
|
||||
');
|
||||
$stmt->execute([$newOpeId, $newSectId, $userId, $newOpeId, $oldOpeId, $oldSectId]);
|
||||
$stmt->execute([$newOpeId, $newSectId, $userId, $oldOpeId, $newOpeId, $oldOpeId, $oldSectId]);
|
||||
$duplicatedUsersSectors += $stmt->rowCount();
|
||||
|
||||
// Étape 4.4 : Dupliquer les passages avec les valeurs par défaut spécifiées
|
||||
// Étape 4.4 : Dupliquer les passages en convertissant ancien ope_users.id → nouvel ope_users.id
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO ope_pass (
|
||||
fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis, ville,
|
||||
fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis, ville,
|
||||
fk_habitat, appt, niveau, residence, gps_lat, gps_lng, encrypted_name,
|
||||
fk_type, passed_at, montant, fk_type_reglement, chk_email_sent, chk_striped,
|
||||
docremis, nb_passages, chk_map_create, chk_mobile, chk_synchro, anomalie,
|
||||
fk_type, passed_at, montant, fk_type_reglement, chk_email_sent, chk_striped,
|
||||
docremis, nb_passages, chk_map_create, chk_mobile, chk_synchro, anomalie,
|
||||
fk_user_creat, chk_active
|
||||
)
|
||||
SELECT
|
||||
?, ?, fk_user, fk_adresse, numero, rue, rue_bis, ville,
|
||||
fk_habitat, appt, niveau, residence, gps_lat, gps_lng, encrypted_name,
|
||||
2, NULL, 0, 4, 0, 0, 0, 1, 0, 0, 1, 0, ?, 1
|
||||
FROM ope_pass
|
||||
WHERE fk_operation = ? AND fk_sector = ? AND chk_active = 1
|
||||
SELECT
|
||||
?, ?, new_ou.id, op.fk_adresse, op.numero, op.rue, op.rue_bis, op.ville,
|
||||
op.fk_habitat, op.appt, op.niveau, op.residence, op.gps_lat, op.gps_lng, op.encrypted_name,
|
||||
2, NULL, 0, 4, 0, 0, 0, 0, 0, 0, 1, 0, ?, 1
|
||||
FROM ope_pass op
|
||||
INNER JOIN ope_users old_ou ON old_ou.id = op.fk_user AND old_ou.fk_operation = ?
|
||||
INNER JOIN ope_users new_ou ON new_ou.fk_user = old_ou.fk_user AND new_ou.fk_operation = ?
|
||||
WHERE op.fk_operation = ? AND op.fk_sector = ? AND op.chk_active = 1
|
||||
');
|
||||
$stmt->execute([$newOpeId, $newSectId, $userId, $oldOpeId, $oldSectId]);
|
||||
$stmt->execute([$newOpeId, $newSectId, $userId, $oldOpeId, $newOpeId, $oldOpeId, $oldSectId]);
|
||||
$duplicatedPassages += $stmt->rowCount();
|
||||
}
|
||||
|
||||
@@ -455,19 +460,12 @@ class OperationController {
|
||||
// Étape 7 : Préparer la réponse avec les groupes JSON
|
||||
$response = OperationDataService::prepareOperationResponse($this->db, $newOpeId, $entiteId);
|
||||
|
||||
LogService::log('Création opération terminée avec succès', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'entiteId' => $entiteId,
|
||||
'newOpeId' => $newOpeId,
|
||||
'oldOpeId' => $oldOpeId,
|
||||
'stats' => [
|
||||
'insertedUsers' => $insertedUsers,
|
||||
'duplicatedSectors' => $duplicatedSectors,
|
||||
'duplicatedUsersSectors' => $duplicatedUsersSectors,
|
||||
'duplicatedPassages' => $duplicatedPassages
|
||||
]
|
||||
]);
|
||||
// Log de création de l'opération
|
||||
EventLogService::logOperationCreated(
|
||||
$newOpeId,
|
||||
$data['date_deb'],
|
||||
$data['date_fin']
|
||||
);
|
||||
|
||||
Response::json($response, 201);
|
||||
} catch (Exception $e) {
|
||||
@@ -621,12 +619,24 @@ class OperationController {
|
||||
$operationId
|
||||
]);
|
||||
|
||||
LogService::log('Mise à jour d\'une opération', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'entiteId' => $entiteId,
|
||||
'operationId' => $operationId
|
||||
]);
|
||||
// Log de mise à jour de l'opération
|
||||
$changes = [];
|
||||
if (isset($data['libelle']) || isset($data['name'])) {
|
||||
$changes['libelle'] = ['new' => $libelle];
|
||||
}
|
||||
if (isset($data['date_deb'])) {
|
||||
$changes['date_deb'] = ['new' => $data['date_deb']];
|
||||
}
|
||||
if (isset($data['date_fin'])) {
|
||||
$changes['date_fin'] = ['new' => $data['date_fin']];
|
||||
}
|
||||
if (isset($data['chk_distinct_sectors'])) {
|
||||
$changes['chk_distinct_sectors'] = ['new' => (int)$data['chk_distinct_sectors']];
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logOperationUpdated($operationId, $changes);
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
@@ -820,25 +830,8 @@ class OperationController {
|
||||
// Valider la transaction
|
||||
$this->db->commit();
|
||||
|
||||
LogService::log('Suppression complète d\'une opération et de toutes ses données', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'userRole' => $userRole,
|
||||
'userEntiteId' => $userEntiteId,
|
||||
'operationEntiteId' => $operationEntiteId,
|
||||
'operationId' => $operationId,
|
||||
'operationActive' => $operationActive,
|
||||
'deletedCounts' => [
|
||||
'medias' => $deletedMedias,
|
||||
'ope_pass_histo' => $deletedPassHisto,
|
||||
'ope_pass' => $deletedPass,
|
||||
'ope_users_sectors' => $deletedUsersSectors,
|
||||
'sectors_adresses' => $deletedSectorsAdresses,
|
||||
'ope_sectors' => $deletedSectors,
|
||||
'ope_users' => $deletedUsers,
|
||||
'operations' => 1
|
||||
]
|
||||
]);
|
||||
// Log de suppression de l'opération (suppression physique)
|
||||
EventLogService::logOperationDeleted($operationId, false);
|
||||
|
||||
// Préparer la réponse selon le statut de l'opération supprimée
|
||||
$response = [
|
||||
@@ -948,13 +941,14 @@ class OperationController {
|
||||
|
||||
// Récupérer les relations utilisateurs-secteurs
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT
|
||||
SELECT
|
||||
ous.id, ous.fk_operation, ous.fk_user, ous.fk_sector,
|
||||
ous.created_at, ous.updated_at, ous.chk_active,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name,
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name,
|
||||
s.libelle as sector_name
|
||||
FROM ope_users_sectors ous
|
||||
INNER JOIN users u ON u.id = ous.fk_user
|
||||
INNER JOIN ope_users ou ON ou.id = ous.fk_user
|
||||
INNER JOIN users u ON u.id = ou.fk_user
|
||||
INNER JOIN ope_sectors s ON s.id = ous.fk_sector
|
||||
WHERE ous.fk_operation = ? AND ous.chk_active = 1
|
||||
ORDER BY s.libelle, u.encrypted_name
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/ReceiptService.php';
|
||||
|
||||
@@ -15,8 +16,9 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ApiService;
|
||||
use Exception;
|
||||
use DateTime;
|
||||
|
||||
@@ -233,13 +235,14 @@ class PassageController {
|
||||
p.fk_habitat, p.appt, p.niveau, p.residence, p.gps_lat, p.gps_lng,
|
||||
p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
|
||||
p.encrypted_email, p.encrypted_phone, p.nom_recu, p.date_recu,
|
||||
p.chk_email_sent, p.stripe_payment_id, p.docremis, p.date_repasser, p.nb_passages,
|
||||
p.chk_email_sent, p.stripe_payment_id, p.stripe_payment_link_id, p.docremis, p.date_repasser, p.nb_passages,
|
||||
p.chk_mobile, p.anomalie, p.created_at, p.updated_at, p.chk_active,
|
||||
o.libelle as operation_libelle,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name
|
||||
FROM ope_pass p
|
||||
INNER JOIN operations o ON p.fk_operation = o.id
|
||||
INNER JOIN users u ON p.fk_user = u.id
|
||||
INNER JOIN ope_users ou ON p.fk_user = ou.id
|
||||
INNER JOIN users u ON ou.fk_user = u.id
|
||||
WHERE $whereClause AND p.chk_active = 1
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
@@ -324,13 +327,14 @@ class PassageController {
|
||||
$passageId = (int)$id;
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT
|
||||
p.*,
|
||||
SELECT
|
||||
p.*,
|
||||
o.libelle as operation_libelle,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name
|
||||
FROM ope_pass p
|
||||
INNER JOIN operations o ON p.fk_operation = o.id
|
||||
INNER JOIN users u ON p.fk_user = u.id
|
||||
INNER JOIN ope_users ou ON p.fk_user = ou.id
|
||||
INNER JOIN users u ON ou.fk_user = u.id
|
||||
WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
|
||||
');
|
||||
|
||||
@@ -410,12 +414,13 @@ class PassageController {
|
||||
p.id, p.fk_operation, p.fk_sector, p.fk_user, p.passed_at,
|
||||
p.numero, p.rue, p.rue_bis, p.ville, p.gps_lat, p.gps_lng,
|
||||
p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
|
||||
p.encrypted_email, p.encrypted_phone, p.stripe_payment_id, p.chk_email_sent,
|
||||
p.encrypted_email, p.encrypted_phone, p.stripe_payment_id, p.stripe_payment_link_id, p.chk_email_sent,
|
||||
p.docremis, p.date_repasser, p.nb_passages, p.chk_mobile,
|
||||
p.anomalie, p.created_at, p.updated_at,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name
|
||||
FROM ope_pass p
|
||||
INNER JOIN users u ON p.fk_user = u.id
|
||||
INNER JOIN ope_users ou ON p.fk_user = ou.id
|
||||
INNER JOIN users u ON ou.fk_user = u.id
|
||||
WHERE p.fk_operation = ? AND p.chk_active = 1
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
@@ -510,6 +515,24 @@ class PassageController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer ope_users.id pour l'utilisateur du passage
|
||||
// $data['fk_user'] contient users.id, on doit le convertir en ope_users.id
|
||||
$passageUserId = (int)$data['fk_user'];
|
||||
$stmtOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtOpeUser->execute([$passageUserId, $operationId]);
|
||||
$opeUserId = $stmtOpeUser->fetchColumn();
|
||||
|
||||
if (!$opeUserId) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Utilisateur non trouvé dans cette opération'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chiffrement des données sensibles
|
||||
$encryptedName = '';
|
||||
if (isset($data['name']) && !empty(trim($data['name']))) {
|
||||
@@ -527,7 +550,7 @@ class PassageController {
|
||||
$insertData = [
|
||||
'fk_operation' => $operationId,
|
||||
'fk_sector' => isset($data['fk_sector']) ? (int)$data['fk_sector'] : 0,
|
||||
'fk_user' => (int)$data['fk_user'],
|
||||
'fk_user' => $opeUserId,
|
||||
'fk_adresse' => $data['fk_adresse'] ?? '',
|
||||
'passed_at' => isset($data['passed_at']) ? $data['passed_at'] : null,
|
||||
'fk_type' => isset($data['fk_type']) ? (int)$data['fk_type'] : 0,
|
||||
@@ -569,12 +592,14 @@ class PassageController {
|
||||
|
||||
$passageId = $this->db->lastInsertId();
|
||||
|
||||
LogService::log('Création d\'un nouveau passage', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'passageId' => $passageId,
|
||||
'operationId' => $operationId
|
||||
]);
|
||||
// Log de création du passage
|
||||
EventLogService::logPassageCreated(
|
||||
(int)$passageId,
|
||||
$insertData['fk_operation'],
|
||||
$insertData['fk_sector'],
|
||||
$insertData['montant'],
|
||||
(string)$insertData['fk_type_reglement']
|
||||
);
|
||||
|
||||
// Enregistrer la génération du reçu dans shutdown_function pour garantir son exécution
|
||||
// Même si le worker FPM est tué après fastcgi_finish_request()
|
||||
@@ -702,16 +727,33 @@ class PassageController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer ope_users.id pour l'utilisateur connecté
|
||||
$operationId = $passage['fk_operation'];
|
||||
$stmtCurrentOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtCurrentOpeUser->execute([$userId, $operationId]);
|
||||
$currentOpeUserId = $stmtCurrentOpeUser->fetchColumn();
|
||||
|
||||
if (!$currentOpeUserId) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Utilisateur connecté non trouvé dans cette opération'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Si le passage était de type 2 et que l'utilisateur actuel est différent du créateur
|
||||
// On force l'attribution du passage à l'utilisateur actuel
|
||||
if ((int)$passage['fk_type'] === 2 && (int)$passage['fk_user'] !== $userId) {
|
||||
$data['fk_user'] = $userId;
|
||||
if ((int)$passage['fk_type'] === 2 && (int)$passage['fk_user'] !== $currentOpeUserId) {
|
||||
$data['fk_user'] = $currentOpeUserId;
|
||||
|
||||
LogService::log('Attribution automatique d\'un passage type 2 à l\'utilisateur', [
|
||||
'level' => 'info',
|
||||
'passageId' => $passageId,
|
||||
'ancien_user' => $passage['fk_user'],
|
||||
'nouveau_user' => $userId
|
||||
'nouveau_user' => $currentOpeUserId
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -722,7 +764,7 @@ class PassageController {
|
||||
// Champs pouvant être mis à jour
|
||||
$updatableFields = [
|
||||
'fk_sector',
|
||||
'fk_user',
|
||||
// Note: fk_user est traité séparément pour conversion users.id -> ope_users.id
|
||||
'fk_adresse',
|
||||
'passed_at',
|
||||
'fk_type',
|
||||
@@ -740,6 +782,7 @@ class PassageController {
|
||||
'fk_type_reglement',
|
||||
'remarque',
|
||||
'stripe_payment_id',
|
||||
'stripe_payment_link_id',
|
||||
'nom_recu',
|
||||
'date_recu',
|
||||
'docremis',
|
||||
@@ -756,6 +799,48 @@ class PassageController {
|
||||
}
|
||||
}
|
||||
|
||||
// Traitement spécial pour fk_user : conversion users.id -> ope_users.id
|
||||
if (isset($data['fk_user'])) {
|
||||
// Si $data['fk_user'] vient de l'attribution automatique, c'est déjà ope_users.id
|
||||
// Sinon, on doit convertir users.id en ope_users.id
|
||||
$providedUserId = (int)$data['fk_user'];
|
||||
|
||||
// Vérifier si c'est déjà un ope_users.id valide
|
||||
$stmtCheckOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE id = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtCheckOpeUser->execute([$providedUserId, $operationId]);
|
||||
$isOpeUserId = $stmtCheckOpeUser->fetchColumn();
|
||||
|
||||
if ($isOpeUserId) {
|
||||
// C'est déjà un ope_users.id valide
|
||||
$updateFields[] = "fk_user = ?";
|
||||
$params[] = $providedUserId;
|
||||
} else {
|
||||
// C'est probablement un users.id, on le convertit
|
||||
$stmtGetOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtGetOpeUser->execute([$providedUserId, $operationId]);
|
||||
$convertedOpeUserId = $stmtGetOpeUser->fetchColumn();
|
||||
|
||||
if ($convertedOpeUserId) {
|
||||
$updateFields[] = "fk_user = ?";
|
||||
$params[] = $convertedOpeUserId;
|
||||
} else {
|
||||
// Utilisateur non trouvé, on ignore cette mise à jour
|
||||
LogService::log('Tentative de mise à jour avec un utilisateur invalide', [
|
||||
'level' => 'warning',
|
||||
'passageId' => $passageId,
|
||||
'provided_user_id' => $providedUserId,
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion des champs chiffrés
|
||||
if (array_key_exists('name', $data)) {
|
||||
$updateFields[] = "encrypted_name = ?";
|
||||
@@ -791,11 +876,21 @@ class PassageController {
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
LogService::log('Mise à jour d\'un passage', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
// Log de mise à jour du passage (changements simplifiés)
|
||||
$changes = [];
|
||||
foreach ($data as $key => $value) {
|
||||
// Ne logger que les champs non sensibles
|
||||
if (!in_array($key, ['name', 'email', 'phone', 'encrypted_name', 'encrypted_email', 'encrypted_phone'])) {
|
||||
$changes[$key] = ['new' => $value];
|
||||
} else {
|
||||
// Indiquer qu'un champ chiffré a été modifié
|
||||
$changes[$key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logPassageUpdated((int)$passageId, $changes);
|
||||
}
|
||||
|
||||
// Enregistrer la génération du reçu dans shutdown_function pour garantir son exécution
|
||||
// Même si le worker FPM est tué après fastcgi_finish_request()
|
||||
@@ -944,7 +1039,7 @@ class PassageController {
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.id
|
||||
SELECT p.id, p.fk_operation
|
||||
FROM ope_pass p
|
||||
INNER JOIN operations o ON p.fk_operation = o.id
|
||||
WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
|
||||
@@ -962,18 +1057,19 @@ class PassageController {
|
||||
|
||||
// Désactiver le passage (soft delete)
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE ope_pass
|
||||
UPDATE ope_pass
|
||||
SET chk_active = 0, updated_at = NOW(), fk_user_modif = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
|
||||
$stmt->execute([$userId, $passageId]);
|
||||
|
||||
LogService::log('Suppression d\'un passage', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
// Log de suppression du passage
|
||||
EventLogService::logPassageDeleted(
|
||||
$passageId,
|
||||
(int)$passage['fk_operation'],
|
||||
true // soft delete
|
||||
);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
|
||||
@@ -9,7 +9,7 @@ require_once __DIR__ . '/../Services/LogService.php';
|
||||
|
||||
use Request;
|
||||
use Response;
|
||||
use LogService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\PasswordSecurityService;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,14 +3,14 @@ namespace App\Controllers;
|
||||
|
||||
use Database;
|
||||
use Response;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use AddressService;
|
||||
use DepartmentBoundaryService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\AddressService;
|
||||
use App\Services\DepartmentBoundaryService;
|
||||
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/AddressService.php';
|
||||
require_once __DIR__ . '/../Services/DepartmentBoundaryService.php';
|
||||
|
||||
class SectorController
|
||||
{
|
||||
@@ -193,14 +193,31 @@ class SectorController
|
||||
|
||||
// Affectation des users si fournis
|
||||
if (!empty($users)) {
|
||||
$queryMember = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, fk_user_creat, chk_active)
|
||||
VALUES (:operation_id, :user_id, :sector_id, NOW(), :user_creat, 1)";
|
||||
$queryMember = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, updated_at, fk_user_creat, chk_active)
|
||||
VALUES (:operation_id, :user_id, :sector_id, NOW(), NOW(), :user_creat, 1)";
|
||||
$stmtMember = $this->db->prepare($queryMember);
|
||||
|
||||
|
||||
foreach ($users as $memberId) {
|
||||
// $memberId est DÉJÀ ope_users.id (envoyé par Flutter)
|
||||
// Vérifier que cet ope_users.id existe et appartient bien à l'opération
|
||||
$stmtOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE id = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtOpeUser->execute([$memberId, $operationId]);
|
||||
$opeUserId = $stmtOpeUser->fetchColumn();
|
||||
|
||||
if (!$opeUserId) {
|
||||
$this->logService->warning('ope_users.id non trouvé pour cette opération', [
|
||||
'ope_users_id' => $memberId,
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmtMember->execute([
|
||||
'operation_id' => $operationId,
|
||||
'user_id' => $memberId,
|
||||
'user_id' => $opeUserId,
|
||||
'sector_id' => $sectorId,
|
||||
'user_creat' => $userId
|
||||
]);
|
||||
@@ -268,16 +285,24 @@ class SectorController
|
||||
$passagesCreated = 0; // Initialiser le compteur de passages
|
||||
try {
|
||||
$addresses = $this->addressService->getAddressesInPolygon($coordinates, $entityId);
|
||||
|
||||
|
||||
// Enrichir les adresses avec les données bâtiments
|
||||
$addresses = $this->addressService->enrichAddressesWithBuildings($addresses, $entityId);
|
||||
|
||||
if (!empty($addresses)) {
|
||||
$queryAddress = "INSERT INTO sectors_adresses (fk_sector, fk_adresse, numero, rue, rue_bis, cp, ville, gps_lat, gps_lng)
|
||||
VALUES (:sector_id, :address_id, :numero, :rue, :rue_bis, :cp, :ville, :gps_lat, :gps_lng)";
|
||||
$queryAddress = "INSERT INTO sectors_adresses (
|
||||
fk_sector, fk_adresse, numero, rue, rue_bis, cp, ville, gps_lat, gps_lng,
|
||||
fk_batiment, fk_habitat, nb_niveau, nb_log, residence, alt_sol
|
||||
) VALUES (
|
||||
:sector_id, :address_id, :numero, :rue, :rue_bis, :cp, :ville, :gps_lat, :gps_lng,
|
||||
:fk_batiment, :fk_habitat, :nb_niveau, :nb_log, :residence, :alt_sol
|
||||
)";
|
||||
$stmtAddress = $this->db->prepare($queryAddress);
|
||||
|
||||
|
||||
foreach ($addresses as $address) {
|
||||
// Extraire le rue_bis si présent (généralement vide)
|
||||
$rueBis = '';
|
||||
|
||||
|
||||
$stmtAddress->execute([
|
||||
'sector_id' => $sectorId,
|
||||
'address_id' => $address['id'],
|
||||
@@ -287,60 +312,111 @@ class SectorController
|
||||
'cp' => $address['code_postal'],
|
||||
'ville' => $address['commune'],
|
||||
'gps_lat' => $address['latitude'],
|
||||
'gps_lng' => $address['longitude']
|
||||
'gps_lng' => $address['longitude'],
|
||||
'fk_batiment' => $address['fk_batiment'] ?? null,
|
||||
'fk_habitat' => $address['fk_habitat'] ?? 1,
|
||||
'nb_niveau' => $address['nb_niveau'] ?? null,
|
||||
'nb_log' => $address['nb_log'] ?? null,
|
||||
'residence' => $address['residence'] ?? '',
|
||||
'alt_sol' => $address['alt_sol'] ?? null
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// Créer les passages pour chaque adresse
|
||||
if (!empty($users)) {
|
||||
$firstUserId = $users[0]; // Premier user pour l'affectation des passages
|
||||
$passageQuery = "INSERT INTO ope_pass (
|
||||
fk_operation, fk_sector, fk_user, fk_adresse,
|
||||
numero, rue, rue_bis, ville,
|
||||
gps_lat, gps_lng, fk_type, encrypted_name,
|
||||
created_at, fk_user_creat, chk_active
|
||||
) VALUES (
|
||||
:operation_id, :sector_id, :user_id, :fk_adresse,
|
||||
:numero, :rue, :rue_bis, :ville,
|
||||
:gps_lat, :gps_lng, 2, '',
|
||||
NOW(), :user_creat, 1
|
||||
)";
|
||||
$passageStmt = $this->db->prepare($passageQuery);
|
||||
|
||||
$passagesCreated = 0;
|
||||
foreach ($addresses as $address) {
|
||||
// Vérifier si cette adresse n'est pas déjà utilisée par un passage orphelin
|
||||
if (in_array($address['id'], $addressesToExclude)) {
|
||||
continue; // Passer à l'adresse suivante
|
||||
}
|
||||
|
||||
try {
|
||||
// Extraire le rue_bis si présent (généralement vide)
|
||||
$rueBis = '';
|
||||
|
||||
$passageStmt->execute([
|
||||
'operation_id' => $operationId,
|
||||
'sector_id' => $sectorId,
|
||||
'user_id' => $firstUserId,
|
||||
'fk_adresse' => $address['id'],
|
||||
'numero' => $address['numero'],
|
||||
'rue' => $address['voie'],
|
||||
'rue_bis' => $rueBis,
|
||||
'ville' => $address['commune'],
|
||||
'gps_lat' => $address['latitude'],
|
||||
'gps_lng' => $address['longitude'],
|
||||
'user_creat' => $userId
|
||||
]);
|
||||
$passagesCreated++;
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->warning('Erreur lors de la création d\'un passage', [
|
||||
'address_id' => $address['id'],
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
// Récupérer ope_users.id pour le premier utilisateur
|
||||
// $users[0] est DÉJÀ ope_users.id (envoyé par Flutter)
|
||||
$stmtFirstOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE id = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtFirstOpeUser->execute([$users[0], $operationId]);
|
||||
$firstOpeUserId = $stmtFirstOpeUser->fetchColumn();
|
||||
|
||||
if (!$firstOpeUserId) {
|
||||
$this->logService->warning('Premier ope_users.id non trouvé pour cette opération', [
|
||||
'ope_users_id' => $users[0],
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
// Pas de création de passages sans utilisateur valide dans ope_users
|
||||
} else {
|
||||
$passageQuery = "INSERT INTO ope_pass (
|
||||
fk_operation, fk_sector, fk_user, fk_adresse,
|
||||
numero, rue, rue_bis, ville, residence, appt, fk_habitat,
|
||||
gps_lat, gps_lng, fk_type, nb_passages, encrypted_name,
|
||||
created_at, fk_user_creat, chk_active
|
||||
) VALUES (
|
||||
:operation_id, :sector_id, :user_id, :fk_adresse,
|
||||
:numero, :rue, :rue_bis, :ville, :residence, :appt, :fk_habitat,
|
||||
:gps_lat, :gps_lng, 2, 0, '',
|
||||
NOW(), :user_creat, 1
|
||||
)";
|
||||
$passageStmt = $this->db->prepare($passageQuery);
|
||||
|
||||
$passagesCreated = 0;
|
||||
foreach ($addresses as $address) {
|
||||
// Vérifier si cette adresse n'est pas déjà utilisée par un passage orphelin
|
||||
if (in_array($address['id'], $addressesToExclude)) {
|
||||
continue; // Passer à l'adresse suivante
|
||||
}
|
||||
|
||||
try {
|
||||
// Extraire le rue_bis si présent (généralement vide)
|
||||
$rueBis = '';
|
||||
|
||||
// Déterminer le nombre de passages à créer
|
||||
$fkHabitat = $address['fk_habitat'] ?? 1;
|
||||
$nbLog = ($fkHabitat == 2 && isset($address['nb_log'])) ? (int)$address['nb_log'] : 1;
|
||||
$residence = $address['residence'] ?? '';
|
||||
|
||||
// IMPORTANT : Uniformisation GPS pour les immeubles
|
||||
// Tous les passages d'une même adresse partagent les mêmes coordonnées GPS
|
||||
// Issues de la table adresses enrichie (gps_lat, gps_lng)
|
||||
$gpsLat = $address['latitude'];
|
||||
$gpsLng = $address['longitude'];
|
||||
|
||||
// Créer 1 passage pour maison individuelle, nb_log passages pour immeuble
|
||||
for ($i = 1; $i <= $nbLog; $i++) {
|
||||
$appt = ($fkHabitat == 2) ? (string)$i : ''; // Numéro d'appartement pour immeubles
|
||||
|
||||
$passageStmt->execute([
|
||||
'operation_id' => $operationId,
|
||||
'sector_id' => $sectorId,
|
||||
'user_id' => $firstOpeUserId,
|
||||
'fk_adresse' => $address['id'],
|
||||
'numero' => $address['numero'],
|
||||
'rue' => $address['voie'],
|
||||
'rue_bis' => $rueBis,
|
||||
'ville' => $address['commune'],
|
||||
'residence' => $residence,
|
||||
'appt' => $appt,
|
||||
'fk_habitat' => $fkHabitat,
|
||||
'gps_lat' => $gpsLat,
|
||||
'gps_lng' => $gpsLng,
|
||||
'user_creat' => $userId
|
||||
]);
|
||||
$passagesCreated++;
|
||||
}
|
||||
|
||||
// Log pour vérifier l'uniformisation GPS (surtout pour immeubles)
|
||||
if ($fkHabitat == 2 && $nbLog > 1) {
|
||||
$this->logService->info('[SectorController] Création passages immeuble avec GPS uniformisés', [
|
||||
'address_id' => $address['id'],
|
||||
'nb_passages' => $nbLog,
|
||||
'gps_lat' => $gpsLat,
|
||||
'gps_lng' => $gpsLng,
|
||||
'residence' => $residence
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->warning('Erreur lors de la création d\'un passage', [
|
||||
'address_id' => $address['id'],
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
@@ -351,9 +427,16 @@ class SectorController
|
||||
'entity_id' => $entityId
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
|
||||
// Log de création du secteur
|
||||
EventLogService::logSectorCreated(
|
||||
(int)$sectorId,
|
||||
(int)$operationId,
|
||||
$sectorData['libelle']
|
||||
);
|
||||
|
||||
// Préparer les données de réponse
|
||||
$responseData = [
|
||||
'sector_id' => $sectorId
|
||||
@@ -413,9 +496,10 @@ class SectorController
|
||||
}
|
||||
|
||||
// Récupérer les users affectés
|
||||
$usersQuery = "SELECT u.id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector
|
||||
$usersQuery = "SELECT u.id, ou.id as ope_user_id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector
|
||||
FROM ope_users_sectors ous
|
||||
JOIN users u ON ous.fk_user = u.id
|
||||
JOIN ope_users ou ON ous.fk_user = ou.id
|
||||
JOIN users u ON ou.fk_user = u.id
|
||||
WHERE ous.fk_sector = :sector_id";
|
||||
$usersStmt = $this->db->prepare($usersQuery);
|
||||
$usersStmt->execute(['sector_id' => $sectorId]);
|
||||
@@ -425,7 +509,8 @@ class SectorController
|
||||
$responseData['users_sectors'] = [];
|
||||
foreach ($usersSectors as $userSector) {
|
||||
$userData = [
|
||||
'id' => $userSector['id'],
|
||||
'user_id' => $userSector['id'],
|
||||
'ope_user_id' => $userSector['ope_user_id'],
|
||||
'first_name' => $userSector['first_name'] ?? '',
|
||||
'sect_name' => $userSector['sect_name'] ?? '',
|
||||
'fk_sector' => $userSector['fk_sector'],
|
||||
@@ -498,24 +583,27 @@ class SectorController
|
||||
try {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$entityId = $_SESSION['entity_id'] ?? null;
|
||||
|
||||
|
||||
if (!$entityId) {
|
||||
Response::json(['status' => 'error', 'message' => 'Entité non définie'], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Vérifier que le secteur appartient à l'entité
|
||||
$checkQuery = "SELECT s.id
|
||||
$checkQuery = "SELECT s.id, s.fk_operation, s.libelle
|
||||
FROM ope_sectors s
|
||||
JOIN operations o ON s.fk_operation = o.id
|
||||
WHERE s.id = :id AND o.fk_entite = :entity_id";
|
||||
$checkStmt = $this->db->prepare($checkQuery);
|
||||
$checkStmt->execute(['id' => $id, 'entity_id' => $entityId]);
|
||||
|
||||
if (!$checkStmt->fetch()) {
|
||||
|
||||
$existingSector = $checkStmt->fetch();
|
||||
if (!$existingSector) {
|
||||
Response::json(['status' => 'error', 'message' => 'Secteur non trouvé'], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$operationId = $existingSector['fk_operation'];
|
||||
|
||||
$this->db->beginTransaction();
|
||||
|
||||
@@ -580,8 +668,8 @@ class SectorController
|
||||
|
||||
// Ajouter les nouvelles affectations
|
||||
if (!empty($data['users'])) {
|
||||
$insertQuery = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, fk_user_creat, chk_active)
|
||||
VALUES (:operation_id, :user_id, :sector_id, NOW(), :user_creat, 1)";
|
||||
$insertQuery = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, updated_at, fk_user_creat, chk_active)
|
||||
VALUES (:operation_id, :user_id, :sector_id, NOW(), NOW(), :user_creat, 1)";
|
||||
$this->logService->info('[UPDATE USERS] SQL - Requête INSERT préparée', [
|
||||
'query' => $insertQuery
|
||||
]);
|
||||
@@ -591,9 +679,27 @@ class SectorController
|
||||
$failedUsers = [];
|
||||
foreach ($data['users'] as $memberId) {
|
||||
try {
|
||||
// $memberId est DÉJÀ ope_users.id (envoyé par Flutter)
|
||||
// Vérifier que cet ope_users.id existe et appartient bien à l'opération
|
||||
$stmtOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE id = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtOpeUser->execute([$memberId, $operationId]);
|
||||
$opeUserId = $stmtOpeUser->fetchColumn();
|
||||
|
||||
if (!$opeUserId) {
|
||||
$this->logService->warning('[UPDATE USERS] ope_users.id non trouvé pour cette opération', [
|
||||
'ope_users_id' => $memberId,
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
$failedUsers[] = $memberId;
|
||||
continue;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'operation_id' => $operationId,
|
||||
'user_id' => $memberId,
|
||||
'user_id' => $opeUserId,
|
||||
'sector_id' => $id,
|
||||
'user_creat' => $_SESSION['user_id'] ?? null
|
||||
];
|
||||
@@ -626,14 +732,25 @@ class SectorController
|
||||
}
|
||||
}
|
||||
|
||||
// Gérer les passages si le secteur a changé
|
||||
// Gérer les passages si le secteur a changé ET si chk_adresses_change = 1
|
||||
$passageCounters = [
|
||||
'passages_orphaned' => 0,
|
||||
'passages_updated' => 0,
|
||||
'passages_created' => 0,
|
||||
'passages_kept' => 0
|
||||
];
|
||||
if (isset($data['sector'])) {
|
||||
|
||||
// chk_adresses_change : 0=ne pas toucher aux adresses/passages, 1=recalculer (défaut)
|
||||
$chkAdressesChange = $data['chk_adresses_change'] ?? 1;
|
||||
|
||||
if (isset($data['sector']) && $chkAdressesChange == 0) {
|
||||
$this->logService->info('[UPDATE] Modification secteur sans recalcul adresses/passages', [
|
||||
'sector_id' => $id,
|
||||
'chk_adresses_change' => $chkAdressesChange
|
||||
]);
|
||||
}
|
||||
|
||||
if (isset($data['sector']) && $chkAdressesChange == 1) {
|
||||
// Mettre à jour les adresses du secteur AVANT de traiter les passages
|
||||
try {
|
||||
// Supprimer les anciennes adresses
|
||||
@@ -660,17 +777,25 @@ class SectorController
|
||||
]);
|
||||
|
||||
$addresses = $this->addressService->getAddressesInPolygon($coordinates, $entityId);
|
||||
|
||||
|
||||
// Enrichir les adresses avec les données bâtiments
|
||||
$addresses = $this->addressService->enrichAddressesWithBuildings($addresses, $entityId);
|
||||
|
||||
$this->logService->info('[UPDATE] Adresses récupérées', [
|
||||
'sector_id' => $id,
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
|
||||
|
||||
if (!empty($addresses)) {
|
||||
$queryAddress = "INSERT INTO sectors_adresses (fk_sector, fk_adresse, numero, rue, cp, ville, gps_lat, gps_lng)
|
||||
VALUES (:sector_id, :address_id, :numero, :rue, :cp, :ville, :gps_lat, :gps_lng)";
|
||||
$queryAddress = "INSERT INTO sectors_adresses (
|
||||
fk_sector, fk_adresse, numero, rue, cp, ville, gps_lat, gps_lng,
|
||||
fk_batiment, fk_habitat, nb_niveau, nb_log, residence, alt_sol
|
||||
) VALUES (
|
||||
:sector_id, :address_id, :numero, :rue, :cp, :ville, :gps_lat, :gps_lng,
|
||||
:fk_batiment, :fk_habitat, :nb_niveau, :nb_log, :residence, :alt_sol
|
||||
)";
|
||||
$stmtAddress = $this->db->prepare($queryAddress);
|
||||
|
||||
|
||||
foreach ($addresses as $address) {
|
||||
$stmtAddress->execute([
|
||||
'sector_id' => $id,
|
||||
@@ -680,7 +805,13 @@ class SectorController
|
||||
'cp' => $address['code_postal'],
|
||||
'ville' => $address['commune'],
|
||||
'gps_lat' => $address['latitude'],
|
||||
'gps_lng' => $address['longitude']
|
||||
'gps_lng' => $address['longitude'],
|
||||
'fk_batiment' => $address['fk_batiment'] ?? null,
|
||||
'fk_habitat' => $address['fk_habitat'] ?? 1,
|
||||
'nb_niveau' => $address['nb_niveau'] ?? null,
|
||||
'nb_log' => $address['nb_log'] ?? null,
|
||||
'residence' => $address['residence'] ?? '',
|
||||
'alt_sol' => $address['alt_sol'] ?? null
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -715,10 +846,29 @@ class SectorController
|
||||
|
||||
// Commit des modifications (users et/ou secteur)
|
||||
$this->db->commit();
|
||||
|
||||
|
||||
// Log de mise à jour du secteur
|
||||
$changes = [];
|
||||
if (isset($data['libelle'])) {
|
||||
$changes['libelle'] = ['new' => $data['libelle']];
|
||||
}
|
||||
if (isset($data['color'])) {
|
||||
$changes['color'] = ['new' => $data['color']];
|
||||
}
|
||||
if (isset($data['sector'])) {
|
||||
$changes['sector'] = true; // Polygon modifié
|
||||
}
|
||||
if (isset($data['users'])) {
|
||||
$changes['users'] = true; // Affectation modifiée
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logSectorUpdated((int)$id, (int)$operationId, $changes);
|
||||
}
|
||||
|
||||
// Récupérer le secteur mis à jour
|
||||
$query = "
|
||||
SELECT
|
||||
SELECT
|
||||
s.id,
|
||||
s.libelle,
|
||||
s.color,
|
||||
@@ -726,57 +876,61 @@ class SectorController
|
||||
FROM ope_sectors s
|
||||
WHERE s.id = :id
|
||||
";
|
||||
|
||||
|
||||
$stmt = $this->db->prepare($query);
|
||||
$stmt->execute(['id' => $id]);
|
||||
$sector = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
// Récupérer tous les passages du secteur
|
||||
$passagesQuery = "SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at,
|
||||
numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email,
|
||||
encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
FROM ope_pass
|
||||
WHERE fk_sector = :sector_id
|
||||
ORDER BY id";
|
||||
$passagesStmt = $this->db->prepare($passagesQuery);
|
||||
$passagesStmt->execute(['sector_id' => $id]);
|
||||
$passages = $passagesStmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
// Déchiffrer les données sensibles des passages
|
||||
|
||||
// Récupérer les passages UNIQUEMENT si chk_adresses_change = 1
|
||||
$passagesDecrypted = [];
|
||||
foreach ($passages as $passage) {
|
||||
// Déchiffrement du nom
|
||||
$passage['name'] = '';
|
||||
if (!empty($passage['encrypted_name'])) {
|
||||
$passage['name'] = ApiService::decryptData($passage['encrypted_name']);
|
||||
}
|
||||
unset($passage['encrypted_name']);
|
||||
|
||||
// Déchiffrement de l'email
|
||||
$passage['email'] = '';
|
||||
if (!empty($passage['encrypted_email'])) {
|
||||
$decryptedEmail = ApiService::decryptSearchableData($passage['encrypted_email']);
|
||||
if ($decryptedEmail) {
|
||||
$passage['email'] = $decryptedEmail;
|
||||
if ($chkAdressesChange == 1) {
|
||||
// Récupérer tous les passages du secteur
|
||||
$passagesQuery = "SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at,
|
||||
numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email,
|
||||
encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
FROM ope_pass
|
||||
WHERE fk_sector = :sector_id
|
||||
ORDER BY id";
|
||||
$passagesStmt = $this->db->prepare($passagesQuery);
|
||||
$passagesStmt->execute(['sector_id' => $id]);
|
||||
$passages = $passagesStmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
// Déchiffrer les données sensibles des passages
|
||||
foreach ($passages as $passage) {
|
||||
// Déchiffrement du nom
|
||||
$passage['name'] = '';
|
||||
if (!empty($passage['encrypted_name'])) {
|
||||
$passage['name'] = ApiService::decryptData($passage['encrypted_name']);
|
||||
}
|
||||
unset($passage['encrypted_name']);
|
||||
|
||||
// Déchiffrement de l'email
|
||||
$passage['email'] = '';
|
||||
if (!empty($passage['encrypted_email'])) {
|
||||
$decryptedEmail = ApiService::decryptSearchableData($passage['encrypted_email']);
|
||||
if ($decryptedEmail) {
|
||||
$passage['email'] = $decryptedEmail;
|
||||
}
|
||||
}
|
||||
unset($passage['encrypted_email']);
|
||||
|
||||
// Déchiffrement du téléphone
|
||||
$passage['phone'] = '';
|
||||
if (!empty($passage['encrypted_phone'])) {
|
||||
$passage['phone'] = ApiService::decryptData($passage['encrypted_phone']);
|
||||
}
|
||||
unset($passage['encrypted_phone']);
|
||||
|
||||
$passagesDecrypted[] = $passage;
|
||||
}
|
||||
unset($passage['encrypted_email']);
|
||||
|
||||
// Déchiffrement du téléphone
|
||||
$passage['phone'] = '';
|
||||
if (!empty($passage['encrypted_phone'])) {
|
||||
$passage['phone'] = ApiService::decryptData($passage['encrypted_phone']);
|
||||
}
|
||||
unset($passage['encrypted_phone']);
|
||||
|
||||
$passagesDecrypted[] = $passage;
|
||||
}
|
||||
|
||||
// Récupérer les users affectés (avec READ UNCOMMITTED pour forcer la lecture des données fraîches)
|
||||
$usersQuery = "SELECT u.id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector
|
||||
$usersQuery = "SELECT u.id, ou.id as ope_user_id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector
|
||||
FROM ope_users_sectors ous
|
||||
JOIN users u ON ous.fk_user = u.id
|
||||
JOIN ope_users ou ON ous.fk_user = ou.id
|
||||
JOIN users u ON ou.fk_user = u.id
|
||||
WHERE ous.fk_sector = :sector_id
|
||||
ORDER BY u.id";
|
||||
|
||||
@@ -801,7 +955,8 @@ class SectorController
|
||||
$usersDecrypted = [];
|
||||
foreach ($usersSectors as $userSector) {
|
||||
$userData = [
|
||||
'id' => $userSector['id'],
|
||||
'user_id' => $userSector['id'],
|
||||
'ope_user_id' => $userSector['ope_user_id'],
|
||||
'first_name' => $userSector['first_name'] ?? '',
|
||||
'sect_name' => $userSector['sect_name'] ?? '',
|
||||
'fk_sector' => $userSector['fk_sector'],
|
||||
@@ -934,18 +1089,20 @@ class SectorController
|
||||
}
|
||||
|
||||
// Vérifier que le secteur existe et récupérer ses informations
|
||||
$checkQuery = "SELECT s.id, s.libelle, o.fk_entite
|
||||
$checkQuery = "SELECT s.id, s.libelle, s.fk_operation, o.fk_entite
|
||||
FROM ope_sectors s
|
||||
JOIN operations o ON s.fk_operation = o.id
|
||||
WHERE s.id = :id";
|
||||
$checkStmt = $this->db->prepare($checkQuery);
|
||||
$checkStmt->execute(['id' => $id]);
|
||||
$sector = $checkStmt->fetch();
|
||||
|
||||
|
||||
if (!$sector || $sector['fk_entite'] != $entityId) {
|
||||
Response::json(['status' => 'error', 'message' => 'Secteur non trouvé ou non autorisé'], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$operationId = $sector['fk_operation'];
|
||||
|
||||
$this->db->beginTransaction();
|
||||
|
||||
@@ -1001,9 +1158,16 @@ class SectorController
|
||||
$deleteQuery = "DELETE FROM ope_sectors WHERE id = :id";
|
||||
$deleteStmt = $this->db->prepare($deleteQuery);
|
||||
$deleteStmt->execute(['id' => $id]);
|
||||
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
|
||||
// Log de suppression du secteur (suppression physique = false)
|
||||
EventLogService::logSectorDeleted(
|
||||
(int)$id,
|
||||
(int)$operationId,
|
||||
false // suppression physique (DELETE)
|
||||
);
|
||||
|
||||
// Déchiffrer les données sensibles des passages
|
||||
$passagesDecrypted = [];
|
||||
foreach ($passagesToUpdate as $passage) {
|
||||
@@ -1249,8 +1413,11 @@ class SectorController
|
||||
}
|
||||
|
||||
// 2. CRÉATION/MISE À JOUR DES PASSAGES POUR LES NOUVELLES ADRESSES (OPTIMISÉE)
|
||||
// Récupérer toutes les adresses du secteur depuis sectors_adresses
|
||||
$addressesQuery = "SELECT * FROM sectors_adresses WHERE fk_sector = :sector_id";
|
||||
// Récupérer toutes les adresses du secteur depuis sectors_adresses (avec colonnes bâtiments)
|
||||
$addressesQuery = "SELECT
|
||||
fk_sector, fk_adresse, numero, rue, rue_bis, cp, ville, gps_lat, gps_lng,
|
||||
fk_batiment, fk_habitat, nb_niveau, nb_log, residence, alt_sol
|
||||
FROM sectors_adresses WHERE fk_sector = :sector_id";
|
||||
$addressesStmt = $this->db->prepare($addressesQuery);
|
||||
$addressesStmt->execute(['sector_id' => $sectorId]);
|
||||
$addresses = $addressesStmt->fetchAll();
|
||||
@@ -1268,93 +1435,121 @@ class SectorController
|
||||
$firstUserId = $firstUser ? $firstUser['fk_user'] : null;
|
||||
|
||||
if ($firstUserId && !empty($addresses)) {
|
||||
$this->logService->info('[updatePassagesForSector] Optimisation passages', [
|
||||
$this->logService->info('[updatePassagesForSector] Traitement des passages', [
|
||||
'user_id' => $firstUserId,
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
|
||||
// OPTIMISATION : Récupérer TOUS les passages existants en UNE requête
|
||||
$addressIds = array_filter(array_column($addresses, 'fk_adresse'));
|
||||
|
||||
// Construire la requête pour récupérer tous les passages existants
|
||||
// Récupérer TOUS les passages existants pour cette opération en UNE requête
|
||||
$existingQuery = "
|
||||
SELECT id, fk_adresse, numero, rue, rue_bis, ville
|
||||
SELECT id, fk_adresse, numero, rue, rue_bis, ville, residence, appt, fk_habitat,
|
||||
fk_type, encrypted_name, created_at
|
||||
FROM ope_pass
|
||||
WHERE fk_operation = :operation_id
|
||||
AND (";
|
||||
|
||||
$params = ['operation_id' => $operationId];
|
||||
$conditions = [];
|
||||
|
||||
// Condition pour les fk_adresse
|
||||
if (!empty($addressIds)) {
|
||||
$placeholders = [];
|
||||
foreach ($addressIds as $idx => $addrId) {
|
||||
$key = 'addr_' . $idx;
|
||||
$placeholders[] = ':' . $key;
|
||||
$params[$key] = $addrId;
|
||||
}
|
||||
$conditions[] = "fk_adresse IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
// Condition pour les données d'adresse (numero, rue, ville)
|
||||
$addressConditions = [];
|
||||
foreach ($addresses as $idx => $addr) {
|
||||
$numKey = 'num_' . $idx;
|
||||
$rueKey = 'rue_' . $idx;
|
||||
$bisKey = 'bis_' . $idx;
|
||||
$villeKey = 'ville_' . $idx;
|
||||
|
||||
$addressConditions[] = "(numero = :$numKey AND rue = :$rueKey AND rue_bis = :$bisKey AND ville = :$villeKey)";
|
||||
$params[$numKey] = $addr['numero'];
|
||||
$params[$rueKey] = $addr['rue'];
|
||||
$params[$bisKey] = $addr['rue_bis'];
|
||||
$params[$villeKey] = $addr['ville'];
|
||||
}
|
||||
|
||||
if (!empty($addressConditions)) {
|
||||
$conditions[] = "(" . implode(' OR ', $addressConditions) . ")";
|
||||
}
|
||||
|
||||
$existingQuery .= implode(' OR ', $conditions) . ")";
|
||||
WHERE fk_operation = :operation_id";
|
||||
|
||||
$existingStmt = $this->db->prepare($existingQuery);
|
||||
$existingStmt->execute($params);
|
||||
$existingStmt->execute(['operation_id' => $operationId]);
|
||||
$existingPassages = $existingStmt->fetchAll();
|
||||
|
||||
// Indexer les passages existants pour recherche rapide
|
||||
// Indexer les passages existants par clé : numero|rue|rue_bis|ville
|
||||
$passagesByAddress = [];
|
||||
$passagesByData = [];
|
||||
foreach ($existingPassages as $p) {
|
||||
if (!empty($p['fk_adresse'])) {
|
||||
$passagesByAddress[$p['fk_adresse']] = $p;
|
||||
$addressKey = $p['numero'] . '|' . $p['rue'] . '|' . ($p['rue_bis'] ?? '') . '|' . $p['ville'];
|
||||
if (!isset($passagesByAddress[$addressKey])) {
|
||||
$passagesByAddress[$addressKey] = [];
|
||||
}
|
||||
$dataKey = $p['numero'] . '|' . $p['rue'] . '|' . $p['rue_bis'] . '|' . $p['ville'];
|
||||
$passagesByData[$dataKey] = $p;
|
||||
$passagesByAddress[$addressKey][] = $p;
|
||||
}
|
||||
|
||||
// Préparer les listes pour batch insert/update
|
||||
// Traiter chaque adresse du secteur
|
||||
$toInsert = [];
|
||||
$toUpdate = [];
|
||||
$toDelete = [];
|
||||
|
||||
foreach ($addresses as $address) {
|
||||
// Vérification en mémoire PHP (0 requête)
|
||||
if (!empty($address['fk_adresse']) && isset($passagesByAddress[$address['fk_adresse']])) {
|
||||
continue; // Déjà existant avec bon fk_adresse
|
||||
}
|
||||
$addressKey = $address['numero'] . '|' . $address['rue'] . '|' . ($address['rue_bis'] ?? '') . '|' . $address['ville'];
|
||||
$existingAtAddress = $passagesByAddress[$addressKey] ?? [];
|
||||
$nbExisting = count($existingAtAddress);
|
||||
|
||||
$dataKey = $address['numero'] . '|' . $address['rue'] . '|' . $address['rue_bis'] . '|' . $address['ville'];
|
||||
if (isset($passagesByData[$dataKey])) {
|
||||
// Passage existant mais sans fk_adresse ou avec fk_adresse différent
|
||||
if (!empty($address['fk_adresse']) && $passagesByData[$dataKey]['fk_adresse'] != $address['fk_adresse']) {
|
||||
$toUpdate[] = [
|
||||
'id' => $passagesByData[$dataKey]['id'],
|
||||
'fk_adresse' => $address['fk_adresse']
|
||||
$fkHabitat = $address['fk_habitat'] ?? 1;
|
||||
$nbLog = ($fkHabitat == 2 && isset($address['nb_log'])) ? (int)$address['nb_log'] : 1;
|
||||
$residence = $address['residence'] ?? '';
|
||||
|
||||
// IMPORTANT : Uniformisation GPS pour les immeubles
|
||||
// Tous les passages d'une même adresse doivent partager les mêmes coordonnées GPS
|
||||
// Issues de sectors_adresses (gps_lat, gps_lng)
|
||||
$gpsLat = $address['gps_lat'];
|
||||
$gpsLng = $address['gps_lng'];
|
||||
|
||||
// CAS 1 : Maison individuelle (fk_habitat=1)
|
||||
if ($fkHabitat == 1) {
|
||||
if ($nbExisting == 0) {
|
||||
// INSERT 1 passage
|
||||
$toInsert[] = [
|
||||
'address' => $address,
|
||||
'residence' => '',
|
||||
'appt' => '',
|
||||
'fk_habitat' => 1
|
||||
];
|
||||
} else {
|
||||
// UPDATE le premier passage avec fk_habitat=1
|
||||
$toUpdate[] = [
|
||||
'id' => $existingAtAddress[0]['id'],
|
||||
'fk_habitat' => 1,
|
||||
'residence' => '',
|
||||
'gps_lat' => $gpsLat,
|
||||
'gps_lng' => $gpsLng
|
||||
];
|
||||
// Les autres passages (si >1) ne sont PAS touchés
|
||||
}
|
||||
}
|
||||
// CAS 2 : Immeuble (fk_habitat=2)
|
||||
else if ($fkHabitat == 2) {
|
||||
// UPDATE TOUS les passages existants avec fk_habitat=2, residence et GPS
|
||||
foreach ($existingAtAddress as $existing) {
|
||||
$updates = [
|
||||
'id' => $existing['id'],
|
||||
'fk_habitat' => 2,
|
||||
'gps_lat' => $gpsLat,
|
||||
'gps_lng' => $gpsLng
|
||||
];
|
||||
// Update residence seulement si non vide
|
||||
if (!empty($residence)) {
|
||||
$updates['residence'] = $residence;
|
||||
}
|
||||
$toUpdate[] = $updates;
|
||||
}
|
||||
|
||||
// Si moins de nb_log passages : INSERT les manquants
|
||||
if ($nbExisting < $nbLog) {
|
||||
$nbToInsert = $nbLog - $nbExisting;
|
||||
for ($i = 0; $i < $nbToInsert; $i++) {
|
||||
$toInsert[] = [
|
||||
'address' => $address,
|
||||
'residence' => $residence,
|
||||
'appt' => '', // Pas de numéro d'appt prédéfini
|
||||
'fk_habitat' => 2
|
||||
];
|
||||
}
|
||||
}
|
||||
// Si plus de nb_log passages : DELETE les non visités en trop
|
||||
else if ($nbExisting > $nbLog) {
|
||||
$nbToDelete = $nbExisting - $nbLog;
|
||||
// Trier les passages par created_at ASC (les plus anciens d'abord)
|
||||
usort($existingAtAddress, function($a, $b) {
|
||||
return strtotime($a['created_at']) - strtotime($b['created_at']);
|
||||
});
|
||||
|
||||
$deleted = 0;
|
||||
foreach ($existingAtAddress as $existing) {
|
||||
if ($deleted >= $nbToDelete) break;
|
||||
// Supprimer seulement si fk_type=2 ET encrypted_name vide
|
||||
if ($existing['fk_type'] == 2 && ($existing['encrypted_name'] === '' || $existing['encrypted_name'] === null)) {
|
||||
$toDelete[] = $existing['id'];
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Nouveau passage à créer
|
||||
$toInsert[] = $address;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1364,19 +1559,24 @@ class SectorController
|
||||
$insertParams = [];
|
||||
$paramIndex = 0;
|
||||
|
||||
foreach ($toInsert as $addr) {
|
||||
foreach ($toInsert as $item) {
|
||||
$addr = $item['address'];
|
||||
$values[] = "(:op$paramIndex, :sect$paramIndex, :usr$paramIndex, :addr$paramIndex,
|
||||
:num$paramIndex, :rue$paramIndex, :bis$paramIndex, :ville$paramIndex,
|
||||
:lat$paramIndex, :lng$paramIndex, 2, '', NOW(), :creat$paramIndex, 1)";
|
||||
:res$paramIndex, :appt$paramIndex, :habitat$paramIndex,
|
||||
:lat$paramIndex, :lng$paramIndex, 2, 0, '', NOW(), :creat$paramIndex, 1)";
|
||||
|
||||
$insertParams["op$paramIndex"] = $operationId;
|
||||
$insertParams["sect$paramIndex"] = $sectorId;
|
||||
$insertParams["usr$paramIndex"] = $firstUserId;
|
||||
$insertParams["addr$paramIndex"] = $addr['fk_adresse'];
|
||||
$insertParams["addr$paramIndex"] = $addr['fk_adresse'] ?? null;
|
||||
$insertParams["num$paramIndex"] = $addr['numero'];
|
||||
$insertParams["rue$paramIndex"] = $addr['rue'];
|
||||
$insertParams["bis$paramIndex"] = $addr['rue_bis'];
|
||||
$insertParams["bis$paramIndex"] = $addr['rue_bis'] ?? '';
|
||||
$insertParams["ville$paramIndex"] = $addr['ville'];
|
||||
$insertParams["res$paramIndex"] = $item['residence'];
|
||||
$insertParams["appt$paramIndex"] = $item['appt'];
|
||||
$insertParams["habitat$paramIndex"] = $item['fk_habitat'];
|
||||
$insertParams["lat$paramIndex"] = $addr['gps_lat'];
|
||||
$insertParams["lng$paramIndex"] = $addr['gps_lng'];
|
||||
$insertParams["creat$paramIndex"] = $_SESSION['user_id'] ?? null;
|
||||
@@ -1386,7 +1586,7 @@ class SectorController
|
||||
|
||||
$insertQuery = "INSERT INTO ope_pass
|
||||
(fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis,
|
||||
ville, gps_lat, gps_lng, fk_type, encrypted_name, created_at, fk_user_creat, chk_active)
|
||||
ville, residence, appt, fk_habitat, gps_lat, gps_lng, fk_type, nb_passages, encrypted_name, created_at, fk_user_creat, chk_active)
|
||||
VALUES " . implode(',', $values);
|
||||
|
||||
try {
|
||||
@@ -1401,28 +1601,67 @@ class SectorController
|
||||
}
|
||||
}
|
||||
|
||||
// UPDATE MULTIPLE avec CASE WHEN
|
||||
// UPDATE MULTIPLE avec CASE WHEN (inclut GPS pour uniformisation)
|
||||
if (!empty($toUpdate)) {
|
||||
$updateIds = array_column($toUpdate, 'id');
|
||||
$placeholders = str_repeat('?,', count($updateIds) - 1) . '?';
|
||||
|
||||
$caseWhen = [];
|
||||
$caseWhenHabitat = [];
|
||||
$caseWhenResidence = [];
|
||||
$caseWhenGpsLat = [];
|
||||
$caseWhenGpsLng = [];
|
||||
$updateParams = [];
|
||||
|
||||
foreach ($toUpdate as $upd) {
|
||||
$caseWhen[] = "WHEN id = ? THEN ?";
|
||||
// fk_habitat est toujours présent
|
||||
$caseWhenHabitat[] = "WHEN id = ? THEN ?";
|
||||
$updateParams[] = $upd['id'];
|
||||
$updateParams[] = $upd['fk_adresse'];
|
||||
$updateParams[] = $upd['fk_habitat'];
|
||||
|
||||
// GPS : toujours présent maintenant (uniformisation)
|
||||
if (isset($upd['gps_lat']) && isset($upd['gps_lng'])) {
|
||||
$caseWhenGpsLat[] = "WHEN id = ? THEN ?";
|
||||
$updateParams[] = $upd['id'];
|
||||
$updateParams[] = $upd['gps_lat'];
|
||||
|
||||
$caseWhenGpsLng[] = "WHEN id = ? THEN ?";
|
||||
$updateParams[] = $upd['id'];
|
||||
$updateParams[] = $upd['gps_lng'];
|
||||
}
|
||||
|
||||
// residence est optionnel
|
||||
if (isset($upd['residence'])) {
|
||||
$caseWhenResidence[] = "WHEN id = ? THEN ?";
|
||||
$updateParams[] = $upd['id'];
|
||||
$updateParams[] = $upd['residence'];
|
||||
}
|
||||
}
|
||||
|
||||
$setClause = ["fk_habitat = CASE " . implode(' ', $caseWhenHabitat) . " ELSE fk_habitat END"];
|
||||
if (!empty($caseWhenGpsLat)) {
|
||||
$setClause[] = "gps_lat = CASE " . implode(' ', $caseWhenGpsLat) . " ELSE gps_lat END";
|
||||
}
|
||||
if (!empty($caseWhenGpsLng)) {
|
||||
$setClause[] = "gps_lng = CASE " . implode(' ', $caseWhenGpsLng) . " ELSE gps_lng END";
|
||||
}
|
||||
if (!empty($caseWhenResidence)) {
|
||||
$setClause[] = "residence = CASE " . implode(' ', $caseWhenResidence) . " ELSE residence END";
|
||||
}
|
||||
|
||||
$updateQuery = "UPDATE ope_pass
|
||||
SET fk_adresse = CASE " . implode(' ', $caseWhen) . " END
|
||||
SET " . implode(', ', $setClause) . "
|
||||
WHERE id IN ($placeholders)";
|
||||
|
||||
try {
|
||||
$updateStmt = $this->db->prepare($updateQuery);
|
||||
$updateStmt->execute(array_merge($updateParams, $updateIds));
|
||||
$counters['passages_updated'] = count($toUpdate);
|
||||
|
||||
// Log pour vérifier l'uniformisation GPS (surtout pour immeubles)
|
||||
$this->logService->info('[updatePassagesForSector] Passages mis à jour avec GPS uniformisés', [
|
||||
'nb_updated' => count($toUpdate),
|
||||
'sector_id' => $sectorId
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de la mise à jour multiple des passages', [
|
||||
'sector_id' => $sectorId,
|
||||
@@ -1431,6 +1670,23 @@ class SectorController
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE MULTIPLE en une seule requête
|
||||
if (!empty($toDelete)) {
|
||||
$placeholders = str_repeat('?,', count($toDelete) - 1) . '?';
|
||||
$deleteQuery = "DELETE FROM ope_pass WHERE id IN ($placeholders)";
|
||||
|
||||
try {
|
||||
$deleteStmt = $this->db->prepare($deleteQuery);
|
||||
$deleteStmt->execute($toDelete);
|
||||
$counters['passages_deleted'] += count($toDelete);
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de la suppression multiple des passages', [
|
||||
'sector_id' => $sectorId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->logService->warning('[updatePassagesForSector] Pas de création de passages', [
|
||||
'reason' => !$firstUserId ? 'Pas d\'utilisateur affecté' : 'Pas d\'adresses',
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Services\StripeService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\FileService;
|
||||
use App\Services\ApiService;
|
||||
use Session;
|
||||
use Exception;
|
||||
|
||||
@@ -77,7 +80,7 @@ class StripeController extends Controller {
|
||||
$this->requireAuth();
|
||||
|
||||
// Log du début de la requête
|
||||
\LogService::log('Début createOnboardingLink', [
|
||||
LogService::log('Début createOnboardingLink', [
|
||||
'account_id' => $accountId,
|
||||
'user_id' => Session::getUserId()
|
||||
]);
|
||||
@@ -98,7 +101,7 @@ class StripeController extends Controller {
|
||||
$returnUrl = $data['return_url'] ?? '';
|
||||
$refreshUrl = $data['refresh_url'] ?? '';
|
||||
|
||||
\LogService::log('URLs reçues', [
|
||||
LogService::log('URLs reçues', [
|
||||
'return_url' => $returnUrl,
|
||||
'refresh_url' => $refreshUrl
|
||||
]);
|
||||
@@ -110,7 +113,7 @@ class StripeController extends Controller {
|
||||
|
||||
$result = $this->stripeService->createOnboardingLink($accountId, $returnUrl, $refreshUrl);
|
||||
|
||||
\LogService::log('Résultat createOnboardingLink', [
|
||||
LogService::log('Résultat createOnboardingLink', [
|
||||
'success' => $result['success'] ?? false,
|
||||
'has_url' => isset($result['url'])
|
||||
]);
|
||||
@@ -127,7 +130,7 @@ class StripeController extends Controller {
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
\LogService::log('Erreur createOnboardingLink', [
|
||||
LogService::log('Erreur createOnboardingLink', [
|
||||
'level' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
@@ -190,7 +193,7 @@ class StripeController extends Controller {
|
||||
|
||||
// Vérifier que le passage existe et appartient à l'utilisateur
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.*, o.fk_entite
|
||||
SELECT p.*, o.fk_entite, o.id as operation_id
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
WHERE p.id = ? AND p.fk_user = ?
|
||||
@@ -210,13 +213,15 @@ class StripeController extends Controller {
|
||||
}
|
||||
|
||||
// Vérifier que le montant correspond (passage.montant est en euros, amount en centimes)
|
||||
$expectedAmount = (int)($passage['montant'] * 100);
|
||||
$expectedAmount = (int)round($passage['montant'] * 100);
|
||||
if ($amount !== $expectedAmount) {
|
||||
$this->sendError("Le montant ne correspond pas au passage (attendu: {$expectedAmount} centimes, reçu: {$amount} centimes)", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$entiteId = $passage['fk_entite'];
|
||||
$operationId = $passage['operation_id'];
|
||||
$fkUser = $passage['fk_user']; // ope_users.id
|
||||
|
||||
// Déterminer le type de paiement (Tap to Pay ou Web)
|
||||
$paymentMethodTypes = $data['payment_method_types'] ?? ['card_present'];
|
||||
@@ -230,14 +235,16 @@ class StripeController extends Controller {
|
||||
'payment_method_types' => $paymentMethodTypes,
|
||||
'capture_method' => $data['capture_method'] ?? 'automatic',
|
||||
'passage_id' => $passageId,
|
||||
'amicale_id' => $data['amicale_id'] ?? $entiteId,
|
||||
'member_id' => $data['member_id'] ?? Session::getUserId(),
|
||||
'fk_entite' => $data['amicale_id'] ?? $entiteId,
|
||||
'fk_user' => $data['member_id'] ?? $fkUser,
|
||||
'stripe_account' => $data['stripe_account'] ?? null,
|
||||
'metadata' => array_merge(
|
||||
[
|
||||
'passage_id' => (string)$passageId,
|
||||
'operation_id' => (string)$operationId,
|
||||
'amicale_id' => (string)($data['amicale_id'] ?? $entiteId),
|
||||
'member_id' => (string)($data['member_id'] ?? Session::getUserId()),
|
||||
'fk_user' => (string)$fkUser,
|
||||
'created_at' => (string)time(),
|
||||
'type' => $isTapToPay ? 'tap_to_pay' : 'web'
|
||||
],
|
||||
$data['metadata'] ?? []
|
||||
@@ -291,11 +298,12 @@ class StripeController extends Controller {
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT p.*, o.fk_entite,
|
||||
e.encrypted_name as entite_nom,
|
||||
u.first_name as user_prenom, u.sect_name as user_nom
|
||||
ou.first_name as user_prenom, u.sect_name as user_nom
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
LEFT JOIN entites e ON o.fk_entite = e.id
|
||||
LEFT JOIN users u ON p.fk_user = u.id
|
||||
LEFT JOIN ope_users ou ON p.fk_user = ou.id
|
||||
LEFT JOIN users u ON ou.fk_user = u.id
|
||||
WHERE p.stripe_payment_id = :pi_id
|
||||
");
|
||||
$stmt->execute(['pi_id' => $paymentIntentId]);
|
||||
@@ -330,7 +338,7 @@ class StripeController extends Controller {
|
||||
$entiteNom = '';
|
||||
if (!empty($passage['entite_nom'])) {
|
||||
try {
|
||||
$entiteNom = \ApiService::decryptData($passage['entite_nom']);
|
||||
$entiteNom = ApiService::decryptData($passage['entite_nom']);
|
||||
} catch (Exception $e) {
|
||||
$entiteNom = 'Entité inconnue';
|
||||
}
|
||||
@@ -400,6 +408,7 @@ class StripeController extends Controller {
|
||||
$this->sendSuccess([
|
||||
'has_account' => false,
|
||||
'account_id' => null,
|
||||
'location_id' => null,
|
||||
'charges_enabled' => false,
|
||||
'payouts_enabled' => false,
|
||||
'onboarding_completed' => false
|
||||
@@ -415,6 +424,7 @@ class StripeController extends Controller {
|
||||
$this->sendSuccess([
|
||||
'has_account' => true,
|
||||
'account_id' => $account['stripe_account_id'],
|
||||
'location_id' => $account['stripe_location_id'] ?? null,
|
||||
'charges_enabled' => false,
|
||||
'payouts_enabled' => false,
|
||||
'onboarding_completed' => false,
|
||||
@@ -440,6 +450,7 @@ class StripeController extends Controller {
|
||||
$this->sendSuccess([
|
||||
'has_account' => true,
|
||||
'account_id' => $account['stripe_account_id'],
|
||||
'location_id' => $account['stripe_location_id'] ?? null,
|
||||
'charges_enabled' => $stripeAccount->charges_enabled,
|
||||
'payouts_enabled' => $stripeAccount->payouts_enabled,
|
||||
'onboarding_completed' => $stripeAccount->details_submitted,
|
||||
@@ -529,17 +540,17 @@ class StripeController extends Controller {
|
||||
public function getPublicConfig(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
|
||||
$this->sendSuccess([
|
||||
'public_key' => $this->stripeService->getPublicKey(),
|
||||
'test_mode' => $this->stripeService->isTestMode()
|
||||
]);
|
||||
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/stripe/stats
|
||||
* Récupérer les statistiques de paiement
|
||||
@@ -613,9 +624,164 @@ class StripeController extends Controller {
|
||||
'to' => $dateTo
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/payment-links
|
||||
* Créer un Payment Link Stripe pour paiement par QR Code
|
||||
*
|
||||
* Payload:
|
||||
* {
|
||||
* "amount": 2500,
|
||||
* "currency": "eur",
|
||||
* "description": "Calendrier pompiers",
|
||||
* "passage_id": 789,
|
||||
* "metadata": {...}
|
||||
* }
|
||||
*/
|
||||
public function createPaymentLink(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
$data = $this->getJsonInput();
|
||||
|
||||
// Validation
|
||||
if (!isset($data['amount']) || !isset($data['passage_id'])) {
|
||||
$this->sendError('Montant et passage_id requis', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$amount = (int)$data['amount'];
|
||||
$passageId = (int)$data['passage_id'];
|
||||
|
||||
// Validation du montant (doit être > 0)
|
||||
if ($amount <= 0) {
|
||||
$this->sendError('Le montant doit être supérieur à 0', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que le passage appartient à l'utilisateur ou à son entité
|
||||
$userId = Session::getUserId();
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.*, o.fk_entite, ou.fk_user as ope_user_id
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
JOIN ope_users ou ON p.fk_user = ou.id
|
||||
WHERE p.id = ?
|
||||
');
|
||||
$stmt->execute([$passageId]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if (!$passage) {
|
||||
$this->sendError('Passage non trouvé', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier les droits : soit l'utilisateur est le créateur du passage, soit il appartient à la même entité
|
||||
$userEntityId = Session::getEntityId();
|
||||
if ($passage['ope_user_id'] != $userId && $passage['fk_entite'] != $userEntityId) {
|
||||
$this->sendError('Passage non autorisé', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier qu'il n'y a pas déjà un paiement ou un payment link pour ce passage
|
||||
if (!empty($passage['stripe_payment_id'])) {
|
||||
$this->sendError('Un paiement Stripe existe déjà pour ce passage', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($passage['stripe_payment_link_id'])) {
|
||||
$this->sendError('Un Payment Link existe déjà pour ce passage', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que le montant correspond (passage.montant est en euros, amount en centimes)
|
||||
$expectedAmount = (int)round($passage['montant'] * 100);
|
||||
if ($amount !== $expectedAmount) {
|
||||
$this->sendError("Le montant ne correspond pas au passage (attendu: {$expectedAmount} centimes, reçu: {$amount} centimes)", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Préparer les paramètres
|
||||
$params = [
|
||||
'amount' => $amount,
|
||||
'currency' => $data['currency'] ?? 'eur',
|
||||
'description' => $data['description'] ?? 'Calendrier pompiers',
|
||||
'passage_id' => $passageId,
|
||||
'metadata' => $data['metadata'] ?? []
|
||||
];
|
||||
|
||||
// Créer le Payment Link
|
||||
$result = $this->stripeService->createPaymentLink($params);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->sendSuccess([
|
||||
'payment_link_id' => $result['payment_link_id'],
|
||||
'url' => $result['url'],
|
||||
'amount' => $result['amount'],
|
||||
'passage_id' => $passageId,
|
||||
'type' => 'qr_code'
|
||||
]);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/locations
|
||||
* Créer une Location Stripe Terminal pour une entité (nécessaire pour Tap to Pay)
|
||||
*/
|
||||
public function createLocation(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
// Vérifier le rôle de l'utilisateur
|
||||
$userId = Session::getUserId();
|
||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$result = $stmt->fetch();
|
||||
$userRole = $result ? (int)$result['fk_role'] : 0;
|
||||
|
||||
if ($userRole < 2) {
|
||||
$this->sendError('Droits insuffisants - Admin amicale minimum requis', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $this->getJsonInput();
|
||||
$entiteId = $data['fk_entite'] ?? Session::getEntityId();
|
||||
|
||||
if (!$entiteId) {
|
||||
$this->sendError('ID entité requis', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier les droits sur cette entité
|
||||
if (Session::getEntityId() != $entiteId && $userRole < 3) {
|
||||
$this->sendError('Non autorisé pour cette entité', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->stripeService->createLocation($entiteId);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->sendSuccess([
|
||||
'location_id' => $result['location_id'],
|
||||
'message' => $result['message']
|
||||
]);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur lors de la création de la location: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,8 @@ class StripeWebhookController extends Controller {
|
||||
}
|
||||
|
||||
// Récupérer le secret webhook selon le mode
|
||||
$stripeConfig = $this->config->get('stripe');
|
||||
$webhookSecret = $this->stripeService->isTestMode()
|
||||
$stripeConfig = $this->config->getStripeConfig();
|
||||
$webhookSecret = $this->stripeService->isTestMode()
|
||||
? $stripeConfig['webhook_secret_test']
|
||||
: $stripeConfig['webhook_secret_live'];
|
||||
|
||||
@@ -95,31 +95,35 @@ class StripeWebhookController extends Controller {
|
||||
case 'account.updated':
|
||||
$this->handleAccountUpdated($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'account.application.authorized':
|
||||
$this->handleAccountAuthorized($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'payment_intent.succeeded':
|
||||
$this->handlePaymentIntentSucceeded($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'payment_intent.payment_failed':
|
||||
$this->handlePaymentIntentFailed($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'checkout.session.completed':
|
||||
$this->handleCheckoutSessionCompleted($event->data->object);
|
||||
break;
|
||||
|
||||
case 'charge.dispute.created':
|
||||
$this->handleChargeDisputeCreated($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'terminal.reader.action_succeeded':
|
||||
$this->handleTerminalReaderActionSucceeded($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'terminal.reader.action_failed':
|
||||
$this->handleTerminalReaderActionFailed($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
// Événement non géré mais valide
|
||||
error_log("Unhandled Stripe event type: {$event->type}");
|
||||
@@ -278,7 +282,60 @@ class StripeWebhookController extends Controller {
|
||||
|
||||
error_log("Payment failed: {$paymentIntent->id}, reason: " . json_encode($paymentIntent->last_payment_error));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gérer la complétion d'une session de paiement (Payment Link / Checkout)
|
||||
*/
|
||||
private function handleCheckoutSessionCompleted($session): void {
|
||||
$metadata = $session->metadata;
|
||||
|
||||
// Logger l'événement
|
||||
error_log("Checkout session completed: {$session->id}, payment_intent: {$session->payment_intent}");
|
||||
|
||||
// Vérifier si un passage_id est présent dans les metadata
|
||||
if (isset($metadata->passage_id) && !empty($metadata->passage_id)) {
|
||||
$passageId = (int)$metadata->passage_id;
|
||||
|
||||
// Mettre à jour le passage avec le stripe_payment_id
|
||||
$stmt = $this->db->prepare("
|
||||
UPDATE ope_pass
|
||||
SET stripe_payment_id = :payment_intent_id,
|
||||
updated_at = NOW()
|
||||
WHERE id = :passage_id
|
||||
");
|
||||
$stmt->execute([
|
||||
'payment_intent_id' => $session->payment_intent,
|
||||
'passage_id' => $passageId
|
||||
]);
|
||||
|
||||
// Vérifier si la mise à jour a réussi
|
||||
if ($stmt->rowCount() > 0) {
|
||||
error_log("Passage {$passageId} updated with payment_intent {$session->payment_intent}");
|
||||
|
||||
// TODO: Envoyer un email de confirmation avec le reçu fiscal
|
||||
// TODO: Mettre à jour les statistiques en temps réel
|
||||
} else {
|
||||
error_log("Warning: Passage {$passageId} not found or already updated");
|
||||
}
|
||||
} else {
|
||||
error_log("Warning: checkout.session.completed without passage_id in metadata");
|
||||
}
|
||||
|
||||
// Enregistrer l'historique de la session dans stripe_payment_history si nécessaire
|
||||
if (isset($metadata->passage_id)) {
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT id FROM ope_pass WHERE id = :passage_id
|
||||
");
|
||||
$stmt->execute(['passage_id' => $metadata->passage_id]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if ($passage) {
|
||||
// Log dans l'historique
|
||||
error_log("Checkout session completed for passage {$metadata->passage_id}: amount={$session->amount_total}, currency={$session->currency}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gérer un litige (chargeback)
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/PasswordSecurityService.php';
|
||||
|
||||
@@ -15,8 +16,9 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\PasswordSecurityService;
|
||||
|
||||
class UserController {
|
||||
@@ -529,16 +531,13 @@ class UserController {
|
||||
]);
|
||||
}
|
||||
|
||||
LogService::log('Utilisateur GeoSector créé', [
|
||||
'level' => 'info',
|
||||
'createdBy' => $currentUserId,
|
||||
'newUserId' => $userId,
|
||||
'email' => !empty($email) ? $email : 'non fourni',
|
||||
'username' => $username,
|
||||
'usernameManual' => $chkUsernameManuel === 1 ? 'oui' : 'non',
|
||||
'passwordManual' => $chkMdpManuel === 1 ? 'oui' : 'non',
|
||||
'emailsSent' => !empty($email) ? '2 emails (username + password)' : 'aucun (pas d\'email)'
|
||||
]);
|
||||
// Log de création utilisateur
|
||||
EventLogService::logUserCreated(
|
||||
(int)$userId,
|
||||
(int)$entiteId,
|
||||
(int)$role,
|
||||
$username
|
||||
);
|
||||
|
||||
// Préparer la réponse avec les informations de connexion si générées automatiquement
|
||||
$responseData = [
|
||||
@@ -762,12 +761,23 @@ class UserController {
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Utilisateur GeoSector mis à jour', [
|
||||
'level' => 'info',
|
||||
'modifiedBy' => $currentUserId,
|
||||
'userId' => $id,
|
||||
'fields' => array_keys($data),
|
||||
]);
|
||||
// Log de mise à jour utilisateur
|
||||
$changes = [];
|
||||
$encryptedFields = ['name', 'email', 'phone', 'mobile', 'username', 'password'];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array($key, $encryptedFields)) {
|
||||
// Champs sensibles : booléen uniquement
|
||||
$changes['encrypted_' . $key] = true;
|
||||
} else {
|
||||
// Champs non sensibles : valeur
|
||||
$changes[$key] = ['new' => $value];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logUserUpdated((int)$id, $changes);
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
@@ -858,24 +868,72 @@ class UserController {
|
||||
|
||||
if ($transferTo) {
|
||||
try {
|
||||
// Transférer TOUS les passages de l'utilisateur vers l'utilisateur désigné
|
||||
$stmt3 = $this->db->prepare('
|
||||
UPDATE ope_pass
|
||||
SET fk_user = :new_user_id
|
||||
WHERE fk_user = :delete_user_id
|
||||
// Transférer les passages opération par opération
|
||||
// (car fk_user dans ope_pass pointe vers ope_users.id, pas users.id)
|
||||
|
||||
// Récupérer toutes les opérations où l'utilisateur à supprimer a des entrées dans ope_users
|
||||
$stmtOps = $this->db->prepare('
|
||||
SELECT DISTINCT fk_operation
|
||||
FROM ope_users
|
||||
WHERE fk_user = ?
|
||||
');
|
||||
$stmt3->execute([
|
||||
'new_user_id' => $transferTo,
|
||||
'delete_user_id' => $id
|
||||
]);
|
||||
|
||||
$transferredCount = $stmt3->rowCount();
|
||||
|
||||
$stmtOps->execute([$id]);
|
||||
$operations = $stmtOps->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
$totalTransferred = 0;
|
||||
|
||||
foreach ($operations as $operationId) {
|
||||
// Trouver ope_users.id de l'utilisateur à supprimer dans cette opération
|
||||
$stmtOldOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtOldOpeUser->execute([$id, $operationId]);
|
||||
$oldOpeUserId = $stmtOldOpeUser->fetchColumn();
|
||||
|
||||
if (!$oldOpeUserId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trouver ope_users.id de l'utilisateur de destination dans cette opération
|
||||
$stmtNewOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtNewOpeUser->execute([$transferTo, $operationId]);
|
||||
$newOpeUserId = $stmtNewOpeUser->fetchColumn();
|
||||
|
||||
if (!$newOpeUserId) {
|
||||
LogService::log('Impossible de transférer passages - utilisateur destination absent', [
|
||||
'level' => 'warning',
|
||||
'operation_id' => $operationId,
|
||||
'from_user' => $id,
|
||||
'to_user' => $transferTo
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transférer les passages
|
||||
$stmtTransfer = $this->db->prepare('
|
||||
UPDATE ope_pass
|
||||
SET fk_user = :new_ope_user_id
|
||||
WHERE fk_user = :old_ope_user_id AND fk_operation = :operation_id
|
||||
');
|
||||
$stmtTransfer->execute([
|
||||
'new_ope_user_id' => $newOpeUserId,
|
||||
'old_ope_user_id' => $oldOpeUserId,
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
|
||||
$totalTransferred += $stmtTransfer->rowCount();
|
||||
}
|
||||
|
||||
LogService::log('Passages transférés avant suppression utilisateur', [
|
||||
'level' => 'info',
|
||||
'from_user' => $id,
|
||||
'to_user' => $transferTo,
|
||||
'passages_transferred' => $transferredCount
|
||||
'operations_count' => count($operations),
|
||||
'passages_transferred' => $totalTransferred
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
Response::json([
|
||||
@@ -890,13 +948,10 @@ class UserController {
|
||||
// —— Suppression réelle de l'utilisateur ——
|
||||
try {
|
||||
// Supprimer les enregistrements dépendants dans ope_users
|
||||
// (CASCADE supprime automatiquement ope_users_sectors et ope_pass)
|
||||
$stmtOpeUsers = $this->db->prepare('DELETE FROM ope_users WHERE fk_user = ?');
|
||||
$stmtOpeUsers->execute([$id]);
|
||||
|
||||
// Supprimer les enregistrements dépendants dans ope_users_sectors
|
||||
$stmtOpeUsersSectors = $this->db->prepare('DELETE FROM ope_users_sectors WHERE fk_user = ?');
|
||||
$stmtOpeUsersSectors->execute([$id]);
|
||||
|
||||
$stmt = $this->db->prepare('DELETE FROM users WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
|
||||
@@ -908,12 +963,8 @@ class UserController {
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Utilisateur GeoSector supprimé', [
|
||||
'level' => 'info',
|
||||
'deletedBy' => $currentUserId,
|
||||
'userId' => $id,
|
||||
'passage_transfer' => $transferTo ? "Tous les passages transférés vers utilisateur $transferTo" : 'Aucun transfert'
|
||||
]);
|
||||
// Log de suppression utilisateur (suppression physique = false pour soft_delete)
|
||||
EventLogService::logUserDeleted((int)$id, false);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
|
||||
@@ -14,8 +14,8 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
use Exception;
|
||||
|
||||
class VilleController {
|
||||
|
||||
@@ -11,10 +11,11 @@ class Router {
|
||||
'register',
|
||||
'lostpassword',
|
||||
'log',
|
||||
'health', // Health check endpoint pour monitoring et déploiement
|
||||
'villes', // Ajout de la route villes comme endpoint public pour l'autocomplétion du code postal
|
||||
'password/check', // Vérification de la force des mots de passe (public pour l'inscription)
|
||||
'password/compromised', // Vérification si un mot de passe est compromis
|
||||
'stripe/webhook', // Webhook Stripe (doit être public pour recevoir les événements)
|
||||
'stripe/webhooks', // Webhook Stripe (doit être public pour recevoir les événements)
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
@@ -34,6 +35,9 @@ class Router {
|
||||
// Route pour les logs
|
||||
$this->post('log', ['LogController', 'index']);
|
||||
|
||||
// Route health check (monitoring et déploiement)
|
||||
$this->get('health', ['HealthController', 'check']);
|
||||
|
||||
// Routes privées utilisateurs
|
||||
// IMPORTANT: Les routes spécifiques doivent être déclarées AVANT les routes avec paramètres
|
||||
$this->post('users/check-username', ['UserController', 'checkUsername']); // Déplacé avant les routes avec :id
|
||||
@@ -131,12 +135,14 @@ class Router {
|
||||
$this->get('stripe/accounts/:entityId/status', ['StripeController', 'getAccountStatus']);
|
||||
$this->post('stripe/accounts/:accountId/onboarding-link', ['StripeController', 'createOnboardingLink']);
|
||||
|
||||
// Tap to Pay - Vérification compatibilité
|
||||
// Tap to Pay - Vérification compatibilité et configuration
|
||||
$this->post('stripe/devices/check-tap-to-pay', ['StripeController', 'checkTapToPayCapability']);
|
||||
$this->get('stripe/devices/certified-android', ['StripeController', 'getCertifiedAndroidDevices']);
|
||||
$this->post('stripe/locations', ['StripeController', 'createLocation']);
|
||||
|
||||
// Paiements
|
||||
$this->post('stripe/payments/create-intent', ['StripeController', 'createPaymentIntent']);
|
||||
$this->post('stripe/payment-links', ['StripeController', 'createPaymentLink']);
|
||||
$this->get('stripe/payments/:paymentIntentId', ['StripeController', 'getPaymentStatus']);
|
||||
|
||||
// Statistiques et configuration
|
||||
@@ -144,7 +150,21 @@ class Router {
|
||||
$this->get('stripe/config', ['StripeController', 'getPublicConfig']);
|
||||
|
||||
// Webhook (IMPORTANT: pas d'authentification requise pour les webhooks Stripe)
|
||||
$this->post('stripe/webhook', ['StripeWebhookController', 'handleWebhook']);
|
||||
$this->post('stripe/webhooks', ['StripeWebhookController', 'handleWebhook']);
|
||||
|
||||
// Routes Migration (Admin uniquement)
|
||||
$this->get('migrations/test-connections', ['MigrationController', 'testConnections']);
|
||||
$this->get('migrations/entities/available', ['MigrationController', 'getAvailableEntities']);
|
||||
$this->get('migrations/entities/:id', ['MigrationController', 'getEntityDetails']);
|
||||
$this->post('migrations/entity', ['MigrationController', 'migrateEntity']);
|
||||
$this->post('migrations/entity/step', ['MigrationController', 'migrateEntityStep']);
|
||||
$this->get('migrations/entity/:id/status', ['MigrationController', 'getMigrationStatus']);
|
||||
$this->get('migrations/entity/:id/logs', ['MigrationController', 'getMigrationLogs']);
|
||||
$this->get('migrations/entity/:id/report', ['MigrationController', 'getMigrationReport']);
|
||||
$this->get('migrations/entity/:id/compare', ['MigrationController', 'compareEntityData']);
|
||||
$this->get('migrations/entity/:id/verify', ['MigrationController', 'verifyMigration']);
|
||||
$this->delete('migrations/entity/:id', ['MigrationController', 'rollbackEntity']);
|
||||
$this->delete('migrations/entity/:id/step/:step', ['MigrationController', 'rollbackStep']);
|
||||
}
|
||||
|
||||
public function handle(): void {
|
||||
@@ -180,7 +200,6 @@ class Router {
|
||||
|
||||
// Check if endpoint is public
|
||||
if ($this->isPublicEndpoint($endpoint)) {
|
||||
error_log("Public endpoint found: $endpoint");
|
||||
$route = $this->findRoute($method, $endpoint);
|
||||
if ($route) {
|
||||
$this->executeRoute($route);
|
||||
|
||||
@@ -5,22 +5,55 @@ declare(strict_types=1);
|
||||
class Session {
|
||||
public static function start(): void {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
// Configuration d'un répertoire de sessions dédié et persistant
|
||||
$sessionPath = __DIR__ . '/../../sessions';
|
||||
if (!is_dir($sessionPath)) {
|
||||
mkdir($sessionPath, 0700, true);
|
||||
}
|
||||
ini_set('session.save_path', $sessionPath);
|
||||
|
||||
// Configuration des sessions adaptée pour les applications mobiles
|
||||
ini_set('session.use_strict_mode', '1');
|
||||
ini_set('session.cookie_httponly', '1');
|
||||
|
||||
|
||||
// Permettre les connexions non-HTTPS en développement
|
||||
$isProduction = (getenv('APP_ENV') === 'production');
|
||||
ini_set('session.cookie_secure', $isProduction ? '1' : '0');
|
||||
|
||||
|
||||
// SameSite None pour permettre les requêtes cross-origin (applications mobiles)
|
||||
ini_set('session.cookie_samesite', 'None');
|
||||
ini_set('session.gc_maxlifetime', '86400'); // 24 heures
|
||||
|
||||
// Configuration de la durée de vie des sessions : 24 heures
|
||||
$sessionLifetime = 86400; // 24 heures
|
||||
ini_set('session.gc_maxlifetime', (string)$sessionLifetime);
|
||||
ini_set('session.cookie_lifetime', (string)$sessionLifetime);
|
||||
|
||||
// Configuration du garbage collector pour qu'il ne supprime pas trop tôt
|
||||
// gc_probability / gc_divisor = probabilité d'exécution (1/100 = 1%)
|
||||
ini_set('session.gc_probability', '1');
|
||||
ini_set('session.gc_divisor', '100');
|
||||
|
||||
// Récupérer le session_id du Bearer token si présent
|
||||
self::getSessionFromBearer();
|
||||
|
||||
session_start();
|
||||
|
||||
// Log détaillé après le démarrage de la session (DEBUG)
|
||||
$logFile = __DIR__ . '/../../logs/session_' . date('Y-m-d') . '.log';
|
||||
$sessionId = session_id();
|
||||
$sessionExists = isset($_SESSION) && !empty($_SESSION);
|
||||
$sessionData = $sessionExists ? json_encode($_SESSION) : 'empty';
|
||||
$sessionFile = $sessionPath . '/sess_' . $sessionId;
|
||||
$sessionFileExists = file_exists($sessionFile);
|
||||
|
||||
$logMessage = date('Y-m-d H:i:s') . " - Session started\n";
|
||||
$logMessage .= " Session ID: $sessionId\n";
|
||||
$logMessage .= " Session path: $sessionPath\n";
|
||||
$logMessage .= " Session file exists: " . ($sessionFileExists ? 'YES' : 'NO') . "\n";
|
||||
$logMessage .= " Session data exists: " . ($sessionExists ? 'YES' : 'NO') . "\n";
|
||||
$logMessage .= " Session data: $sessionData\n";
|
||||
|
||||
file_put_contents($logFile, $logMessage, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/LogService.php';
|
||||
namespace App\Services;
|
||||
|
||||
class AddressService {
|
||||
use Database;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Service de gestion des adresses
|
||||
*
|
||||
* Ce service interroge la base de données externe 'adresses' pour récupérer
|
||||
* les adresses géographiques dans des secteurs définis.
|
||||
*/
|
||||
class AddressService
|
||||
{
|
||||
private ?PDO $addressesDb = null;
|
||||
private PDO $mainDb;
|
||||
private LogService $logService;
|
||||
|
||||
public function __construct() {
|
||||
private $logService;
|
||||
private $buildingService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->logService = new LogService();
|
||||
|
||||
try {
|
||||
$this->addressesDb = AddressesDatabase::getInstance();
|
||||
$this->addressesDb = \AddressesDatabase::getInstance();
|
||||
$this->logService->info('[AddressService] Connexion à la base d\'adresses réussie');
|
||||
} catch (\Exception $e) {
|
||||
// Si la connexion échoue, on continue sans la base d'adresses
|
||||
@@ -21,53 +39,59 @@ class AddressService {
|
||||
]);
|
||||
$this->addressesDb = null;
|
||||
}
|
||||
$this->mainDb = Database::getInstance();
|
||||
|
||||
$this->mainDb = \Database::getInstance();
|
||||
$this->buildingService = new BuildingService();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Vérifie si la connexion à la base d'adresses est active
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isConnected(): bool {
|
||||
public function isConnected(): bool
|
||||
{
|
||||
return $this->addressesDb !== null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Détermine le département de l'entité courante
|
||||
*
|
||||
*
|
||||
* @param int|null $entityId ID de l'entité
|
||||
* @return string|null Code département (ex: "22", "23")
|
||||
*/
|
||||
private function getDepartmentForEntity(?int $entityId = null): ?string {
|
||||
private function getDepartmentForEntity(?int $entityId = null): ?string
|
||||
{
|
||||
if (!$entityId) {
|
||||
$entityId = $_SESSION['entity_id'] ?? null;
|
||||
}
|
||||
|
||||
|
||||
if (!$entityId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
$query = "SELECT departement FROM entites WHERE id = :entity_id";
|
||||
$stmt = $this->mainDb->prepare($query);
|
||||
$stmt->execute(['entity_id' => $entityId]);
|
||||
$result = $stmt->fetch();
|
||||
|
||||
|
||||
return $result ? $result['departement'] : null;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Récupère toutes les adresses contenues dans un polygone défini par des coordonnées
|
||||
* Gère automatiquement les secteurs multi-départements
|
||||
*
|
||||
*
|
||||
* @param array $coordinates Array de coordonnées [[lat, lng], [lat, lng], ...]
|
||||
* @param int|null $entityId ID de l'entité (pour déterminer le département principal)
|
||||
* @return array Array des adresses trouvées
|
||||
*/
|
||||
public function getAddressesInPolygon(array $coordinates, ?int $entityId = null): array {
|
||||
public function getAddressesInPolygon(array $coordinates, ?int $entityId = null): array
|
||||
{
|
||||
// Si pas de connexion à la base d'adresses, retourner un tableau vide
|
||||
if (!$this->addressesDb) {
|
||||
$this->logService->error('[AddressService] Pas de connexion à la base d\'adresses externe', [
|
||||
@@ -75,21 +99,20 @@ class AddressService {
|
||||
]);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
$this->logService->info('[AddressService] Début recherche adresses', [
|
||||
'entity_id' => $entityId,
|
||||
'nb_coordinates' => count($coordinates)
|
||||
]);
|
||||
|
||||
|
||||
if (count($coordinates) < 3) {
|
||||
throw new InvalidArgumentException("Un polygone doit avoir au moins 3 points");
|
||||
}
|
||||
|
||||
|
||||
// D'abord, déterminer tous les départements touchés par ce secteur
|
||||
require_once __DIR__ . '/DepartmentBoundaryService.php';
|
||||
$boundaryService = new \DepartmentBoundaryService();
|
||||
$boundaryService = new DepartmentBoundaryService();
|
||||
$departmentsTouched = $boundaryService->getDepartmentsForSector($coordinates);
|
||||
|
||||
|
||||
if (empty($departmentsTouched)) {
|
||||
// Si aucun département n'est trouvé par analyse spatiale,
|
||||
// chercher d'abord dans le département de l'entité et ses limitrophes
|
||||
@@ -103,22 +126,22 @@ class AddressService {
|
||||
]);
|
||||
throw new RuntimeException("Impossible de déterminer le département");
|
||||
}
|
||||
|
||||
|
||||
// Obtenir les départements prioritaires (entité + limitrophes)
|
||||
$priorityDepts = $boundaryService->getPriorityDepartments($entityDept);
|
||||
|
||||
|
||||
// Log pour debug
|
||||
$this->logService->warning('[AddressService] Aucun département trouvé par analyse spatiale', [
|
||||
'departements_prioritaires' => implode(', ', $priorityDepts)
|
||||
]);
|
||||
|
||||
|
||||
// Utiliser les départements prioritaires pour la recherche
|
||||
$departmentsTouched = [];
|
||||
foreach ($priorityDepts as $deptCode) {
|
||||
$departmentsTouched[] = ['code_dept' => $deptCode];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Créer le polygone SQL à partir des coordonnées
|
||||
$polygonPoints = [];
|
||||
foreach ($coordinates as $coord) {
|
||||
@@ -127,22 +150,22 @@ class AddressService {
|
||||
}
|
||||
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // MySQL attend longitude latitude
|
||||
}
|
||||
|
||||
|
||||
// Fermer le polygone
|
||||
$polygonPoints[] = $polygonPoints[0];
|
||||
|
||||
|
||||
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
|
||||
|
||||
|
||||
// Collecter les adresses de tous les départements touchés
|
||||
$allAddresses = [];
|
||||
|
||||
|
||||
foreach ($departmentsTouched as $dept) {
|
||||
$deptCode = $dept['code_dept'];
|
||||
$tableName = "cp" . $deptCode;
|
||||
|
||||
|
||||
try {
|
||||
// Requête pour récupérer les adresses dans le polygone pour ce département
|
||||
$sql = "SELECT
|
||||
$sql = "SELECT
|
||||
id,
|
||||
numero,
|
||||
rue as voie,
|
||||
@@ -161,32 +184,32 @@ class AddressService {
|
||||
:dept_code as departement
|
||||
FROM `$tableName`
|
||||
WHERE ST_Contains(
|
||||
ST_GeomFromText(:polygon, 4326),
|
||||
ST_GeomFromText(:polygon, 4326),
|
||||
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8)))
|
||||
)
|
||||
AND gps_lat != ''
|
||||
AND gps_lng != ''";
|
||||
|
||||
|
||||
$stmt = $this->addressesDb->prepare($sql);
|
||||
$stmt->execute([
|
||||
'polygon' => $polygonString,
|
||||
'dept_code' => $deptCode
|
||||
]);
|
||||
|
||||
|
||||
$addresses = $stmt->fetchAll();
|
||||
|
||||
|
||||
// Ajouter les adresses à la collection globale
|
||||
foreach ($addresses as $address) {
|
||||
$allAddresses[] = $address;
|
||||
}
|
||||
|
||||
|
||||
// Log pour debug
|
||||
$this->logService->info('[AddressService] Recherche dans table', [
|
||||
'table' => $tableName,
|
||||
'departement' => $deptCode,
|
||||
'nb_adresses' => count($addresses)
|
||||
]);
|
||||
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Log l'erreur mais continue avec les autres départements
|
||||
$this->logService->error('[AddressService] Erreur SQL', [
|
||||
@@ -197,35 +220,90 @@ class AddressService {
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$this->logService->info('[AddressService] Fin recherche adresses', [
|
||||
'total_adresses' => count($allAddresses)
|
||||
]);
|
||||
return $allAddresses;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enrichit les adresses avec les données bâtiments depuis la base 'batiments'
|
||||
*
|
||||
* Pour chaque adresse trouvée, cette méthode cherche si un bâtiment existe
|
||||
* et ajoute les métadonnées (nb_log, residence, fk_habitat, etc.)
|
||||
*
|
||||
* @param array $addresses Liste d'adresses depuis getAddressesInPolygon()
|
||||
* @param int|null $entityId ID de l'entité (pour logs)
|
||||
* @return array Adresses enrichies avec données bâtiment
|
||||
*/
|
||||
public function enrichAddressesWithBuildings(array $addresses, ?int $entityId = null): array
|
||||
{
|
||||
if (empty($addresses)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->logService->info('[AddressService] Début enrichissement avec bâtiments', [
|
||||
'entity_id' => $entityId,
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
|
||||
try {
|
||||
$enrichedAddresses = $this->buildingService->enrichAddresses($addresses);
|
||||
|
||||
// Compter les immeubles vs maisons
|
||||
$nbImmeubles = 0;
|
||||
$nbMaisons = 0;
|
||||
foreach ($enrichedAddresses as $addr) {
|
||||
if (isset($addr['fk_habitat']) && $addr['fk_habitat'] == 2) {
|
||||
$nbImmeubles++;
|
||||
} else {
|
||||
$nbMaisons++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->logService->info('[AddressService] Fin enrichissement avec bâtiments', [
|
||||
'total_adresses' => count($enrichedAddresses),
|
||||
'nb_immeubles' => $nbImmeubles,
|
||||
'nb_maisons' => $nbMaisons
|
||||
]);
|
||||
|
||||
return $enrichedAddresses;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('[AddressService] Erreur lors de l\'enrichissement', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
// En cas d'erreur, retourner les adresses sans enrichissement
|
||||
return $addresses;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les adresses dans un rayon autour d'un point
|
||||
*
|
||||
*
|
||||
* @param float $latitude Latitude du centre
|
||||
* @param float $longitude Longitude du centre
|
||||
* @param float $radiusMeters Rayon en mètres
|
||||
* @param int|null $entityId ID de l'entité (pour déterminer le département)
|
||||
* @return array Array des adresses trouvées
|
||||
*/
|
||||
public function getAddressesInRadius(float $latitude, float $longitude, float $radiusMeters, ?int $entityId = null): array {
|
||||
public function getAddressesInRadius(float $latitude, float $longitude, float $radiusMeters, ?int $entityId = null): array
|
||||
{
|
||||
// Déterminer le département
|
||||
$dept = $this->getDepartmentForEntity($entityId);
|
||||
if (!$dept) {
|
||||
throw new RuntimeException("Impossible de déterminer le département de l'entité");
|
||||
}
|
||||
|
||||
|
||||
// Nom de la table selon le département
|
||||
$tableName = "cp" . $dept;
|
||||
|
||||
|
||||
try {
|
||||
// Utiliser ST_Distance_Sphere pour calculer la distance en mètres
|
||||
$sql = "SELECT
|
||||
$sql = "SELECT
|
||||
id,
|
||||
numero,
|
||||
rue as voie,
|
||||
@@ -245,45 +323,44 @@ class AddressService {
|
||||
AND gps_lat != ''
|
||||
AND gps_lng != ''
|
||||
ORDER BY distance";
|
||||
|
||||
|
||||
$point = "POINT($longitude $latitude)";
|
||||
|
||||
|
||||
$stmt = $this->addressesDb->prepare($sql);
|
||||
$stmt->execute([
|
||||
'point' => $point,
|
||||
'radius' => $radiusMeters
|
||||
]);
|
||||
|
||||
|
||||
return $stmt->fetchAll();
|
||||
} catch (PDOException $e) {
|
||||
throw new RuntimeException("Erreur lors de la récupération des adresses dans la table $tableName : " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compte le nombre d'adresses dans un polygone
|
||||
* Gère automatiquement les secteurs multi-départements
|
||||
*
|
||||
*
|
||||
* @param array $coordinates Array de coordonnées [[lat, lng], [lat, lng], ...]
|
||||
* @param int|null $entityId ID de l'entité (pour déterminer le département principal)
|
||||
* @return int Nombre d'adresses
|
||||
*/
|
||||
public function countAddressesInPolygon(array $coordinates, ?int $entityId = null): int {
|
||||
public function countAddressesInPolygon(array $coordinates, ?int $entityId = null): int
|
||||
{
|
||||
// Si pas de connexion à la base d'adresses, retourner 0
|
||||
if (!$this->addressesDb) {
|
||||
error_log("AddressService: Pas de connexion à la base d'adresses, retour de 0 adresses");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
if (count($coordinates) < 3) {
|
||||
throw new InvalidArgumentException("Un polygone doit avoir au moins 3 points");
|
||||
}
|
||||
|
||||
|
||||
// D'abord, déterminer tous les départements touchés par ce secteur
|
||||
require_once __DIR__ . '/DepartmentBoundaryService.php';
|
||||
$boundaryService = new \DepartmentBoundaryService();
|
||||
$boundaryService = new DepartmentBoundaryService();
|
||||
$departmentsTouched = $boundaryService->getDepartmentsForSector($coordinates);
|
||||
|
||||
|
||||
if (empty($departmentsTouched)) {
|
||||
// Si aucun département n'est trouvé, utiliser le département de l'entité
|
||||
$dept = $this->getDepartmentForEntity($entityId);
|
||||
@@ -292,7 +369,7 @@ class AddressService {
|
||||
}
|
||||
$departmentsTouched = [['code_dept' => $dept]];
|
||||
}
|
||||
|
||||
|
||||
// Créer le polygone SQL à partir des coordonnées
|
||||
$polygonPoints = [];
|
||||
foreach ($coordinates as $coord) {
|
||||
@@ -301,19 +378,19 @@ class AddressService {
|
||||
}
|
||||
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // MySQL attend longitude latitude
|
||||
}
|
||||
|
||||
|
||||
// Fermer le polygone
|
||||
$polygonPoints[] = $polygonPoints[0];
|
||||
|
||||
|
||||
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
|
||||
|
||||
|
||||
// Compter les adresses dans tous les départements touchés
|
||||
$totalCount = 0;
|
||||
|
||||
|
||||
foreach ($departmentsTouched as $dept) {
|
||||
$deptCode = $dept['code_dept'];
|
||||
$tableName = "cp" . $deptCode;
|
||||
|
||||
|
||||
try {
|
||||
$sql = "SELECT COUNT(*) as count
|
||||
FROM `$tableName`
|
||||
@@ -323,23 +400,20 @@ class AddressService {
|
||||
)
|
||||
AND gps_lat != ''
|
||||
AND gps_lng != ''";
|
||||
|
||||
|
||||
$stmt = $this->addressesDb->prepare($sql);
|
||||
$stmt->execute(['polygon' => $polygonString]);
|
||||
|
||||
|
||||
$result = $stmt->fetch();
|
||||
$deptCount = (int)$result['count'];
|
||||
$totalCount += $deptCount;
|
||||
|
||||
// Log pour debug
|
||||
error_log("Département $deptCode : $deptCount adresses comptées");
|
||||
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Log l'erreur mais continue avec les autres départements
|
||||
error_log("Erreur de comptage pour le département $deptCode : " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $totalCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
use App\Services\PasswordSecurityService;
|
||||
use AppConfig;
|
||||
use App\Services\LogService;
|
||||
|
||||
require_once __DIR__ . '/EmailTemplates.php';
|
||||
require_once __DIR__ . '/PasswordSecurityService.php';
|
||||
@@ -70,11 +73,21 @@ class ApiService {
|
||||
$mail->Body = EmailTemplates::getLostPasswordTemplate($name, $data['username'] ?? '', $data['password']);
|
||||
break;
|
||||
|
||||
case 'password_reset':
|
||||
$mail->Subject = 'Réinitialisation de votre mot de passe GEOSECTOR';
|
||||
$mail->Body = EmailTemplates::getLostPasswordTemplate($name, $data['username'] ?? '', $data['password'] ?? '');
|
||||
break;
|
||||
|
||||
case 'alert':
|
||||
$mail->Subject = $data['subject'] ?? 'Alerte GEOSECTOR';
|
||||
$mail->Body = EmailTemplates::getAlertTemplate($data['subject'] ?? 'Alerte', $data['message'] ?? '');
|
||||
break;
|
||||
|
||||
case 'security_alert':
|
||||
$mail->Subject = $data['subject'] ?? 'Alerte de Sécurité GEOSECTOR';
|
||||
$mail->Body = $data['body'] ?? EmailTemplates::getAlertTemplate($data['subject'] ?? 'Alerte de Sécurité', $data['message'] ?? '');
|
||||
break;
|
||||
|
||||
case 'receipt':
|
||||
$mail->Subject = 'Reçu de passage GEOSECTOR';
|
||||
$mail->Body = EmailTemplates::getReceiptTemplate(
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
require_once __DIR__ . '/../Config/AppConfig.php';
|
||||
require_once __DIR__ . '/LogService.php';
|
||||
|
||||
use Exception;
|
||||
use AppConfig;
|
||||
use App\Services\LogService;
|
||||
|
||||
/**
|
||||
* Service de chiffrement et compression des sauvegardes
|
||||
*
|
||||
*
|
||||
* Ce service gère le processus complet de sécurisation des backups JSON :
|
||||
* 1. Compression GZIP pour réduire la taille
|
||||
* 2. Chiffrement AES-256-CBC pour la sécurité
|
||||
|
||||
319
api/src/Services/BuildingService.php
Normal file
319
api/src/Services/BuildingService.php
Normal file
@@ -0,0 +1,319 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use AppConfig;
|
||||
|
||||
require_once __DIR__ . '/../Config/AppConfig.php';
|
||||
|
||||
/**
|
||||
* Service de gestion des bâtiments
|
||||
*
|
||||
* Ce service enrichit les adresses trouvées par AddressService avec les métadonnées
|
||||
* des bâtiments depuis la base de données 'batiments'.
|
||||
*
|
||||
* Fonctionnalités :
|
||||
* - Enrichissement des adresses avec données bâtiment (nb_log, residence, etc.)
|
||||
* - Identification automatique des immeubles (fk_habitat=2)
|
||||
* - Gestion des adresses sans bâtiment (maisons individuelles, fk_habitat=1)
|
||||
*/
|
||||
class BuildingService
|
||||
{
|
||||
private ?PDO $dbBuildings = null;
|
||||
private bool $connected = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->initConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise la connexion à la base de données batiments
|
||||
*/
|
||||
private function initConnection(): void
|
||||
{
|
||||
try {
|
||||
$config = AppConfig::getInstance()->getBuildingsDatabaseConfig();
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=utf8mb4',
|
||||
$config['host'],
|
||||
$config['name']
|
||||
);
|
||||
|
||||
$this->dbBuildings = new PDO(
|
||||
$dsn,
|
||||
$config['username'],
|
||||
$config['password'],
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]
|
||||
);
|
||||
|
||||
$this->connected = true;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("Erreur de connexion à la base batiments : " . $e->getMessage());
|
||||
$this->connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la connexion à la base batiments est active
|
||||
*
|
||||
* @return bool True si connecté
|
||||
*/
|
||||
public function isConnected(): bool
|
||||
{
|
||||
return $this->connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrichit une liste d'adresses avec les données bâtiment
|
||||
*
|
||||
* Pour chaque adresse trouvée par AddressService, cette méthode cherche
|
||||
* si un bâtiment existe dans bat{dept} avec le lien cle_interop_adr.
|
||||
*
|
||||
* @param array $addresses Liste d'adresses depuis AddressService
|
||||
* Format attendu : ['id' => 'cp22.12345', 'departement' => '22', ...]
|
||||
* @return array Adresses enrichies avec :
|
||||
* - fk_batiment : batiment_groupe_id (ou null)
|
||||
* - fk_habitat : 1=individuel, 2=collectif
|
||||
* - nb_niveau : Nombre d'étages (ou null)
|
||||
* - nb_log : Nombre de logements (ou null)
|
||||
* - residence : Nom de la copropriété (ou '')
|
||||
* - alt_sol : Altitude sol (ou null)
|
||||
*/
|
||||
public function enrichAddresses(array $addresses): array
|
||||
{
|
||||
// Si pas de connexion, retourner les adresses sans enrichissement
|
||||
if (!$this->isConnected() || empty($addresses)) {
|
||||
// Ajouter fk_habitat=1 par défaut (maison individuelle)
|
||||
foreach ($addresses as &$address) {
|
||||
$address['fk_batiment'] = null;
|
||||
$address['fk_habitat'] = 1;
|
||||
$address['nb_niveau'] = null;
|
||||
$address['nb_log'] = null;
|
||||
$address['residence'] = '';
|
||||
$address['alt_sol'] = null;
|
||||
}
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
try {
|
||||
// Grouper les adresses par département
|
||||
$addressesByDept = [];
|
||||
foreach ($addresses as $address) {
|
||||
$dept = $this->extractDepartmentFromAddress($address);
|
||||
if ($dept) {
|
||||
$addressesByDept[$dept][] = $address;
|
||||
}
|
||||
}
|
||||
|
||||
// Enrichir les adresses département par département
|
||||
$enrichedAddresses = [];
|
||||
|
||||
foreach ($addressesByDept as $dept => $deptAddresses) {
|
||||
$enrichedDept = $this->enrichAddressesByDepartment((string)$dept, $deptAddresses);
|
||||
$enrichedAddresses = array_merge($enrichedAddresses, $enrichedDept);
|
||||
}
|
||||
|
||||
return $enrichedAddresses;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("Erreur lors de l'enrichissement des adresses avec batiments : " . $e->getMessage());
|
||||
|
||||
// En cas d'erreur, retourner les adresses avec valeurs par défaut
|
||||
foreach ($addresses as &$address) {
|
||||
$address['fk_batiment'] = null;
|
||||
$address['fk_habitat'] = 1;
|
||||
$address['nb_niveau'] = null;
|
||||
$address['nb_log'] = null;
|
||||
$address['residence'] = '';
|
||||
$address['alt_sol'] = null;
|
||||
}
|
||||
return $addresses;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrichit les adresses d'un département spécifique
|
||||
*
|
||||
* @param string $dept Code du département (ex: '22', '35')
|
||||
* @param array $addresses Liste des adresses du département
|
||||
* @return array Adresses enrichies
|
||||
*/
|
||||
private function enrichAddressesByDepartment(string $dept, array $addresses): array
|
||||
{
|
||||
// Vérifier que la table bat{dept} existe
|
||||
if (!$this->tableExists("bat{$dept}")) {
|
||||
// Table inexistante, retourner avec valeurs par défaut
|
||||
foreach ($addresses as &$address) {
|
||||
$address['fk_batiment'] = null;
|
||||
$address['fk_habitat'] = 1;
|
||||
$address['nb_niveau'] = null;
|
||||
$address['nb_log'] = null;
|
||||
$address['residence'] = '';
|
||||
$address['alt_sol'] = null;
|
||||
}
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
// Créer un mapping address_id => address pour retrouver rapidement
|
||||
$addressMap = [];
|
||||
$addressIds = [];
|
||||
|
||||
foreach ($addresses as $address) {
|
||||
// Extraire l'ID BAN (partie après le point)
|
||||
$addressId = $this->extractAddressId($address['id']);
|
||||
if ($addressId) {
|
||||
$addressIds[] = $addressId;
|
||||
$addressMap[$addressId] = $address;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($addressIds)) {
|
||||
// Pas d'IDs valides, retourner avec valeurs par défaut
|
||||
foreach ($addresses as &$address) {
|
||||
$address['fk_batiment'] = null;
|
||||
$address['fk_habitat'] = 1;
|
||||
$address['nb_niveau'] = null;
|
||||
$address['nb_log'] = null;
|
||||
$address['residence'] = '';
|
||||
$address['alt_sol'] = null;
|
||||
}
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
// Requête pour récupérer les bâtiments
|
||||
$placeholders = str_repeat('?,', count($addressIds) - 1) . '?';
|
||||
$query = "
|
||||
SELECT
|
||||
b.cle_interop_adr,
|
||||
b.batiment_groupe_id,
|
||||
b.nb_niveau,
|
||||
b.nb_log,
|
||||
b.residence,
|
||||
b.altitude_sol_mean
|
||||
FROM bat{$dept} b
|
||||
WHERE b.cle_interop_adr IN ($placeholders)
|
||||
";
|
||||
|
||||
$stmt = $this->dbBuildings->prepare($query);
|
||||
$stmt->execute($addressIds);
|
||||
$buildings = $stmt->fetchAll();
|
||||
|
||||
// Créer un mapping cle_interop_adr => building
|
||||
$buildingMap = [];
|
||||
foreach ($buildings as $building) {
|
||||
$buildingMap[$building['cle_interop_adr']] = $building;
|
||||
}
|
||||
|
||||
// Enrichir les adresses
|
||||
$enrichedAddresses = [];
|
||||
foreach ($addresses as $address) {
|
||||
$addressId = $this->extractAddressId($address['id']);
|
||||
|
||||
if ($addressId && isset($buildingMap[$addressId])) {
|
||||
// Bâtiment trouvé : enrichir avec ses données
|
||||
$building = $buildingMap[$addressId];
|
||||
$address['fk_batiment'] = $building['batiment_groupe_id'];
|
||||
$address['fk_habitat'] = 2; // Collectif
|
||||
$address['nb_niveau'] = $building['nb_niveau'];
|
||||
$address['nb_log'] = $building['nb_log'];
|
||||
$address['residence'] = $building['residence'] ?? '';
|
||||
$address['alt_sol'] = $building['altitude_sol_mean'];
|
||||
} else {
|
||||
// Pas de bâtiment : maison individuelle
|
||||
$address['fk_batiment'] = null;
|
||||
$address['fk_habitat'] = 1; // Individuel
|
||||
$address['nb_niveau'] = null;
|
||||
$address['nb_log'] = null;
|
||||
$address['residence'] = '';
|
||||
$address['alt_sol'] = null;
|
||||
}
|
||||
|
||||
$enrichedAddresses[] = $address;
|
||||
}
|
||||
|
||||
return $enrichedAddresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait le code département depuis une adresse
|
||||
*
|
||||
* @param array $address Adresse depuis AddressService
|
||||
* @return string|null Code département (ex: '22', '35')
|
||||
*/
|
||||
private function extractDepartmentFromAddress(array $address): ?string
|
||||
{
|
||||
// Méthode 1 : Clé 'departement' directement
|
||||
if (isset($address['departement'])) {
|
||||
return $address['departement'];
|
||||
}
|
||||
|
||||
// Méthode 2 : Depuis code_postal
|
||||
if (isset($address['code_postal'])) {
|
||||
$cp = $address['code_postal'];
|
||||
if (strlen($cp) === 4) {
|
||||
return '0' . substr($cp, 0, 1);
|
||||
}
|
||||
return substr($cp, 0, 2);
|
||||
}
|
||||
|
||||
// Méthode 3 : Depuis l'ID (format cp22.12345)
|
||||
if (isset($address['id']) && strpos($address['id'], 'cp') === 0) {
|
||||
preg_match('/^cp(\d{2})\./', $address['id'], $matches);
|
||||
if (isset($matches[1])) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait l'ID BAN depuis l'ID complet (ex: cp22.12345 → 12345)
|
||||
*
|
||||
* @param string $fullId ID complet de l'adresse
|
||||
* @return string|null ID BAN
|
||||
*/
|
||||
private function extractAddressId(string $fullId): ?string
|
||||
{
|
||||
if (strpos($fullId, '.') !== false) {
|
||||
$parts = explode('.', $fullId);
|
||||
return $parts[1] ?? null;
|
||||
}
|
||||
return $fullId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une table existe dans la base batiments
|
||||
*
|
||||
* @param string $tableName Nom de la table (ex: 'bat22')
|
||||
* @return bool True si la table existe
|
||||
*/
|
||||
private function tableExists(string $tableName): bool
|
||||
{
|
||||
try {
|
||||
// SHOW TABLES LIKE ne supporte pas les placeholders PDO
|
||||
// On échappe manuellement le nom de table (alphanumérique uniquement)
|
||||
if (!preg_match('/^[a-zA-Z0-9_]+$/', $tableName)) {
|
||||
error_log("Nom de table invalide : {$tableName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $this->dbBuildings->query("SHOW TABLES LIKE '{$tableName}'");
|
||||
return $stmt && $stmt->rowCount() > 0;
|
||||
} catch (PDOException $e) {
|
||||
error_log("Erreur lors de la vérification de la table {$tableName} : " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Database;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use RuntimeException;
|
||||
|
||||
class DepartmentBoundaryService {
|
||||
private PDO $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance();
|
||||
$this->db = \Database::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class EmailTemplates {
|
||||
/**
|
||||
* Template d'email de bienvenue
|
||||
@@ -12,7 +14,7 @@ class EmailTemplates {
|
||||
Votre compte a été créé avec succès sur <b>GeoSector</b>.<br><br>
|
||||
<b>Identifiant :</b> $username<br>
|
||||
<b>Mot de passe :</b> $password<br><br>
|
||||
Vous pouvez vous connecter dès maintenant sur <a href=\"https://app.geosector.fr\">app.geosector.fr</a><br><br>
|
||||
Vous pouvez vous connecter dès maintenant sur <a href=\"https://app3.geosector.fr\">app3.geosector.fr</a><br><br>
|
||||
À très bientôt,<br>
|
||||
L'équipe GeoSector";
|
||||
}
|
||||
@@ -36,7 +38,7 @@ class EmailTemplates {
|
||||
</p>
|
||||
<p>
|
||||
Une fois que vous aurez reçu votre mot de passe, vous pourrez vous connecter sur
|
||||
<a href=\"https://app.geosector.fr\" style='color:#007bff;'>app.geosector.fr</a>
|
||||
<a href=\"https://app3.geosector.fr\" style='color:#007bff;'>app3.geosector.fr</a>
|
||||
</p>
|
||||
<br>
|
||||
À très bientôt,<br>
|
||||
@@ -59,7 +61,7 @@ class EmailTemplates {
|
||||
</p>
|
||||
<p>
|
||||
Vous pouvez maintenant vous connecter avec votre identifiant (reçu dans un email précédent)
|
||||
et ce mot de passe sur <a href=\"https://app.geosector.fr\" style='color:#007bff;'>app.geosector.fr</a>
|
||||
et ce mot de passe sur <a href=\"https://app3.geosector.fr\" style='color:#007bff;'>app3.geosector.fr</a>
|
||||
</p>
|
||||
<p style='background:#fff3cd; padding:10px; border-radius:5px; margin-top:20px;'>
|
||||
<b>Rappel :</b> Ne communiquez jamais votre mot de passe à un tiers. L'équipe GeoSector
|
||||
@@ -78,7 +80,7 @@ class EmailTemplates {
|
||||
Bonjour $name,<br><br>
|
||||
Vous avez demandé la réinitialisation de votre mot de passe sur <b>GeoSector</b>.<br><br>
|
||||
<b>Nouveau mot de passe :</b> $password<br><br>
|
||||
Vous pouvez vous connecter avec ce nouveau mot de passe sur <a href=\"https://app.geosector.fr\">app.geosector.fr</a><br><br>
|
||||
Vous pouvez vous connecter avec ce nouveau mot de passe sur <a href=\"https://app3.geosector.fr\">app3.geosector.fr</a><br><br>
|
||||
À très bientôt,<br>
|
||||
L'équipe GeoSector";
|
||||
}
|
||||
@@ -95,7 +97,8 @@ class EmailTemplates {
|
||||
}
|
||||
|
||||
/**
|
||||
* Template de reçu de passage
|
||||
* Template de reçu de passage (ancien format simple)
|
||||
* @deprecated Utiliser getReceiptDonationTemplate() pour les reçus de don
|
||||
*/
|
||||
public static function getReceiptTemplate(string $name, string $date, string $address, string $amount, string $paymentMethod): string {
|
||||
return "
|
||||
@@ -126,4 +129,69 @@ class EmailTemplates {
|
||||
<p>À bientôt,</p>
|
||||
<p>L'équipe GeoSector</p>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Template d'email pour reçu de don avec pièce jointe PDF
|
||||
* Utilisé lors de l'envoi automatique des reçus pour les dons (fk_type=1)
|
||||
*
|
||||
* @param array $data Données du reçu (passage_id, entite_name, donor_name, amount, donation_date, payment_method, entite_address, entite_email)
|
||||
* @return string HTML de l'email
|
||||
*/
|
||||
public static function getReceiptDonationTemplate(array $data): string {
|
||||
// Extraction des données avec valeurs par défaut
|
||||
$passageId = $data['passage_id'] ?? '';
|
||||
$entiteName = htmlspecialchars($data['entite_name'] ?? 'Amicale des Sapeurs-Pompiers');
|
||||
$donorName = htmlspecialchars($data['donor_name'] ?? '');
|
||||
$amount = htmlspecialchars($data['amount'] ?? '0,00');
|
||||
$donationDate = htmlspecialchars($data['donation_date'] ?? date('d/m/Y'));
|
||||
$paymentMethod = htmlspecialchars($data['payment_method'] ?? 'Espèces');
|
||||
$entiteAddress = htmlspecialchars($data['entite_address'] ?? '');
|
||||
$entiteEmail = htmlspecialchars($data['entite_email'] ?? '');
|
||||
|
||||
return "
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.passage-id { text-align: right; font-size: 10px; color: #999; margin-bottom: 20px; }
|
||||
.header { background-color: #f4f4f4; padding: 20px; text-align: center; border-radius: 5px; }
|
||||
.content { padding: 20px 0; }
|
||||
.footer { background-color: #f4f4f4; padding: 15px; text-align: center; font-size: 12px; border-radius: 5px; margin-top: 20px; }
|
||||
.amount { font-size: 24px; font-weight: bold; color: #2c5aa0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='container'>
|
||||
<div class='passage-id'>$passageId</div>
|
||||
|
||||
<div class='header'>
|
||||
<h2>$entiteName</h2>
|
||||
</div>
|
||||
|
||||
<div class='content'>
|
||||
<p>Bonjour Mme/M. $donorName,</p>
|
||||
|
||||
<p>Nous vous remercions chaleureusement pour votre don de <span class='amount'>$amount €</span> effectué le $donationDate.</p>
|
||||
|
||||
<p><strong>Mode de paiement :</strong> $paymentMethod</p>
|
||||
|
||||
<p>Vous trouverez ci-joint votre reçu.</p>
|
||||
|
||||
<p>Votre soutien est précieux pour nous permettre de poursuivre nos actions.</p>
|
||||
|
||||
<p>Cordialement,<br>L'équipe de $entiteName</p>
|
||||
</div>
|
||||
|
||||
<div class='footer'>
|
||||
<p><strong>$entiteName</strong><br>
|
||||
$entiteAddress<br>
|
||||
$entiteEmail</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
}
|
||||
}
|
||||
|
||||
533
api/src/Services/EventLogService.php
Normal file
533
api/src/Services/EventLogService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,24 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xls;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Csv;
|
||||
use PDO;
|
||||
use Database;
|
||||
use Session;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
|
||||
require_once __DIR__ . '/../Services/FileService.php';
|
||||
|
||||
|
||||
class ExportService {
|
||||
private \PDO $db;
|
||||
private PDO $db;
|
||||
private FileService $fileService;
|
||||
|
||||
public function __construct() {
|
||||
@@ -249,10 +256,11 @@ class ExportService {
|
||||
p.fk_habitat, p.appt, p.niveau, p.encrypted_name, p.encrypted_email,
|
||||
p.encrypted_phone, p.montant, p.fk_type_reglement, p.remarque,
|
||||
p.fk_user, p.fk_sector, p.fk_operation,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name, u.sect_name,
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name, u.sect_name,
|
||||
xtr.libelle as reglement_libelle
|
||||
FROM ope_pass p
|
||||
LEFT JOIN users u ON u.id = p.fk_user
|
||||
LEFT JOIN ope_users ou ON ou.id = p.fk_user
|
||||
LEFT JOIN users u ON u.id = ou.fk_user
|
||||
LEFT JOIN x_types_reglements xtr ON xtr.id = p.fk_type_reglement
|
||||
WHERE p.fk_operation = ? AND p.chk_active = 1
|
||||
';
|
||||
@@ -457,10 +465,11 @@ class ExportService {
|
||||
SELECT
|
||||
ous.id, ous.fk_sector, ous.fk_user, ous.created_at, ous.fk_operation,
|
||||
s.libelle as sector_name,
|
||||
u.encrypted_name as user_name, u.first_name
|
||||
u.encrypted_name as user_name, ou.first_name
|
||||
FROM ope_users_sectors ous
|
||||
INNER JOIN ope_sectors s ON s.id = ous.fk_sector
|
||||
INNER JOIN users u ON u.id = ous.fk_user
|
||||
INNER JOIN ope_users ou ON ou.id = ous.fk_user
|
||||
INNER JOIN users u ON u.id = ou.fk_user
|
||||
WHERE ous.fk_operation = ? AND ous.chk_active = 1
|
||||
ORDER BY s.libelle, u.encrypted_name
|
||||
';
|
||||
@@ -619,10 +628,11 @@ class ExportService {
|
||||
p.passed_at, p.fk_type, p.numero, p.rue_bis, p.rue, p.ville,
|
||||
p.fk_habitat, p.appt, p.niveau, p.encrypted_name, p.encrypted_email,
|
||||
p.encrypted_phone, p.montant, p.fk_type_reglement, p.remarque,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name, u.sect_name,
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name, u.sect_name,
|
||||
xtr.libelle as reglement_libelle
|
||||
FROM ope_pass p
|
||||
LEFT JOIN users u ON u.id = p.fk_user
|
||||
LEFT JOIN ope_users ou ON ou.id = p.fk_user
|
||||
LEFT JOIN users u ON u.id = ou.fk_user
|
||||
LEFT JOIN x_types_reglements xtr ON xtr.id = p.fk_type_reglement
|
||||
WHERE p.fk_operation = ? AND p.chk_active = 1
|
||||
';
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
use PDO;
|
||||
use Database;
|
||||
use Session;
|
||||
use App\Services\LogService;
|
||||
|
||||
class FileService {
|
||||
private const BASE_UPLOADS_DIR = '/var/www/geosector/api/uploads';
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use AppConfig;
|
||||
use ClientDetector;
|
||||
|
||||
class LogService {
|
||||
public static function log(string $message, array $metadata = []): void {
|
||||
|
||||
791
api/src/Services/MigrationService.php
Normal file
791
api/src/Services/MigrationService.php
Normal file
@@ -0,0 +1,791 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Database;
|
||||
|
||||
require_once __DIR__ . '/LogService.php';
|
||||
require_once __DIR__ . '/ApiService.php';
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Exception;
|
||||
use AppConfig;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
|
||||
class MigrationService {
|
||||
private ?PDO $sourceDb = null;
|
||||
private PDO $targetDb;
|
||||
private AppConfig $appConfig;
|
||||
private bool $sshTunnelCreated = false;
|
||||
|
||||
// Configuration SSH et base source (TODO: à déplacer dans AppConfig)
|
||||
private const SSH_HOST = '212.83.164.111';
|
||||
private const SSH_PORT = 52266;
|
||||
private const SSH_USER = 'root';
|
||||
private const SSH_KEY_FILE = '/root/.ssh/id_rsa_db2';
|
||||
private const REMOTE_DB_HOST = '127.0.0.1';
|
||||
private const REMOTE_DB_PORT = 3306;
|
||||
private const SOURCE_DB_HOST = '127.0.0.1';
|
||||
private const SOURCE_DB_NAME = 'geosector';
|
||||
private const SOURCE_DB_USER = 'geo_front_user';
|
||||
private const SOURCE_DB_PASS = 'd66,GeoFront.User';
|
||||
private const SOURCE_DB_PORT = 13306;
|
||||
|
||||
// Ordre des étapes de migration
|
||||
private const MIGRATION_STEPS = [
|
||||
'x_devises',
|
||||
'x_entites_types',
|
||||
'x_types_passages',
|
||||
'x_types_reglements',
|
||||
'x_users_roles',
|
||||
'x_pays',
|
||||
'x_regions',
|
||||
'x_departements',
|
||||
'x_villes',
|
||||
'entites',
|
||||
'users',
|
||||
'operations',
|
||||
'ope_sectors',
|
||||
'sectors_adresses',
|
||||
'ope_users',
|
||||
'ope_users_sectors',
|
||||
'ope_pass',
|
||||
'ope_pass_histo',
|
||||
'medias'
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
$this->targetDb = Database::getInstance();
|
||||
$this->appConfig = AppConfig::getInstance();
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->closeSshTunnel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un tunnel SSH vers le serveur distant
|
||||
*/
|
||||
private function createSshTunnel(): bool {
|
||||
if ($this->sshTunnelCreated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vérifier si un tunnel est déjà en cours d'exécution
|
||||
$checkCommand = "ps aux | grep 'ssh -[vf]* -N -L " . self::SOURCE_DB_PORT . ":' | grep -v grep";
|
||||
exec($checkCommand, $output, $return_var);
|
||||
|
||||
if (empty($output)) {
|
||||
// Créer le tunnel SSH
|
||||
$command = sprintf(
|
||||
"ssh -f -N -o StrictHostKeyChecking=no -L %d:%s:%d -p %d %s@%s -i %s 2>&1",
|
||||
self::SOURCE_DB_PORT,
|
||||
self::REMOTE_DB_HOST,
|
||||
self::REMOTE_DB_PORT,
|
||||
self::SSH_PORT,
|
||||
self::SSH_USER,
|
||||
self::SSH_HOST,
|
||||
self::SSH_KEY_FILE
|
||||
);
|
||||
|
||||
exec($command, $output, $return_var);
|
||||
|
||||
if ($return_var !== 0) {
|
||||
LogService::log('Erreur lors de la création du tunnel SSH', [
|
||||
'level' => 'error',
|
||||
'output' => implode("\n", $output)
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attendre que le tunnel soit établi
|
||||
sleep(2);
|
||||
|
||||
// Vérification du tunnel
|
||||
$checkTunnel = "netstat -an | grep " . self::SOURCE_DB_PORT . " | grep LISTEN";
|
||||
exec($checkTunnel, $tunnelOutput);
|
||||
|
||||
if (empty($tunnelOutput)) {
|
||||
LogService::log('Le tunnel SSH semble créé mais le port n\'est pas en écoute', [
|
||||
'level' => 'warning',
|
||||
'port' => self::SOURCE_DB_PORT
|
||||
]);
|
||||
}
|
||||
|
||||
LogService::log('Tunnel SSH établi', [
|
||||
'level' => 'info',
|
||||
'port' => self::SOURCE_DB_PORT
|
||||
]);
|
||||
} else {
|
||||
LogService::log('Un tunnel SSH est déjà en cours d\'exécution', [
|
||||
'level' => 'info'
|
||||
]);
|
||||
}
|
||||
|
||||
$this->sshTunnelCreated = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ferme le tunnel SSH
|
||||
*/
|
||||
private function closeSshTunnel(): void {
|
||||
if (!$this->sshTunnelCreated) {
|
||||
return;
|
||||
}
|
||||
|
||||
$command = "ps aux | grep 'ssh -[vf]* -N -L " . self::SOURCE_DB_PORT . ":' | grep -v grep | awk '{print $2}' | xargs -r kill 2>/dev/null";
|
||||
exec($command);
|
||||
|
||||
$this->sshTunnelCreated = false;
|
||||
$this->sourceDb = null;
|
||||
|
||||
LogService::log('Tunnel SSH fermé', ['level' => 'info']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la connexion à la base source
|
||||
*/
|
||||
private function getSourceConnection(): PDO {
|
||||
if ($this->sourceDb !== null) {
|
||||
return $this->sourceDb;
|
||||
}
|
||||
|
||||
// Établir le tunnel SSH
|
||||
if (!$this->createSshTunnel()) {
|
||||
throw new Exception("Impossible d'établir le tunnel SSH");
|
||||
}
|
||||
|
||||
// Options de connexion PDO
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
|
||||
PDO::ATTR_TIMEOUT => 600
|
||||
];
|
||||
|
||||
if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) {
|
||||
$options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Se connecter à la base spécifique
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;port=%d',
|
||||
self::SOURCE_DB_HOST,
|
||||
self::SOURCE_DB_NAME,
|
||||
self::SOURCE_DB_PORT
|
||||
);
|
||||
|
||||
$this->sourceDb = new PDO($dsn, self::SOURCE_DB_USER, self::SOURCE_DB_PASS, $options);
|
||||
|
||||
LogService::log('Connexion établie à la base source', [
|
||||
'level' => 'info',
|
||||
'database' => self::SOURCE_DB_NAME
|
||||
]);
|
||||
|
||||
return $this->sourceDb;
|
||||
} catch (PDOException $e) {
|
||||
$this->closeSshTunnel();
|
||||
throw new Exception("Erreur de connexion à la base source: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Teste les connexions aux deux bases de données
|
||||
*/
|
||||
public function testConnections(): array {
|
||||
$result = [
|
||||
'source' => ['status' => 'error', 'message' => ''],
|
||||
'target' => ['status' => 'error', 'message' => '']
|
||||
];
|
||||
|
||||
// Test connexion source
|
||||
try {
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
$stmt = $sourceDb->query('SELECT DATABASE() as db, VERSION() as version');
|
||||
$info = $stmt->fetch();
|
||||
|
||||
$result['source'] = [
|
||||
'status' => 'success',
|
||||
'database' => $info['db'],
|
||||
'version' => $info['version'],
|
||||
'message' => 'Connexion réussie'
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$result['source']['message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
// Test connexion cible
|
||||
try {
|
||||
$stmt = $this->targetDb->query('SELECT DATABASE() as db, VERSION() as version');
|
||||
$info = $stmt->fetch();
|
||||
|
||||
$result['target'] = [
|
||||
'status' => 'success',
|
||||
'database' => $info['db'],
|
||||
'version' => $info['version'],
|
||||
'message' => 'Connexion réussie'
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$result['target']['message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les entités disponibles à migrer depuis la base source
|
||||
*/
|
||||
public function getAvailableEntities(): array {
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
|
||||
$stmt = $sourceDb->query("
|
||||
SELECT
|
||||
rowid as id,
|
||||
libelle as name,
|
||||
cp as postal_code,
|
||||
ville as city,
|
||||
active,
|
||||
date_creat as created_at
|
||||
FROM users_entites
|
||||
WHERE active = 1
|
||||
ORDER BY libelle ASC
|
||||
");
|
||||
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les détails d'une entité source
|
||||
*/
|
||||
public function getEntityDetails(int $id): ?array {
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
|
||||
$stmt = $sourceDb->prepare("
|
||||
SELECT
|
||||
e.rowid as id,
|
||||
e.libelle as name,
|
||||
e.cp as postal_code,
|
||||
e.ville as city,
|
||||
e.active,
|
||||
e.date_creat as created_at,
|
||||
(SELECT COUNT(*) FROM users WHERE fk_entite = e.rowid) as users_count,
|
||||
(SELECT COUNT(*) FROM operations WHERE fk_entite = e.rowid) as operations_count,
|
||||
(SELECT COUNT(*) FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.rowid
|
||||
WHERE o.fk_entite = e.rowid) as passages_count
|
||||
FROM users_entites e
|
||||
WHERE e.rowid = ?
|
||||
");
|
||||
|
||||
$stmt->execute([$id]);
|
||||
$entity = $stmt->fetch();
|
||||
|
||||
return $entity ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre une entité complète
|
||||
*/
|
||||
public function migrateEntity(
|
||||
int $entityId,
|
||||
?array $steps = null,
|
||||
bool $dryRun = false,
|
||||
bool $truncate = false
|
||||
): array {
|
||||
$startTime = microtime(true);
|
||||
$migrationId = 'mig_' . time() . '_' . $entityId;
|
||||
|
||||
// Si aucune étape spécifiée, migrer toutes les étapes
|
||||
if ($steps === null) {
|
||||
$steps = self::MIGRATION_STEPS;
|
||||
}
|
||||
|
||||
// Récupérer le nom de l'entité
|
||||
$entityDetails = $this->getEntityDetails($entityId);
|
||||
if (!$entityDetails) {
|
||||
throw new Exception("Entité $entityId non trouvée");
|
||||
}
|
||||
|
||||
$stepsCompleted = [];
|
||||
$totalRecords = 0;
|
||||
$totalErrors = 0;
|
||||
$totalWarnings = 0;
|
||||
|
||||
LogService::log('Début de migration entité', [
|
||||
'level' => 'info',
|
||||
'migration_id' => $migrationId,
|
||||
'entity_id' => $entityId,
|
||||
'entity_name' => $entityDetails['name'],
|
||||
'steps' => $steps,
|
||||
'dry_run' => $dryRun
|
||||
]);
|
||||
|
||||
foreach ($steps as $step) {
|
||||
try {
|
||||
$stepResult = $this->migrateStep($entityId, $step, $dryRun, []);
|
||||
$stepsCompleted[] = [
|
||||
'step' => $step,
|
||||
'status' => 'success',
|
||||
'records_migrated' => $stepResult['records_migrated'],
|
||||
'duration_ms' => $stepResult['duration_ms']
|
||||
];
|
||||
|
||||
$totalRecords += $stepResult['records_migrated'];
|
||||
$totalWarnings += count($stepResult['warnings'] ?? []);
|
||||
} catch (Exception $e) {
|
||||
$stepsCompleted[] = [
|
||||
'step' => $step,
|
||||
'status' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'duration_ms' => 0
|
||||
];
|
||||
$totalErrors++;
|
||||
|
||||
LogService::log('Erreur lors de la migration d\'une étape', [
|
||||
'level' => 'error',
|
||||
'migration_id' => $migrationId,
|
||||
'step' => $step,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
// Arrêter la migration en cas d'erreur critique
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$totalDuration = (microtime(true) - $startTime) * 1000;
|
||||
|
||||
LogService::log('Fin de migration entité', [
|
||||
'level' => 'info',
|
||||
'migration_id' => $migrationId,
|
||||
'entity_id' => $entityId,
|
||||
'total_records' => $totalRecords,
|
||||
'total_errors' => $totalErrors,
|
||||
'duration_ms' => $totalDuration
|
||||
]);
|
||||
|
||||
return [
|
||||
'entity_name' => $entityDetails['name'],
|
||||
'migration_id' => $migrationId,
|
||||
'steps_completed' => $stepsCompleted,
|
||||
'total_duration_ms' => round($totalDuration, 2),
|
||||
'summary' => [
|
||||
'total_records' => $totalRecords,
|
||||
'total_errors' => $totalErrors,
|
||||
'total_warnings' => $totalWarnings
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre une étape spécifique pour une entité
|
||||
*/
|
||||
public function migrateStep(
|
||||
int $entityId,
|
||||
string $step,
|
||||
bool $dryRun = false,
|
||||
array $options = []
|
||||
): array {
|
||||
$startTime = microtime(true);
|
||||
|
||||
LogService::log("Début migration étape: $step", [
|
||||
'level' => 'info',
|
||||
'entity_id' => $entityId,
|
||||
'step' => $step,
|
||||
'dry_run' => $dryRun
|
||||
]);
|
||||
|
||||
// Appeler la méthode spécifique pour chaque étape
|
||||
$methodName = 'migrate' . $this->snakeToPascal($step);
|
||||
|
||||
if (!method_exists($this, $methodName)) {
|
||||
throw new Exception("Méthode de migration non trouvée pour l'étape: $step");
|
||||
}
|
||||
|
||||
$result = $this->$methodName($entityId, $dryRun, $options);
|
||||
|
||||
$duration = (microtime(true) - $startTime) * 1000;
|
||||
|
||||
LogService::log("Fin migration étape: $step", [
|
||||
'level' => 'info',
|
||||
'entity_id' => $entityId,
|
||||
'records_migrated' => $result['records_migrated'],
|
||||
'duration_ms' => $duration
|
||||
]);
|
||||
|
||||
return [
|
||||
'records_migrated' => $result['records_migrated'],
|
||||
'duration_ms' => round($duration, 2),
|
||||
'warnings' => $result['warnings'] ?? [],
|
||||
'details' => $result['details'] ?? []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit snake_case en PascalCase
|
||||
*/
|
||||
private function snakeToPascal(string $string): string {
|
||||
return str_replace('_', '', ucwords($string, '_'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration des devises (x_devises)
|
||||
*/
|
||||
private function migrateXDevises(int $entityId, bool $dryRun, array $options): array {
|
||||
// Les tables x_* sont globales, pas liées à une entité
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
|
||||
$stmt = $sourceDb->query("SELECT * FROM x_devises WHERE active = 1");
|
||||
$records = $stmt->fetchAll();
|
||||
|
||||
if ($dryRun) {
|
||||
return ['records_migrated' => count($records), 'warnings' => []];
|
||||
}
|
||||
|
||||
$inserted = 0;
|
||||
foreach ($records as $record) {
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO x_devises (id, code, libelle, symbole, chk_active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
code = VALUES(code),
|
||||
libelle = VALUES(libelle),
|
||||
symbole = VALUES(symbole),
|
||||
chk_active = VALUES(chk_active),
|
||||
updated_at = NOW()
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$record['rowid'],
|
||||
$record['code'],
|
||||
$record['libelle'],
|
||||
$record['symbole'],
|
||||
$record['active']
|
||||
]);
|
||||
|
||||
$inserted++;
|
||||
}
|
||||
|
||||
return ['records_migrated' => $inserted, 'warnings' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration des types d'entités (x_entites_types)
|
||||
*/
|
||||
private function migrateXEntitesTypes(int $entityId, bool $dryRun, array $options): array {
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
|
||||
$stmt = $sourceDb->query("SELECT * FROM x_entites_types WHERE active = 1");
|
||||
$records = $stmt->fetchAll();
|
||||
|
||||
if ($dryRun) {
|
||||
return ['records_migrated' => count($records), 'warnings' => []];
|
||||
}
|
||||
|
||||
$inserted = 0;
|
||||
foreach ($records as $record) {
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO x_entites_types (id, libelle, chk_active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
libelle = VALUES(libelle),
|
||||
chk_active = VALUES(chk_active),
|
||||
updated_at = NOW()
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$record['rowid'],
|
||||
$record['libelle'],
|
||||
$record['active']
|
||||
]);
|
||||
|
||||
$inserted++;
|
||||
}
|
||||
|
||||
return ['records_migrated' => $inserted, 'warnings' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthodes de migration pour les autres tables x_* (structure similaire)
|
||||
* TODO: Implémenter les autres méthodes
|
||||
*/
|
||||
private function migrateXTypesPassages(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateXTypesReglements(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateXUsersRoles(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateXPays(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateXRegions(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateXDepartements(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateXVilles(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration de l'entité (users_entites -> entites)
|
||||
*/
|
||||
private function migrateEntites(int $entityId, bool $dryRun, array $options): array {
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
|
||||
$stmt = $sourceDb->prepare("SELECT * FROM users_entites WHERE rowid = ?");
|
||||
$stmt->execute([$entityId]);
|
||||
$entity = $stmt->fetch();
|
||||
|
||||
if (!$entity) {
|
||||
throw new Exception("Entité $entityId non trouvée dans la base source");
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
return ['records_migrated' => 1, 'warnings' => []];
|
||||
}
|
||||
|
||||
// Chiffrement des données sensibles
|
||||
$encryptedName = ApiService::encryptData($entity['libelle']);
|
||||
$encryptedEmail = !empty($entity['email']) ? ApiService::encryptSearchableData($entity['email']) : null;
|
||||
|
||||
// Gestion des téléphones
|
||||
$phone = $entity['tel1'] ?? '';
|
||||
$mobile = $entity['tel2'] ?? '';
|
||||
|
||||
// Détection mobile (commence par 06 ou 07)
|
||||
if (preg_match('/^0[67]/', $phone)) {
|
||||
$mobile = $phone;
|
||||
$phone = $entity['tel2'] ?? '';
|
||||
}
|
||||
|
||||
$encryptedPhone = !empty($phone) ? ApiService::encryptData($phone) : null;
|
||||
$encryptedMobile = !empty($mobile) ? ApiService::encryptData($mobile) : null;
|
||||
$encryptedIban = !empty($entity['iban']) ? ApiService::encryptData($entity['iban']) : null;
|
||||
$encryptedBic = !empty($entity['bic']) ? ApiService::encryptData($entity['bic']) : null;
|
||||
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO entites (
|
||||
id, fk_type, encrypted_name, encrypted_email, encrypted_phone, encrypted_mobile,
|
||||
code_postal, ville, encrypted_iban, encrypted_bic, chk_active, chk_demo,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
fk_type = VALUES(fk_type),
|
||||
encrypted_name = VALUES(encrypted_name),
|
||||
encrypted_email = VALUES(encrypted_email),
|
||||
encrypted_phone = VALUES(encrypted_phone),
|
||||
encrypted_mobile = VALUES(encrypted_mobile),
|
||||
code_postal = VALUES(code_postal),
|
||||
ville = VALUES(ville),
|
||||
encrypted_iban = VALUES(encrypted_iban),
|
||||
encrypted_bic = VALUES(encrypted_bic),
|
||||
chk_active = VALUES(chk_active),
|
||||
updated_at = NOW()
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$entity['rowid'],
|
||||
$entity['fk_type'] ?? 1,
|
||||
$encryptedName,
|
||||
$encryptedEmail,
|
||||
$encryptedPhone,
|
||||
$encryptedMobile,
|
||||
$entity['cp'] ?? null,
|
||||
$entity['ville'] ?? null,
|
||||
$encryptedIban,
|
||||
$encryptedBic,
|
||||
$entity['active']
|
||||
]);
|
||||
|
||||
return ['records_migrated' => 1, 'warnings' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration des utilisateurs
|
||||
*/
|
||||
private function migrateUsers(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter (voir migrate_users.php pour la logique)
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration des opérations
|
||||
*/
|
||||
private function migrateOperations(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Autres méthodes de migration
|
||||
* TODO: Implémenter toutes les méthodes pour chaque étape
|
||||
*/
|
||||
private function migrateOpeSectors(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateSectorsAdresses(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateOpeUsers(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateOpeUsersSectors(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateOpePass(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateOpePassHisto(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateMedias(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le statut de migration d'une entité
|
||||
*/
|
||||
public function getMigrationStatus(int $entityId): array {
|
||||
// TODO: Implémenter la vérification du statut
|
||||
return [
|
||||
'entity_id' => $entityId,
|
||||
'migrated' => false,
|
||||
'steps_completed' => [],
|
||||
'last_migration_date' => null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les logs de migration d'une entité
|
||||
*/
|
||||
public function getMigrationLogs(int $entityId): array {
|
||||
// TODO: Lire les logs depuis LogService
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un rapport de migration
|
||||
*/
|
||||
public function generateMigrationReport(int $entityId): array {
|
||||
// TODO: Implémenter la génération de rapport
|
||||
return [
|
||||
'entity_id' => $entityId,
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
'summary' => []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare les données source vs cible
|
||||
*/
|
||||
public function compareEntityData(int $entityId): array {
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
|
||||
$comparison = [];
|
||||
|
||||
// Comparer les counts pour chaque table
|
||||
$tables = [
|
||||
'users' => 'fk_entite',
|
||||
'operations' => 'fk_entite'
|
||||
];
|
||||
|
||||
foreach ($tables as $table => $fkColumn) {
|
||||
$sourceTable = $table;
|
||||
$targetTable = $table;
|
||||
|
||||
// Cas spéciaux
|
||||
if ($table === 'users_entites') {
|
||||
$sourceTable = 'users_entites';
|
||||
$targetTable = 'entites';
|
||||
}
|
||||
|
||||
// Count source
|
||||
$stmt = $sourceDb->prepare("SELECT COUNT(*) as count FROM $sourceTable WHERE $fkColumn = ?");
|
||||
$stmt->execute([$entityId]);
|
||||
$sourceCount = $stmt->fetch()['count'];
|
||||
|
||||
// Count cible
|
||||
$stmt = $this->targetDb->prepare("SELECT COUNT(*) as count FROM $targetTable WHERE $fkColumn = ?");
|
||||
$stmt->execute([$entityId]);
|
||||
$targetCount = $stmt->fetch()['count'];
|
||||
|
||||
$comparison[$table] = [
|
||||
'source_count' => $sourceCount,
|
||||
'target_count' => $targetCount,
|
||||
'difference' => $targetCount - $sourceCount,
|
||||
'status' => $sourceCount === $targetCount ? 'ok' : 'warning'
|
||||
];
|
||||
}
|
||||
|
||||
return $comparison;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'intégrité des données migrées
|
||||
*/
|
||||
public function verifyMigration(int $entityId): array {
|
||||
// TODO: Implémenter les vérifications d'intégrité
|
||||
return [
|
||||
'entity_id' => $entityId,
|
||||
'verified_at' => date('Y-m-d H:i:s'),
|
||||
'checks' => [],
|
||||
'errors' => [],
|
||||
'warnings' => []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Annule la migration d'une entité (rollback)
|
||||
*/
|
||||
public function rollbackEntity(int $entityId): array {
|
||||
// TODO: Implémenter le rollback complet
|
||||
$deletedRecords = [];
|
||||
|
||||
// Supprimer dans l'ordre inverse des dépendances
|
||||
$tables = array_reverse(self::MIGRATION_STEPS);
|
||||
|
||||
foreach ($tables as $table) {
|
||||
// Logique de suppression selon la table
|
||||
}
|
||||
|
||||
return ['deleted_records' => $deletedRecords];
|
||||
}
|
||||
|
||||
/**
|
||||
* Annule une étape spécifique de migration
|
||||
*/
|
||||
public function rollbackStep(int $entityId, string $step): array {
|
||||
// TODO: Implémenter le rollback d'une étape
|
||||
return ['deleted_records' => []];
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
require_once __DIR__ . '/ApiService.php';
|
||||
require_once __DIR__ . '/LogService.php';
|
||||
|
||||
use PDO;
|
||||
use ApiService;
|
||||
use LogService;
|
||||
|
||||
class OperationDataService {
|
||||
|
||||
@@ -221,13 +221,13 @@ class OperationDataService {
|
||||
if (!empty($sectorIdsString)) {
|
||||
// Utiliser ope_users au lieu de users pour avoir les données historiques
|
||||
$usersSectorsStmt = $db->prepare(
|
||||
"SELECT DISTINCT ou.fk_user as id, ou.first_name, ou.encrypted_name, ou.sect_name, us.fk_sector
|
||||
"SELECT DISTINCT ou.fk_user as user_id, ou.id as ope_user_id, ou.first_name, ou.encrypted_name, ou.sect_name, us.fk_sector
|
||||
FROM ope_users ou
|
||||
JOIN ope_users_sectors us ON ou.fk_user = us.fk_user AND ou.fk_operation = us.fk_operation
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
AND ou.chk_active = 1
|
||||
JOIN ope_users_sectors us ON ou.id = us.fk_user AND ou.fk_operation = us.fk_operation
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
AND ou.chk_active = 1
|
||||
AND ou.fk_user != ?" // Exclure l'utilisateur connecté
|
||||
);
|
||||
$usersSectorsStmt->execute([$activeOperationId, $userId]);
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use LogService;
|
||||
use App\Services\LogService;
|
||||
|
||||
require_once __DIR__ . '/LogService.php';
|
||||
|
||||
|
||||
@@ -6,147 +6,99 @@ namespace App\Services;
|
||||
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
use FPDF;
|
||||
use setasign\Fpdi\Fpdi;
|
||||
|
||||
/**
|
||||
* Générateur de reçus PDF avec FPDF
|
||||
* Supporte les logos PNG/JPG
|
||||
* Générateur de reçus PDF avec FPDI (utilise un template PDF)
|
||||
* Génère des PDF légers et valides en format paysage (A4 Landscape)
|
||||
*/
|
||||
class ReceiptPDFGenerator extends FPDF {
|
||||
|
||||
class ReceiptPDFGenerator extends Fpdi {
|
||||
|
||||
private const TEMPLATE_PATH = __DIR__ . '/../../docs/_recu_template.pdf';
|
||||
private const DEFAULT_LOGO_PATH = __DIR__ . '/../../docs/_logo_recu.png';
|
||||
private const LOGO_WIDTH = 40; // Largeur du logo en mm
|
||||
private const LOGO_HEIGHT = 40; // Hauteur du logo en mm
|
||||
|
||||
private const LOGO_WIDTH = 45; // Largeur du logo en mm
|
||||
|
||||
/**
|
||||
* Génère un reçu fiscal PDF
|
||||
* Génère un reçu fiscal PDF et l'enregistre directement dans un fichier
|
||||
*
|
||||
* @param array $data Données du reçu
|
||||
* @param string $outputPath Chemin complet du fichier PDF à créer
|
||||
* @param string|null $logoPath Chemin vers le logo (optionnel)
|
||||
* @return bool True si la génération a réussi
|
||||
*/
|
||||
public function generateReceipt(array $data, ?string $logoPath = null): string {
|
||||
$this->AddPage();
|
||||
$this->SetFont('Arial', '', 12);
|
||||
|
||||
// Déterminer quel logo utiliser
|
||||
$logoToUse = null;
|
||||
if ($logoPath && file_exists($logoPath)) {
|
||||
$logoToUse = $logoPath;
|
||||
} elseif (file_exists(self::DEFAULT_LOGO_PATH)) {
|
||||
$logoToUse = self::DEFAULT_LOGO_PATH;
|
||||
}
|
||||
|
||||
// Ajouter le logo (PNG ou JPG)
|
||||
if ($logoToUse) {
|
||||
try {
|
||||
// Déterminer le type d'image
|
||||
$imageInfo = getimagesize($logoToUse);
|
||||
if ($imageInfo !== false) {
|
||||
$type = '';
|
||||
switch ($imageInfo[2]) {
|
||||
case IMAGETYPE_JPEG:
|
||||
$type = 'JPG';
|
||||
break;
|
||||
case IMAGETYPE_PNG:
|
||||
$type = 'PNG';
|
||||
break;
|
||||
}
|
||||
|
||||
if ($type) {
|
||||
// Position du logo : x=10, y=10, largeur=40mm, hauteur=40mm
|
||||
$this->Image($logoToUse, 10, 10, self::LOGO_WIDTH, self::LOGO_HEIGHT, $type);
|
||||
public function generateReceipt(array $data, string $outputPath, ?string $logoPath = null): bool {
|
||||
try {
|
||||
// Créer la page en orientation paysage (Landscape)
|
||||
$this->AddPage('L');
|
||||
|
||||
// Importer le template PDF
|
||||
if (file_exists(self::TEMPLATE_PATH)) {
|
||||
$this->setSourceFile(self::TEMPLATE_PATH);
|
||||
$tplIdx = $this->importPage(1);
|
||||
$this->useTemplate($tplIdx);
|
||||
}
|
||||
|
||||
// Configuration de base
|
||||
$this->SetFont('Arial');
|
||||
$this->SetFontSize(16);
|
||||
$this->SetTextColor(50, 50, 50);
|
||||
|
||||
// Nom de l'amicale (en haut à droite du template)
|
||||
$this->SetXY(116, 26);
|
||||
$this->Write(0, $this->cleanText($data['entite_city'] ?? ''));
|
||||
|
||||
// Nom du donateur
|
||||
$this->SetXY(35, 41);
|
||||
$this->Write(0, $this->cleanText($data['donor_name'] ?? ''));
|
||||
|
||||
// Adresse du donateur
|
||||
$this->SetXY(35, 55);
|
||||
$this->Write(0, $this->cleanText($data['donor_address'] ?? ''));
|
||||
|
||||
// Montant et mode de règlement
|
||||
$this->SetXY(48, 68);
|
||||
$amount = $data['amount'] ?? '0';
|
||||
$paymentMethod = !empty($data['payment_method'])
|
||||
? ' en ' . mb_strtolower($data['payment_method'], 'UTF-8')
|
||||
: '';
|
||||
$this->Write(0, $this->cleanText($amount . ' euros' . $paymentMethod));
|
||||
|
||||
// Date du don
|
||||
$this->SetXY(20, 82);
|
||||
$this->Write(0, $this->cleanText($data['donation_date'] ?? date('d/m/Y')));
|
||||
|
||||
// Logo de l'entité (en haut à droite)
|
||||
$logoToUse = null;
|
||||
if ($logoPath && file_exists($logoPath)) {
|
||||
$logoToUse = $logoPath;
|
||||
} elseif (file_exists(self::DEFAULT_LOGO_PATH)) {
|
||||
$logoToUse = self::DEFAULT_LOGO_PATH;
|
||||
}
|
||||
|
||||
if ($logoToUse) {
|
||||
try {
|
||||
$this->Image($logoToUse, 245, 8, self::LOGO_WIDTH);
|
||||
} catch (\Exception $e) {
|
||||
// Si erreur avec le logo custom, utiliser le logo par défaut
|
||||
if ($logoToUse !== self::DEFAULT_LOGO_PATH && file_exists(self::DEFAULT_LOGO_PATH)) {
|
||||
try {
|
||||
$this->Image(self::DEFAULT_LOGO_PATH, 245, 8, self::LOGO_WIDTH);
|
||||
} catch (\Exception $e2) {
|
||||
// Continuer sans logo
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Si erreur avec le logo, continuer sans
|
||||
}
|
||||
|
||||
// Écrire directement dans le fichier (mode 'F')
|
||||
$this->Output($outputPath, 'F');
|
||||
|
||||
return true;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
error_log('Erreur génération PDF: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// En-tête à droite du logo
|
||||
$this->SetXY(60, 20);
|
||||
$this->SetFont('Arial', 'B', 14);
|
||||
$this->Cell(130, 6, $this->cleanText(strtoupper($data['entite_name'] ?? '')), 0, 1, 'C');
|
||||
|
||||
if (!empty($data['entite_city'])) {
|
||||
$this->SetX(60);
|
||||
$this->SetFont('Arial', '', 11);
|
||||
$this->Cell(130, 5, $this->cleanText(strtoupper($data['entite_city'])), 0, 1, 'C');
|
||||
}
|
||||
|
||||
if (!empty($data['entite_address'])) {
|
||||
$this->SetX(60);
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$this->Cell(130, 5, $this->cleanText($data['entite_address']), 0, 1, 'C');
|
||||
}
|
||||
|
||||
// Titre du reçu
|
||||
$this->SetY(65);
|
||||
$this->SetFont('Arial', 'B', 16);
|
||||
$this->Cell(0, 10, $this->cleanText('REÇU DE DON'), 0, 1, 'C');
|
||||
|
||||
$this->SetFont('Arial', 'B', 14);
|
||||
$this->Cell(0, 8, $this->cleanText('N° ' . ($data['receipt_number'] ?? '')), 0, 1, 'C');
|
||||
|
||||
// Ligne de séparation
|
||||
$this->Ln(5);
|
||||
$this->Line(20, $this->GetY(), 190, $this->GetY());
|
||||
$this->Ln(8);
|
||||
|
||||
// Informations du donateur
|
||||
$this->SetFont('Arial', 'B', 12);
|
||||
$this->Cell(0, 6, $this->cleanText('DONATEUR :'), 0, 1, 'L');
|
||||
|
||||
$this->SetFont('Arial', '', 11);
|
||||
$this->Cell(0, 6, $this->cleanText($data['donor_name'] ?? ''), 0, 1, 'L');
|
||||
|
||||
if (!empty($data['donor_address'])) {
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$this->Cell(0, 5, $this->cleanText($data['donor_address']), 0, 1, 'L');
|
||||
}
|
||||
|
||||
$this->Ln(8);
|
||||
|
||||
// Cadre pour le montant
|
||||
$this->SetFillColor(240, 240, 240);
|
||||
$this->Rect(20, $this->GetY(), 170, 25, 'F');
|
||||
|
||||
// Montant en gros et centré
|
||||
$this->Ln(5);
|
||||
$this->SetFont('Arial', 'B', 18);
|
||||
$this->Cell(0, 8, $this->cleanText($data['amount'] ?? '0') . $this->cleanText(' euros'), 0, 1, 'C');
|
||||
|
||||
// Date centrée
|
||||
$this->SetFont('Arial', '', 12);
|
||||
$this->Cell(0, 6, $this->cleanText('Don du ' . ($data['donation_date'] ?? date('d/m/Y'))), 0, 1, 'C');
|
||||
|
||||
$this->Ln(10);
|
||||
|
||||
if (!empty($data['payment_method'])) {
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$this->Cell(0, 5, $this->cleanText('Mode de règlement : ') . $this->cleanText($data['payment_method']), 0, 1, 'L');
|
||||
}
|
||||
|
||||
if (!empty($data['operation_name'])) {
|
||||
$this->SetFont('Arial', 'I', 10);
|
||||
$this->Cell(0, 5, $this->cleanText('Campagne : ') . $this->cleanText($data['operation_name']), 0, 1, 'L');
|
||||
}
|
||||
|
||||
// Mention de remerciement
|
||||
$this->Ln(15);
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$this->MultiCell(0, 5, $this->cleanText(
|
||||
"Nous vous remercions pour votre généreux soutien à notre amicale.\n" .
|
||||
"Votre don contribue au financement de nos activités et équipements."
|
||||
), 0, 'C');
|
||||
|
||||
// Signature
|
||||
$this->SetY(-60);
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$this->Cell(0, 5, $this->cleanText('Fait à ' . ($data['entite_city'] ?? '') . ', le ' . ($data['signature_date'] ?? date('d/m/Y'))), 0, 1, 'R');
|
||||
$this->Ln(5);
|
||||
$this->Cell(0, 5, $this->cleanText('Le Président'), 0, 1, 'R');
|
||||
$this->Ln(15);
|
||||
$this->Cell(0, 5, $this->cleanText('(Signature et cachet)'), 0, 1, 'R');
|
||||
|
||||
// Retourner le PDF en string
|
||||
return $this->Output('S');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,12 +8,13 @@ require_once __DIR__ . '/LogService.php';
|
||||
require_once __DIR__ . '/ApiService.php';
|
||||
require_once __DIR__ . '/FileService.php';
|
||||
require_once __DIR__ . '/ReceiptPDFGenerator.php';
|
||||
require_once __DIR__ . '/EmailTemplates.php';
|
||||
|
||||
use PDO;
|
||||
use Database;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use FileService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\FileService;
|
||||
use App\Services\ApiService;
|
||||
use Exception;
|
||||
use DateTime;
|
||||
|
||||
@@ -88,26 +89,28 @@ class ReceiptService {
|
||||
|
||||
// Préparer les données pour la génération du PDF
|
||||
$receiptData = $this->prepareReceiptData($passageData, $operationData, $entiteData, $email);
|
||||
|
||||
// Générer le PDF optimisé
|
||||
$pdfContent = $this->generateOptimizedPDF($receiptData, $logoPath);
|
||||
|
||||
|
||||
// Créer le répertoire de stockage
|
||||
$uploadPath = "/{$operationData['fk_entite']}/recus/{$operationData['id']}";
|
||||
$fullPath = $this->fileService->createDirectory($operationData['fk_entite'], $uploadPath);
|
||||
|
||||
|
||||
// Nom du fichier
|
||||
$fileName = 'recu_' . $passageId . '.pdf';
|
||||
$filePath = $fullPath . '/' . $fileName;
|
||||
|
||||
// Sauvegarder le fichier
|
||||
if (file_put_contents($filePath, $pdfContent) === false) {
|
||||
throw new Exception('Impossible de sauvegarder le fichier PDF');
|
||||
|
||||
// Générer le PDF directement dans le fichier
|
||||
$pdfGenerated = $this->generateOptimizedPDF($receiptData, $filePath, $logoPath);
|
||||
|
||||
if (!$pdfGenerated || !file_exists($filePath)) {
|
||||
throw new Exception('Impossible de générer le fichier PDF');
|
||||
}
|
||||
|
||||
|
||||
// Appliquer les permissions
|
||||
$this->fileService->setFilePermissions($filePath);
|
||||
|
||||
|
||||
// Récupérer la taille du fichier généré
|
||||
$fileSize = filesize($filePath);
|
||||
|
||||
// Enregistrer dans la table medias
|
||||
$mediaId = $this->saveToMedias(
|
||||
$operationData['fk_entite'],
|
||||
@@ -115,21 +118,21 @@ class ReceiptService {
|
||||
$passageId,
|
||||
$fileName,
|
||||
$filePath,
|
||||
strlen($pdfContent)
|
||||
$fileSize
|
||||
);
|
||||
|
||||
|
||||
// Mettre à jour le passage avec les infos du reçu
|
||||
$this->updatePassageReceipt($passageId, $fileName);
|
||||
|
||||
// Ajouter à la queue d'email
|
||||
$this->queueReceiptEmail($passageId, $email, $receiptData, $pdfContent);
|
||||
|
||||
// Ajouter à la queue d'email (le PDF sera lu depuis le fichier)
|
||||
$this->queueReceiptEmail($passageId, $email, $receiptData, $filePath);
|
||||
|
||||
LogService::log('Reçu généré avec succès', [
|
||||
'level' => 'info',
|
||||
'passageId' => $passageId,
|
||||
'mediaId' => $mediaId,
|
||||
'fileName' => $fileName,
|
||||
'fileSize' => strlen($pdfContent)
|
||||
'fileSize' => $fileSize
|
||||
]);
|
||||
|
||||
return true;
|
||||
@@ -146,10 +149,15 @@ class ReceiptService {
|
||||
|
||||
/**
|
||||
* Génère un PDF optimisé avec logo et mise en page épurée
|
||||
*
|
||||
* @param array $data Données du reçu
|
||||
* @param string $outputPath Chemin du fichier PDF à créer
|
||||
* @param string|null $logoPath Chemin du logo
|
||||
* @return bool True si la génération a réussi
|
||||
*/
|
||||
private function generateOptimizedPDF(array $data, ?string $logoPath): string {
|
||||
private function generateOptimizedPDF(array $data, string $outputPath, ?string $logoPath): bool {
|
||||
$pdf = new ReceiptPDFGenerator();
|
||||
return $pdf->generateReceipt($data, $logoPath);
|
||||
return $pdf->generateReceipt($data, $outputPath, $logoPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -158,12 +166,13 @@ class ReceiptService {
|
||||
*/
|
||||
private function getPassageData(int $passageId): ?array {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.*,
|
||||
SELECT p.*,
|
||||
u.encrypted_name as user_encrypted_name,
|
||||
u.encrypted_email as user_encrypted_email,
|
||||
u.encrypted_phone as user_encrypted_phone
|
||||
FROM ope_pass p
|
||||
LEFT JOIN users u ON p.fk_user = u.id
|
||||
LEFT JOIN ope_users ou ON p.fk_user = ou.id
|
||||
LEFT JOIN users u ON ou.fk_user = u.id
|
||||
WHERE p.id = ? AND p.chk_active = 1
|
||||
');
|
||||
$stmt->execute([$passageId]);
|
||||
@@ -345,25 +354,52 @@ class ReceiptService {
|
||||
|
||||
/**
|
||||
* Ajoute le reçu à la queue d'email
|
||||
*
|
||||
* @param int $passageId ID du passage
|
||||
* @param string $email Email du destinataire
|
||||
* @param array $receiptData Données du reçu
|
||||
* @param string $pdfFilePath Chemin vers le fichier PDF
|
||||
*/
|
||||
private function queueReceiptEmail(int $passageId, string $email, array $receiptData, string $pdfContent): void {
|
||||
private function queueReceiptEmail(int $passageId, string $email, array $receiptData, string $pdfFilePath): void {
|
||||
// Lire le contenu du PDF depuis le fichier
|
||||
if (!file_exists($pdfFilePath)) {
|
||||
throw new \Exception('Fichier PDF introuvable pour la mise en queue: ' . $pdfFilePath);
|
||||
}
|
||||
|
||||
$pdfContent = file_get_contents($pdfFilePath);
|
||||
if ($pdfContent === false) {
|
||||
throw new \Exception('Impossible de lire le fichier PDF: ' . $pdfFilePath);
|
||||
}
|
||||
|
||||
// Préparer le sujet
|
||||
$subject = "Votre reçu de don N°" . $receiptData['receipt_number'];
|
||||
|
||||
// Préparer le corps de l'email
|
||||
$body = $this->generateEmailBody($receiptData);
|
||||
|
||||
$subject = "Votre reçu de don - " . $receiptData['entite_name'];
|
||||
|
||||
// Préparer les données pour le template
|
||||
$templateData = [
|
||||
'passage_id' => $passageId,
|
||||
'entite_name' => $receiptData['entite_name'],
|
||||
'donor_name' => $receiptData['donor_name'],
|
||||
'amount' => $receiptData['amount'],
|
||||
'donation_date' => $receiptData['donation_date'],
|
||||
'payment_method' => $receiptData['payment_method'],
|
||||
'entite_address' => $receiptData['entite_address'],
|
||||
'entite_email' => $receiptData['entite_email']
|
||||
];
|
||||
|
||||
// Générer le corps de l'email via le template centralisé
|
||||
$body = EmailTemplates::getReceiptDonationTemplate($templateData);
|
||||
|
||||
// Préparer les headers avec pièce jointe
|
||||
$boundary = md5((string)time());
|
||||
$headers = "MIME-Version: 1.0\r\n";
|
||||
$headers .= "Content-Type: multipart/mixed; boundary=\"$boundary\"\r\n";
|
||||
|
||||
|
||||
// Corps complet avec pièce jointe
|
||||
$fullBody = "--$boundary\r\n";
|
||||
$fullBody .= "Content-Type: text/html; charset=UTF-8\r\n";
|
||||
$fullBody .= "Content-Transfer-Encoding: 7bit\r\n\r\n";
|
||||
$fullBody .= $body . "\r\n\r\n";
|
||||
|
||||
|
||||
// Pièce jointe PDF
|
||||
$fullBody .= "--$boundary\r\n";
|
||||
$fullBody .= "Content-Type: application/pdf; name=\"recu_" . $receiptData['receipt_number'] . ".pdf\"\r\n";
|
||||
@@ -371,14 +407,14 @@ class ReceiptService {
|
||||
$fullBody .= "Content-Disposition: attachment; filename=\"recu_" . $receiptData['receipt_number'] . ".pdf\"\r\n\r\n";
|
||||
$fullBody .= chunk_split(base64_encode($pdfContent)) . "\r\n";
|
||||
$fullBody .= "--$boundary--";
|
||||
|
||||
|
||||
// Insérer dans la queue
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO email_queue (
|
||||
fk_pass, to_email, subject, body, headers, created_at, status
|
||||
) VALUES (?, ?, ?, ?, ?, NOW(), ?)
|
||||
');
|
||||
|
||||
|
||||
$stmt->execute([
|
||||
$passageId,
|
||||
$email,
|
||||
@@ -388,62 +424,7 @@ class ReceiptService {
|
||||
'pending'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère le corps HTML de l'email
|
||||
*/
|
||||
private function generateEmailBody(array $data): string {
|
||||
// Convertir toutes les valeurs en string pour htmlspecialchars
|
||||
$safeData = array_map(function($value) {
|
||||
return is_string($value) ? $value : (string)$value;
|
||||
}, $data);
|
||||
|
||||
$html = '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background-color: #f4f4f4; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; }
|
||||
.footer { background-color: #f4f4f4; padding: 10px; text-align: center; font-size: 12px; }
|
||||
.amount { font-size: 24px; font-weight: bold; color: #2c5aa0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h2>' . htmlspecialchars($safeData['entite_name']) . '</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Bonjour ' . htmlspecialchars($safeData['donor_name']) . ',</p>
|
||||
|
||||
<p>Nous vous remercions chaleureusement pour votre don de <span class="amount">' .
|
||||
htmlspecialchars($safeData['amount']) . ' €</span> effectué le ' .
|
||||
htmlspecialchars($safeData['donation_date']) . '.</p>
|
||||
|
||||
<p>Vous trouverez ci-joint votre reçu fiscal N°' . htmlspecialchars($safeData['receipt_number']) .
|
||||
' qui vous permettra de bénéficier d\'une réduction d\'impôt égale à 66% du montant de votre don.</p>
|
||||
|
||||
<p>Votre soutien est précieux pour nous permettre de poursuivre nos actions.</p>
|
||||
|
||||
<p>Cordialement,<br>
|
||||
L\'équipe de ' . htmlspecialchars($safeData['entite_name']) . '</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Conservez ce reçu pour votre déclaration fiscale</p>
|
||||
<p>' . htmlspecialchars($safeData['entite_name']) . '<br>
|
||||
' . htmlspecialchars($safeData['entite_address']) . '<br>
|
||||
' . htmlspecialchars($safeData['entite_email']) . '</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Met à jour la date d'envoi du reçu
|
||||
*/
|
||||
|
||||
@@ -9,8 +9,9 @@ require_once __DIR__ . '/EmailThrottler.php';
|
||||
|
||||
use PDO;
|
||||
use Database;
|
||||
use ApiService;
|
||||
use App\Services\ApiService;
|
||||
use AppConfig;
|
||||
use App\Services\LogService;
|
||||
|
||||
/**
|
||||
* Service central de gestion des alertes de sécurité et monitoring
|
||||
@@ -94,7 +95,7 @@ class AlertService {
|
||||
$context['request'] = [
|
||||
'uri' => $_SERVER['REQUEST_URI'],
|
||||
'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
|
||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'ip' => \AppConfig::getInstance()->getClientIp(),
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ class PerformanceMonitor {
|
||||
$memoryUsed = $memoryPeak - $memoryStart;
|
||||
|
||||
// Enrichir avec les infos de requête
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? null;
|
||||
$ip = \AppConfig::getInstance()->getClientIp();
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||
$requestSize = strlen(file_get_contents('php://input'));
|
||||
|
||||
|
||||
@@ -74,13 +74,10 @@ class SecurityMonitor {
|
||||
// Critères de détection
|
||||
$isBruteForce = false;
|
||||
$reason = '';
|
||||
|
||||
if ($attempts >= 5) {
|
||||
|
||||
if ($attempts >= 8) {
|
||||
$isBruteForce = true;
|
||||
$reason = "Plus de 5 tentatives en 5 minutes";
|
||||
} elseif ($uniqueUsers >= 3) {
|
||||
$isBruteForce = true;
|
||||
$reason = "Tentatives sur 3 usernames différents";
|
||||
$reason = "Plus de 8 tentatives en 5 minutes";
|
||||
}
|
||||
|
||||
if ($isBruteForce) {
|
||||
@@ -114,15 +111,19 @@ class SecurityMonitor {
|
||||
|
||||
try {
|
||||
// Chercher si le username existe (pour stocker la version chiffrée)
|
||||
require_once __DIR__ . '/../ApiService.php';
|
||||
$encryptedUsername = null;
|
||||
if ($username) {
|
||||
// Chiffrer le username pour la recherche
|
||||
$searchUsername = \ApiService::encryptSearchableData($username);
|
||||
|
||||
$userStmt = $db->prepare('
|
||||
SELECT encrypted_user_name
|
||||
FROM users
|
||||
WHERE username = :username
|
||||
SELECT encrypted_user_name
|
||||
FROM users
|
||||
WHERE encrypted_user_name = :encrypted_username
|
||||
LIMIT 1
|
||||
');
|
||||
$userStmt->execute(['username' => $username]);
|
||||
$userStmt->execute(['encrypted_username' => $searchUsername]);
|
||||
$user = $userStmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($user) {
|
||||
$encryptedUsername = $user['encrypted_user_name'];
|
||||
@@ -178,7 +179,7 @@ class SecurityMonitor {
|
||||
if (isset($_SERVER['REQUEST_URI'])) {
|
||||
$context['endpoint'] = $_SERVER['REQUEST_URI'];
|
||||
$context['method'] = $_SERVER['REQUEST_METHOD'] ?? 'unknown';
|
||||
$context['ip'] = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$context['ip'] = \AppConfig::getInstance()->getClientIp();
|
||||
}
|
||||
|
||||
AlertService::trigger('SQL_INJECTION', $context, 'SECURITY');
|
||||
|
||||
@@ -12,6 +12,8 @@ use AppConfig;
|
||||
use Database;
|
||||
use PDO;
|
||||
use Exception;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
|
||||
/**
|
||||
* Service principal pour gérer l'intégration Stripe
|
||||
@@ -85,10 +87,67 @@ class StripeService {
|
||||
// Si le compte existe, vérifier s'il est complet
|
||||
try {
|
||||
$stripeAccount = $this->stripe->accounts->retrieve($existingAccount['stripe_account_id']);
|
||||
|
||||
|
||||
// Si pas de location_id, créer la Location maintenant
|
||||
$locationId = $existingAccount['stripe_location_id'];
|
||||
if (empty($locationId)) {
|
||||
try {
|
||||
// Déchiffrer le nom pour la Location
|
||||
$nom = !empty($entite['encrypted_name']) ? ApiService::decryptData($entite['encrypted_name']) : 'Amicale';
|
||||
|
||||
// Construire l'adresse
|
||||
$adresse1 = !empty($entite['adresse1']) ? $entite['adresse1'] : 'Adresse non renseignée';
|
||||
$adresse2 = !empty($entite['adresse2']) ? $entite['adresse2'] : '';
|
||||
$ville = !empty($entite['ville']) ? $entite['ville'] : 'Ville';
|
||||
$codePostal = !empty($entite['code_postal']) ? $entite['code_postal'] : '00000';
|
||||
|
||||
$location = $this->stripe->terminal->locations->create([
|
||||
'display_name' => $nom,
|
||||
'address' => [
|
||||
'line1' => $adresse1,
|
||||
'line2' => $adresse2,
|
||||
'city' => $ville,
|
||||
'postal_code' => $codePostal,
|
||||
'country' => 'FR',
|
||||
],
|
||||
'metadata' => [
|
||||
'entite_id' => $entiteId,
|
||||
'type' => 'tap_to_pay'
|
||||
]
|
||||
], [
|
||||
'stripe_account' => $existingAccount['stripe_account_id']
|
||||
]);
|
||||
|
||||
$locationId = $location->id;
|
||||
|
||||
// Mettre à jour en base
|
||||
$stmt = $this->db->prepare(
|
||||
"UPDATE stripe_accounts
|
||||
SET stripe_location_id = :location_id, updated_at = NOW()
|
||||
WHERE fk_entite = :fk_entite"
|
||||
);
|
||||
$stmt->execute([
|
||||
'location_id' => $locationId,
|
||||
'fk_entite' => $entiteId
|
||||
]);
|
||||
|
||||
LogService::log('Location créée pour compte existant', [
|
||||
'entite_id' => $entiteId,
|
||||
'location_id' => $locationId
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur création Location pour compte existant', [
|
||||
'level' => 'warning',
|
||||
'entite_id' => $entiteId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'account_id' => $existingAccount['stripe_account_id'],
|
||||
'location_id' => $locationId,
|
||||
'message' => 'Compte Stripe existant',
|
||||
'existing' => true,
|
||||
'charges_enabled' => $stripeAccount->charges_enabled,
|
||||
@@ -106,8 +165,8 @@ class StripeService {
|
||||
}
|
||||
|
||||
// Déchiffrer les données
|
||||
$nom = !empty($entite['encrypted_name']) ? \ApiService::decryptData($entite['encrypted_name']) : '';
|
||||
$email = !empty($entite['encrypted_email']) ? \ApiService::decryptSearchableData($entite['encrypted_email']) : null;
|
||||
$nom = !empty($entite['encrypted_name']) ? ApiService::decryptData($entite['encrypted_name']) : '';
|
||||
$email = !empty($entite['encrypted_email']) ? ApiService::decryptSearchableData($entite['encrypted_email']) : null;
|
||||
|
||||
// Créer le compte Stripe Connect Express
|
||||
$accountData = [
|
||||
@@ -147,21 +206,64 @@ class StripeService {
|
||||
}
|
||||
|
||||
$account = $this->stripe->accounts->create($accountData);
|
||||
|
||||
// Sauvegarder en base de données
|
||||
|
||||
// Créer automatiquement la Location Terminal pour Tap to Pay
|
||||
$location = null;
|
||||
$locationId = null;
|
||||
try {
|
||||
// Construire l'adresse complète
|
||||
$adresse1 = !empty($entite['adresse1']) ? $entite['adresse1'] : 'Adresse non renseignée';
|
||||
$adresse2 = !empty($entite['adresse2']) ? $entite['adresse2'] : '';
|
||||
$ville = !empty($entite['ville']) ? $entite['ville'] : 'Ville';
|
||||
$codePostal = !empty($entite['code_postal']) ? $entite['code_postal'] : '00000';
|
||||
|
||||
$location = $this->stripe->terminal->locations->create([
|
||||
'display_name' => $nom,
|
||||
'address' => [
|
||||
'line1' => $adresse1,
|
||||
'line2' => $adresse2,
|
||||
'city' => $ville,
|
||||
'postal_code' => $codePostal,
|
||||
'country' => 'FR',
|
||||
],
|
||||
'metadata' => [
|
||||
'entite_id' => $entiteId,
|
||||
'type' => 'tap_to_pay'
|
||||
]
|
||||
], [
|
||||
'stripe_account' => $account->id
|
||||
]);
|
||||
|
||||
$locationId = $location->id;
|
||||
LogService::log('Location Stripe créée automatiquement', [
|
||||
'entite_id' => $entiteId,
|
||||
'location_id' => $locationId
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
// Si la création de la Location échoue, logger mais continuer
|
||||
LogService::log('Erreur création Location', [
|
||||
'level' => 'warning',
|
||||
'entite_id' => $entiteId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
// Sauvegarder en base de données avec le location_id
|
||||
$stmt = $this->db->prepare(
|
||||
"INSERT INTO stripe_accounts (fk_entite, stripe_account_id, created_at)
|
||||
VALUES (:fk_entite, :stripe_account_id, NOW())"
|
||||
"INSERT INTO stripe_accounts (fk_entite, stripe_account_id, stripe_location_id, created_at)
|
||||
VALUES (:fk_entite, :stripe_account_id, :stripe_location_id, NOW())"
|
||||
);
|
||||
$stmt->execute([
|
||||
'fk_entite' => $entiteId,
|
||||
'stripe_account_id' => $account->id
|
||||
'stripe_account_id' => $account->id,
|
||||
'stripe_location_id' => $locationId
|
||||
]);
|
||||
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'account_id' => $account->id,
|
||||
'message' => 'Compte Stripe créé avec succès'
|
||||
'location_id' => $locationId,
|
||||
'message' => 'Compte Stripe créé avec succès' . ($locationId ? ' (Location Terminal créée)' : '')
|
||||
];
|
||||
|
||||
} catch (ApiErrorException $e) {
|
||||
@@ -197,41 +299,41 @@ class StripeService {
|
||||
*/
|
||||
public function createOnboardingLink(string $accountId, string $returnUrl, string $refreshUrl): array {
|
||||
try {
|
||||
\LogService::log('StripeService::createOnboardingLink début', [
|
||||
LogService::log('StripeService::createOnboardingLink début', [
|
||||
'account_id' => $accountId,
|
||||
'return_url' => $returnUrl,
|
||||
'refresh_url' => $refreshUrl
|
||||
]);
|
||||
|
||||
|
||||
$accountLink = $this->stripe->accountLinks->create([
|
||||
'account' => $accountId,
|
||||
'refresh_url' => $refreshUrl,
|
||||
'return_url' => $returnUrl,
|
||||
'type' => 'account_onboarding',
|
||||
]);
|
||||
|
||||
\LogService::log('StripeService::createOnboardingLink succès', [
|
||||
|
||||
LogService::log('StripeService::createOnboardingLink succès', [
|
||||
'url' => $accountLink->url
|
||||
]);
|
||||
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'url' => $accountLink->url
|
||||
];
|
||||
|
||||
|
||||
} catch (ApiErrorException $e) {
|
||||
\LogService::log('StripeService::createOnboardingLink erreur Stripe', [
|
||||
LogService::log('StripeService::createOnboardingLink erreur Stripe', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'code' => $e->getCode()
|
||||
]);
|
||||
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Erreur Stripe: ' . $e->getMessage()
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
\LogService::log('StripeService::createOnboardingLink erreur générale', [
|
||||
LogService::log('StripeService::createOnboardingLink erreur générale', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
@@ -263,7 +365,7 @@ class StripeService {
|
||||
}
|
||||
|
||||
// Déchiffrer les données de l'entité
|
||||
$nom = !empty($data['encrypted_name']) ? \ApiService::decryptData($data['encrypted_name']) : 'Amicale';
|
||||
$nom = !empty($data['encrypted_name']) ? ApiService::decryptData($data['encrypted_name']) : 'Amicale';
|
||||
|
||||
// Construire l'adresse complète
|
||||
$adresse1 = !empty($data['adresse1']) ? $data['adresse1'] : '';
|
||||
@@ -500,20 +602,165 @@ class StripeService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer un Payment Link Stripe pour paiement par QR Code
|
||||
*
|
||||
* @param array $params [
|
||||
* 'amount' => int (en centimes),
|
||||
* 'currency' => string (défaut: 'eur'),
|
||||
* 'description' => string,
|
||||
* 'passage_id' => int,
|
||||
* 'metadata' => array
|
||||
* ]
|
||||
* @return array ['success' => bool, 'payment_link_id' => string, 'url' => string, 'amount' => int]
|
||||
*/
|
||||
public function createPaymentLink(array $params): array {
|
||||
try {
|
||||
$amount = $params['amount'] ?? 0;
|
||||
$passageId = $params['passage_id'] ?? 0;
|
||||
|
||||
if ($amount <= 0) {
|
||||
throw new Exception("Le montant doit être supérieur à 0");
|
||||
}
|
||||
|
||||
// Récupérer les infos du passage avec opération et entité
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT p.*, o.fk_entite, o.id as operation_id, sa.stripe_account_id
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
JOIN stripe_accounts sa ON o.fk_entite = sa.fk_entite
|
||||
WHERE p.id = :passage_id
|
||||
");
|
||||
$stmt->execute(['passage_id' => $passageId]);
|
||||
$passage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$passage) {
|
||||
throw new Exception("Passage non trouvé");
|
||||
}
|
||||
|
||||
if (!$passage['stripe_account_id']) {
|
||||
throw new Exception("Stripe non activé pour cette amicale");
|
||||
}
|
||||
|
||||
// Préparer les metadata
|
||||
$metadata = array_merge($params['metadata'] ?? [], [
|
||||
'passage_id' => (string)$passageId,
|
||||
'operation_id' => (string)$passage['operation_id'],
|
||||
'amicale_id' => (string)$passage['fk_entite'],
|
||||
'fk_user' => (string)$passage['fk_user'], // ID du membre (ope_users.id)
|
||||
'created_at' => (string)time(), // Timestamp Unix de création du Payment Link
|
||||
'type' => 'qr_code_payment'
|
||||
]);
|
||||
|
||||
// Créer le Payment Link sur le compte Connect
|
||||
$paymentLink = $this->stripe->paymentLinks->create([
|
||||
'line_items' => [
|
||||
[
|
||||
'price_data' => [
|
||||
'currency' => $params['currency'] ?? 'eur',
|
||||
'product_data' => [
|
||||
'name' => $params['description'] ?? 'Calendrier pompiers',
|
||||
],
|
||||
'unit_amount' => $amount,
|
||||
],
|
||||
'quantity' => 1,
|
||||
],
|
||||
],
|
||||
'metadata' => $metadata,
|
||||
'after_completion' => [
|
||||
'type' => 'hosted_confirmation',
|
||||
'hosted_confirmation' => [
|
||||
'custom_message' => 'Merci pour votre paiement ! Votre reçu vous sera envoyé par email.',
|
||||
],
|
||||
],
|
||||
'payment_method_types' => ['card'],
|
||||
'billing_address_collection' => 'auto',
|
||||
], [
|
||||
'stripe_account' => $passage['stripe_account_id']
|
||||
]);
|
||||
|
||||
// Logger la création
|
||||
LogService::log('Payment Link créé', [
|
||||
'payment_link_id' => $paymentLink->id,
|
||||
'passage_id' => $passageId,
|
||||
'amount' => $amount,
|
||||
'amicale_id' => $passage['fk_entite']
|
||||
]);
|
||||
|
||||
// Mettre à jour le passage avec le payment_link_id
|
||||
$stmt = $this->db->prepare("
|
||||
UPDATE ope_pass
|
||||
SET stripe_payment_link_id = :link_id, updated_at = NOW()
|
||||
WHERE id = :passage_id
|
||||
");
|
||||
$stmt->execute([
|
||||
'link_id' => $paymentLink->id,
|
||||
'passage_id' => $passageId
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'payment_link_id' => $paymentLink->id,
|
||||
'url' => $paymentLink->url,
|
||||
'amount' => $amount
|
||||
];
|
||||
|
||||
} catch (ApiErrorException $e) {
|
||||
LogService::log('Erreur Stripe Payment Link', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'passage_id' => $passageId ?? null
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Erreur Stripe: ' . $e->getMessage()
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Erreur: ' . $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer le statut d'un PaymentIntent depuis Stripe
|
||||
*/
|
||||
public function getPaymentIntentStatus(string $paymentIntentId): array {
|
||||
try {
|
||||
$paymentIntent = $this->stripe->paymentIntents->retrieve($paymentIntentId);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'status' => $paymentIntent->status,
|
||||
'amount' => $paymentIntent->amount,
|
||||
'currency' => $paymentIntent->currency,
|
||||
'payment_method' => $paymentIntent->payment_method,
|
||||
'created' => $paymentIntent->created
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Erreur: ' . $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtenir le mode actuel (test ou live)
|
||||
*/
|
||||
public function isTestMode(): bool {
|
||||
return $this->testMode;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Obtenir la clé publique pour le frontend
|
||||
*/
|
||||
public function getPublicKey(): string {
|
||||
$stripeConfig = $this->config->getStripeConfig();
|
||||
return $this->testMode
|
||||
? $stripeConfig['public_key_test']
|
||||
return $this->testMode
|
||||
? $stripeConfig['public_key_test']
|
||||
: $stripeConfig['public_key_live'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user