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:
2026-01-16 14:11:15 +01:00
parent 7b78037175
commit 232940b1eb
196 changed files with 8483 additions and 7966 deletions

View File

@@ -94,30 +94,7 @@ Ce dossier contient les scripts automatisés de maintenance et de traitement pou
---
### 5. `update_stripe_devices.php`
**Fonction** : Met à jour la liste des appareils Android certifiés pour Tap to Pay
**Caractéristiques** :
- Liste de 95+ devices intégrée
- Ajoute les nouveaux appareils certifiés
- Met à jour les versions Android minimales
- Désactive les appareils obsolètes
- Notification email si changements importants
- Possibilité de personnaliser via `/data/stripe_certified_devices.json`
**Fréquence recommandée** : Hebdomadaire le dimanche à 3h
**Ligne crontab** :
```bash
0 3 * * 0 /usr/bin/php /var/www/geosector/api/scripts/cron/update_stripe_devices.php >> /var/www/geosector/api/logs/stripe_devices.log 2>&1
```
---
### 6. `sync_databases.php`
### 5. `sync_databases.php`
**Fonction** : Synchronise les bases de données entre environnements
@@ -175,9 +152,6 @@ crontab -e
# Rotation des logs événements (mensuel le 1er à 3h)
0 3 1 * * /usr/bin/php /var/www/geosector/api/scripts/cron/rotate_event_logs.php >> /var/www/geosector/api/logs/rotation_events.log 2>&1
# Mise à jour des devices Stripe (hebdomadaire dimanche à 3h)
0 3 * * 0 /usr/bin/php /var/www/geosector/api/scripts/cron/update_stripe_devices.php >> /var/www/geosector/api/logs/stripe_devices.log 2>&1
```
### 4. Vérifier que les CRONs sont actifs
@@ -203,7 +177,6 @@ Tous les logs CRON sont stockés dans `/var/www/geosector/api/logs/` :
- `cleanup_security.log` : Nettoyage des données de sécurité
- `cleanup_logs.log` : Nettoyage des anciens fichiers logs
- `rotation_events.log` : Rotation des logs événements JSONL
- `stripe_devices.log` : Mise à jour des devices Tap to Pay
### Vérification de l'exécution
@@ -216,9 +189,6 @@ tail -n 50 /var/www/geosector/api/logs/cleanup_logs.log
# Voir les dernières rotations des logs événements
tail -n 50 /var/www/geosector/api/logs/rotation_events.log
# Voir les dernières mises à jour Stripe
tail -n 50 /var/www/geosector/api/logs/stripe_devices.log
```
---

View File

@@ -0,0 +1,456 @@
#!/usr/bin/env php
<?php
/**
* Script CRON pour agrégation des statistiques d'événements
*
* Parse les fichiers JSONL et agrège les données dans event_stats_daily
*
* Usage:
* php aggregate_event_stats.php # Agrège J-1
* php aggregate_event_stats.php --date=2025-12-20 # Agrège une date spécifique
* php aggregate_event_stats.php --from=2025-12-01 --to=2025-12-21 # Rattrapage plage
*
* À exécuter quotidiennement via crontab (1h du matin) :
* 0 1 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/aggregate_event_stats.php >> /var/www/geosector/api/logs/aggregate_stats.log 2>&1
*/
declare(strict_types=1);
// Configuration
define('LOCK_FILE', '/tmp/aggregate_event_stats.lock');
define('EVENT_LOG_DIR', __DIR__ . '/../../logs/events');
// Empêcher l'exécution multiple simultanée
if (file_exists(LOCK_FILE)) {
$lockTime = filemtime(LOCK_FILE);
if (time() - $lockTime > 3600) {
unlink(LOCK_FILE);
} else {
die("[" . date('Y-m-d H:i:s') . "] Le processus est déjà en cours d'exécution\n");
}
}
file_put_contents(LOCK_FILE, (string) getmypid());
register_shutdown_function(function () {
if (file_exists(LOCK_FILE)) {
unlink(LOCK_FILE);
}
});
// Simuler l'environnement web pour AppConfig en CLI
if (php_sapi_name() === 'cli') {
$hostname = gethostname();
if (strpos($hostname, 'pra') !== false) {
$_SERVER['SERVER_NAME'] = 'app3.geosector.fr';
} elseif (strpos($hostname, 'rca') !== false) {
$_SERVER['SERVER_NAME'] = 'rapp.geosector.fr';
} else {
$_SERVER['SERVER_NAME'] = 'dapp.geosector.fr';
}
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
if (!function_exists('getallheaders')) {
function getallheaders()
{
return [];
}
}
}
// Chargement de l'environnement
require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/../../src/Config/AppConfig.php';
require_once __DIR__ . '/../../src/Core/Database.php';
require_once __DIR__ . '/../../src/Services/LogService.php';
use App\Services\LogService;
/**
* Parse les arguments CLI
*/
function parseArgs(array $argv): array
{
$args = [
'date' => null,
'from' => null,
'to' => null,
];
foreach ($argv as $arg) {
if (strpos($arg, '--date=') === 0) {
$args['date'] = substr($arg, 7);
} elseif (strpos($arg, '--from=') === 0) {
$args['from'] = substr($arg, 7);
} elseif (strpos($arg, '--to=') === 0) {
$args['to'] = substr($arg, 5);
}
}
return $args;
}
/**
* Génère la liste des dates à traiter
*/
function getDatesToProcess(array $args): array
{
$dates = [];
if ($args['date']) {
$dates[] = $args['date'];
} elseif ($args['from'] && $args['to']) {
$current = new DateTime($args['from']);
$end = new DateTime($args['to']);
while ($current <= $end) {
$dates[] = $current->format('Y-m-d');
$current->modify('+1 day');
}
} else {
// Par défaut : J-1
$dates[] = date('Y-m-d', strtotime('-1 day'));
}
return $dates;
}
/**
* Parse un fichier JSONL et retourne les événements
*/
function parseJsonlFile(string $filePath): array
{
$events = [];
if (!file_exists($filePath)) {
return $events;
}
$handle = fopen($filePath, 'r');
if (!$handle) {
return $events;
}
while (($line = fgets($handle)) !== false) {
$line = trim($line);
if (empty($line)) {
continue;
}
$event = json_decode($line, true);
if ($event && isset($event['event'])) {
$events[] = $event;
}
}
fclose($handle);
return $events;
}
/**
* Agrège les événements par entity_id et event_type
*/
function aggregateEvents(array $events): array
{
$stats = [];
foreach ($events as $event) {
$entityId = $event['entity_id'] ?? null;
$eventType = $event['event'] ?? 'unknown';
$userId = $event['user_id'] ?? null;
// Clé d'agrégation : entity_id peut être NULL (stats globales)
$key = ($entityId ?? 'NULL') . '|' . $eventType;
if (!isset($stats[$key])) {
$stats[$key] = [
'entity_id' => $entityId,
'event_type' => $eventType,
'count' => 0,
'sum_amount' => 0.0,
'user_ids' => [],
'metadata' => [],
];
}
$stats[$key]['count']++;
// Collecter les user_ids pour unique_users
if ($userId !== null) {
$stats[$key]['user_ids'][$userId] = true;
}
// Somme des montants pour les passages et paiements Stripe
if (in_array($eventType, ['passage_created', 'passage_updated'])) {
$amount = $event['amount'] ?? 0;
$stats[$key]['sum_amount'] += (float) $amount;
} elseif (in_array($eventType, ['stripe_payment_success', 'stripe_payment_created'])) {
// Montant en centimes -> euros
$amount = ($event['amount'] ?? 0) / 100;
$stats[$key]['sum_amount'] += (float) $amount;
}
// Collecter metadata spécifiques
collectMetadata($stats[$key], $event);
}
// Convertir user_ids en count
foreach ($stats as &$stat) {
$stat['unique_users'] = count($stat['user_ids']);
unset($stat['user_ids']);
// Finaliser les metadata
$stat['metadata'] = finalizeMetadata($stat['metadata'], $stat['event_type']);
}
return $stats;
}
/**
* Collecte les métadonnées spécifiques par type d'événement
*/
function collectMetadata(array &$stat, array $event): void
{
$eventType = $event['event'] ?? '';
switch ($eventType) {
case 'login_failed':
$reason = $event['reason'] ?? 'unknown';
$stat['metadata']['reasons'][$reason] = ($stat['metadata']['reasons'][$reason] ?? 0) + 1;
break;
case 'passage_created':
$sectorId = $event['sector_id'] ?? null;
if ($sectorId) {
$stat['metadata']['sectors'][$sectorId] = ($stat['metadata']['sectors'][$sectorId] ?? 0) + 1;
}
$paymentType = $event['payment_type'] ?? 'unknown';
$stat['metadata']['payment_types'][$paymentType] = ($stat['metadata']['payment_types'][$paymentType] ?? 0) + 1;
break;
case 'stripe_payment_failed':
$errorCode = $event['error_code'] ?? 'unknown';
$stat['metadata']['error_codes'][$errorCode] = ($stat['metadata']['error_codes'][$errorCode] ?? 0) + 1;
break;
case 'stripe_terminal_error':
$errorCode = $event['error_code'] ?? 'unknown';
$stat['metadata']['error_codes'][$errorCode] = ($stat['metadata']['error_codes'][$errorCode] ?? 0) + 1;
break;
}
}
/**
* Finalise les métadonnées (top 5, tri, etc.)
*/
function finalizeMetadata(array $metadata, string $eventType): ?array
{
if (empty($metadata)) {
return null;
}
$result = [];
// Top 5 secteurs
if (isset($metadata['sectors'])) {
arsort($metadata['sectors']);
$result['top_sectors'] = array_slice($metadata['sectors'], 0, 5, true);
}
// Raisons d'échec login
if (isset($metadata['reasons'])) {
arsort($metadata['reasons']);
$result['failure_reasons'] = $metadata['reasons'];
}
// Types de paiement
if (isset($metadata['payment_types'])) {
arsort($metadata['payment_types']);
$result['payment_types'] = $metadata['payment_types'];
}
// Codes d'erreur
if (isset($metadata['error_codes'])) {
arsort($metadata['error_codes']);
$result['error_codes'] = $metadata['error_codes'];
}
return empty($result) ? null : $result;
}
/**
* Insère ou met à jour les stats dans la base de données
*/
function upsertStats(PDO $db, string $date, array $stats): int
{
$upsertedCount = 0;
$sql = "
INSERT INTO event_stats_daily
(stat_date, entity_id, event_type, count, sum_amount, unique_users, metadata)
VALUES
(:stat_date, :entity_id, :event_type, :count, :sum_amount, :unique_users, :metadata)
ON DUPLICATE KEY UPDATE
count = VALUES(count),
sum_amount = VALUES(sum_amount),
unique_users = VALUES(unique_users),
metadata = VALUES(metadata),
updated_at = CURRENT_TIMESTAMP
";
$stmt = $db->prepare($sql);
foreach ($stats as $stat) {
try {
$stmt->execute([
'stat_date' => $date,
'entity_id' => $stat['entity_id'],
'event_type' => $stat['event_type'],
'count' => $stat['count'],
'sum_amount' => $stat['sum_amount'],
'unique_users' => $stat['unique_users'],
'metadata' => $stat['metadata'] ? json_encode($stat['metadata'], JSON_UNESCAPED_UNICODE) : null,
]);
$upsertedCount++;
} catch (PDOException $e) {
echo " ERREUR insertion {$stat['event_type']}: " . $e->getMessage() . "\n";
}
}
return $upsertedCount;
}
/**
* Génère également les stats globales (entity_id = NULL)
*/
function generateGlobalStats(array $statsByEntity): array
{
$globalStats = [];
foreach ($statsByEntity as $stat) {
$eventType = $stat['event_type'];
if (!isset($globalStats[$eventType])) {
$globalStats[$eventType] = [
'entity_id' => null,
'event_type' => $eventType,
'count' => 0,
'sum_amount' => 0.0,
'unique_users' => 0,
'metadata' => null,
];
}
$globalStats[$eventType]['count'] += $stat['count'];
$globalStats[$eventType]['sum_amount'] += $stat['sum_amount'];
$globalStats[$eventType]['unique_users'] += $stat['unique_users'];
}
return array_values($globalStats);
}
// ============================================================
// MAIN
// ============================================================
try {
echo "[" . date('Y-m-d H:i:s') . "] Démarrage de l'agrégation des statistiques\n";
// Initialisation
$appConfig = AppConfig::getInstance();
$config = $appConfig->getFullConfig();
$environment = $appConfig->getEnvironment();
echo "Environnement: {$environment}\n";
Database::init($config['database']);
$db = Database::getInstance();
// Parser les arguments
$args = parseArgs($argv);
$dates = getDatesToProcess($args);
echo "Dates à traiter: " . implode(', ', $dates) . "\n\n";
$totalStats = 0;
$totalEvents = 0;
foreach ($dates as $date) {
$jsonlFile = EVENT_LOG_DIR . '/' . $date . '.jsonl';
echo "--- Traitement de {$date} ---\n";
if (!file_exists($jsonlFile)) {
echo " Fichier non trouvé: {$jsonlFile}\n";
continue;
}
$fileSize = filesize($jsonlFile);
echo " Fichier: " . basename($jsonlFile) . " (" . number_format($fileSize / 1024, 2) . " KB)\n";
// Parser le fichier
$events = parseJsonlFile($jsonlFile);
$eventCount = count($events);
echo " Événements parsés: {$eventCount}\n";
if ($eventCount === 0) {
echo " Aucun événement à agréger\n";
continue;
}
$totalEvents += $eventCount;
// Agréger par entity/event_type
$stats = aggregateEvents($events);
echo " Agrégations par entité: " . count($stats) . "\n";
// Générer les stats globales
$globalStats = generateGlobalStats($stats);
echo " Agrégations globales: " . count($globalStats) . "\n";
// Fusionner stats entités + globales
$allStats = array_merge(array_values($stats), $globalStats);
// Insérer en base
$upserted = upsertStats($db, $date, $allStats);
echo " Stats insérées/mises à jour: {$upserted}\n";
$totalStats += $upserted;
}
// Résumé
echo "\n=== RÉSUMÉ ===\n";
echo "Dates traitées: " . count($dates) . "\n";
echo "Événements traités: {$totalEvents}\n";
echo "Stats agrégées: {$totalStats}\n";
// Log
LogService::log('Agrégation des statistiques terminée', [
'level' => 'info',
'script' => 'aggregate_event_stats.php',
'environment' => $environment,
'dates_count' => count($dates),
'events_count' => $totalEvents,
'stats_count' => $totalStats,
]);
echo "\n[" . date('Y-m-d H:i:s') . "] Agrégation terminée avec succès\n";
} catch (Exception $e) {
$errorMsg = 'Erreur lors de l\'agrégation: ' . $e->getMessage();
LogService::log($errorMsg, [
'level' => 'error',
'script' => 'aggregate_event_stats.php',
'trace' => $e->getTraceAsString(),
]);
echo "\n❌ ERREUR: {$errorMsg}\n";
echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
exit(1);
}
exit(0);

View File

@@ -1,444 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Script CRON pour mettre à jour la liste des appareils certifiés Stripe Tap to Pay
*
* Ce script récupère et met à jour la liste des appareils Android certifiés
* pour Tap to Pay en France dans la table stripe_android_certified_devices
*
* À exécuter hebdomadairement via crontab :
* Exemple: 0 3 * * 0 /usr/bin/php /path/to/api/scripts/cron/update_stripe_devices.php
*/
declare(strict_types=1);
// Configuration
define('LOCK_FILE', '/tmp/update_stripe_devices.lock');
define('DEVICES_JSON_URL', 'https://raw.githubusercontent.com/stripe/stripe-terminal-android/master/tap-to-pay/certified-devices.json');
define('DEVICES_LOCAL_FILE', __DIR__ . '/../../data/stripe_certified_devices.json');
// Empêcher l'exécution multiple simultanée
if (file_exists(LOCK_FILE)) {
$lockTime = filemtime(LOCK_FILE);
if (time() - $lockTime > 3600) { // Lock de plus d'1 heure = processus bloqué
unlink(LOCK_FILE);
} else {
die("[" . date('Y-m-d H:i:s') . "] Le processus est déjà en cours d'exécution\n");
}
}
// Créer le fichier de lock
file_put_contents(LOCK_FILE, getmypid());
// Enregistrer un handler pour supprimer le lock en cas d'arrêt
register_shutdown_function(function() {
if (file_exists(LOCK_FILE)) {
unlink(LOCK_FILE);
}
});
// Simuler l'environnement web pour AppConfig en CLI
if (php_sapi_name() === 'cli') {
$hostname = gethostname();
if (strpos($hostname, 'prod') !== false || strpos($hostname, 'pra') !== false) {
$_SERVER['SERVER_NAME'] = 'app3.geosector.fr';
} elseif (strpos($hostname, 'rec') !== false || strpos($hostname, 'rca') !== false) {
$_SERVER['SERVER_NAME'] = 'rapp.geosector.fr';
} else {
$_SERVER['SERVER_NAME'] = 'dapp.geosector.fr'; // DVA par défaut
}
$_SERVER['REQUEST_URI'] = '/cron/update_stripe_devices';
$_SERVER['REQUEST_METHOD'] = 'CLI';
$_SERVER['HTTP_HOST'] = $_SERVER['SERVER_NAME'];
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
// Définir getallheaders si elle n'existe pas (CLI)
if (!function_exists('getallheaders')) {
function getallheaders() {
return [];
}
}
}
// Charger l'environnement
require_once dirname(dirname(__DIR__)) . '/vendor/autoload.php';
require_once dirname(dirname(__DIR__)) . '/src/Config/AppConfig.php';
require_once dirname(dirname(__DIR__)) . '/src/Core/Database.php';
require_once dirname(dirname(__DIR__)) . '/src/Services/LogService.php';
use App\Services\LogService;
try {
echo "[" . date('Y-m-d H:i:s') . "] Début de la mise à jour des devices Stripe certifiés\n";
// Initialiser la configuration et la base de données
$appConfig = AppConfig::getInstance();
$dbConfig = $appConfig->getDatabaseConfig();
Database::init($dbConfig);
$db = Database::getInstance();
// Logger le début
LogService::log("Début de la mise à jour des devices Stripe certifiés", [
'source' => 'cron',
'script' => 'update_stripe_devices.php'
]);
// Étape 1: Récupérer la liste des devices
$devicesData = fetchCertifiedDevices();
if (empty($devicesData)) {
echo "[" . date('Y-m-d H:i:s') . "] Aucune donnée de devices récupérée\n";
LogService::log("Aucune donnée de devices récupérée", ['level' => 'warning']);
exit(1);
}
// Étape 2: Traiter et mettre à jour la base de données
$stats = updateDatabase($db, $devicesData);
// Étape 3: Logger les résultats
$message = sprintf(
"Mise à jour terminée : %d ajoutés, %d modifiés, %d désactivés, %d inchangés",
$stats['added'],
$stats['updated'],
$stats['disabled'],
$stats['unchanged']
);
echo "[" . date('Y-m-d H:i:s') . "] $message\n";
LogService::log($message, [
'source' => 'cron',
'stats' => $stats
]);
// Étape 4: Envoyer une notification si changements significatifs
if ($stats['added'] > 0 || $stats['disabled'] > 0) {
sendNotification($stats);
}
echo "[" . date('Y-m-d H:i:s') . "] Mise à jour terminée avec succès\n";
} catch (Exception $e) {
$errorMsg = "Erreur lors de la mise à jour des devices: " . $e->getMessage();
echo "[" . date('Y-m-d H:i:s') . "] $errorMsg\n";
LogService::log($errorMsg, [
'level' => 'error',
'trace' => $e->getTraceAsString()
]);
exit(1);
}
/**
* Récupère la liste des devices certifiés
* Essaie d'abord depuis une URL externe, puis depuis un fichier local en fallback
*/
function fetchCertifiedDevices(): array {
// Liste maintenue manuellement des devices certifiés en France
// Source: Documentation Stripe Terminal et tests confirmés
$frenchCertifiedDevices = [
// Samsung Galaxy S Series
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21', 'model_identifier' => 'SM-G991B', 'min_android_version' => 11],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21+', 'model_identifier' => 'SM-G996B', 'min_android_version' => 11],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21 Ultra', 'model_identifier' => 'SM-G998B', 'min_android_version' => 11],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21 FE', 'model_identifier' => 'SM-G990B', 'min_android_version' => 11],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S22', 'model_identifier' => 'SM-S901B', 'min_android_version' => 12],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S22+', 'model_identifier' => 'SM-S906B', 'min_android_version' => 12],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S22 Ultra', 'model_identifier' => 'SM-S908B', 'min_android_version' => 12],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S23', 'model_identifier' => 'SM-S911B', 'min_android_version' => 13],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S23+', 'model_identifier' => 'SM-S916B', 'min_android_version' => 13],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S23 Ultra', 'model_identifier' => 'SM-S918B', 'min_android_version' => 13],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S23 FE', 'model_identifier' => 'SM-S711B', 'min_android_version' => 13],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S24', 'model_identifier' => 'SM-S921B', 'min_android_version' => 14],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S24+', 'model_identifier' => 'SM-S926B', 'min_android_version' => 14],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S24 Ultra', 'model_identifier' => 'SM-S928B', 'min_android_version' => 14],
// Samsung Galaxy Note
['manufacturer' => 'Samsung', 'model' => 'Galaxy Note 20', 'model_identifier' => 'SM-N980F', 'min_android_version' => 10],
['manufacturer' => 'Samsung', 'model' => 'Galaxy Note 20 Ultra', 'model_identifier' => 'SM-N986B', 'min_android_version' => 10],
// Samsung Galaxy Z Fold
['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Fold3', 'model_identifier' => 'SM-F926B', 'min_android_version' => 11],
['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Fold4', 'model_identifier' => 'SM-F936B', 'min_android_version' => 12],
['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Fold5', 'model_identifier' => 'SM-F946B', 'min_android_version' => 13],
['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Fold6', 'model_identifier' => 'SM-F956B', 'min_android_version' => 14],
// Samsung Galaxy Z Flip
['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Flip3', 'model_identifier' => 'SM-F711B', 'min_android_version' => 11],
['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Flip4', 'model_identifier' => 'SM-F721B', 'min_android_version' => 12],
['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Flip5', 'model_identifier' => 'SM-F731B', 'min_android_version' => 13],
['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Flip6', 'model_identifier' => 'SM-F741B', 'min_android_version' => 14],
// Samsung Galaxy A Series (haut de gamme)
['manufacturer' => 'Samsung', 'model' => 'Galaxy A54', 'model_identifier' => 'SM-A546B', 'min_android_version' => 13],
['manufacturer' => 'Samsung', 'model' => 'Galaxy A73', 'model_identifier' => 'SM-A736B', 'min_android_version' => 12],
// Google Pixel
['manufacturer' => 'Google', 'model' => 'Pixel 6', 'model_identifier' => 'oriole', 'min_android_version' => 12],
['manufacturer' => 'Google', 'model' => 'Pixel 6 Pro', 'model_identifier' => 'raven', 'min_android_version' => 12],
['manufacturer' => 'Google', 'model' => 'Pixel 6a', 'model_identifier' => 'bluejay', 'min_android_version' => 12],
['manufacturer' => 'Google', 'model' => 'Pixel 7', 'model_identifier' => 'panther', 'min_android_version' => 13],
['manufacturer' => 'Google', 'model' => 'Pixel 7 Pro', 'model_identifier' => 'cheetah', 'min_android_version' => 13],
['manufacturer' => 'Google', 'model' => 'Pixel 7a', 'model_identifier' => 'lynx', 'min_android_version' => 13],
['manufacturer' => 'Google', 'model' => 'Pixel 8', 'model_identifier' => 'shiba', 'min_android_version' => 14],
['manufacturer' => 'Google', 'model' => 'Pixel 8 Pro', 'model_identifier' => 'husky', 'min_android_version' => 14],
['manufacturer' => 'Google', 'model' => 'Pixel 8a', 'model_identifier' => 'akita', 'min_android_version' => 14],
['manufacturer' => 'Google', 'model' => 'Pixel 9', 'model_identifier' => 'tokay', 'min_android_version' => 14],
['manufacturer' => 'Google', 'model' => 'Pixel 9 Pro', 'model_identifier' => 'caiman', 'min_android_version' => 14],
['manufacturer' => 'Google', 'model' => 'Pixel 9 Pro XL', 'model_identifier' => 'komodo', 'min_android_version' => 14],
['manufacturer' => 'Google', 'model' => 'Pixel Fold', 'model_identifier' => 'felix', 'min_android_version' => 13],
['manufacturer' => 'Google', 'model' => 'Pixel Tablet', 'model_identifier' => 'tangorpro', 'min_android_version' => 13],
// OnePlus
['manufacturer' => 'OnePlus', 'model' => '9', 'model_identifier' => 'LE2113', 'min_android_version' => 11],
['manufacturer' => 'OnePlus', 'model' => '9 Pro', 'model_identifier' => 'LE2123', 'min_android_version' => 11],
['manufacturer' => 'OnePlus', 'model' => '10 Pro', 'model_identifier' => 'NE2213', 'min_android_version' => 12],
['manufacturer' => 'OnePlus', 'model' => '10T', 'model_identifier' => 'CPH2413', 'min_android_version' => 12],
['manufacturer' => 'OnePlus', 'model' => '11', 'model_identifier' => 'CPH2449', 'min_android_version' => 13],
['manufacturer' => 'OnePlus', 'model' => '11R', 'model_identifier' => 'CPH2487', 'min_android_version' => 13],
['manufacturer' => 'OnePlus', 'model' => '12', 'model_identifier' => 'CPH2581', 'min_android_version' => 14],
['manufacturer' => 'OnePlus', 'model' => '12R', 'model_identifier' => 'CPH2585', 'min_android_version' => 14],
['manufacturer' => 'OnePlus', 'model' => 'Open', 'model_identifier' => 'CPH2551', 'min_android_version' => 13],
// Xiaomi
['manufacturer' => 'Xiaomi', 'model' => 'Mi 11', 'model_identifier' => 'M2011K2G', 'min_android_version' => 11],
['manufacturer' => 'Xiaomi', 'model' => 'Mi 11 Ultra', 'model_identifier' => 'M2102K1G', 'min_android_version' => 11],
['manufacturer' => 'Xiaomi', 'model' => '12', 'model_identifier' => '2201123G', 'min_android_version' => 12],
['manufacturer' => 'Xiaomi', 'model' => '12 Pro', 'model_identifier' => '2201122G', 'min_android_version' => 12],
['manufacturer' => 'Xiaomi', 'model' => '12T Pro', 'model_identifier' => '2207122MC', 'min_android_version' => 12],
['manufacturer' => 'Xiaomi', 'model' => '13', 'model_identifier' => '2211133G', 'min_android_version' => 13],
['manufacturer' => 'Xiaomi', 'model' => '13 Pro', 'model_identifier' => '2210132G', 'min_android_version' => 13],
['manufacturer' => 'Xiaomi', 'model' => '13T Pro', 'model_identifier' => '23078PND5G', 'min_android_version' => 13],
['manufacturer' => 'Xiaomi', 'model' => '14', 'model_identifier' => '23127PN0CG', 'min_android_version' => 14],
['manufacturer' => 'Xiaomi', 'model' => '14 Pro', 'model_identifier' => '23116PN5BG', 'min_android_version' => 14],
['manufacturer' => 'Xiaomi', 'model' => '14 Ultra', 'model_identifier' => '24030PN60G', 'min_android_version' => 14],
// OPPO
['manufacturer' => 'OPPO', 'model' => 'Find X3 Pro', 'model_identifier' => 'CPH2173', 'min_android_version' => 11],
['manufacturer' => 'OPPO', 'model' => 'Find X5 Pro', 'model_identifier' => 'CPH2305', 'min_android_version' => 12],
['manufacturer' => 'OPPO', 'model' => 'Find X6 Pro', 'model_identifier' => 'CPH2449', 'min_android_version' => 13],
['manufacturer' => 'OPPO', 'model' => 'Find N2', 'model_identifier' => 'CPH2399', 'min_android_version' => 13],
['manufacturer' => 'OPPO', 'model' => 'Find N3', 'model_identifier' => 'CPH2499', 'min_android_version' => 13],
// Realme
['manufacturer' => 'Realme', 'model' => 'GT 2 Pro', 'model_identifier' => 'RMX3301', 'min_android_version' => 12],
['manufacturer' => 'Realme', 'model' => 'GT 3', 'model_identifier' => 'RMX3709', 'min_android_version' => 13],
['manufacturer' => 'Realme', 'model' => 'GT 5 Pro', 'model_identifier' => 'RMX3888', 'min_android_version' => 14],
// Honor
['manufacturer' => 'Honor', 'model' => 'Magic5 Pro', 'model_identifier' => 'PGT-N19', 'min_android_version' => 13],
['manufacturer' => 'Honor', 'model' => 'Magic6 Pro', 'model_identifier' => 'BVL-N49', 'min_android_version' => 14],
['manufacturer' => 'Honor', 'model' => '90', 'model_identifier' => 'REA-NX9', 'min_android_version' => 13],
// ASUS
['manufacturer' => 'ASUS', 'model' => 'Zenfone 9', 'model_identifier' => 'AI2202', 'min_android_version' => 12],
['manufacturer' => 'ASUS', 'model' => 'Zenfone 10', 'model_identifier' => 'AI2302', 'min_android_version' => 13],
['manufacturer' => 'ASUS', 'model' => 'ROG Phone 7', 'model_identifier' => 'AI2205', 'min_android_version' => 13],
// Nothing
['manufacturer' => 'Nothing', 'model' => 'Phone (1)', 'model_identifier' => 'A063', 'min_android_version' => 12],
['manufacturer' => 'Nothing', 'model' => 'Phone (2)', 'model_identifier' => 'A065', 'min_android_version' => 13],
['manufacturer' => 'Nothing', 'model' => 'Phone (2a)', 'model_identifier' => 'A142', 'min_android_version' => 14],
];
// Essayer de charger depuis un fichier JSON local si présent
if (file_exists(DEVICES_LOCAL_FILE)) {
$localData = json_decode(file_get_contents(DEVICES_LOCAL_FILE), true);
if (!empty($localData)) {
echo "[" . date('Y-m-d H:i:s') . "] Données chargées depuis le fichier local\n";
return array_merge($frenchCertifiedDevices, $localData);
}
}
echo "[" . date('Y-m-d H:i:s') . "] Utilisation de la liste intégrée des devices certifiés\n";
return $frenchCertifiedDevices;
}
/**
* Met à jour la base de données avec les nouvelles données
*/
function updateDatabase($db, array $devices): array {
$stats = [
'added' => 0,
'updated' => 0,
'disabled' => 0,
'unchanged' => 0,
'total' => 0
];
// Récupérer tous les devices existants
$stmt = $db->prepare("SELECT * FROM stripe_android_certified_devices WHERE country = 'FR'");
$stmt->execute();
$existingDevices = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$key = $row['manufacturer'] . '|' . $row['model'] . '|' . $row['model_identifier'];
$existingDevices[$key] = $row;
}
// Marquer tous les devices pour tracking
$processedKeys = [];
// Traiter chaque device de la nouvelle liste
foreach ($devices as $device) {
$key = $device['manufacturer'] . '|' . $device['model'] . '|' . $device['model_identifier'];
$processedKeys[$key] = true;
if (isset($existingDevices[$key])) {
// Le device existe, vérifier s'il faut le mettre à jour
$existing = $existingDevices[$key];
// Vérifier si des champs ont changé
$needsUpdate = false;
if ($existing['min_android_version'] != $device['min_android_version']) {
$needsUpdate = true;
}
if ($existing['tap_to_pay_certified'] != 1) {
$needsUpdate = true;
}
if ($needsUpdate) {
$stmt = $db->prepare("
UPDATE stripe_android_certified_devices
SET min_android_version = :min_version,
tap_to_pay_certified = 1,
last_verified = NOW(),
updated_at = NOW()
WHERE manufacturer = :manufacturer
AND model = :model
AND model_identifier = :model_identifier
AND country = 'FR'
");
$stmt->execute([
'min_version' => $device['min_android_version'],
'manufacturer' => $device['manufacturer'],
'model' => $device['model'],
'model_identifier' => $device['model_identifier']
]);
$stats['updated']++;
LogService::log("Device mis à jour", [
'device' => $device['manufacturer'] . ' ' . $device['model']
]);
} else {
// Juste mettre à jour last_verified
$stmt = $db->prepare("
UPDATE stripe_android_certified_devices
SET last_verified = NOW()
WHERE manufacturer = :manufacturer
AND model = :model
AND model_identifier = :model_identifier
AND country = 'FR'
");
$stmt->execute([
'manufacturer' => $device['manufacturer'],
'model' => $device['model'],
'model_identifier' => $device['model_identifier']
]);
$stats['unchanged']++;
}
} else {
// Nouveau device, l'ajouter
$stmt = $db->prepare("
INSERT INTO stripe_android_certified_devices
(manufacturer, model, model_identifier, tap_to_pay_certified,
certification_date, min_android_version, country, notes, last_verified)
VALUES
(:manufacturer, :model, :model_identifier, 1,
NOW(), :min_version, 'FR', 'Ajouté automatiquement via CRON', NOW())
");
$stmt->execute([
'manufacturer' => $device['manufacturer'],
'model' => $device['model'],
'model_identifier' => $device['model_identifier'],
'min_version' => $device['min_android_version']
]);
$stats['added']++;
LogService::log("Nouveau device ajouté", [
'device' => $device['manufacturer'] . ' ' . $device['model']
]);
}
}
// Désactiver les devices qui ne sont plus dans la liste
foreach ($existingDevices as $key => $existing) {
if (!isset($processedKeys[$key]) && $existing['tap_to_pay_certified'] == 1) {
$stmt = $db->prepare("
UPDATE stripe_android_certified_devices
SET tap_to_pay_certified = 0,
notes = CONCAT(IFNULL(notes, ''), ' | Désactivé le ', NOW(), ' (non présent dans la mise à jour)'),
updated_at = NOW()
WHERE id = :id
");
$stmt->execute(['id' => $existing['id']]);
$stats['disabled']++;
LogService::log("Device désactivé", [
'device' => $existing['manufacturer'] . ' ' . $existing['model'],
'reason' => 'Non présent dans la liste mise à jour'
]);
}
}
$stats['total'] = count($devices);
return $stats;
}
/**
* Envoie une notification email aux administrateurs si changements importants
*/
function sendNotification(array $stats): void {
try {
// Récupérer la configuration
$appConfig = AppConfig::getInstance();
$emailConfig = $appConfig->getEmailConfig();
if (empty($emailConfig['admin_email'])) {
return; // Pas d'email admin configuré
}
$db = Database::getInstance();
// Préparer le contenu de l'email
$subject = "Mise à jour des devices Stripe Tap to Pay";
$body = "Bonjour,\n\n";
$body .= "La mise à jour automatique de la liste des appareils certifiés Stripe Tap to Pay a été effectuée.\n\n";
$body .= "Résumé des changements :\n";
$body .= "- Nouveaux appareils ajoutés : " . $stats['added'] . "\n";
$body .= "- Appareils mis à jour : " . $stats['updated'] . "\n";
$body .= "- Appareils désactivés : " . $stats['disabled'] . "\n";
$body .= "- Appareils inchangés : " . $stats['unchanged'] . "\n";
$body .= "- Total d'appareils traités : " . $stats['total'] . "\n\n";
if ($stats['added'] > 0) {
$body .= "Les nouveaux appareils ont été automatiquement ajoutés à la base de données.\n";
}
if ($stats['disabled'] > 0) {
$body .= "Certains appareils ont été désactivés car ils ne sont plus certifiés.\n";
}
$body .= "\nConsultez les logs pour plus de détails.\n";
$body .= "\nCordialement,\nLe système GeoSector";
// Insérer dans la queue d'emails
$stmt = $db->prepare("
INSERT INTO email_queue
(to_email, subject, body, status, created_at, attempts)
VALUES
(:to_email, :subject, :body, 'pending', NOW(), 0)
");
$stmt->execute([
'to_email' => $emailConfig['admin_email'],
'subject' => $subject,
'body' => $body
]);
echo "[" . date('Y-m-d H:i:s') . "] Notification ajoutée à la queue d'emails\n";
} catch (Exception $e) {
// Ne pas faire échouer le script si l'email ne peut pas être envoyé
echo "[" . date('Y-m-d H:i:s') . "] Impossible d'envoyer la notification: " . $e->getMessage() . "\n";
}
}

View File

@@ -0,0 +1,19 @@
-- Migration: Modifier fk_sector pour avoir DEFAULT NULL au lieu de DEFAULT 0
-- Raison: La FK vers ope_sectors(id) ne permet pas la valeur 0 (aucun secteur avec id=0)
-- Date: 2026-01-16
-- 1. D'abord, mettre à NULL les passages qui ont fk_sector = 0
UPDATE ope_pass SET fk_sector = NULL WHERE fk_sector = 0;
-- 2. Modifier la colonne pour avoir DEFAULT NULL
ALTER TABLE ope_pass MODIFY COLUMN fk_sector INT UNSIGNED DEFAULT NULL;
-- Vérification
SELECT
COLUMN_NAME,
COLUMN_DEFAULT,
IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ope_pass'
AND COLUMN_NAME = 'fk_sector';

View File

@@ -0,0 +1,57 @@
-- ============================================================
-- Table event_stats_daily - Statistiques d'événements agrégées
-- Version: 1.0
-- Date: 2025-12-22
-- ============================================================
--
-- Usage: Exécuter dans les 3 environnements (DEV, REC, PROD)
-- mysql -u user -p database < create_event_stats_daily.sql
--
-- Description:
-- Stocke les statistiques quotidiennes agrégées depuis les
-- fichiers JSONL (/logs/events/YYYY-MM-DD.jsonl)
-- Alimentée par le CRON aggregate_event_stats.php (1x/nuit)
--
-- ============================================================
CREATE TABLE IF NOT EXISTS event_stats_daily (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
-- Clés d'agrégation
stat_date DATE NOT NULL COMMENT 'Date des statistiques',
entity_id INT UNSIGNED NULL COMMENT 'ID entité (NULL = stats globales super-admin)',
event_type VARCHAR(50) NOT NULL COMMENT 'Type événement (login_success, passage_created, etc.)',
-- Compteurs
count INT UNSIGNED DEFAULT 0 COMMENT 'Nombre d\'occurrences',
sum_amount DECIMAL(12,2) DEFAULT 0.00 COMMENT 'Somme des montants (passages/paiements)',
unique_users INT UNSIGNED DEFAULT 0 COMMENT 'Nombre d\'utilisateurs distincts',
-- Métadonnées agrégées (JSON)
metadata JSON NULL COMMENT 'Données agrégées: top secteurs, erreurs fréquentes, etc.',
-- Timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Contraintes
UNIQUE KEY uk_date_entity_event (stat_date, entity_id, event_type),
INDEX idx_entity_date (entity_id, stat_date),
INDEX idx_date (stat_date),
INDEX idx_event_type (event_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Statistiques quotidiennes agrégées des événements (EventLogService)';
-- ============================================================
-- Exemples de données attendues
-- ============================================================
--
-- | stat_date | entity_id | event_type | count | sum_amount | unique_users |
-- |------------|-----------|------------------|-------|------------|--------------|
-- | 2025-12-22 | 5 | login_success | 45 | 0.00 | 12 |
-- | 2025-12-22 | 5 | passage_created | 128 | 2450.00 | 8 |
-- | 2025-12-22 | 5 | stripe_payment_success | 12 | 890.00 | 6 |
-- | 2025-12-22 | NULL | login_success | 320 | 0.00 | 85 | <- Global
--
-- ============================================================

View File

@@ -0,0 +1,5 @@
-- Suppression de la table stripe_android_certified_devices
-- Cette table n'est plus utilisée : la vérification de compatibilité Tap to Pay
-- se fait maintenant directement côté client via le SDK Stripe Terminal
DROP TABLE IF EXISTS stripe_android_certified_devices;