feat: Version 3.6.2 - Correctifs tâches #17-20
- #17: Amélioration gestion des secteurs et statistiques - #18: Optimisation services API et logs - #19: Corrections Flutter widgets et repositories - #20: Fix création passage - détection automatique ope_users.id vs users.id Suppression dossier web/ (migration vers app Flutter) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
484
api/src/Controllers/EventStatsController.php
Normal file
484
api/src/Controllers/EventStatsController.php
Normal file
@@ -0,0 +1,484 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventStatsService.php';
|
||||
|
||||
use PDO;
|
||||
use Database;
|
||||
use Response;
|
||||
use Session;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventStatsService;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* EventStatsController - Contrôleur des statistiques d'événements
|
||||
*
|
||||
* Endpoints pour consulter les stats agrégées et le détail des événements.
|
||||
* Accès réservé aux Admin entité (role_id = 2) et Super-admin (role_id = 1).
|
||||
*/
|
||||
class EventStatsController
|
||||
{
|
||||
private PDO $db;
|
||||
private EventStatsService $statsService;
|
||||
|
||||
/** @var array Rôles autorisés à consulter les stats */
|
||||
private const ALLOWED_ROLES = [1, 2]; // Super-admin, Admin
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance();
|
||||
$this->statsService = new EventStatsService();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/events/stats/summary
|
||||
* Récupère le résumé des stats pour une date
|
||||
*
|
||||
* Query params:
|
||||
* - date: Date (YYYY-MM-DD), défaut = aujourd'hui
|
||||
*/
|
||||
public function summary(): void
|
||||
{
|
||||
try {
|
||||
if (!$this->checkAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entityId = $this->getEntityIdForQuery();
|
||||
$date = $_GET['date'] ?? date('Y-m-d');
|
||||
|
||||
// Validation de la date
|
||||
if (!$this->isValidDate($date)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Format de date invalide (attendu: YYYY-MM-DD)',
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$summary = $this->statsService->getSummary($entityId, $date);
|
||||
|
||||
$this->jsonWithCache([
|
||||
'status' => 'success',
|
||||
'data' => $summary,
|
||||
], true);
|
||||
} catch (Exception $e) {
|
||||
LogService::error('Erreur lors de la récupération du résumé des stats', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la récupération des statistiques',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/events/stats/daily
|
||||
* Récupère les stats journalières sur une plage de dates
|
||||
*
|
||||
* Query params:
|
||||
* - from: Date début (YYYY-MM-DD), requis
|
||||
* - to: Date fin (YYYY-MM-DD), requis
|
||||
* - events: Types d'événements (comma-separated), optionnel
|
||||
*/
|
||||
public function daily(): void
|
||||
{
|
||||
try {
|
||||
if (!$this->checkAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entityId = $this->getEntityIdForQuery();
|
||||
$from = $_GET['from'] ?? null;
|
||||
$to = $_GET['to'] ?? null;
|
||||
$eventTypes = isset($_GET['events']) ? explode(',', $_GET['events']) : [];
|
||||
|
||||
// Validation des dates
|
||||
if (!$from || !$to) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Les paramètres from et to sont requis',
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->isValidDate($from) || !$this->isValidDate($to)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Format de date invalide (attendu: YYYY-MM-DD)',
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($from > $to) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'La date de début doit être antérieure à la date de fin',
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Limiter la plage à 90 jours
|
||||
$daysDiff = (strtotime($to) - strtotime($from)) / 86400;
|
||||
if ($daysDiff > 90) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'La plage de dates ne peut pas dépasser 90 jours',
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$daily = $this->statsService->getDaily($entityId, $from, $to, $eventTypes);
|
||||
|
||||
$this->jsonWithCache([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'days' => $daily,
|
||||
],
|
||||
], true);
|
||||
} catch (Exception $e) {
|
||||
LogService::error('Erreur lors de la récupération des stats journalières', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la récupération des statistiques',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/events/stats/weekly
|
||||
* Récupère les stats hebdomadaires sur une plage de dates
|
||||
*
|
||||
* Query params:
|
||||
* - from: Date début (YYYY-MM-DD), requis
|
||||
* - to: Date fin (YYYY-MM-DD), requis
|
||||
* - events: Types d'événements (comma-separated), optionnel
|
||||
*/
|
||||
public function weekly(): void
|
||||
{
|
||||
try {
|
||||
if (!$this->checkAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entityId = $this->getEntityIdForQuery();
|
||||
$from = $_GET['from'] ?? null;
|
||||
$to = $_GET['to'] ?? null;
|
||||
$eventTypes = isset($_GET['events']) ? explode(',', $_GET['events']) : [];
|
||||
|
||||
// Validation des dates
|
||||
if (!$from || !$to) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Les paramètres from et to sont requis',
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->isValidDate($from) || !$this->isValidDate($to)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Format de date invalide (attendu: YYYY-MM-DD)',
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Limiter la plage à 1 an
|
||||
$daysDiff = (strtotime($to) - strtotime($from)) / 86400;
|
||||
if ($daysDiff > 365) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'La plage de dates ne peut pas dépasser 1 an',
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$weekly = $this->statsService->getWeekly($entityId, $from, $to, $eventTypes);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'weeks' => $weekly,
|
||||
],
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::error('Erreur lors de la récupération des stats hebdomadaires', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la récupération des statistiques',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/events/stats/monthly
|
||||
* Récupère les stats mensuelles pour une année
|
||||
*
|
||||
* Query params:
|
||||
* - year: Année (YYYY), défaut = année courante
|
||||
* - events: Types d'événements (comma-separated), optionnel
|
||||
*/
|
||||
public function monthly(): void
|
||||
{
|
||||
try {
|
||||
if (!$this->checkAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entityId = $this->getEntityIdForQuery();
|
||||
$year = isset($_GET['year']) ? (int) $_GET['year'] : (int) date('Y');
|
||||
$eventTypes = isset($_GET['events']) ? explode(',', $_GET['events']) : [];
|
||||
|
||||
// Validation de l'année
|
||||
if ($year < 2020 || $year > (int) date('Y') + 1) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Année invalide',
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$monthly = $this->statsService->getMonthly($entityId, $year, $eventTypes);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'year' => $year,
|
||||
'months' => $monthly,
|
||||
],
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::error('Erreur lors de la récupération des stats mensuelles', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la récupération des statistiques',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/events/stats/details
|
||||
* Récupère le détail des événements (lecture JSONL paginée)
|
||||
*
|
||||
* Query params:
|
||||
* - date: Date (YYYY-MM-DD), requis
|
||||
* - event: Type d'événement, optionnel
|
||||
* - limit: Nombre max (défaut 50, max 100)
|
||||
* - offset: Décalage pour pagination (défaut 0)
|
||||
*/
|
||||
public function details(): void
|
||||
{
|
||||
try {
|
||||
if (!$this->checkAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entityId = $this->getEntityIdForQuery();
|
||||
$date = $_GET['date'] ?? null;
|
||||
$eventType = $_GET['event'] ?? null;
|
||||
$limit = isset($_GET['limit']) ? min((int) $_GET['limit'], 100) : 50;
|
||||
$offset = isset($_GET['offset']) ? max((int) $_GET['offset'], 0) : 0;
|
||||
|
||||
// Validation de la date
|
||||
if (!$date) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le paramètre date est requis',
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->isValidDate($date)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Format de date invalide (attendu: YYYY-MM-DD)',
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$details = $this->statsService->getDetails($entityId, $date, $eventType, $limit, $offset);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'data' => $details,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::error('Erreur lors de la récupération du détail des événements', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la récupération des événements',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/events/stats/types
|
||||
* Récupère la liste des types d'événements disponibles
|
||||
*/
|
||||
public function types(): void
|
||||
{
|
||||
try {
|
||||
if (!$this->checkAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$types = $this->statsService->getEventTypes();
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'data' => $types,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::error('Erreur lors de la récupération des types d\'événements', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la récupération des types',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES PRIVÉES ====================
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur a accès aux stats
|
||||
*/
|
||||
private function checkAccess(): bool
|
||||
{
|
||||
$roleId = Session::getRole();
|
||||
|
||||
if (!in_array($roleId, self::ALLOWED_ROLES)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé. Rôle Admin ou Super-admin requis.',
|
||||
], 403);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine l'entity_id à utiliser pour la requête
|
||||
* Super-admin (role_id = 1) peut voir toutes les entités (null)
|
||||
* Admin (role_id = 2) voit uniquement son entité
|
||||
*/
|
||||
private function getEntityIdForQuery(): ?int
|
||||
{
|
||||
$roleId = Session::getRole();
|
||||
|
||||
// Super-admin : accès global
|
||||
if ($roleId === 1) {
|
||||
// Permettre de filtrer par entité si spécifié
|
||||
if (isset($_GET['entity_id'])) {
|
||||
return (int) $_GET['entity_id'];
|
||||
}
|
||||
return null; // Stats globales
|
||||
}
|
||||
|
||||
// Admin : uniquement son entité
|
||||
return Session::getEntityId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide le format d'une date
|
||||
*/
|
||||
private function isValidDate(string $date): bool
|
||||
{
|
||||
$d = \DateTime::createFromFormat('Y-m-d', $date);
|
||||
return $d && $d->format('Y-m-d') === $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie une réponse JSON avec support ETag et compression gzip
|
||||
*
|
||||
* @param array $data Données à envoyer
|
||||
* @param bool $useCache Activer le cache ETag
|
||||
*/
|
||||
private function jsonWithCache(array $data, bool $useCache = true): void
|
||||
{
|
||||
// Nettoyer tout buffer existant
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Encoder en JSON
|
||||
$jsonResponse = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
if ($jsonResponse === false) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur d\'encodage de la réponse',
|
||||
], 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// Headers CORS
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '*';
|
||||
header("Access-Control-Allow-Origin: $origin");
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Origin, Content-Type, X-Auth-Token, Authorization, X-Requested-With');
|
||||
header('Access-Control-Expose-Headers: Content-Length, ETag');
|
||||
|
||||
// Content-Type
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
// ETag pour le cache
|
||||
if ($useCache) {
|
||||
$etag = '"' . md5($jsonResponse) . '"';
|
||||
header('ETag: ' . $etag);
|
||||
header('Cache-Control: private, max-age=300'); // 5 minutes
|
||||
|
||||
// Vérifier If-None-Match
|
||||
$ifNoneMatch = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';
|
||||
if ($ifNoneMatch === $etag) {
|
||||
http_response_code(304); // Not Modified
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Compression gzip si supportée
|
||||
$supportsGzip = isset($_SERVER['HTTP_ACCEPT_ENCODING'])
|
||||
&& strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false;
|
||||
|
||||
if ($supportsGzip && strlen($jsonResponse) > 1024) {
|
||||
$compressed = gzencode($jsonResponse, 6);
|
||||
if ($compressed !== false) {
|
||||
header('Content-Encoding: gzip');
|
||||
header('Content-Length: ' . strlen($compressed));
|
||||
http_response_code(200);
|
||||
echo $compressed;
|
||||
flush();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Réponse non compressée
|
||||
header('Content-Length: ' . strlen($jsonResponse));
|
||||
http_response_code(200);
|
||||
echo $jsonResponse;
|
||||
flush();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -2086,18 +2086,38 @@ class LoginController {
|
||||
], 201);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->db->rollBack();
|
||||
LogService::log('Erreur lors de la création du compte GeoSector', [
|
||||
// Vérifier si une transaction est active avant de faire rollback
|
||||
if ($this->db->inTransaction()) {
|
||||
$this->db->rollBack();
|
||||
}
|
||||
|
||||
// Construire un message d'erreur détaillé pour le logging
|
||||
$errorDetails = [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'email' => $email,
|
||||
'amicaleName' => $amicaleName,
|
||||
'postalCode' => $postalCode
|
||||
]);
|
||||
'exception_class' => get_class($e),
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_code' => $e->getCode(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'email' => $email ?? 'non disponible',
|
||||
'amicaleName' => $amicaleName ?? 'non disponible',
|
||||
'postalCode' => $postalCode ?? 'non disponible',
|
||||
'trace' => $e->getTraceAsString()
|
||||
];
|
||||
|
||||
// Si c'est une PDOException, ajouter les infos SQL
|
||||
if ($e instanceof PDOException) {
|
||||
$errorDetails['pdo_error_info'] = $this->db->errorInfo();
|
||||
}
|
||||
|
||||
LogService::log('Erreur lors de la création du compte GeoSector', $errorDetails);
|
||||
|
||||
// Retourner un message utilisateur clair (ne pas exposer les détails techniques)
|
||||
$userMessage = 'Une erreur est survenue lors de la création du compte. Veuillez réessayer ou contacter le support.';
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
'message' => $userMessage
|
||||
], 500);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/ReceiptService.php';
|
||||
require_once __DIR__ . '/../Services/SectorService.php';
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
@@ -19,6 +20,7 @@ use Session;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\SectorService;
|
||||
use Exception;
|
||||
use DateTime;
|
||||
|
||||
@@ -516,14 +518,26 @@ class PassageController {
|
||||
}
|
||||
|
||||
// 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
|
||||
// $data['fk_user'] peut contenir soit users.id soit ope_users.id
|
||||
$passageUserId = (int)$data['fk_user'];
|
||||
$stmtOpeUser = $this->db->prepare('
|
||||
|
||||
// Vérifier d'abord si c'est déjà un ope_users.id valide
|
||||
$stmtCheckOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
WHERE id = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtOpeUser->execute([$passageUserId, $operationId]);
|
||||
$opeUserId = $stmtOpeUser->fetchColumn();
|
||||
$stmtCheckOpeUser->execute([$passageUserId, $operationId]);
|
||||
$opeUserId = $stmtCheckOpeUser->fetchColumn();
|
||||
|
||||
if (!$opeUserId) {
|
||||
// Ce n'est pas un ope_users.id, essayer de le convertir depuis users.id
|
||||
$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([
|
||||
@@ -533,6 +547,88 @@ class PassageController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Détermination automatique du secteur
|
||||
$sectorId = null;
|
||||
$gpsLat = isset($data['gps_lat']) && $data['gps_lat'] !== '' ? (float)$data['gps_lat'] : null;
|
||||
$gpsLng = isset($data['gps_lng']) && $data['gps_lng'] !== '' ? (float)$data['gps_lng'] : null;
|
||||
|
||||
// 1. Si fk_sector > 0 fourni → l'utiliser directement
|
||||
if (isset($data['fk_sector']) && (int)$data['fk_sector'] > 0) {
|
||||
$sectorId = (int)$data['fk_sector'];
|
||||
LogService::info('[PassageController] Secteur fourni par le client', [
|
||||
'sector_id' => $sectorId
|
||||
]);
|
||||
}
|
||||
|
||||
// 2. Si pas de secteur et GPS valide (différent de 0.0) → recherche par GPS
|
||||
if ($sectorId === null && $gpsLat !== null && $gpsLng !== null && ($gpsLat != 0.0 || $gpsLng != 0.0)) {
|
||||
$sectorService = new SectorService();
|
||||
$sectorId = $sectorService->findSectorByGps($operationId, $gpsLat, $gpsLng);
|
||||
LogService::info('[PassageController] Recherche secteur par GPS', [
|
||||
'operation_id' => $operationId,
|
||||
'lat' => $gpsLat,
|
||||
'lng' => $gpsLng,
|
||||
'sector_found' => $sectorId
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. Si toujours pas de secteur et adresse fournie → géocodage + recherche
|
||||
if ($sectorId === null && !empty($data['numero']) && !empty($data['rue']) && !empty($data['ville'])) {
|
||||
// Récupérer le code postal de l'entité pour la vérification du département
|
||||
$stmtEntite = $this->db->prepare('
|
||||
SELECT e.code_postal FROM entites e
|
||||
INNER JOIN operations o ON o.fk_entite = e.id
|
||||
WHERE o.id = ?
|
||||
');
|
||||
$stmtEntite->execute([$operationId]);
|
||||
$entiteCp = $stmtEntite->fetchColumn() ?: '';
|
||||
|
||||
$sectorService = new SectorService();
|
||||
$result = $sectorService->findSectorByAddress(
|
||||
$operationId,
|
||||
trim($data['numero']),
|
||||
$data['rue_bis'] ?? '',
|
||||
trim($data['rue']),
|
||||
trim($data['ville']),
|
||||
$entiteCp
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
$sectorId = $result['sector_id'];
|
||||
// Mettre à jour les coordonnées GPS si le géocodage les a trouvées
|
||||
if ($result['gps_lat'] && $result['gps_lng']) {
|
||||
$gpsLat = $result['gps_lat'];
|
||||
$gpsLng = $result['gps_lng'];
|
||||
}
|
||||
LogService::info('[PassageController] Recherche secteur par adresse', [
|
||||
'operation_id' => $operationId,
|
||||
'adresse' => $data['numero'] . ' ' . $data['rue'] . ' ' . $data['ville'],
|
||||
'sector_found' => $sectorId,
|
||||
'gps_geocoded' => ($result['gps_lat'] && $result['gps_lng'])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fallback : si toujours pas de secteur, prendre le 1er secteur de l'opération
|
||||
if ($sectorId === null) {
|
||||
$stmtFirstSector = $this->db->prepare('
|
||||
SELECT id FROM ope_sectors
|
||||
WHERE fk_operation = ? AND chk_active = 1
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmtFirstSector->execute([$operationId]);
|
||||
$firstSectorId = $stmtFirstSector->fetchColumn();
|
||||
|
||||
if ($firstSectorId) {
|
||||
$sectorId = (int)$firstSectorId;
|
||||
LogService::info('[PassageController] Fallback: premier secteur de l\'opération', [
|
||||
'operation_id' => $operationId,
|
||||
'sector_id' => $sectorId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Chiffrement des données sensibles
|
||||
$encryptedName = '';
|
||||
if (isset($data['name']) && !empty(trim($data['name']))) {
|
||||
@@ -549,7 +645,7 @@ class PassageController {
|
||||
// Préparation des données pour l'insertion
|
||||
$insertData = [
|
||||
'fk_operation' => $operationId,
|
||||
'fk_sector' => isset($data['fk_sector']) ? (int)$data['fk_sector'] : 0,
|
||||
'fk_sector' => $sectorId, // Peut être NULL si aucun secteur trouvé
|
||||
'fk_user' => $opeUserId,
|
||||
'fk_adresse' => $data['fk_adresse'] ?? '',
|
||||
'passed_at' => isset($data['passed_at']) ? $data['passed_at'] : null,
|
||||
@@ -562,8 +658,8 @@ class PassageController {
|
||||
'appt' => $data['appt'] ?? '',
|
||||
'niveau' => $data['niveau'] ?? '',
|
||||
'residence' => $data['residence'] ?? '',
|
||||
'gps_lat' => $data['gps_lat'] ?? '',
|
||||
'gps_lng' => $data['gps_lng'] ?? '',
|
||||
'gps_lat' => $gpsLat ?? '',
|
||||
'gps_lng' => $gpsLng ?? '',
|
||||
'encrypted_name' => $encryptedName,
|
||||
'montant' => isset($data['montant']) ? (float)$data['montant'] : 0.00,
|
||||
'fk_type_reglement' => isset($data['fk_type_reglement']) ? (int)$data['fk_type_reglement'] : 1,
|
||||
@@ -596,7 +692,7 @@ class PassageController {
|
||||
EventLogService::logPassageCreated(
|
||||
(int)$passageId,
|
||||
$insertData['fk_operation'],
|
||||
$insertData['fk_sector'],
|
||||
$insertData['fk_sector'] ?? 0, // 0 si secteur non trouvé
|
||||
$insertData['montant'],
|
||||
(string)$insertData['fk_type_reglement']
|
||||
);
|
||||
|
||||
@@ -12,17 +12,15 @@ use App\Services\DepartmentBoundaryService;
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
|
||||
class SectorController
|
||||
class SectorController
|
||||
{
|
||||
private \PDO $db;
|
||||
private LogService $logService;
|
||||
private AddressService $addressService;
|
||||
private DepartmentBoundaryService $boundaryService;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance();
|
||||
$this->logService = new LogService();
|
||||
$this->addressService = new AddressService();
|
||||
$this->boundaryService = new DepartmentBoundaryService();
|
||||
}
|
||||
@@ -72,7 +70,7 @@ class SectorController
|
||||
Response::json(['status' => 'success', 'data' => $sectors]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de la récupération des secteurs', [
|
||||
LogService::error('Erreur lors de la récupération des secteurs', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
@@ -152,14 +150,14 @@ class SectorController
|
||||
$departmentsTouched = $this->boundaryService->getDepartmentsForSector($coordinates);
|
||||
|
||||
if (empty($departmentsTouched)) {
|
||||
$this->logService->warning('Aucun département trouvé pour le secteur', [
|
||||
LogService::warning('Aucun département trouvé pour le secteur', [
|
||||
'libelle' => $data['libelle'],
|
||||
'entity_id' => $entityId,
|
||||
'entity_dept' => $departement
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->warning('Impossible de vérifier les limites départementales', [
|
||||
LogService::warning('Impossible de vérifier les limites départementales', [
|
||||
'error' => $e->getMessage(),
|
||||
'libelle' => $data['libelle']
|
||||
]);
|
||||
@@ -169,7 +167,7 @@ class SectorController
|
||||
try {
|
||||
$addressCount = $this->addressService->countAddressesInPolygon($coordinates, $entityId);
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->warning('Impossible de récupérer les adresses du secteur', [
|
||||
LogService::warning('Impossible de récupérer les adresses du secteur', [
|
||||
'error' => $e->getMessage(),
|
||||
'libelle' => $data['libelle'],
|
||||
'entity_id' => $entityId
|
||||
@@ -208,7 +206,7 @@ class SectorController
|
||||
$opeUserId = $stmtOpeUser->fetchColumn();
|
||||
|
||||
if (!$opeUserId) {
|
||||
$this->logService->warning('ope_users.id non trouvé pour cette opération', [
|
||||
LogService::warning('ope_users.id non trouvé pour cette opération', [
|
||||
'ope_users_id' => $memberId,
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
@@ -275,7 +273,7 @@ class SectorController
|
||||
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->warning('Erreur lors de la récupération des passages orphelins', [
|
||||
LogService::warning('Erreur lors de la récupération des passages orphelins', [
|
||||
'sector_id' => $sectorId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
@@ -335,7 +333,7 @@ class SectorController
|
||||
$firstOpeUserId = $stmtFirstOpeUser->fetchColumn();
|
||||
|
||||
if (!$firstOpeUserId) {
|
||||
$this->logService->warning('Premier ope_users.id non trouvé pour cette opération', [
|
||||
LogService::warning('Premier ope_users.id non trouvé pour cette opération', [
|
||||
'ope_users_id' => $users[0],
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
@@ -401,7 +399,7 @@ class SectorController
|
||||
|
||||
// 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', [
|
||||
LogService::info('[SectorController] Création passages immeuble avec GPS uniformisés', [
|
||||
'address_id' => $address['id'],
|
||||
'nb_passages' => $nbLog,
|
||||
'gps_lat' => $gpsLat,
|
||||
@@ -410,7 +408,7 @@ class SectorController
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->warning('Erreur lors de la création d\'un passage', [
|
||||
LogService::warning('Erreur lors de la création d\'un passage', [
|
||||
'address_id' => $address['id'],
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
@@ -421,7 +419,7 @@ class SectorController
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// En cas d'erreur avec les adresses, on ne bloque pas la création du secteur
|
||||
$this->logService->error('Erreur lors du stockage des adresses du secteur', [
|
||||
LogService::error('Erreur lors du stockage des adresses du secteur', [
|
||||
'sector_id' => $sectorId,
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $entityId
|
||||
@@ -525,7 +523,7 @@ class SectorController
|
||||
$responseData['users_sectors'][] = $userData;
|
||||
}
|
||||
|
||||
$this->logService->info('Secteur créé', [
|
||||
LogService::info('Secteur créé', [
|
||||
'sector_id' => $sectorId,
|
||||
'libelle' => $sectorData['libelle'],
|
||||
'entity_id' => $entityId,
|
||||
@@ -567,7 +565,7 @@ class SectorController
|
||||
if ($this->db->inTransaction()) {
|
||||
$this->db->rollBack();
|
||||
}
|
||||
$this->logService->error('Erreur lors de la création du secteur', [
|
||||
LogService::error('Erreur lors de la création du secteur', [
|
||||
'error' => $e->getMessage(),
|
||||
'data' => $data ?? null
|
||||
]);
|
||||
@@ -634,7 +632,7 @@ class SectorController
|
||||
|
||||
// Gestion des membres (reçus comme 'users' depuis Flutter)
|
||||
if (isset($data['users'])) {
|
||||
$this->logService->info('[UPDATE USERS] Début modification des membres', [
|
||||
LogService::info('[UPDATE USERS] Début modification des membres', [
|
||||
'sector_id' => $id,
|
||||
'users_demandes' => $data['users'],
|
||||
'nb_users' => count($data['users'])
|
||||
@@ -642,27 +640,27 @@ class SectorController
|
||||
|
||||
// Récupérer l'opération du secteur pour l'INSERT
|
||||
$opQuery = "SELECT fk_operation FROM ope_sectors WHERE id = :sector_id";
|
||||
$this->logService->info('[UPDATE USERS] SQL - Récupération fk_operation', [
|
||||
LogService::info('[UPDATE USERS] SQL - Récupération fk_operation', [
|
||||
'query' => $opQuery,
|
||||
'params' => ['sector_id' => $id]
|
||||
]);
|
||||
$opStmt = $this->db->prepare($opQuery);
|
||||
$opStmt->execute(['sector_id' => $id]);
|
||||
$operationId = $opStmt->fetch()['fk_operation'];
|
||||
$this->logService->info('[UPDATE USERS] fk_operation récupéré', [
|
||||
LogService::info('[UPDATE USERS] fk_operation récupéré', [
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
|
||||
// Supprimer les affectations existantes
|
||||
$deleteQuery = "DELETE FROM ope_users_sectors WHERE fk_sector = :sector_id";
|
||||
$this->logService->info('[UPDATE USERS] SQL - Suppression des anciens membres', [
|
||||
LogService::info('[UPDATE USERS] SQL - Suppression des anciens membres', [
|
||||
'query' => $deleteQuery,
|
||||
'params' => ['sector_id' => $id]
|
||||
]);
|
||||
$deleteStmt = $this->db->prepare($deleteQuery);
|
||||
$deleteStmt->execute(['sector_id' => $id]);
|
||||
$deletedCount = $deleteStmt->rowCount();
|
||||
$this->logService->info('[UPDATE USERS] Membres supprimés', [
|
||||
LogService::info('[UPDATE USERS] Membres supprimés', [
|
||||
'nb_deleted' => $deletedCount
|
||||
]);
|
||||
|
||||
@@ -670,7 +668,7 @@ class SectorController
|
||||
if (!empty($data['users'])) {
|
||||
$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', [
|
||||
LogService::info('[UPDATE USERS] SQL - Requête INSERT préparée', [
|
||||
'query' => $insertQuery
|
||||
]);
|
||||
$insertStmt = $this->db->prepare($insertQuery);
|
||||
@@ -689,7 +687,7 @@ class SectorController
|
||||
$opeUserId = $stmtOpeUser->fetchColumn();
|
||||
|
||||
if (!$opeUserId) {
|
||||
$this->logService->warning('[UPDATE USERS] ope_users.id non trouvé pour cette opération', [
|
||||
LogService::warning('[UPDATE USERS] ope_users.id non trouvé pour cette opération', [
|
||||
'ope_users_id' => $memberId,
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
@@ -703,17 +701,17 @@ class SectorController
|
||||
'sector_id' => $id,
|
||||
'user_creat' => $_SESSION['user_id'] ?? null
|
||||
];
|
||||
$this->logService->info('[UPDATE USERS] SQL - INSERT user', [
|
||||
LogService::info('[UPDATE USERS] SQL - INSERT user', [
|
||||
'params' => $params
|
||||
]);
|
||||
$insertStmt->execute($params);
|
||||
$insertedUsers[] = $memberId;
|
||||
$this->logService->info('[UPDATE USERS] User inséré avec succès', [
|
||||
LogService::info('[UPDATE USERS] User inséré avec succès', [
|
||||
'user_id' => $memberId
|
||||
]);
|
||||
} catch (\PDOException $e) {
|
||||
$failedUsers[] = $memberId;
|
||||
$this->logService->warning('[UPDATE USERS] ERREUR insertion user', [
|
||||
LogService::warning('[UPDATE USERS] ERREUR insertion user', [
|
||||
'sector_id' => $id,
|
||||
'user_id' => $memberId,
|
||||
'error' => $e->getMessage(),
|
||||
@@ -722,7 +720,7 @@ class SectorController
|
||||
}
|
||||
}
|
||||
|
||||
$this->logService->info('[UPDATE USERS] Résultat des insertions', [
|
||||
LogService::info('[UPDATE USERS] Résultat des insertions', [
|
||||
'users_demandes' => $data['users'],
|
||||
'users_inseres' => $insertedUsers,
|
||||
'users_echoues' => $failedUsers,
|
||||
@@ -744,7 +742,7 @@ class SectorController
|
||||
$chkAdressesChange = $data['chk_adresses_change'] ?? 1;
|
||||
|
||||
if (isset($data['sector']) && $chkAdressesChange == 0) {
|
||||
$this->logService->info('[UPDATE] Modification secteur sans recalcul adresses/passages', [
|
||||
LogService::info('[UPDATE] Modification secteur sans recalcul adresses/passages', [
|
||||
'sector_id' => $id,
|
||||
'chk_adresses_change' => $chkAdressesChange
|
||||
]);
|
||||
@@ -770,7 +768,7 @@ class SectorController
|
||||
}
|
||||
|
||||
// Récupérer et stocker les nouvelles adresses
|
||||
$this->logService->info('[UPDATE] Récupération des adresses', [
|
||||
LogService::info('[UPDATE] Récupération des adresses', [
|
||||
'sector_id' => $id,
|
||||
'entity_id' => $entityId,
|
||||
'nb_points' => count($coordinates)
|
||||
@@ -781,7 +779,7 @@ class SectorController
|
||||
// Enrichir les adresses avec les données bâtiments
|
||||
$addresses = $this->addressService->enrichAddressesWithBuildings($addresses, $entityId);
|
||||
|
||||
$this->logService->info('[UPDATE] Adresses récupérées', [
|
||||
LogService::info('[UPDATE] Adresses récupérées', [
|
||||
'sector_id' => $id,
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
@@ -815,12 +813,12 @@ class SectorController
|
||||
]);
|
||||
}
|
||||
|
||||
$this->logService->info('[UPDATE] Adresses stockées dans sectors_adresses', [
|
||||
LogService::info('[UPDATE] Adresses stockées dans sectors_adresses', [
|
||||
'sector_id' => $id,
|
||||
'nb_stored' => count($addresses)
|
||||
]);
|
||||
} else {
|
||||
$this->logService->warning('[UPDATE] Aucune adresse trouvée pour le secteur', [
|
||||
LogService::warning('[UPDATE] Aucune adresse trouvée pour le secteur', [
|
||||
'sector_id' => $id,
|
||||
'entity_id' => $entityId
|
||||
]);
|
||||
@@ -828,19 +826,19 @@ class SectorController
|
||||
|
||||
// Vérifier si c'est un problème de connexion à la base d'adresses
|
||||
if (!$this->addressService->isConnected()) {
|
||||
$this->logService->warning('[UPDATE] Base d\'adresses non accessible - passages créés sans adresses', [
|
||||
LogService::warning('[UPDATE] Base d\'adresses non accessible - passages créés sans adresses', [
|
||||
'sector_id' => $id
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('[UPDATE] Erreur lors de la mise à jour des adresses du secteur', [
|
||||
LogService::error('[UPDATE] Erreur lors de la mise à jour des adresses du secteur', [
|
||||
'sector_id' => $id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
// Maintenant que les adresses sont mises à jour, traiter les passages
|
||||
$this->logService->info('[UPDATE] Début mise à jour des passages', ['sector_id' => $id]);
|
||||
LogService::info('[UPDATE] Début mise à jour des passages', ['sector_id' => $id]);
|
||||
$passageCounters = $this->updatePassagesForSector($id, $data['sector']);
|
||||
}
|
||||
|
||||
@@ -934,7 +932,7 @@ class SectorController
|
||||
WHERE ous.fk_sector = :sector_id
|
||||
ORDER BY u.id";
|
||||
|
||||
$this->logService->info('[UPDATE USERS] SQL - Récupération finale des users', [
|
||||
LogService::info('[UPDATE USERS] SQL - Récupération finale des users', [
|
||||
'query' => $usersQuery,
|
||||
'params' => ['sector_id' => $id]
|
||||
]);
|
||||
@@ -944,7 +942,7 @@ class SectorController
|
||||
$usersSectors = $usersStmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
$userIds = array_column($usersSectors, 'id');
|
||||
$this->logService->info('[UPDATE USERS] Users récupérés après commit', [
|
||||
LogService::info('[UPDATE USERS] Users récupérés après commit', [
|
||||
'sector_id' => $id,
|
||||
'users_ids' => $userIds,
|
||||
'nb_users' => count($userIds),
|
||||
@@ -971,7 +969,7 @@ class SectorController
|
||||
$usersDecrypted[] = $userData;
|
||||
}
|
||||
|
||||
$this->logService->info('Secteur modifié', [
|
||||
LogService::info('Secteur modifié', [
|
||||
'sector_id' => $id,
|
||||
'updates' => array_keys($data),
|
||||
'passage_counters' => $passageCounters,
|
||||
@@ -999,7 +997,7 @@ class SectorController
|
||||
if ($this->db->inTransaction()) {
|
||||
$this->db->rollBack();
|
||||
}
|
||||
$this->logService->error('Erreur lors de la modification du secteur', [
|
||||
LogService::error('Erreur lors de la modification du secteur', [
|
||||
'sector_id' => $id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
@@ -1065,7 +1063,7 @@ class SectorController
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de la récupération des adresses du secteur', [
|
||||
LogService::error('Erreur lors de la récupération des adresses du secteur', [
|
||||
'sector_id' => $id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
@@ -1198,7 +1196,7 @@ class SectorController
|
||||
$passagesDecrypted[] = $passage;
|
||||
}
|
||||
|
||||
$this->logService->info('Secteur supprimé', [
|
||||
LogService::info('Secteur supprimé', [
|
||||
'sector_id' => $id,
|
||||
'libelle' => $sector['libelle'],
|
||||
'passages_deleted' => $passagesToDelete,
|
||||
@@ -1216,7 +1214,7 @@ class SectorController
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->db->rollBack();
|
||||
$this->logService->error('Erreur lors de la suppression du secteur', [
|
||||
LogService::error('Erreur lors de la suppression du secteur', [
|
||||
'sector_id' => $id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
@@ -1238,7 +1236,7 @@ class SectorController
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de la vérification des contours départementaux', [
|
||||
LogService::error('Erreur lors de la vérification des contours départementaux', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
Response::json(['status' => 'error', 'message' => 'Erreur lors de la vérification'], 500);
|
||||
@@ -1298,7 +1296,7 @@ class SectorController
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de la vérification des limites', [
|
||||
LogService::error('Erreur lors de la vérification des limites', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
Response::json(['status' => 'error', 'message' => 'Erreur lors de la vérification'], 500);
|
||||
@@ -1422,7 +1420,7 @@ class SectorController
|
||||
$addressesStmt->execute(['sector_id' => $sectorId]);
|
||||
$addresses = $addressesStmt->fetchAll();
|
||||
|
||||
$this->logService->info('[updatePassagesForSector] Adresses dans sectors_adresses', [
|
||||
LogService::info('[updatePassagesForSector] Adresses dans sectors_adresses', [
|
||||
'sector_id' => $sectorId,
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
@@ -1435,7 +1433,7 @@ class SectorController
|
||||
$firstUserId = $firstUser ? $firstUser['fk_user'] : null;
|
||||
|
||||
if ($firstUserId && !empty($addresses)) {
|
||||
$this->logService->info('[updatePassagesForSector] Traitement des passages', [
|
||||
LogService::info('[updatePassagesForSector] Traitement des passages', [
|
||||
'user_id' => $firstUserId,
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
@@ -1594,7 +1592,7 @@ class SectorController
|
||||
$insertStmt->execute($insertParams);
|
||||
$counters['passages_created'] = count($toInsert);
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de l\'insertion multiple des passages', [
|
||||
LogService::error('Erreur lors de l\'insertion multiple des passages', [
|
||||
'sector_id' => $sectorId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
@@ -1658,12 +1656,12 @@ class SectorController
|
||||
$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', [
|
||||
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', [
|
||||
LogService::error('Erreur lors de la mise à jour multiple des passages', [
|
||||
'sector_id' => $sectorId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
@@ -1680,7 +1678,7 @@ class SectorController
|
||||
$deleteStmt->execute($toDelete);
|
||||
$counters['passages_deleted'] += count($toDelete);
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de la suppression multiple des passages', [
|
||||
LogService::error('Erreur lors de la suppression multiple des passages', [
|
||||
'sector_id' => $sectorId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
@@ -1688,7 +1686,7 @@ class SectorController
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->logService->warning('[updatePassagesForSector] Pas de création de passages', [
|
||||
LogService::warning('[updatePassagesForSector] Pas de création de passages', [
|
||||
'reason' => !$firstUserId ? 'Pas d\'utilisateur affecté' : 'Pas d\'adresses',
|
||||
'first_user_id' => $firstUserId,
|
||||
'nb_addresses' => count($addresses)
|
||||
@@ -1697,14 +1695,14 @@ class SectorController
|
||||
|
||||
|
||||
// Retourner les compteurs détaillés
|
||||
$this->logService->info('[updatePassagesForSector] Fin traitement', [
|
||||
LogService::info('[updatePassagesForSector] Fin traitement', [
|
||||
'sector_id' => $sectorId,
|
||||
'counters' => $counters
|
||||
]);
|
||||
return $counters;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de la mise à jour des passages', [
|
||||
LogService::error('Erreur lors de la mise à jour des passages', [
|
||||
'sector_id' => $sectorId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
@@ -196,7 +196,8 @@ class StripeController extends Controller {
|
||||
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 = ?
|
||||
JOIN ope_users ou ON p.fk_user = ou.id
|
||||
WHERE p.id = ? AND ou.fk_user = ?
|
||||
');
|
||||
$stmt->execute([$passageId, Session::getUserId()]);
|
||||
$passage = $stmt->fetch();
|
||||
@@ -468,71 +469,7 @@ class StripeController extends Controller {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/devices/check-tap-to-pay
|
||||
* Vérifier la compatibilité Tap to Pay d'un appareil
|
||||
*/
|
||||
public function checkTapToPayCapability(): void {
|
||||
try {
|
||||
$data = $this->getJsonInput();
|
||||
|
||||
$platform = $data['platform'] ?? '';
|
||||
|
||||
if ($platform === 'ios') {
|
||||
// Pour iOS, on vérifie côté client (iPhone XS+ avec iOS 16.4+)
|
||||
$this->sendSuccess([
|
||||
'message' => 'Vérification iOS à faire côté client',
|
||||
'requirements' => 'iPhone XS ou plus récent avec iOS 16.4+',
|
||||
'details' => 'iOS 16.4 minimum requis pour le support PIN complet'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($platform === 'android') {
|
||||
$manufacturer = $data['manufacturer'] ?? '';
|
||||
$model = $data['model'] ?? '';
|
||||
|
||||
if (!$manufacturer || !$model) {
|
||||
$this->sendError('Manufacturer et model requis pour Android', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->stripeService->checkAndroidTapToPayCompatibility($manufacturer, $model);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->sendSuccess($result);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
} else {
|
||||
$this->sendError('Platform doit être ios ou android', 400);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/stripe/devices/certified-android
|
||||
* Récupérer la liste des appareils Android certifiés
|
||||
*/
|
||||
public function getCertifiedAndroidDevices(): void {
|
||||
try {
|
||||
$result = $this->stripeService->getCertifiedAndroidDevices();
|
||||
|
||||
if ($result['success']) {
|
||||
$this->sendSuccess(['devices' => $result['devices']]);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/stripe/config
|
||||
* Récupérer la configuration publique Stripe
|
||||
@@ -784,4 +721,117 @@ class StripeController extends Controller {
|
||||
$this->sendError('Erreur lors de la création de la location: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/terminal/connection-token
|
||||
* Créer un Connection Token pour Stripe Terminal/Tap to Pay
|
||||
* Requis par le SDK Stripe Terminal pour se connecter aux readers
|
||||
*/
|
||||
public function createConnectionToken(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
$data = $this->getJsonInput();
|
||||
$entiteId = $data['amicale_id'] ?? Session::getEntityId();
|
||||
|
||||
if (!$entiteId) {
|
||||
$this->sendError('ID entité requis', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier les droits sur cette entité
|
||||
$userRole = Session::getRole() ?? 0;
|
||||
if (Session::getEntityId() != $entiteId && $userRole < 3) {
|
||||
$this->sendError('Non autorisé pour cette entité', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->stripeService->createConnectionToken($entiteId);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->sendSuccess([
|
||||
'secret' => $result['secret']
|
||||
]);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur lors de la création du connection token: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/payments/cancel
|
||||
* Annuler un PaymentIntent Stripe
|
||||
*
|
||||
* Payload:
|
||||
* {
|
||||
* "payment_intent_id": "pi_3SWvho2378xpV4Rn1J43Ks7M"
|
||||
* }
|
||||
*/
|
||||
public function cancelPayment(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
$data = $this->getJsonInput();
|
||||
|
||||
// Validation
|
||||
if (!isset($data['payment_intent_id'])) {
|
||||
$this->sendError('payment_intent_id requis', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$paymentIntentId = $data['payment_intent_id'];
|
||||
|
||||
// Vérifier que le passage existe et appartient à l'utilisateur
|
||||
$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.stripe_payment_id = ?
|
||||
');
|
||||
$stmt->execute([$paymentIntentId]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if (!$passage) {
|
||||
$this->sendError('Paiement non trouvé', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier les droits
|
||||
$userId = Session::getUserId();
|
||||
$userEntityId = Session::getEntityId();
|
||||
|
||||
if ($passage['ope_user_id'] != $userId && $passage['fk_entite'] != $userEntityId) {
|
||||
$this->sendError('Non autorisé', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// Annuler le PaymentIntent via StripeService
|
||||
$result = $this->stripeService->cancelPaymentIntent($paymentIntentId);
|
||||
|
||||
if ($result['success']) {
|
||||
// Retirer le stripe_payment_id du passage
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE ope_pass
|
||||
SET stripe_payment_id = NULL, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([$passage['id']]);
|
||||
|
||||
$this->sendSuccess([
|
||||
'status' => 'canceled',
|
||||
'payment_intent_id' => $paymentIntentId,
|
||||
'passage_id' => $passage['id']
|
||||
]);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,85 +202,46 @@ class StripeWebhookController extends Controller {
|
||||
* Gérer un paiement réussi
|
||||
*/
|
||||
private function handlePaymentIntentSucceeded($paymentIntent): void {
|
||||
// Mettre à jour le statut en base
|
||||
$stmt = $this->db->prepare(
|
||||
"UPDATE stripe_payment_intents
|
||||
SET status = :status, updated_at = NOW()
|
||||
WHERE stripe_payment_intent_id = :pi_id"
|
||||
);
|
||||
$stmt->execute([
|
||||
'status' => 'succeeded',
|
||||
'pi_id' => $paymentIntent->id
|
||||
]);
|
||||
|
||||
// Enregistrer dans l'historique
|
||||
$stmt = $this->db->prepare(
|
||||
"SELECT id FROM stripe_payment_intents WHERE stripe_payment_intent_id = :pi_id"
|
||||
);
|
||||
// Pour Tap to Pay, le paiement est instantané et déjà enregistré dans ope_pass
|
||||
// Ce webhook sert principalement pour les Payment Links (QR Code) asynchrones
|
||||
|
||||
// Vérifier si le passage existe et mettre à jour si nécessaire
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT id FROM ope_pass
|
||||
WHERE stripe_payment_id = :pi_id
|
||||
");
|
||||
$stmt->execute(['pi_id' => $paymentIntent->id]);
|
||||
$localPayment = $stmt->fetch();
|
||||
|
||||
if ($localPayment) {
|
||||
$stmt = $this->db->prepare(
|
||||
"INSERT INTO stripe_payment_history
|
||||
(fk_payment_intent, event_type, event_data, created_at)
|
||||
VALUES (:fk_pi, 'succeeded', :data, NOW())"
|
||||
);
|
||||
$stmt->execute([
|
||||
'fk_pi' => $localPayment['id'],
|
||||
'data' => json_encode([
|
||||
'amount' => $paymentIntent->amount,
|
||||
'currency' => $paymentIntent->currency,
|
||||
'payment_method' => $paymentIntent->payment_method,
|
||||
'charges' => $paymentIntent->charges->data
|
||||
])
|
||||
]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if ($passage) {
|
||||
error_log("Payment succeeded: {$paymentIntent->id} for {$paymentIntent->amount} {$paymentIntent->currency} - Passage {$passage['id']}");
|
||||
|
||||
// TODO: Envoyer un reçu par email
|
||||
// TODO: Mettre à jour les statistiques en temps réel
|
||||
} else {
|
||||
error_log("Payment succeeded: {$paymentIntent->id} but no passage found in ope_pass");
|
||||
}
|
||||
|
||||
// TODO: Envoyer un reçu par email
|
||||
// TODO: Mettre à jour les statistiques en temps réel
|
||||
|
||||
error_log("Payment succeeded: {$paymentIntent->id} for {$paymentIntent->amount} {$paymentIntent->currency}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gérer un paiement échoué
|
||||
*/
|
||||
private function handlePaymentIntentFailed($paymentIntent): void {
|
||||
// Mettre à jour le statut
|
||||
$stmt = $this->db->prepare(
|
||||
"UPDATE stripe_payment_intents
|
||||
SET status = :status, updated_at = NOW()
|
||||
WHERE stripe_payment_intent_id = :pi_id"
|
||||
);
|
||||
$stmt->execute([
|
||||
'status' => 'failed',
|
||||
'pi_id' => $paymentIntent->id
|
||||
]);
|
||||
|
||||
// Enregistrer dans l'historique avec la raison de l'échec
|
||||
$stmt = $this->db->prepare(
|
||||
"SELECT id FROM stripe_payment_intents WHERE stripe_payment_intent_id = :pi_id"
|
||||
);
|
||||
// Vérifier si le passage existe
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT id FROM ope_pass
|
||||
WHERE stripe_payment_id = :pi_id
|
||||
");
|
||||
$stmt->execute(['pi_id' => $paymentIntent->id]);
|
||||
$localPayment = $stmt->fetch();
|
||||
|
||||
if ($localPayment) {
|
||||
$stmt = $this->db->prepare(
|
||||
"INSERT INTO stripe_payment_history
|
||||
(fk_payment_intent, event_type, event_data, created_at)
|
||||
VALUES (:fk_pi, 'failed', :data, NOW())"
|
||||
);
|
||||
$stmt->execute([
|
||||
'fk_pi' => $localPayment['id'],
|
||||
'data' => json_encode([
|
||||
'error' => $paymentIntent->last_payment_error,
|
||||
'cancellation_reason' => $paymentIntent->cancellation_reason
|
||||
])
|
||||
]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if ($passage) {
|
||||
// Optionnel : Marquer le passage comme échec ou supprimer le stripe_payment_id
|
||||
// Pour l'instant on log seulement
|
||||
error_log("Payment failed: {$paymentIntent->id} for passage {$passage['id']}, reason: " . json_encode($paymentIntent->last_payment_error));
|
||||
} else {
|
||||
error_log("Payment failed: {$paymentIntent->id}, reason: " . json_encode($paymentIntent->last_payment_error));
|
||||
}
|
||||
|
||||
error_log("Payment failed: {$paymentIntent->id}, reason: " . json_encode($paymentIntent->last_payment_error));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,17 +319,8 @@ class StripeWebhookController extends Controller {
|
||||
* Gérer une action réussie sur un Terminal reader
|
||||
*/
|
||||
private function handleTerminalReaderActionSucceeded($reader): void {
|
||||
// Mettre à jour le statut du reader
|
||||
$stmt = $this->db->prepare(
|
||||
"UPDATE stripe_terminal_readers
|
||||
SET status = :status, last_seen_at = NOW()
|
||||
WHERE stripe_reader_id = :reader_id"
|
||||
);
|
||||
$stmt->execute([
|
||||
'status' => 'online',
|
||||
'reader_id' => $reader->id
|
||||
]);
|
||||
|
||||
// Note: Pour Tap to Pay, il n'y a pas de readers physiques
|
||||
// Ce webhook concerne uniquement les terminaux externes (non utilisés pour l'instant)
|
||||
error_log("Terminal reader action succeeded: {$reader->id}");
|
||||
}
|
||||
|
||||
@@ -376,17 +328,8 @@ class StripeWebhookController extends Controller {
|
||||
* Gérer une action échouée sur un Terminal reader
|
||||
*/
|
||||
private function handleTerminalReaderActionFailed($reader): void {
|
||||
// Mettre à jour le statut du reader
|
||||
$stmt = $this->db->prepare(
|
||||
"UPDATE stripe_terminal_readers
|
||||
SET status = :status, last_seen_at = NOW()
|
||||
WHERE stripe_reader_id = :reader_id"
|
||||
);
|
||||
$stmt->execute([
|
||||
'status' => 'error',
|
||||
'reader_id' => $reader->id
|
||||
]);
|
||||
|
||||
// Note: Pour Tap to Pay, il n'y a pas de readers physiques
|
||||
// Ce webhook concerne uniquement les terminaux externes (non utilisés pour l'instant)
|
||||
error_log("Terminal reader action failed: {$reader->id}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user