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:
176
api/scripts/migration2/php/lib/DataMigrator.php
Normal file
176
api/scripts/migration2/php/lib/DataMigrator.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Classe abstraite de base pour tous les migrators
|
||||
*
|
||||
* Fournit les méthodes communes pour migrer des données d'une table
|
||||
*/
|
||||
abstract class DataMigrator
|
||||
{
|
||||
protected $connection;
|
||||
protected $logger;
|
||||
protected $sourceDb;
|
||||
protected $targetDb;
|
||||
|
||||
/**
|
||||
* Constructeur
|
||||
*
|
||||
* @param DatabaseConnection $connection Connexion aux bases
|
||||
* @param MigrationLogger $logger Logger
|
||||
*/
|
||||
public function __construct(DatabaseConnection $connection, MigrationLogger $logger)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->logger = $logger;
|
||||
$this->sourceDb = $connection->getSourceDb();
|
||||
$this->targetDb = $connection->getTargetDb();
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode principale de migration (à implémenter dans chaque migrator)
|
||||
*
|
||||
* @param int|null $entityId ID de l'entité à migrer (null = toutes)
|
||||
* @param bool $deleteBefore Supprimer les données existantes avant migration
|
||||
* @return array ['success' => int, 'errors' => int]
|
||||
*/
|
||||
abstract public function migrate(?int $entityId = null, bool $deleteBefore = false): array;
|
||||
|
||||
/**
|
||||
* Retourne le nom de la table gérée par ce migrator
|
||||
*/
|
||||
abstract public function getTableName(): string;
|
||||
|
||||
/**
|
||||
* Supprime les données d'une entité dans la cible
|
||||
* À surcharger si la logique de suppression est spécifique
|
||||
*
|
||||
* @param int $entityId ID de l'entité
|
||||
* @return int Nombre de lignes supprimées
|
||||
*/
|
||||
protected function deleteEntityData(int $entityId): int
|
||||
{
|
||||
$table = $this->getTableName();
|
||||
|
||||
try {
|
||||
// Par défaut: suppression simple avec fk_entite
|
||||
$stmt = $this->targetDb->prepare("DELETE FROM $table WHERE fk_entite = ?");
|
||||
$stmt->execute([$entityId]);
|
||||
$deleted = $stmt->rowCount();
|
||||
|
||||
if ($deleted > 0) {
|
||||
$this->logger->debug(" Supprimé $deleted ligne(s) de $table pour entité #$entityId");
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$this->logger->warning(" Erreur suppression $table: " . $e->getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte les lignes dans la source
|
||||
*
|
||||
* @param int|null $entityId ID de l'entité (null = toutes)
|
||||
* @return int Nombre de lignes
|
||||
*/
|
||||
protected function countSourceRows(?int $entityId = null): int
|
||||
{
|
||||
return $this->connection->countSourceRows($this->getTableName(), $entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte les lignes dans la cible
|
||||
*
|
||||
* @param int|null $entityId ID de l'entité (null = toutes)
|
||||
* @return int Nombre de lignes
|
||||
*/
|
||||
protected function countTargetRows(?int $entityId = null): int
|
||||
{
|
||||
return $this->connection->countTargetRows($this->getTableName(), $entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log le début de la migration d'une table
|
||||
*/
|
||||
protected function logStart(?int $entityId = null): void
|
||||
{
|
||||
$table = $this->getTableName();
|
||||
$entityStr = $entityId ? " pour entité #$entityId" : " (toutes les entités)";
|
||||
$this->logger->info("🔄 Migration de $table{$entityStr}...");
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la fin de la migration avec statistiques
|
||||
*
|
||||
* @param int $success Nombre de succès
|
||||
* @param int $errors Nombre d'erreurs
|
||||
* @param int|null $entityId ID de l'entité
|
||||
*/
|
||||
protected function logEnd(int $success, int $errors, ?int $entityId = null): void
|
||||
{
|
||||
$table = $this->getTableName();
|
||||
$sourceCount = $this->countSourceRows($entityId);
|
||||
$targetCount = $this->countTargetRows($entityId);
|
||||
$diff = $targetCount - $sourceCount;
|
||||
$diffStr = $diff >= 0 ? "+$diff" : "$diff";
|
||||
|
||||
if ($errors > 0) {
|
||||
$this->logger->warning(" ⚠️ $table: $success succès, $errors erreurs");
|
||||
} else {
|
||||
$this->logger->success(" ✓ $table: $success enregistrement(s) migré(s)");
|
||||
}
|
||||
|
||||
$this->logger->info(" 📊 SOURCE: $sourceCount → CIBLE: $targetCount (différence: $diffStr)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute une requête INSERT avec ON DUPLICATE KEY UPDATE
|
||||
*
|
||||
* @param string $insertSql SQL d'insertion
|
||||
* @param array $data Données à insérer
|
||||
* @return bool True si succès
|
||||
*/
|
||||
protected function insertOrUpdate(string $insertSql, array $data): bool
|
||||
{
|
||||
try {
|
||||
$stmt = $this->targetDb->prepare($insertSql);
|
||||
$stmt->execute($data);
|
||||
return true;
|
||||
} catch (PDOException $e) {
|
||||
$this->logger->debug(" Erreur INSERT: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Démarre une transaction sur la cible
|
||||
*/
|
||||
protected function beginTransaction(): void
|
||||
{
|
||||
if (!$this->targetDb->inTransaction()) {
|
||||
$this->targetDb->beginTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit la transaction
|
||||
*/
|
||||
protected function commit(): void
|
||||
{
|
||||
if ($this->targetDb->inTransaction()) {
|
||||
$this->targetDb->commit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback la transaction
|
||||
*/
|
||||
protected function rollback(): void
|
||||
{
|
||||
if ($this->targetDb->inTransaction()) {
|
||||
$this->targetDb->rollBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
192
api/scripts/migration2/php/lib/DatabaseConfig.php
Normal file
192
api/scripts/migration2/php/lib/DatabaseConfig.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Configuration des environnements de migration
|
||||
*
|
||||
* Utilise AppConfig pour récupérer la configuration DB
|
||||
* Source: geosector (synchronisée par PM7)
|
||||
* Cibles: dva_geo (IN3/maria3), rca_geo (IN3/maria3) ou pra_geo (IN4/maria4)
|
||||
*/
|
||||
class DatabaseConfig
|
||||
{
|
||||
private const ENV_MAPPING = [
|
||||
'dva' => [
|
||||
'name' => 'DÉVELOPPEMENT',
|
||||
'hostname' => 'dapp.geosector.fr',
|
||||
'source_db' => 'geosector',
|
||||
'target_db' => 'dva_geo'
|
||||
],
|
||||
'rca' => [
|
||||
'name' => 'RECETTE',
|
||||
'hostname' => 'rapp.geosector.fr',
|
||||
'source_db' => 'geosector',
|
||||
'target_db' => 'rca_geo'
|
||||
],
|
||||
'pra' => [
|
||||
'name' => 'PRODUCTION',
|
||||
'hostname' => 'app3.geosector.fr',
|
||||
'source_db' => 'geosector',
|
||||
'target_db' => 'pra_geo'
|
||||
]
|
||||
];
|
||||
|
||||
private $env;
|
||||
private $config;
|
||||
private $appConfig;
|
||||
|
||||
/**
|
||||
* Constructeur
|
||||
*
|
||||
* @param string $env Environnement: 'dva', 'rca' ou 'pra'
|
||||
* @throws Exception Si l'environnement est invalide
|
||||
*/
|
||||
public function __construct(string $env)
|
||||
{
|
||||
if (!isset(self::ENV_MAPPING[$env])) {
|
||||
throw new Exception("Invalid environment: $env. Use 'dva', 'rca' or 'pra'");
|
||||
}
|
||||
|
||||
$this->env = $env;
|
||||
|
||||
// Charger AppConfig (remonter de 4 niveaux: lib -> php -> migration2 -> scripts -> api)
|
||||
$appConfigPath = dirname(__DIR__, 4) . '/src/Config/AppConfig.php';
|
||||
if (!file_exists($appConfigPath)) {
|
||||
throw new Exception("AppConfig not found at: $appConfigPath");
|
||||
}
|
||||
require_once $appConfigPath;
|
||||
|
||||
// Simuler le host pour AppConfig en CLI
|
||||
$hostname = self::ENV_MAPPING[$env]['hostname'];
|
||||
$_SERVER['SERVER_NAME'] = $hostname;
|
||||
$_SERVER['HTTP_HOST'] = $hostname;
|
||||
|
||||
$this->appConfig = AppConfig::getInstance();
|
||||
|
||||
// Récupérer la config DB depuis AppConfig
|
||||
$dbConfig = $this->appConfig->getDatabaseConfig();
|
||||
|
||||
if (!$dbConfig || !isset($dbConfig['host'])) {
|
||||
throw new Exception("Database configuration not found for hostname: $hostname");
|
||||
}
|
||||
|
||||
// Construire la config pour la migration
|
||||
$this->config = [
|
||||
'name' => self::ENV_MAPPING[$env]['name'],
|
||||
'host' => $dbConfig['host'],
|
||||
'port' => $dbConfig['port'] ?? 3306,
|
||||
'user' => $dbConfig['username'],
|
||||
'pass' => $dbConfig['password'],
|
||||
'source_db' => self::ENV_MAPPING[$env]['source_db'],
|
||||
'target_db' => self::ENV_MAPPING[$env]['target_db']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'environnement actuel
|
||||
*/
|
||||
public function getEnv(): string
|
||||
{
|
||||
return $this->env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le nom complet de l'environnement
|
||||
*/
|
||||
public function getEnvName(): string
|
||||
{
|
||||
return $this->config['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'hôte de la base de données
|
||||
*/
|
||||
public function getHost(): string
|
||||
{
|
||||
return $this->config['host'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le port de la base de données
|
||||
*/
|
||||
public function getPort(): int
|
||||
{
|
||||
return $this->config['port'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'utilisateur de la base de données
|
||||
*/
|
||||
public function getUser(): string
|
||||
{
|
||||
return $this->config['user'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le mot de passe de la base de données
|
||||
*/
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->config['pass'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le nom de la base source
|
||||
*/
|
||||
public function getSourceDb(): string
|
||||
{
|
||||
return $this->config['source_db'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le nom de la base cible
|
||||
*/
|
||||
public function getTargetDb(): string
|
||||
{
|
||||
return $this->config['target_db'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne toute la configuration
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Détecte automatiquement l'environnement depuis le hostname
|
||||
*
|
||||
* @return string 'dva', 'rca' ou 'pra' (défaut: 'dva')
|
||||
*/
|
||||
public static function autoDetect(): string
|
||||
{
|
||||
$hostname = gethostname();
|
||||
|
||||
switch ($hostname) {
|
||||
case 'dva-geo':
|
||||
return 'dva';
|
||||
case 'rca-geo':
|
||||
return 'rca';
|
||||
case 'pra-geo':
|
||||
return 'pra';
|
||||
default:
|
||||
return 'dva'; // Défaut
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un environnement existe
|
||||
*/
|
||||
public static function exists(string $env): bool
|
||||
{
|
||||
return isset(self::ENV_MAPPING[$env]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la liste des environnements disponibles
|
||||
*/
|
||||
public static function getAvailableEnvironments(): array
|
||||
{
|
||||
return array_keys(self::ENV_MAPPING);
|
||||
}
|
||||
}
|
||||
201
api/scripts/migration2/php/lib/DatabaseConnection.php
Normal file
201
api/scripts/migration2/php/lib/DatabaseConnection.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Gestion des connexions PDO
|
||||
*
|
||||
* Crée et maintient les connexions aux bases source et cible
|
||||
*/
|
||||
class DatabaseConnection
|
||||
{
|
||||
private $config;
|
||||
private $logger;
|
||||
private $sourceDb;
|
||||
private $targetDb;
|
||||
|
||||
/**
|
||||
* Constructeur
|
||||
*
|
||||
* @param DatabaseConfig $config Configuration de l'environnement
|
||||
* @param MigrationLogger $logger Logger pour les messages
|
||||
*/
|
||||
public function __construct(DatabaseConfig $config, MigrationLogger $logger)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Établit les connexions aux bases source et cible
|
||||
*
|
||||
* @return bool True si succès
|
||||
*/
|
||||
public function connect(): bool
|
||||
{
|
||||
try {
|
||||
// Connexion à la base source
|
||||
$this->connectSource();
|
||||
|
||||
// Connexion à la base cible
|
||||
$this->connectTarget();
|
||||
|
||||
// Vérifier les versions MariaDB
|
||||
$this->checkVersions();
|
||||
|
||||
return true;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$this->logger->error("Erreur de connexion: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connexion à la base source
|
||||
*/
|
||||
private function connectSource(): void
|
||||
{
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
|
||||
$this->config->getHost(),
|
||||
$this->config->getPort(),
|
||||
$this->config->getSourceDb()
|
||||
);
|
||||
|
||||
$this->sourceDb = new PDO($dsn, $this->config->getUser(), $this->config->getPassword(), [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_TIMEOUT => 600,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
|
||||
]);
|
||||
|
||||
$this->logger->success("✓ Connexion SOURCE: {$this->config->getSourceDb()} sur {$this->config->getHost()}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Connexion à la base cible
|
||||
*/
|
||||
private function connectTarget(): void
|
||||
{
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
|
||||
$this->config->getHost(),
|
||||
$this->config->getPort(),
|
||||
$this->config->getTargetDb()
|
||||
);
|
||||
|
||||
$this->targetDb = new PDO($dsn, $this->config->getUser(), $this->config->getPassword(), [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_TIMEOUT => 600,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
|
||||
]);
|
||||
|
||||
$this->logger->success("✓ Connexion CIBLE: {$this->config->getTargetDb()} sur {$this->config->getHost()}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie et affiche les versions MariaDB
|
||||
*/
|
||||
private function checkVersions(): void
|
||||
{
|
||||
$sourceVersion = $this->sourceDb->query("SELECT VERSION()")->fetchColumn();
|
||||
$targetVersion = $this->targetDb->query("SELECT VERSION()")->fetchColumn();
|
||||
|
||||
$this->logger->info(" Version SOURCE: $sourceVersion");
|
||||
$this->logger->info(" Version CIBLE: $targetVersion");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la connexion à la base source
|
||||
*/
|
||||
public function getSourceDb(): PDO
|
||||
{
|
||||
if (!$this->sourceDb) {
|
||||
throw new Exception("Source database not connected. Call connect() first.");
|
||||
}
|
||||
return $this->sourceDb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la connexion à la base cible
|
||||
*/
|
||||
public function getTargetDb(): PDO
|
||||
{
|
||||
if (!$this->targetDb) {
|
||||
throw new Exception("Target database not connected. Call connect() first.");
|
||||
}
|
||||
return $this->targetDb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre de lignes dans une table de la source
|
||||
*
|
||||
* @param string $table Nom de la table
|
||||
* @param int|null $entityId Filtrer par fk_entite (optionnel)
|
||||
* @return int Nombre de lignes
|
||||
*/
|
||||
public function countSourceRows(string $table, ?int $entityId = null): int
|
||||
{
|
||||
$sql = "SELECT COUNT(*) FROM $table";
|
||||
|
||||
if ($entityId !== null) {
|
||||
// Tables avec fk_entite direct
|
||||
if (in_array($table, ['users', 'operations', 'entites'])) {
|
||||
$sql .= " WHERE fk_entite = :entity_id";
|
||||
}
|
||||
// Tables liées via operations
|
||||
elseif (in_array($table, ['ope_sectors', 'ope_users', 'ope_pass'])) {
|
||||
$sql .= " WHERE fk_operation IN (SELECT id FROM operations WHERE fk_entite = :entity_id)";
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $this->sourceDb->prepare($sql);
|
||||
if ($entityId !== null) {
|
||||
$stmt->execute(['entity_id' => $entityId]);
|
||||
} else {
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
return (int) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre de lignes dans une table de la cible
|
||||
*
|
||||
* @param string $table Nom de la table
|
||||
* @param int|null $entityId Filtrer par fk_entite (optionnel)
|
||||
* @return int Nombre de lignes
|
||||
*/
|
||||
public function countTargetRows(string $table, ?int $entityId = null): int
|
||||
{
|
||||
$sql = "SELECT COUNT(*) FROM $table";
|
||||
|
||||
if ($entityId !== null) {
|
||||
if (in_array($table, ['users', 'operations', 'entites'])) {
|
||||
$sql .= " WHERE fk_entite = :entity_id";
|
||||
}
|
||||
elseif (in_array($table, ['ope_sectors', 'ope_users', 'ope_pass'])) {
|
||||
$sql .= " WHERE fk_operation IN (SELECT id FROM operations WHERE fk_entite = :entity_id)";
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $this->targetDb->prepare($sql);
|
||||
if ($entityId !== null) {
|
||||
$stmt->execute(['entity_id' => $entityId]);
|
||||
} else {
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
return (int) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ferme les connexions
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
$this->sourceDb = null;
|
||||
$this->targetDb = null;
|
||||
$this->logger->info("Connexions fermées");
|
||||
}
|
||||
}
|
||||
219
api/scripts/migration2/php/lib/MigrationLogger.php
Normal file
219
api/scripts/migration2/php/lib/MigrationLogger.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Gestion des logs de migration
|
||||
*
|
||||
* Écrit dans un fichier et affiche à l'écran avec timestamps
|
||||
*/
|
||||
class MigrationLogger
|
||||
{
|
||||
private $logFile;
|
||||
private $verbose;
|
||||
|
||||
/**
|
||||
* Constructeur
|
||||
*
|
||||
* @param string|null $logFile Chemin du fichier de log (null = auto-généré)
|
||||
* @param bool $verbose Afficher les logs à l'écran
|
||||
*/
|
||||
public function __construct(?string $logFile = null, bool $verbose = true)
|
||||
{
|
||||
// Définir le répertoire de logs par défaut (migration2/logs/)
|
||||
$defaultLogDir = dirname(__DIR__, 2) . '/logs';
|
||||
$this->logFile = $logFile ?? $defaultLogDir . '/migration_' . date('Ymd_His') . '.log';
|
||||
$this->verbose = $verbose;
|
||||
|
||||
// Créer le dossier parent si nécessaire
|
||||
$dir = dirname($this->logFile);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
// Vérifier que le fichier est accessible en écriture
|
||||
if (!is_writable(dirname($this->logFile))) {
|
||||
throw new Exception("Log directory is not writable: " . dirname($this->logFile));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log un message avec niveau INFO
|
||||
*/
|
||||
public function info(string $message): void
|
||||
{
|
||||
$this->log($message, 'INFO');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log un message avec niveau SUCCESS
|
||||
*/
|
||||
public function success(string $message): void
|
||||
{
|
||||
$this->log($message, 'SUCCESS');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log un message avec niveau WARNING
|
||||
*/
|
||||
public function warning(string $message): void
|
||||
{
|
||||
$this->log($message, 'WARNING');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log un message avec niveau ERROR
|
||||
*/
|
||||
public function error(string $message): void
|
||||
{
|
||||
$this->log($message, 'ERROR');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log un message avec niveau DEBUG
|
||||
*/
|
||||
public function debug(string $message): void
|
||||
{
|
||||
$this->log($message, 'DEBUG');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log une ligne de séparation
|
||||
*/
|
||||
public function separator(): void
|
||||
{
|
||||
$this->log(str_repeat('=', 80), 'INFO');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log générique
|
||||
*
|
||||
* @param string $message Message à logger
|
||||
* @param string $level Niveau: INFO, SUCCESS, WARNING, ERROR, DEBUG
|
||||
*/
|
||||
private function log(string $message, string $level = 'INFO'): void
|
||||
{
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$logLine = "[{$timestamp}] [{$level}] {$message}\n";
|
||||
|
||||
// Écriture dans le fichier
|
||||
file_put_contents($this->logFile, $logLine, FILE_APPEND);
|
||||
|
||||
// Affichage à l'écran si verbose
|
||||
if ($this->verbose) {
|
||||
$this->printColored($message, $level);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche un message coloré selon le niveau
|
||||
*/
|
||||
private function printColored(string $message, string $level): void
|
||||
{
|
||||
$colors = [
|
||||
'INFO' => "\033[0;37m", // Blanc
|
||||
'SUCCESS' => "\033[0;32m", // Vert
|
||||
'WARNING' => "\033[0;33m", // Jaune
|
||||
'ERROR' => "\033[0;31m", // Rouge
|
||||
'DEBUG' => "\033[0;36m" // Cyan
|
||||
];
|
||||
|
||||
$reset = "\033[0m";
|
||||
$color = $colors[$level] ?? $colors['INFO'];
|
||||
|
||||
echo $color . $message . $reset . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le chemin du fichier de log
|
||||
*/
|
||||
public function getLogFile(): string
|
||||
{
|
||||
return $this->logFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log des statistiques de migration
|
||||
*
|
||||
* @param array $stats Tableau associatif [table => count]
|
||||
*/
|
||||
public function logStats(array $stats): void
|
||||
{
|
||||
$this->separator();
|
||||
$this->info("📊 Statistiques de migration:");
|
||||
|
||||
foreach ($stats as $table => $count) {
|
||||
$this->info(" - {$table}: {$count} enregistrement(s)");
|
||||
}
|
||||
|
||||
$this->separator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log une ligne spéciale pour parsing automatique
|
||||
* Format: #STATS# KEY1:VAL1 KEY2:VAL2 ...
|
||||
*/
|
||||
public function logParsableStats(array $stats): void
|
||||
{
|
||||
$pairs = [];
|
||||
foreach ($stats as $key => $value) {
|
||||
$pairs[] = strtoupper($key) . ':' . $value;
|
||||
}
|
||||
|
||||
$line = '#STATS# ' . implode(' ', $pairs);
|
||||
$this->log($line, 'INFO');
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche et log un récapitulatif complet de migration
|
||||
*
|
||||
* @param array $summary Tableau de statistiques hiérarchique
|
||||
*/
|
||||
public function logMigrationSummary(array $summary): void
|
||||
{
|
||||
$this->separator();
|
||||
$this->separator();
|
||||
$this->info("📊 RÉCAPITULATIF DE LA MIGRATION");
|
||||
$this->separator();
|
||||
|
||||
// Entité
|
||||
if (isset($summary['entity'])) {
|
||||
$this->info("Entité: {$summary['entity']['name']} (ID: {$summary['entity']['id']})");
|
||||
}
|
||||
$this->info("Date: " . date('Y-m-d H:i:s'));
|
||||
$this->info("");
|
||||
|
||||
// Nombre total d'opérations
|
||||
$totalOperations = count($summary['operations'] ?? []);
|
||||
$this->success("Opérations migrées: {$totalOperations}");
|
||||
$this->info("");
|
||||
|
||||
// Détail par opération
|
||||
$operationNum = 1;
|
||||
foreach ($summary['operations'] ?? [] as $operation) {
|
||||
$this->info("Opération #{$operationNum}: \"{$operation['name']}\" (ID: {$operation['id']})");
|
||||
$this->info(" ├─ Utilisateurs: {$operation['users']}");
|
||||
$this->info(" ├─ Secteurs: {$operation['sectors']}");
|
||||
$this->info(" ├─ Passages totaux: {$operation['total_passages']}");
|
||||
|
||||
if (!empty($operation['sectors_detail'])) {
|
||||
$this->info(" └─ Détail par secteur:");
|
||||
|
||||
$sectorCount = count($operation['sectors_detail']);
|
||||
$sectorNum = 0;
|
||||
foreach ($operation['sectors_detail'] as $sector) {
|
||||
$sectorNum++;
|
||||
$isLast = ($sectorNum === $sectorCount);
|
||||
$prefix = $isLast ? " └─" : " ├─";
|
||||
|
||||
$this->info("{$prefix} {$sector['name']} (ID: {$sector['id']})");
|
||||
$this->info(" " . ($isLast ? " " : "│") . " ├─ Utilisateurs affectés: {$sector['users']}");
|
||||
$this->info(" " . ($isLast ? " " : "│") . " └─ Passages: {$sector['passages']}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("");
|
||||
$operationNum++;
|
||||
}
|
||||
|
||||
$this->separator();
|
||||
}
|
||||
}
|
||||
312
api/scripts/migration2/php/lib/OperationMigrator.php
Normal file
312
api/scripts/migration2/php/lib/OperationMigrator.php
Normal file
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Migration des opérations complètes
|
||||
*
|
||||
* Orchestre la migration d'une opération avec tous ses utilisateurs,
|
||||
* secteurs, passages et médias. Utilise UserMigrator et SectorMigrator.
|
||||
*/
|
||||
class OperationMigrator
|
||||
{
|
||||
private PDO $sourceDb;
|
||||
private PDO $targetDb;
|
||||
private MigrationLogger $logger;
|
||||
private UserMigrator $userMigrator;
|
||||
private SectorMigrator $sectorMigrator;
|
||||
|
||||
/**
|
||||
* Constructeur
|
||||
*
|
||||
* @param PDO $sourceDb Connexion source
|
||||
* @param PDO $targetDb Connexion cible
|
||||
* @param MigrationLogger $logger Logger
|
||||
* @param UserMigrator $userMigrator Migrator d'utilisateurs
|
||||
* @param SectorMigrator $sectorMigrator Migrator de secteurs
|
||||
*/
|
||||
public function __construct(
|
||||
PDO $sourceDb,
|
||||
PDO $targetDb,
|
||||
MigrationLogger $logger,
|
||||
UserMigrator $userMigrator,
|
||||
SectorMigrator $sectorMigrator
|
||||
) {
|
||||
$this->sourceDb = $sourceDb;
|
||||
$this->targetDb = $targetDb;
|
||||
$this->logger = $logger;
|
||||
$this->userMigrator = $userMigrator;
|
||||
$this->sectorMigrator = $sectorMigrator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les opérations à migrer pour une entité
|
||||
* - 1 opération active
|
||||
* - 2 dernières opérations inactives avec au moins 10 passages effectués
|
||||
*
|
||||
* @param int $entityId ID de l'entité
|
||||
* @return array Liste des IDs d'opérations à migrer
|
||||
*/
|
||||
public function getOperationsToMigrate(int $entityId): array
|
||||
{
|
||||
$operationIds = [];
|
||||
|
||||
// 1. Récupérer l'opération active (pour vérification)
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT rowid
|
||||
FROM operations
|
||||
WHERE fk_entite = :entity_id AND active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([':entity_id' => $entityId]);
|
||||
$activeOp = $stmt->fetch(PDO::FETCH_COLUMN);
|
||||
|
||||
// 2. Récupérer les 2 dernières opérations inactives avec >= 10 passages effectués
|
||||
// ORDER BY DESC pour avoir les plus récentes, puis on inverse
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT o.rowid, COUNT(p.rowid) as nb_passages
|
||||
FROM operations o
|
||||
LEFT JOIN ope_pass p ON p.fk_operation = o.rowid AND p.fk_type = 1
|
||||
WHERE o.fk_entite = :entity_id
|
||||
AND o.active = 0
|
||||
" . ($activeOp ? "AND o.rowid != :active_id" : "") . "
|
||||
GROUP BY o.rowid
|
||||
HAVING nb_passages >= 10
|
||||
ORDER BY o.rowid DESC
|
||||
LIMIT 2
|
||||
");
|
||||
|
||||
$params = [':entity_id' => $entityId];
|
||||
if ($activeOp) {
|
||||
$params[':active_id'] = $activeOp;
|
||||
}
|
||||
|
||||
$stmt->execute($params);
|
||||
$inactiveOps = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Inverser pour avoir l'ordre chronologique (plus ancienne → plus récente)
|
||||
$inactiveOps = array_reverse($inactiveOps);
|
||||
|
||||
foreach ($inactiveOps as $op) {
|
||||
$operationIds[] = $op['rowid'];
|
||||
$this->logger->info("✓ Opération inactive trouvée: {$op['rowid']} ({$op['nb_passages']} passages)");
|
||||
}
|
||||
|
||||
// 3. Ajouter l'opération active EN DERNIER
|
||||
if ($activeOp) {
|
||||
$operationIds[] = $activeOp;
|
||||
$this->logger->info("✓ Opération active trouvée: {$activeOp}");
|
||||
}
|
||||
|
||||
$this->logger->info("📊 Total: " . count($operationIds) . " opération(s) à migrer");
|
||||
|
||||
return $operationIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre une opération complète avec tous ses utilisateurs et secteurs
|
||||
*
|
||||
* @param int $oldOperationId ID de l'opération dans l'ancienne base
|
||||
* @return array|null Tableau de statistiques ou null en cas d'erreur
|
||||
*/
|
||||
public function migrateOperation(int $oldOperationId): ?array
|
||||
{
|
||||
$this->logger->separator();
|
||||
$this->logger->info("🔄 Migration de l'opération ID: {$oldOperationId}");
|
||||
|
||||
try {
|
||||
// 1. Récupérer l'opération source
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT * FROM operations
|
||||
WHERE rowid = :id
|
||||
");
|
||||
$stmt->execute([':id' => $oldOperationId]);
|
||||
$operation = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$operation) {
|
||||
$this->logger->warning("Opération {$oldOperationId} non trouvée");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. Créer l'opération dans la nouvelle base
|
||||
$newOperationId = $this->createOperation($operation);
|
||||
|
||||
if (!$newOperationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->logger->success("✓ Opération créée avec ID: {$newOperationId}");
|
||||
|
||||
// 3. Migrer les utilisateurs de l'opération
|
||||
// Pour opération active : tous les users actifs de l'entité
|
||||
// Pour opération inactive : uniquement ceux dans ope_users_sectors
|
||||
$entityId = (int)$operation['fk_entite'];
|
||||
$isActiveOperation = (int)$operation['active'] === 1;
|
||||
|
||||
$userResult = $this->userMigrator->migrateOperationUsers(
|
||||
$oldOperationId,
|
||||
$newOperationId,
|
||||
$entityId,
|
||||
$isActiveOperation
|
||||
);
|
||||
$userMapping = $userResult['mapping'];
|
||||
$usersCount = $userResult['count'];
|
||||
|
||||
if (empty($userMapping)) {
|
||||
$this->logger->warning("Aucun utilisateur migré, abandon de l'opération {$oldOperationId}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4. Récupérer les secteurs DISTINCTS de l'opération
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT DISTINCT fk_sector
|
||||
FROM ope_users_sectors
|
||||
WHERE fk_operation = :operation_id AND active = 1
|
||||
");
|
||||
$stmt->execute([':operation_id' => $oldOperationId]);
|
||||
$sectors = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
$this->logger->info("📍 " . count($sectors) . " secteur(s) distinct(s) à migrer");
|
||||
|
||||
// 5. Migrer chaque secteur et collecter les stats
|
||||
$sectorsDetail = [];
|
||||
$totalPassages = 0;
|
||||
|
||||
foreach ($sectors as $oldSectorId) {
|
||||
$sectorStats = $this->sectorMigrator->migrateSector(
|
||||
$oldOperationId,
|
||||
$newOperationId,
|
||||
$oldSectorId,
|
||||
$userMapping
|
||||
);
|
||||
|
||||
if ($sectorStats) {
|
||||
$sectorsDetail[] = $sectorStats;
|
||||
$totalPassages += $sectorStats['passages'];
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Migrer les médias de l'opération (support='operations')
|
||||
$this->migrateOperationMedias($oldOperationId, $newOperationId);
|
||||
|
||||
$this->logger->success("✅ Migration de l'opération {$oldOperationId} terminée");
|
||||
|
||||
// 7. Retourner les statistiques
|
||||
return [
|
||||
'id' => $newOperationId,
|
||||
'name' => $operation['libelle'],
|
||||
'users' => $usersCount,
|
||||
'sectors' => count($sectorsDetail),
|
||||
'total_passages' => $totalPassages,
|
||||
'sectors_detail' => $sectorsDetail
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error("❌ Erreur migration opération {$oldOperationId}: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une opération dans la nouvelle base
|
||||
*
|
||||
* @param array $operation Données de l'opération
|
||||
* @return int|null ID de la nouvelle opération ou null en cas d'erreur
|
||||
*/
|
||||
private function createOperation(array $operation): ?int
|
||||
{
|
||||
try {
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO operations (
|
||||
fk_entite, libelle, date_deb, date_fin,
|
||||
chk_distinct_sectors,
|
||||
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
|
||||
) VALUES (
|
||||
:fk_entite, :libelle, :date_deb, :date_fin,
|
||||
:chk_distinct_sectors,
|
||||
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
|
||||
)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':fk_entite' => $operation['fk_entite'],
|
||||
':libelle' => $operation['libelle'],
|
||||
':date_deb' => $operation['date_deb'],
|
||||
':date_fin' => $operation['date_fin'],
|
||||
':chk_distinct_sectors' => $operation['chk_distinct_sectors'],
|
||||
':created_at' => $operation['date_creat'],
|
||||
':fk_user_creat' => $operation['fk_user_creat'],
|
||||
':updated_at' => $operation['date_modif'],
|
||||
':fk_user_modif' => $operation['fk_user_modif'] ?? 0,
|
||||
':chk_active' => $operation['active']
|
||||
]);
|
||||
|
||||
return (int)$this->targetDb->lastInsertId();
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error("❌ Erreur création opération: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre les médias d'une opération
|
||||
*
|
||||
* @param int $oldOperationId ID ancienne opération
|
||||
* @param int $newOperationId ID nouvelle opération
|
||||
* @return int Nombre de médias migrés
|
||||
*/
|
||||
private function migrateOperationMedias(int $oldOperationId, int $newOperationId): int
|
||||
{
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT * FROM medias
|
||||
WHERE support = 'operations' AND support_rowid = :operation_id
|
||||
");
|
||||
$stmt->execute([':operation_id' => $oldOperationId]);
|
||||
$medias = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($medias)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($medias as $media) {
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO medias (
|
||||
dir0, dir1, dir2, support, support_rowid,
|
||||
fichier, type_fichier, description, position,
|
||||
hauteur, largeur, niveaugris,
|
||||
created_at, fk_user_creat, updated_at, fk_user_modif
|
||||
) VALUES (
|
||||
:dir0, :dir1, :dir2, :support, :support_rowid,
|
||||
:fichier, :type_fichier, :description, :position,
|
||||
:hauteur, :largeur, :niveaugris,
|
||||
:created_at, :fk_user_creat, :updated_at, :fk_user_modif
|
||||
)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':dir0' => $media['dir0'],
|
||||
':dir1' => $media['dir1'],
|
||||
':dir2' => $media['dir2'],
|
||||
':support' => $media['support'],
|
||||
':support_rowid' => $newOperationId,
|
||||
':fichier' => $media['fichier'],
|
||||
':type_fichier' => $media['type_fichier'],
|
||||
':description' => $media['description'],
|
||||
':position' => $media['position'],
|
||||
':hauteur' => $media['hauteur'],
|
||||
':largeur' => $media['largeur'],
|
||||
':niveaugris' => $media['niveaugris'],
|
||||
':created_at' => $media['date_creat'],
|
||||
':fk_user_creat' => $media['fk_user_creat'],
|
||||
':updated_at' => $media['date_modif'],
|
||||
':fk_user_modif' => $media['fk_user_modif']
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->logger->success("✓ {$count} média(s) migré(s)");
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
256
api/scripts/migration2/php/lib/PassageMigrator.php
Normal file
256
api/scripts/migration2/php/lib/PassageMigrator.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Migration des passages (ope_pass) et historiques (ope_pass_histo)
|
||||
*
|
||||
* Gère la migration des passages avec vérification du trio
|
||||
* (operation, user, sector) et migration des historiques associés
|
||||
*/
|
||||
class PassageMigrator
|
||||
{
|
||||
private PDO $sourceDb;
|
||||
private PDO $targetDb;
|
||||
private MigrationLogger $logger;
|
||||
|
||||
/**
|
||||
* Constructeur
|
||||
*
|
||||
* @param PDO $sourceDb Connexion source
|
||||
* @param PDO $targetDb Connexion cible
|
||||
* @param MigrationLogger $logger Logger
|
||||
*/
|
||||
public function __construct(PDO $sourceDb, PDO $targetDb, MigrationLogger $logger)
|
||||
{
|
||||
$this->sourceDb = $sourceDb;
|
||||
$this->targetDb = $targetDb;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre les passages d'un secteur dans une opération
|
||||
*
|
||||
* @param int $oldOperationId ID ancienne opération
|
||||
* @param int $newOperationId ID nouvelle opération
|
||||
* @param int $oldSectorId ID ancien secteur
|
||||
* @param int $newOpeSectorId ID nouveau ope_sectors
|
||||
* @param array $userMapping Mapping oldUserId => newOpeUserId
|
||||
* @return int Nombre de passages migrés
|
||||
*/
|
||||
public function migratePassages(
|
||||
int $oldOperationId,
|
||||
int $newOperationId,
|
||||
int $oldSectorId,
|
||||
int $newOpeSectorId,
|
||||
array $userMapping
|
||||
): int {
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT * FROM ope_pass
|
||||
WHERE fk_operation = :operation_id
|
||||
AND fk_sector = :sector_id
|
||||
");
|
||||
$stmt->execute([
|
||||
':operation_id' => $oldOperationId,
|
||||
':sector_id' => $oldSectorId
|
||||
]);
|
||||
$passages = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($passages)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($passages as $passage) {
|
||||
// Vérifier que l'utilisateur a été migré
|
||||
if (!isset($userMapping[$passage['fk_user']])) {
|
||||
$this->logger->warning(" ⚠ Passage {$passage['rowid']}: User {$passage['fk_user']} non trouvé dans mapping");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Récupérer l'ID de ope_users depuis le mapping
|
||||
$newOpeUserId = $userMapping[$passage['fk_user']];
|
||||
|
||||
// Vérifier que le trio (operation, user, sector) existe dans ope_users_sectors
|
||||
if (!$this->verifyUserSectorAssociation($newOperationId, $newOpeUserId, $newOpeSectorId)) {
|
||||
$this->logger->warning(" ⚠ Passage {$passage['rowid']}: Trio (op={$newOperationId}, user={$newOpeUserId}, sector={$newOpeSectorId}) inexistant");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Insérer le passage avec l'ID de ope_users
|
||||
$newPassId = $this->insertPassage($passage, $newOperationId, $newOpeSectorId, $newOpeUserId);
|
||||
|
||||
if ($newPassId) {
|
||||
// Migrer l'historique du passage
|
||||
$this->migratePassageHisto($passage['rowid'], $newPassId, $userMapping);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($count > 0) {
|
||||
$this->logger->success(" ✓ {$count} passage(s) migré(s)");
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie qu'une association user-sector existe dans ope_users_sectors
|
||||
*
|
||||
* @param int $operationId ID opération
|
||||
* @param int $userId ID ope_users (mapping)
|
||||
* @param int $sectorId ID ope_sectors
|
||||
* @return bool True si l'association existe
|
||||
*/
|
||||
private function verifyUserSectorAssociation(int $operationId, int $userId, int $sectorId): bool
|
||||
{
|
||||
$stmt = $this->targetDb->prepare("
|
||||
SELECT COUNT(*) FROM ope_users_sectors
|
||||
WHERE fk_operation = :operation_id
|
||||
AND fk_user = :user_id
|
||||
AND fk_sector = :sector_id
|
||||
");
|
||||
$stmt->execute([
|
||||
':operation_id' => $operationId,
|
||||
':user_id' => $userId,
|
||||
':sector_id' => $sectorId
|
||||
]);
|
||||
|
||||
return $stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insère un passage dans la nouvelle base
|
||||
*
|
||||
* @param array $passage Données du passage
|
||||
* @param int $newOperationId ID nouvelle opération
|
||||
* @param int $newOpeSectorId ID nouveau secteur
|
||||
* @param int $userId ID de ope_users (mapping)
|
||||
* @return int|null ID du nouveau passage ou null en cas d'erreur
|
||||
*/
|
||||
private function insertPassage(
|
||||
array $passage,
|
||||
int $newOperationId,
|
||||
int $newOpeSectorId,
|
||||
int $userId
|
||||
): ?int {
|
||||
try {
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO ope_pass (
|
||||
fk_operation, fk_sector, fk_user, fk_adresse,
|
||||
passed_at, fk_type, numero, rue, rue_bis, ville,
|
||||
fk_habitat, appt, niveau, residence,
|
||||
gps_lat, gps_lng, encrypted_name, montant, fk_type_reglement,
|
||||
remarque, nom_recu, encrypted_email, email_erreur, chk_email_sent,
|
||||
encrypted_phone, docremis, date_repasser, nb_passages,
|
||||
chk_gps_maj, chk_map_create, chk_mobile, chk_synchro,
|
||||
chk_api_adresse, chk_maj_adresse, anomalie,
|
||||
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
|
||||
) VALUES (
|
||||
:fk_operation, :fk_sector, :fk_user, :fk_adresse,
|
||||
:passed_at, :fk_type, :numero, :rue, :rue_bis, :ville,
|
||||
:fk_habitat, :appt, :niveau, :residence,
|
||||
:gps_lat, :gps_lng, :encrypted_name, :montant, :fk_type_reglement,
|
||||
:remarque, :nom_recu, :encrypted_email, :email_erreur, :chk_email_sent,
|
||||
:encrypted_phone, :docremis, :date_repasser, :nb_passages,
|
||||
:chk_gps_maj, :chk_map_create, :chk_mobile, :chk_synchro,
|
||||
:chk_api_adresse, :chk_maj_adresse, :anomalie,
|
||||
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
|
||||
)
|
||||
");
|
||||
|
||||
// Chiffrer les données sensibles
|
||||
require_once dirname(__DIR__, 4) . '/src/Services/ApiService.php';
|
||||
|
||||
$stmt->execute([
|
||||
':fk_operation' => $newOperationId,
|
||||
':fk_sector' => $newOpeSectorId,
|
||||
':fk_user' => $userId, // ID de ope_users (mapping)
|
||||
':fk_adresse' => $passage['fk_adresse'],
|
||||
':passed_at' => $passage['date_eve'],
|
||||
':fk_type' => $passage['fk_type'],
|
||||
':numero' => $passage['numero'],
|
||||
':rue' => $passage['rue'],
|
||||
':rue_bis' => $passage['rue_bis'],
|
||||
':ville' => $passage['ville'],
|
||||
':fk_habitat' => $passage['fk_habitat'],
|
||||
':appt' => $passage['appt'],
|
||||
':niveau' => $passage['niveau'],
|
||||
':residence' => $passage['lieudit'] ?? null,
|
||||
':gps_lat' => $passage['gps_lat'],
|
||||
':gps_lng' => $passage['gps_lng'],
|
||||
':encrypted_name' => $passage['libelle'] ? ApiService::encryptData($passage['libelle']) : '', // Chiffrer avec IV aléatoire
|
||||
':montant' => $passage['montant'],
|
||||
':fk_type_reglement' => (!empty($passage['fk_type_reglement']) && $passage['fk_type_reglement'] > 0) ? $passage['fk_type_reglement'] : 4,
|
||||
':remarque' => $passage['remarque'],
|
||||
':nom_recu' => $passage['recu'] ?? null,
|
||||
':encrypted_email' => $passage['email'] ? ApiService::encryptSearchableData($passage['email']) : null,
|
||||
':email_erreur' => $passage['email_erreur'],
|
||||
':chk_email_sent' => $passage['chk_email_sent'],
|
||||
':encrypted_phone' => $passage['phone'] ? ApiService::encryptData($passage['phone']) : '',
|
||||
':docremis' => $passage['docremis'],
|
||||
':date_repasser' => $passage['date_repasser'],
|
||||
':nb_passages' => ($passage['fk_type'] == 2) ? 0 : $passage['nb_passages'],
|
||||
':chk_gps_maj' => $passage['chk_gps_maj'],
|
||||
':chk_map_create' => $passage['chk_map_create'],
|
||||
':chk_mobile' => $passage['chk_mobile'],
|
||||
':chk_synchro' => $passage['chk_synchro'],
|
||||
':chk_api_adresse' => $passage['chk_api_adresse'],
|
||||
':chk_maj_adresse' => $passage['chk_maj_adresse'],
|
||||
':anomalie' => $passage['anomalie'],
|
||||
':created_at' => $passage['date_creat'],
|
||||
':fk_user_creat' => $passage['fk_user_creat'] ?? 0,
|
||||
':updated_at' => $passage['date_modif'],
|
||||
':fk_user_modif' => $passage['fk_user_modif'] ?? 0,
|
||||
':chk_active' => $passage['active']
|
||||
]);
|
||||
|
||||
return (int)$this->targetDb->lastInsertId();
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error(" ❌ Erreur insertion passage {$passage['rowid']}: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre l'historique d'un passage
|
||||
*
|
||||
* @param int $oldPassId ID ancien passage
|
||||
* @param int $newPassId ID nouveau passage
|
||||
* @param array $userMapping Non utilisé (conservé pour compatibilité)
|
||||
* @return int Nombre d'entrées d'historique migrées
|
||||
*/
|
||||
public function migratePassageHisto(int $oldPassId, int $newPassId, array $userMapping): int
|
||||
{
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT * FROM ope_pass_histo WHERE fk_pass = :pass_id
|
||||
");
|
||||
$stmt->execute([':pass_id' => $oldPassId]);
|
||||
$histos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($histos)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($histos as $histo) {
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO ope_pass_histo (
|
||||
fk_pass, date_histo, sujet, remarque
|
||||
) VALUES (
|
||||
:fk_pass, :date_histo, :sujet, :remarque
|
||||
)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':fk_pass' => $newPassId,
|
||||
':date_histo' => $histo['date_histo'],
|
||||
':sujet' => $histo['sujet'],
|
||||
':remarque' => $histo['remarque']
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
289
api/scripts/migration2/php/lib/SectorMigrator.php
Normal file
289
api/scripts/migration2/php/lib/SectorMigrator.php
Normal file
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Migration des secteurs (ope_sectors) et données associées
|
||||
*
|
||||
* Gère la migration des secteurs avec leurs adresses, associations
|
||||
* utilisateurs-secteurs, et passages. Utilise PassageMigrator pour les passages.
|
||||
*/
|
||||
class SectorMigrator
|
||||
{
|
||||
private PDO $sourceDb;
|
||||
private PDO $targetDb;
|
||||
private MigrationLogger $logger;
|
||||
private PassageMigrator $passageMigrator;
|
||||
private array $sectorMapping = [];
|
||||
|
||||
/**
|
||||
* Constructeur
|
||||
*
|
||||
* @param PDO $sourceDb Connexion source
|
||||
* @param PDO $targetDb Connexion cible
|
||||
* @param MigrationLogger $logger Logger
|
||||
* @param PassageMigrator $passageMigrator Migrator de passages
|
||||
*/
|
||||
public function __construct(
|
||||
PDO $sourceDb,
|
||||
PDO $targetDb,
|
||||
MigrationLogger $logger,
|
||||
PassageMigrator $passageMigrator
|
||||
) {
|
||||
$this->sourceDb = $sourceDb;
|
||||
$this->targetDb = $targetDb;
|
||||
$this->logger = $logger;
|
||||
$this->passageMigrator = $passageMigrator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre un secteur dans le contexte d'une opération
|
||||
*
|
||||
* @param int $oldOperationId ID ancienne opération
|
||||
* @param int $newOperationId ID nouvelle opération
|
||||
* @param int $oldSectorId ID ancien secteur
|
||||
* @param array $userMapping Mapping oldUserId => newOpeUserId
|
||||
* @return array|null ['id' => int, 'name' => string, 'users' => int, 'passages' => int] ou null en cas d'erreur
|
||||
*/
|
||||
public function migrateSector(
|
||||
int $oldOperationId,
|
||||
int $newOperationId,
|
||||
int $oldSectorId,
|
||||
array $userMapping
|
||||
): ?array {
|
||||
$this->logger->info(" 📍 Migration secteur ID: {$oldSectorId}");
|
||||
|
||||
try {
|
||||
// 1. Récupérer le secteur source
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT * FROM sectors WHERE rowid = :id
|
||||
");
|
||||
$stmt->execute([':id' => $oldSectorId]);
|
||||
$sector = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$sector) {
|
||||
$this->logger->warning(" Secteur {$oldSectorId} non trouvé");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. Créer dans ope_sectors
|
||||
$newOpeSectorId = $this->createOpeSector($sector, $newOperationId);
|
||||
|
||||
if (!$newOpeSectorId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. Mapper "operationId_sectorId" → newOpeSectorId
|
||||
$mappingKey = "{$oldOperationId}_{$oldSectorId}";
|
||||
$this->sectorMapping[$mappingKey] = $newOpeSectorId;
|
||||
|
||||
$this->logger->success(" ✓ Secteur créé avec ID: {$newOpeSectorId}");
|
||||
|
||||
// 4. Migrer sectors_adresses
|
||||
$this->migrateSectorAddresses($oldSectorId, $newOpeSectorId);
|
||||
|
||||
// 5. Migrer ope_users_sectors
|
||||
$usersCount = $this->migrateUsersSectors($oldOperationId, $newOperationId, $oldSectorId, $newOpeSectorId, $userMapping);
|
||||
|
||||
// 6. Migrer ope_pass
|
||||
$passagesCount = $this->passageMigrator->migratePassages(
|
||||
$oldOperationId,
|
||||
$newOperationId,
|
||||
$oldSectorId,
|
||||
$newOpeSectorId,
|
||||
$userMapping
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $newOpeSectorId,
|
||||
'name' => $sector['libelle'],
|
||||
'users' => $usersCount,
|
||||
'passages' => $passagesCount
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error(" ❌ Erreur migration secteur {$oldSectorId}: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un secteur dans ope_sectors
|
||||
*
|
||||
* @param array $sector Données du secteur
|
||||
* @param int $newOperationId ID nouvelle opération
|
||||
* @return int|null ID du nouveau secteur ou null en cas d'erreur
|
||||
*/
|
||||
private function createOpeSector(array $sector, int $newOperationId): ?int
|
||||
{
|
||||
try {
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO ope_sectors (
|
||||
fk_operation, libelle, sector, color,
|
||||
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
|
||||
) VALUES (
|
||||
:fk_operation, :libelle, :sector, :color,
|
||||
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
|
||||
)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':fk_operation' => $newOperationId,
|
||||
':libelle' => $sector['libelle'],
|
||||
':sector' => $sector['sector'],
|
||||
':color' => $sector['color'],
|
||||
':created_at' => $sector['date_creat'],
|
||||
':fk_user_creat' => $sector['fk_user_creat'] ?? 0,
|
||||
':updated_at' => $sector['date_modif'],
|
||||
':fk_user_modif' => $sector['fk_user_modif'] ?? 0,
|
||||
':chk_active' => $sector['active']
|
||||
]);
|
||||
|
||||
return (int)$this->targetDb->lastInsertId();
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error(" ❌ Erreur création secteur: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre les adresses d'un secteur
|
||||
*
|
||||
* @param int $oldSectorId ID ancien secteur
|
||||
* @param int $newOpeSectorId ID nouveau ope_sectors
|
||||
* @return int Nombre d'adresses migrées
|
||||
*/
|
||||
private function migrateSectorAddresses(int $oldSectorId, int $newOpeSectorId): int
|
||||
{
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT * FROM sectors_adresses WHERE fk_sector = :sector_id
|
||||
");
|
||||
$stmt->execute([':sector_id' => $oldSectorId]);
|
||||
$addresses = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($addresses)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($addresses as $address) {
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO sectors_adresses (
|
||||
fk_adresse, fk_sector, numero, rue_bis, rue, cp, ville,
|
||||
gps_lat, gps_lng
|
||||
) VALUES (
|
||||
:fk_adresse, :fk_sector, :numero, :rue_bis, :rue, :cp, :ville,
|
||||
:gps_lat, :gps_lng
|
||||
)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':fk_adresse' => $address['fk_adresse'], // Garde la valeur telle quelle
|
||||
':fk_sector' => $newOpeSectorId,
|
||||
':numero' => $address['numero'],
|
||||
':rue_bis' => $address['rue_bis'],
|
||||
':rue' => $address['rue'],
|
||||
':cp' => $address['cp'],
|
||||
':ville' => $address['ville'],
|
||||
':gps_lat' => $address['gps_lat'],
|
||||
':gps_lng' => $address['gps_lng']
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->logger->success(" ✓ {$count} adresse(s) migrée(s)");
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre les associations utilisateurs-secteurs
|
||||
*
|
||||
* @param int $oldOperationId ID ancienne opération
|
||||
* @param int $newOperationId ID nouvelle opération
|
||||
* @param int $oldSectorId ID ancien secteur
|
||||
* @param int $newOpeSectorId ID nouveau ope_sectors
|
||||
* @param array $userMapping Mapping oldUserId => newOpeUserId
|
||||
* @return int Nombre d'associations migrées
|
||||
*/
|
||||
private function migrateUsersSectors(
|
||||
int $oldOperationId,
|
||||
int $newOperationId,
|
||||
int $oldSectorId,
|
||||
int $newOpeSectorId,
|
||||
array $userMapping
|
||||
): int {
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT * FROM ope_users_sectors
|
||||
WHERE fk_operation = :operation_id
|
||||
AND fk_sector = :sector_id
|
||||
AND active = 1
|
||||
");
|
||||
$stmt->execute([
|
||||
':operation_id' => $oldOperationId,
|
||||
':sector_id' => $oldSectorId
|
||||
]);
|
||||
$usersSectors = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($usersSectors)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($usersSectors as $us) {
|
||||
// Vérifier que l'utilisateur existe dans le mapping
|
||||
// (le mapping sert juste à vérifier que l'user a été migré)
|
||||
if (!isset($userMapping[$us['fk_user']])) {
|
||||
$this->logger->warning(" ⚠ User {$us['fk_user']} non trouvé dans mapping");
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO ope_users_sectors (
|
||||
fk_operation, fk_user, fk_sector,
|
||||
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
|
||||
) VALUES (
|
||||
:fk_operation, :fk_user, :fk_sector,
|
||||
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
|
||||
)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':fk_operation' => $newOperationId,
|
||||
':fk_user' => $userMapping[$us['fk_user']], // ID de ope_users (mapping)
|
||||
':fk_sector' => $newOpeSectorId,
|
||||
':created_at' => date('Y-m-d H:i:s'),
|
||||
':fk_user_creat' => 0,
|
||||
':updated_at' => null,
|
||||
':fk_user_modif' => null,
|
||||
':chk_active' => $us['active']
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->logger->success(" ✓ {$count} association(s) user-secteur migrée(s)");
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le mapping des secteurs
|
||||
*
|
||||
* @return array "operationId_sectorId" => newOpeSectorId
|
||||
*/
|
||||
public function getSectorMapping(): array
|
||||
{
|
||||
return $this->sectorMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit le mapping des secteurs (utile pour réutilisation)
|
||||
*
|
||||
* @param array $mapping "operationId_sectorId" => newOpeSectorId
|
||||
*/
|
||||
public function setSectorMapping(array $mapping): void
|
||||
{
|
||||
$this->sectorMapping = $mapping;
|
||||
}
|
||||
}
|
||||
163
api/scripts/migration2/php/lib/UserMigrator.php
Normal file
163
api/scripts/migration2/php/lib/UserMigrator.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Migration des utilisateurs d'opérations (ope_users)
|
||||
*
|
||||
* Gère la création des utilisateurs par opération et le mapping
|
||||
* oldUserId (users.rowid) → newOpeUserId (ope_users.id)
|
||||
*/
|
||||
class UserMigrator
|
||||
{
|
||||
private PDO $sourceDb;
|
||||
private PDO $targetDb;
|
||||
private MigrationLogger $logger;
|
||||
private array $userMapping = [];
|
||||
|
||||
/**
|
||||
* Constructeur
|
||||
*
|
||||
* @param PDO $sourceDb Connexion source
|
||||
* @param PDO $targetDb Connexion cible
|
||||
* @param MigrationLogger $logger Logger
|
||||
*/
|
||||
public function __construct(PDO $sourceDb, PDO $targetDb, MigrationLogger $logger)
|
||||
{
|
||||
$this->sourceDb = $sourceDb;
|
||||
$this->targetDb = $targetDb;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre les utilisateurs d'une opération
|
||||
* - Si opération active : TOUS les users actifs de l'entité
|
||||
* - Si opération inactive : Uniquement ceux dans ope_users_sectors
|
||||
*
|
||||
* @param int $oldOperationId ID ancienne opération
|
||||
* @param int $newOperationId ID nouvelle opération
|
||||
* @param int $entityId ID de l'entité
|
||||
* @param bool $isActiveOperation True si opération active
|
||||
* @return array ['mapping' => array, 'count' => int]
|
||||
*/
|
||||
public function migrateOperationUsers(
|
||||
int $oldOperationId,
|
||||
int $newOperationId,
|
||||
int $entityId,
|
||||
bool $isActiveOperation
|
||||
): array {
|
||||
$this->logger->info("👥 Migration des utilisateurs de l'opération...");
|
||||
|
||||
// Réinitialiser le mapping pour cette opération
|
||||
$this->userMapping = [];
|
||||
|
||||
// Récupérer les utilisateurs selon le type d'opération
|
||||
if ($isActiveOperation) {
|
||||
// Pour l'opération active : TOUS les users actifs de l'entité
|
||||
$this->logger->info(" ℹ Opération ACTIVE : migration de tous les users actifs de l'entité");
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT rowid
|
||||
FROM users
|
||||
WHERE fk_entite = :entity_id AND active = 1
|
||||
");
|
||||
$stmt->execute([':entity_id' => $entityId]);
|
||||
$userIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
} else {
|
||||
// Pour les opérations inactives : uniquement ceux dans ope_users_sectors
|
||||
$this->logger->info(" ℹ Opération INACTIVE : migration des users affectés aux secteurs");
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT DISTINCT fk_user
|
||||
FROM ope_users_sectors
|
||||
WHERE fk_operation = :operation_id AND active = 1
|
||||
");
|
||||
$stmt->execute([':operation_id' => $oldOperationId]);
|
||||
$userIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
if (empty($userIds)) {
|
||||
$this->logger->warning("Aucun utilisateur trouvé pour l'opération {$oldOperationId}");
|
||||
return ['mapping' => [], 'count' => 0];
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($userIds as $oldUserId) {
|
||||
// Récupérer les infos utilisateur depuis la table users
|
||||
$stmt = $this->sourceDb->prepare("
|
||||
SELECT * FROM users WHERE rowid = :id AND active = 1
|
||||
");
|
||||
$stmt->execute([':id' => $oldUserId]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user) {
|
||||
$this->logger->warning(" ⚠ Utilisateur {$oldUserId} non trouvé ou inactif");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Créer dans ope_users de la nouvelle base
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO ope_users (
|
||||
fk_operation, fk_user, fk_role,
|
||||
first_name, encrypted_name, sect_name,
|
||||
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
|
||||
) VALUES (
|
||||
:fk_operation, :fk_user, :fk_role,
|
||||
:first_name, :encrypted_name, :sect_name,
|
||||
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
|
||||
)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':fk_operation' => $newOperationId,
|
||||
':fk_user' => $oldUserId, // Référence vers users.id
|
||||
':fk_role' => $user['fk_role'],
|
||||
':first_name' => $user['prenom'],
|
||||
':encrypted_name' => ApiService::encryptData($user['libelle']), // Chiffrer le nom avec IV aléatoire
|
||||
':sect_name' => $user['nom_tournee'],
|
||||
':created_at' => $user['date_creat'],
|
||||
':fk_user_creat' => $user['fk_user_creat'],
|
||||
':updated_at' => $user['date_modif'],
|
||||
':fk_user_modif' => $user['fk_user_modif'],
|
||||
':chk_active' => $user['active']
|
||||
]);
|
||||
|
||||
$newOpeUserId = (int)$this->targetDb->lastInsertId();
|
||||
|
||||
// Mapper oldUserId → newOpeUserId
|
||||
$this->userMapping[$oldUserId] = $newOpeUserId;
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->logger->success(" ✓ {$count} utilisateur(s) migré(s)");
|
||||
|
||||
return ['mapping' => $this->userMapping, 'count' => $count];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le mapping des utilisateurs
|
||||
*
|
||||
* @return array oldUserId => newOpeUserId
|
||||
*/
|
||||
public function getUserMapping(): array
|
||||
{
|
||||
return $this->userMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit le mapping des utilisateurs (utile pour réutilisation)
|
||||
*
|
||||
* @param array $mapping oldUserId => newOpeUserId
|
||||
*/
|
||||
public function setUserMapping(array $mapping): void
|
||||
{
|
||||
$this->userMapping = $mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le nouvel ID ope_users depuis le mapping
|
||||
*
|
||||
* @param int $oldUserId ID ancien utilisateur
|
||||
* @return int|null Nouvel ID ope_users ou null si non trouvé
|
||||
*/
|
||||
public function getMappedUserId(int $oldUserId): ?int
|
||||
{
|
||||
return $this->userMapping[$oldUserId] ?? null;
|
||||
}
|
||||
}
|
||||
471
api/scripts/migration2/php/migrate_from_backup.php
Executable file
471
api/scripts/migration2/php/migrate_from_backup.php
Executable file
@@ -0,0 +1,471 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script de migration v2 - Architecture modulaire
|
||||
*
|
||||
* Utilise les migrators spécialisés pour une migration hiérarchique par opération.
|
||||
* Source fixe: geosector (synchronisée 2x/jour par PM7 depuis nx4)
|
||||
* Cible: dva_geo (développement), rca_geo (recette) ou pra_geo (production)
|
||||
*
|
||||
* Usage:
|
||||
* Migration d'une entité:
|
||||
* php migrate_from_backup.php --mode=entity --entity-id=2
|
||||
*
|
||||
* Migration globale (toutes les entités):
|
||||
* php migrate_from_backup.php --mode=global
|
||||
*
|
||||
* Avec environnement explicite:
|
||||
* php migrate_from_backup.php --env=dva --mode=entity --entity-id=2
|
||||
*/
|
||||
|
||||
// Inclure ApiService pour le chiffrement
|
||||
require_once dirname(__DIR__, 3) . '/src/Services/ApiService.php';
|
||||
|
||||
// Inclure les classes v2
|
||||
require_once __DIR__ . '/lib/DatabaseConfig.php';
|
||||
require_once __DIR__ . '/lib/MigrationLogger.php';
|
||||
require_once __DIR__ . '/lib/DatabaseConnection.php';
|
||||
require_once __DIR__ . '/lib/UserMigrator.php';
|
||||
require_once __DIR__ . '/lib/PassageMigrator.php';
|
||||
require_once __DIR__ . '/lib/SectorMigrator.php';
|
||||
require_once __DIR__ . '/lib/OperationMigrator.php';
|
||||
|
||||
// Configuration PHP pour les grosses migrations
|
||||
ini_set('memory_limit', '512M');
|
||||
ini_set('max_execution_time', '3600'); // 1 heure max
|
||||
|
||||
class DataMigration
|
||||
{
|
||||
private PDO $sourceDb;
|
||||
private PDO $targetDb;
|
||||
private MigrationLogger $logger;
|
||||
private DatabaseConfig $config;
|
||||
private OperationMigrator $operationMigrator;
|
||||
|
||||
// Options
|
||||
private string $mode;
|
||||
private ?int $entityId;
|
||||
private bool $deleteBefore;
|
||||
|
||||
// Statistiques
|
||||
private array $migrationStats = [];
|
||||
|
||||
public function __construct(string $env, string $mode = 'global', ?int $entityId = null, ?string $logFile = null, bool $deleteBefore = true)
|
||||
{
|
||||
// Initialisation config et logger
|
||||
$this->config = new DatabaseConfig($env);
|
||||
$this->mode = $mode;
|
||||
$this->entityId = $entityId;
|
||||
$this->deleteBefore = $deleteBefore;
|
||||
|
||||
// Générer le nom du fichier log selon le mode si non spécifié
|
||||
if (!$logFile) {
|
||||
$logDir = dirname(__DIR__, 2) . '/logs';
|
||||
$timestamp = date('Ymd_His');
|
||||
|
||||
if ($mode === 'entity' && $entityId) {
|
||||
$logFile = "{$logDir}/migration_entite_{$entityId}_{$timestamp}.log";
|
||||
} else {
|
||||
$logFile = "{$logDir}/migration_global_{$timestamp}.log";
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger = new MigrationLogger($logFile);
|
||||
|
||||
// Log header
|
||||
$this->logHeader();
|
||||
|
||||
// Connexions
|
||||
$dbConnection = new DatabaseConnection($this->config, $this->logger);
|
||||
$dbConnection->connect();
|
||||
$this->sourceDb = $dbConnection->getSourceDb();
|
||||
$this->targetDb = $dbConnection->getTargetDb();
|
||||
|
||||
// Initialiser les migrators
|
||||
$this->initializeMigrators();
|
||||
}
|
||||
|
||||
private function initializeMigrators(): void
|
||||
{
|
||||
// Créer les migrators dans l'ordre de dépendance
|
||||
$passageMigrator = new PassageMigrator($this->sourceDb, $this->targetDb, $this->logger);
|
||||
$sectorMigrator = new SectorMigrator($this->sourceDb, $this->targetDb, $this->logger, $passageMigrator);
|
||||
$userMigrator = new UserMigrator($this->sourceDb, $this->targetDb, $this->logger);
|
||||
|
||||
$this->operationMigrator = new OperationMigrator(
|
||||
$this->sourceDb,
|
||||
$this->targetDb,
|
||||
$this->logger,
|
||||
$userMigrator,
|
||||
$sectorMigrator
|
||||
);
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
if ($this->mode === 'entity') {
|
||||
if (!$this->entityId) {
|
||||
throw new Exception("entity-id requis en mode entity");
|
||||
}
|
||||
$this->migrateEntity($this->entityId);
|
||||
} else {
|
||||
$this->migrateAllEntities();
|
||||
}
|
||||
|
||||
// Afficher le récapitulatif
|
||||
if (!empty($this->migrationStats)) {
|
||||
$this->logger->logMigrationSummary($this->migrationStats);
|
||||
}
|
||||
|
||||
$this->logger->separator();
|
||||
$this->logger->success("🎉 Migration terminée !");
|
||||
$this->logger->info("📄 Log: " . $this->logger->getLogFile());
|
||||
}
|
||||
|
||||
private function migrateEntity(int $entityId): void
|
||||
{
|
||||
$this->logger->separator();
|
||||
$this->logger->info("🏢 Migration de l'entité ID: {$entityId}");
|
||||
|
||||
// Supprimer les données existantes si demandé
|
||||
if ($this->deleteBefore) {
|
||||
$this->deleteEntityData($entityId);
|
||||
}
|
||||
|
||||
// Migrer l'entité elle-même
|
||||
$this->migrateEntityRecord($entityId);
|
||||
|
||||
// Migrer les users de l'entité (table centrale users)
|
||||
$this->migrateEntityUsers($entityId);
|
||||
|
||||
// Récupérer le nom de l'entité pour les stats
|
||||
$stmt = $this->sourceDb->prepare("SELECT libelle FROM users_entites WHERE rowid = :id");
|
||||
$stmt->execute([':id' => $entityId]);
|
||||
$entityName = $stmt->fetchColumn();
|
||||
|
||||
// Récupérer et migrer les opérations
|
||||
$operationIds = $this->operationMigrator->getOperationsToMigrate($entityId);
|
||||
|
||||
$operations = [];
|
||||
foreach ($operationIds as $oldOperationId) {
|
||||
$operationStats = $this->operationMigrator->migrateOperation($oldOperationId);
|
||||
if ($operationStats) {
|
||||
$operations[] = $operationStats;
|
||||
}
|
||||
}
|
||||
|
||||
// Stocker les stats pour cette entité
|
||||
$this->migrationStats = [
|
||||
'entity' => [
|
||||
'id' => $entityId,
|
||||
'name' => $entityName ?: "Entité #{$entityId}"
|
||||
],
|
||||
'operations' => $operations
|
||||
];
|
||||
}
|
||||
|
||||
private function migrateAllEntities(): void
|
||||
{
|
||||
// Récupérer toutes les entités actives
|
||||
$stmt = $this->sourceDb->query("SELECT rowid FROM users_entites WHERE active = 1 ORDER BY rowid");
|
||||
$entities = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
$this->logger->info("📊 " . count($entities) . " entité(s) à migrer");
|
||||
|
||||
$allOperations = [];
|
||||
foreach ($entities as $entityId) {
|
||||
// Sauvegarder les stats actuelles avant de migrer
|
||||
$previousStats = $this->migrationStats;
|
||||
|
||||
$this->migrateEntity($entityId);
|
||||
|
||||
// Agréger les opérations de toutes les entités
|
||||
if (!empty($this->migrationStats['operations'])) {
|
||||
$allOperations = array_merge($allOperations, $this->migrationStats['operations']);
|
||||
}
|
||||
}
|
||||
|
||||
// Stocker les stats globales
|
||||
$this->migrationStats = [
|
||||
'operations' => $allOperations
|
||||
];
|
||||
}
|
||||
|
||||
private function deleteEntityData(int $entityId): void
|
||||
{
|
||||
$this->logger->separator();
|
||||
$this->logger->warning("🗑️ Suppression des données de l'entité {$entityId}...");
|
||||
|
||||
// Ordre inverse des contraintes FK
|
||||
$tables = [
|
||||
'medias' => "fk_entite = {$entityId} OR fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId})",
|
||||
'ope_pass_histo' => "fk_pass IN (SELECT id FROM ope_pass WHERE fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId}))",
|
||||
'ope_pass' => "fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId})",
|
||||
'ope_users_sectors' => "fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId})",
|
||||
'ope_users' => "fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId})",
|
||||
'sectors_adresses' => "fk_sector IN (SELECT id FROM ope_sectors WHERE fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId}))",
|
||||
'ope_sectors' => "fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId})",
|
||||
'operations' => "fk_entite = {$entityId}",
|
||||
'users' => "fk_entite = {$entityId}"
|
||||
];
|
||||
|
||||
foreach ($tables as $table => $condition) {
|
||||
$stmt = $this->targetDb->query("DELETE FROM {$table} WHERE {$condition}");
|
||||
$count = $stmt->rowCount();
|
||||
if ($count > 0) {
|
||||
$this->logger->info(" ✓ {$table}: {$count} ligne(s) supprimée(s)");
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->success("✓ Suppression terminée");
|
||||
}
|
||||
|
||||
private function migrateEntityRecord(int $entityId): void
|
||||
{
|
||||
// Vérifier si existe déjà
|
||||
$stmt = $this->targetDb->prepare("SELECT COUNT(*) FROM entites WHERE id = :id");
|
||||
$stmt->execute([':id' => $entityId]);
|
||||
|
||||
if ($stmt->fetchColumn() > 0) {
|
||||
$this->logger->info("Entité {$entityId} existe déjà, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer depuis source
|
||||
$stmt = $this->sourceDb->prepare("SELECT * FROM users_entites WHERE rowid = :id");
|
||||
$stmt->execute([':id' => $entityId]);
|
||||
$entity = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$entity) {
|
||||
throw new Exception("Entité {$entityId} non trouvée");
|
||||
}
|
||||
|
||||
// Insérer dans cible (schéma geo_app)
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO entites (
|
||||
id, encrypted_name, adresse1, adresse2, code_postal, ville,
|
||||
fk_region, fk_type, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
gps_lat, gps_lng, chk_stripe, encrypted_stripe_id, encrypted_iban, encrypted_bic,
|
||||
chk_demo, chk_mdp_manuel, chk_username_manuel, chk_user_delete_pass,
|
||||
chk_copie_mail_recu, chk_accept_sms, chk_lot_actif,
|
||||
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
|
||||
) VALUES (
|
||||
:id, :encrypted_name, :adresse1, :adresse2, :code_postal, :ville,
|
||||
:fk_region, :fk_type, :encrypted_phone, :encrypted_mobile, :encrypted_email,
|
||||
:gps_lat, :gps_lng, :chk_stripe, :encrypted_stripe_id, :encrypted_iban, :encrypted_bic,
|
||||
:chk_demo, :chk_mdp_manuel, :chk_username_manuel, :chk_user_delete_pass,
|
||||
:chk_copie_mail_recu, :chk_accept_sms, :chk_lot_actif,
|
||||
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
|
||||
)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':id' => $entityId,
|
||||
':encrypted_name' => $entity['libelle'] ? ApiService::encryptData($entity['libelle']) : '',
|
||||
':adresse1' => $entity['adresse1'] ?? '',
|
||||
':adresse2' => $entity['adresse2'] ?? '',
|
||||
':code_postal' => $entity['cp'] ?? '',
|
||||
':ville' => $entity['ville'] ?? '',
|
||||
':fk_region' => $entity['fk_region'],
|
||||
':fk_type' => $entity['fk_type'] ?? 1,
|
||||
':encrypted_phone' => $entity['tel1'] ? ApiService::encryptData($entity['tel1']) : '',
|
||||
':encrypted_mobile' => $entity['tel2'] ? ApiService::encryptData($entity['tel2']) : '',
|
||||
':encrypted_email' => $entity['email'] ? ApiService::encryptSearchableData($entity['email']) : '',
|
||||
':gps_lat' => $entity['gps_lat'] ?? '',
|
||||
':gps_lng' => $entity['gps_lng'] ?? '',
|
||||
':chk_stripe' => 0,
|
||||
':encrypted_stripe_id' => '',
|
||||
':encrypted_iban' => $entity['iban'] ? ApiService::encryptData($entity['iban']) : '',
|
||||
':encrypted_bic' => $entity['bic'] ? ApiService::encryptData($entity['bic']) : '',
|
||||
':chk_demo' => $entity['demo'] ?? 1,
|
||||
':chk_mdp_manuel' => $entity['chk_mdp_manuel'] ?? 0,
|
||||
':chk_username_manuel' => 0,
|
||||
':chk_user_delete_pass' => 0,
|
||||
':chk_copie_mail_recu' => $entity['chk_copie_mail_recu'] ?? 0,
|
||||
':chk_accept_sms' => $entity['chk_accept_sms'] ?? 0,
|
||||
':chk_lot_actif' => 0,
|
||||
':created_at' => date('Y-m-d H:i:s'),
|
||||
':fk_user_creat' => 0,
|
||||
':updated_at' => $entity['date_modif'],
|
||||
':fk_user_modif' => $entity['fk_user_modif'] ?? 0,
|
||||
':chk_active' => $entity['active'] ?? 1
|
||||
]);
|
||||
|
||||
$this->logger->success("✓ Entité {$entityId} migrée");
|
||||
}
|
||||
|
||||
private function migrateEntityUsers(int $entityId): void
|
||||
{
|
||||
$stmt = $this->sourceDb->prepare("SELECT * FROM users WHERE fk_entite = :entity_id AND active = 1");
|
||||
$stmt->execute([':entity_id' => $entityId]);
|
||||
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$count = 0;
|
||||
foreach ($users as $user) {
|
||||
// Vérifier si existe déjà
|
||||
$stmt = $this->targetDb->prepare("SELECT COUNT(*) FROM users WHERE id = :id");
|
||||
$stmt->execute([':id' => $user['rowid']]);
|
||||
|
||||
if ($stmt->fetchColumn() > 0) {
|
||||
continue; // Skip si existe
|
||||
}
|
||||
|
||||
// Insérer l'utilisateur
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO users (
|
||||
id, fk_entite, fk_role, first_name, encrypted_name,
|
||||
encrypted_user_name, user_pass_hash, encrypted_email, encrypted_phone, encrypted_mobile,
|
||||
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
|
||||
) VALUES (
|
||||
:id, :fk_entite, :fk_role, :first_name, :encrypted_name,
|
||||
:encrypted_user_name, :user_pass_hash, :encrypted_email, :encrypted_phone, :encrypted_mobile,
|
||||
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
|
||||
)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':id' => $user['rowid'],
|
||||
':fk_entite' => $user['fk_entite'],
|
||||
':fk_role' => $user['fk_role'],
|
||||
':first_name' => $user['prenom'],
|
||||
':encrypted_name' => ApiService::encryptData($user['libelle']), // Chiffrer avec IV aléatoire
|
||||
':encrypted_user_name' => ApiService::encryptSearchableData($user['username']),
|
||||
':user_pass_hash' => $user['userpswd'], // Hash bcrypt du mot de passe
|
||||
':encrypted_email' => $user['email'] ? ApiService::encryptSearchableData($user['email']) : null,
|
||||
':encrypted_phone' => $user['telephone'] ? ApiService::encryptData($user['telephone']) : null,
|
||||
':encrypted_mobile' => $user['mobile'] ? ApiService::encryptData($user['mobile']) : null,
|
||||
':created_at' => $user['date_creat'],
|
||||
':fk_user_creat' => $user['fk_user_creat'],
|
||||
':updated_at' => $user['date_modif'],
|
||||
':fk_user_modif' => $user['fk_user_modif'],
|
||||
':chk_active' => $user['active']
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->logger->success("✓ {$count} utilisateur(s) de l'entité migré(s)");
|
||||
}
|
||||
|
||||
private function logHeader(): void
|
||||
{
|
||||
$this->logger->separator();
|
||||
$this->logger->info("🚀 Migration v2 - Architecture modulaire");
|
||||
$this->logger->info("📅 Date: " . date('Y-m-d H:i:s'));
|
||||
$this->logger->info("🌍 Environnement: " . $this->config->getEnvName());
|
||||
$this->logger->info("🔧 Mode: " . $this->mode);
|
||||
if ($this->entityId) {
|
||||
$this->logger->info("🏢 Entité: " . $this->entityId);
|
||||
}
|
||||
$this->logger->info("🗑️ Suppression avant: " . ($this->deleteBefore ? 'OUI' : 'NON'));
|
||||
$this->logger->separator();
|
||||
}
|
||||
}
|
||||
|
||||
// === GESTION DES ARGUMENTS CLI ===
|
||||
|
||||
function parseArguments(array $argv): array
|
||||
{
|
||||
$options = [
|
||||
'env' => DatabaseConfig::autoDetect(),
|
||||
'mode' => 'global',
|
||||
'entity-id' => null,
|
||||
'log' => null,
|
||||
'delete-before' => true,
|
||||
'help' => false
|
||||
];
|
||||
|
||||
foreach ($argv as $arg) {
|
||||
if ($arg === '--help') {
|
||||
$options['help'] = true;
|
||||
} elseif (preg_match('/^--env=(.+)$/', $arg, $matches)) {
|
||||
$options['env'] = $matches[1];
|
||||
} elseif (preg_match('/^--mode=(.+)$/', $arg, $matches)) {
|
||||
$options['mode'] = $matches[1];
|
||||
} elseif (preg_match('/^--entity-id=(\d+)$/', $arg, $matches)) {
|
||||
$options['entity-id'] = (int)$matches[1];
|
||||
} elseif (preg_match('/^--log=(.+)$/', $arg, $matches)) {
|
||||
$options['log'] = $matches[1];
|
||||
} elseif ($arg === '--delete-before=false') {
|
||||
$options['delete-before'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
function showHelp(): void
|
||||
{
|
||||
echo <<<HELP
|
||||
|
||||
🚀 Migration v2 - Architecture modulaire
|
||||
|
||||
USAGE:
|
||||
php migrate_from_backup.php [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--env=ENV Environnement: 'dva' (développement), 'rca' (recette) ou 'pra' (production)
|
||||
Par défaut: auto-détection selon hostname
|
||||
|
||||
--mode=MODE Mode de migration: 'global' ou 'entity'
|
||||
Par défaut: global
|
||||
|
||||
--entity-id=ID ID de l'entité à migrer (requis si mode=entity)
|
||||
|
||||
--log=PATH Fichier de log personnalisé
|
||||
Par défaut: logs/migration_YYYYMMDD_HHMMSS.log
|
||||
|
||||
--delete-before Supprimer les données existantes avant migration
|
||||
Par défaut: true
|
||||
Utiliser --delete-before=false pour désactiver
|
||||
|
||||
--help Afficher cette aide
|
||||
|
||||
EXEMPLES:
|
||||
# Migration d'une entité avec suppression (recommandé)
|
||||
php migrate_from_backup.php --mode=entity --entity-id=2
|
||||
|
||||
# Migration sans suppression (risque de doublons)
|
||||
php migrate_from_backup.php --mode=entity --entity-id=2 --delete-before=false
|
||||
|
||||
# Migration globale de toutes les entités
|
||||
php migrate_from_backup.php --mode=global
|
||||
|
||||
# Spécifier l'environnement manuellement (DVA, RCA ou PRA)
|
||||
php migrate_from_backup.php --env=dva --mode=entity --entity-id=2
|
||||
|
||||
|
||||
HELP;
|
||||
}
|
||||
|
||||
// === POINT D'ENTRÉE ===
|
||||
|
||||
try {
|
||||
$options = parseArguments($argv);
|
||||
|
||||
if ($options['help']) {
|
||||
showHelp();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Valider l'environnement
|
||||
if (!DatabaseConfig::exists($options['env'])) {
|
||||
throw new Exception("Invalid environment: {$options['env']}. Use 'dva', 'rca' or 'pra'");
|
||||
}
|
||||
|
||||
// Créer et exécuter la migration
|
||||
$migration = new DataMigration(
|
||||
$options['env'],
|
||||
$options['mode'],
|
||||
$options['entity-id'],
|
||||
$options['log'],
|
||||
$options['delete-before']
|
||||
);
|
||||
|
||||
$migration->run();
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ ERREUR: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
2047
api/scripts/migration2/php/migrate_from_backup.php.backup
Executable file
2047
api/scripts/migration2/php/migrate_from_backup.php.backup
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user