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:
115
api/.vscode/settings.json
vendored
Normal file → Executable file
115
api/.vscode/settings.json
vendored
Normal file → Executable file
@@ -1,4 +1,116 @@
|
||||
{
|
||||
"window.zoomLevel": 1, // Permet de zoomer, pratique si vous faites une présentation
|
||||
|
||||
// Apparence
|
||||
// -- Editeur
|
||||
"workbench.startupEditor": "none", // On ne veut pas une page d'accueil chargée
|
||||
"editor.minimap.enabled": true, // On veut voir la minimap
|
||||
"editor.minimap.showSlider": "always", // On veut voir la minimap
|
||||
"editor.minimap.size": "fill", // On veut voir la minimap
|
||||
"editor.minimap.scale": 2,
|
||||
"editor.tokenColorCustomizations": {
|
||||
"textMateRules": [
|
||||
{
|
||||
"scope": ["storage.type.function", "storage.type.class"],
|
||||
"settings": {
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#4B9CD3"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"editor.minimap.renderCharacters": true,
|
||||
"editor.minimap.maxColumn": 120,
|
||||
"breadcrumbs.enabled": false,
|
||||
// -- Tabs
|
||||
"workbench.editor.wrapTabs": true, // On veut voir les tabs
|
||||
"workbench.editor.tabSizing": "shrink", // On veut voir les tabs
|
||||
"workbench.editor.pinnedTabSizing": "compact",
|
||||
"workbench.editor.enablePreview": false, // Un clic sur un fichier l'ouvre
|
||||
|
||||
// -- Sidebar
|
||||
"workbench.tree.indent": 15, // Indente plus pour plus de clarté dans la sidebar
|
||||
"workbench.tree.renderIndentGuides": "always",
|
||||
// -- Code
|
||||
"editor.occurrencesHighlight": "singleFile", // On veut voir les occurences d'une variable
|
||||
"editor.renderWhitespace": "trailing", // On ne veut pas laisser d'espace en fin de ligne
|
||||
"editor.renderControlCharacters": true, // On veut voir les caractères de contrôle
|
||||
// Thème
|
||||
"editor.fontFamily": "'JetBrains Mono', 'Fira Code', 'Operator Mono Lig', monospace",
|
||||
"editor.fontLigatures": false,
|
||||
"editor.fontSize": 13,
|
||||
"editor.lineHeight": 22,
|
||||
"editor.guides.bracketPairs": "active",
|
||||
|
||||
// Ergonomie
|
||||
"editor.wordWrap": "off",
|
||||
"editor.rulers": [],
|
||||
"editor.suggest.insertMode": "replace", // L'autocomplétion remplace le mot en cours
|
||||
"editor.acceptSuggestionOnCommitCharacter": false, // Evite que l'autocomplétion soit accepté lors d'un . par exemple
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.linkedEditing": true, // Quand on change un élément HTML, change la balise fermante
|
||||
"editor.tabSize": 2,
|
||||
"editor.unicodeHighlight.nonBasicASCII": false,
|
||||
|
||||
"[php]": {
|
||||
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true
|
||||
},
|
||||
"intelephense.format.braces": "k&r",
|
||||
"intelephense.format.enable": true,
|
||||
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true
|
||||
},
|
||||
"prettier.printWidth": 360,
|
||||
"prettier.semi": true,
|
||||
"prettier.singleQuote": true,
|
||||
"prettier.tabWidth": 2,
|
||||
"prettier.trailingComma": "es5",
|
||||
|
||||
"explorer.autoReveal": false,
|
||||
"explorer.confirmDragAndDrop": false,
|
||||
"emmet.triggerExpansionOnTab": true,
|
||||
"emmet.includeLanguages": {
|
||||
"javascript": "javascriptreact"
|
||||
},
|
||||
"problems.decorations.enabled": true,
|
||||
"explorer.decorations.colors": true,
|
||||
"explorer.decorations.badges": true,
|
||||
"php.validate.enable": true,
|
||||
"php.suggest.basic": false,
|
||||
"dart.analysisExcludedFolders": [],
|
||||
"dart.enableSdkFormatter": true,
|
||||
|
||||
// Fichiers
|
||||
"files.defaultLanguage": "markdown",
|
||||
"files.autoSaveWorkspaceFilesOnly": true,
|
||||
"files.exclude": {
|
||||
"**/.idea": true
|
||||
},
|
||||
// Languages
|
||||
"javascript.preferences.importModuleSpecifierEnding": "js",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "js",
|
||||
|
||||
// Extensions
|
||||
"tailwindCSS.experimental.configFile": "web/tailwind.config.js",
|
||||
"editor.quickSuggestions": {
|
||||
"strings": true
|
||||
},
|
||||
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"prettier.documentSelectors": ["**/*.svelte"],
|
||||
"svelte.plugin.svelte.diagnostics.enable": false,
|
||||
|
||||
"js/ts.implicitProjectConfig.checkJs": false,
|
||||
"svelte.enable-ts-plugin": false,
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.activeBackground": "#fa1b49",
|
||||
"activityBar.background": "#fa1b49",
|
||||
@@ -18,5 +130,6 @@
|
||||
"titleBar.inactiveBackground": "#dd053199",
|
||||
"titleBar.inactiveForeground": "#e7e7e799"
|
||||
},
|
||||
"peacock.color": "#dd0531"
|
||||
"peacock.color": "#dd0531",
|
||||
|
||||
}
|
||||
|
||||
651
api/PM7/d6back.sh
Normal file
651
api/PM7/d6back.sh
Normal file
@@ -0,0 +1,651 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -uo pipefail
|
||||
# Note: Removed -e to allow script to continue on errors
|
||||
# Errors are handled explicitly with ERROR_COUNT
|
||||
|
||||
# Parse command line arguments
|
||||
ONLY_DB=false
|
||||
if [[ "${1:-}" == "-onlydb" ]]; then
|
||||
ONLY_DB=true
|
||||
echo "Mode: Database backup only"
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_FILE="$SCRIPT_DIR/d6back.yaml"
|
||||
LOG_DIR="$SCRIPT_DIR/logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
LOG_FILE="$LOG_DIR/d6back-$(date +%Y%m%d).log"
|
||||
ERROR_COUNT=0
|
||||
RECAP_FILE="/tmp/backup_recap_$$.txt"
|
||||
|
||||
# Lock file to prevent concurrent executions
|
||||
LOCK_FILE="/var/lock/d6back.lock"
|
||||
exec 200>"$LOCK_FILE"
|
||||
if ! flock -n 200; then
|
||||
echo "ERROR: Another backup is already running" >&2
|
||||
exit 1
|
||||
fi
|
||||
trap 'flock -u 200' EXIT
|
||||
|
||||
# Clean old log files (keep only last 10)
|
||||
find "$LOG_DIR" -maxdepth 1 -name "d6back-*.log" -type f 2>/dev/null | sort -r | tail -n +11 | xargs -r rm -f || true
|
||||
|
||||
# Check dependencies - COMMENTED OUT
|
||||
# for cmd in yq ssh tar openssl; do
|
||||
# if ! command -v "$cmd" &> /dev/null; then
|
||||
# echo "ERROR: $cmd is required but not installed" | tee -a "$LOG_FILE"
|
||||
# exit 1
|
||||
# fi
|
||||
# done
|
||||
|
||||
# Load config
|
||||
DIR_BACKUP=$(yq '.global.dir_backup' "$CONFIG_FILE" | tr -d '"')
|
||||
ENC_KEY_PATH=$(yq '.global.enc_key' "$CONFIG_FILE" | tr -d '"')
|
||||
BACKUP_SERVER=$(yq '.global.backup_server // "BACKUP"' "$CONFIG_FILE" | tr -d '"')
|
||||
EMAIL_TO=$(yq '.global.email_to // "support@unikoffice.com"' "$CONFIG_FILE" | tr -d '"')
|
||||
KEEP_DIRS=$(yq '.global.keep_dirs' "$CONFIG_FILE" | tr -d '"')
|
||||
KEEP_DB=$(yq '.global.keep_db' "$CONFIG_FILE" | tr -d '"')
|
||||
|
||||
# Load encryption key
|
||||
if [[ ! -f "$ENC_KEY_PATH" ]]; then
|
||||
echo "ERROR: Encryption key not found: $ENC_KEY_PATH" | tee -a "$LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
ENC_KEY=$(cat "$ENC_KEY_PATH")
|
||||
|
||||
echo "=== Backup Started $(date) ===" | tee -a "$LOG_FILE"
|
||||
echo "Backup directory: $DIR_BACKUP" | tee -a "$LOG_FILE"
|
||||
|
||||
# Check available disk space
|
||||
DISK_USAGE=$(df "$DIR_BACKUP" | tail -1 | awk '{print $5}' | sed 's/%//')
|
||||
DISK_FREE=$((100 - DISK_USAGE))
|
||||
|
||||
if [[ $DISK_FREE -lt 20 ]]; then
|
||||
echo "WARNING: Low disk space! Only ${DISK_FREE}% free on backup partition" | tee -a "$LOG_FILE"
|
||||
|
||||
# Send warning email
|
||||
echo "Sending DISK SPACE WARNING email to $EMAIL_TO (${DISK_FREE}% free)" | tee -a "$LOG_FILE"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: Backup${BACKUP_SERVER} WARNING - Low disk space (${DISK_FREE}% free)"
|
||||
echo ""
|
||||
echo "WARNING: Low disk space on $(hostname)"
|
||||
echo ""
|
||||
echo "Backup directory: $DIR_BACKUP"
|
||||
echo "Disk usage: ${DISK_USAGE}%"
|
||||
echo "Free space: ${DISK_FREE}%"
|
||||
echo ""
|
||||
echo "The backup will continue but please free up some space soon."
|
||||
echo ""
|
||||
echo "Date: $(date '+%d.%m.%Y %H:%M')"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
echo "DISK SPACE WARNING email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "WARNING: msmtp not found - DISK WARNING email NOT sent" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
else
|
||||
echo "Disk space OK: ${DISK_FREE}% free" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
# Initialize recap file
|
||||
echo "BACKUP REPORT - $(hostname) - $(date '+%d.%m.%Y %H')h" > "$RECAP_FILE"
|
||||
echo "========================================" >> "$RECAP_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
|
||||
# Function to format size in MB with thousand separator
|
||||
format_size_mb() {
|
||||
local file="$1"
|
||||
if [[ -f "$file" ]]; then
|
||||
local size_kb=$(du -k "$file" | cut -f1)
|
||||
local size_mb=$((size_kb / 1024))
|
||||
# Add thousand separator with printf and sed
|
||||
printf "%d" "$size_mb" | sed ':a;s/\B[0-9]\{3\}\>/\.&/;ta'
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to calculate age in days
|
||||
get_age_days() {
|
||||
local file="$1"
|
||||
local now=$(date +%s)
|
||||
local file_time=$(stat -c %Y "$file" 2>/dev/null || echo 0)
|
||||
echo $(( (now - file_time) / 86400 ))
|
||||
}
|
||||
|
||||
# Function to get week number of year for a file
|
||||
get_week_year() {
|
||||
local file="$1"
|
||||
local file_time=$(stat -c %Y "$file" 2>/dev/null || echo 0)
|
||||
date -d "@$file_time" +"%Y-%W"
|
||||
}
|
||||
|
||||
# Function to cleanup old backups according to retention policy
|
||||
cleanup_old_backups() {
|
||||
local DELETED_COUNT=0
|
||||
local KEPT_COUNT=0
|
||||
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
echo "=== Starting Backup Retention Cleanup ===" | tee -a "$LOG_FILE"
|
||||
|
||||
# Parse retention periods
|
||||
local KEEP_DIRS_DAYS=${KEEP_DIRS%d} # Remove 'd' suffix
|
||||
|
||||
# Parse database retention (5d,3w,15m)
|
||||
IFS=',' read -r KEEP_DB_DAILY KEEP_DB_WEEKLY KEEP_DB_MONTHLY <<< "$KEEP_DB"
|
||||
local KEEP_DB_DAILY_DAYS=${KEEP_DB_DAILY%d}
|
||||
local KEEP_DB_WEEKLY_WEEKS=${KEEP_DB_WEEKLY%w}
|
||||
local KEEP_DB_MONTHLY_MONTHS=${KEEP_DB_MONTHLY%m}
|
||||
|
||||
# Convert to days
|
||||
local KEEP_DB_WEEKLY_DAYS=$((KEEP_DB_WEEKLY_WEEKS * 7))
|
||||
local KEEP_DB_MONTHLY_DAYS=$((KEEP_DB_MONTHLY_MONTHS * 30))
|
||||
|
||||
echo "Retention policy: dirs=${KEEP_DIRS_DAYS}d, db=${KEEP_DB_DAILY_DAYS}d/${KEEP_DB_WEEKLY_WEEKS}w/${KEEP_DB_MONTHLY_MONTHS}m" | tee -a "$LOG_FILE"
|
||||
|
||||
# Process each host directory
|
||||
for host_dir in "$DIR_BACKUP"/*; do
|
||||
if [[ ! -d "$host_dir" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local host_name=$(basename "$host_dir")
|
||||
echo " Cleaning host: $host_name" | tee -a "$LOG_FILE"
|
||||
|
||||
# Clean directory backups (*.tar.gz but not *.sql.gz.enc)
|
||||
while IFS= read -r -d '' file; do
|
||||
if [[ $(basename "$file") == *".sql.gz.enc" ]]; then
|
||||
continue # Skip SQL files
|
||||
fi
|
||||
|
||||
local age_days=$(get_age_days "$file")
|
||||
|
||||
if [[ $age_days -gt $KEEP_DIRS_DAYS ]]; then
|
||||
rm -f "$file"
|
||||
echo " Deleted: $(basename "$file") (${age_days}d > ${KEEP_DIRS_DAYS}d)" | tee -a "$LOG_FILE"
|
||||
((DELETED_COUNT++))
|
||||
else
|
||||
((KEPT_COUNT++))
|
||||
fi
|
||||
done < <(find "$host_dir" -name "*.tar.gz" -type f -print0 2>/dev/null)
|
||||
|
||||
# Clean database backups with retention policy
|
||||
declare -A db_files
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
local filename=$(basename "$file")
|
||||
local db_name=${filename%%_*}
|
||||
|
||||
if [[ -z "${db_files[$db_name]:-}" ]]; then
|
||||
db_files[$db_name]="$file"
|
||||
else
|
||||
db_files[$db_name]+=$'\n'"$file"
|
||||
fi
|
||||
done < <(find "$host_dir" -name "*.sql.gz.enc" -type f -print0 2>/dev/null)
|
||||
|
||||
# Process each database
|
||||
for db_name in "${!db_files[@]}"; do
|
||||
# Sort files by age (newest first)
|
||||
mapfile -t files < <(echo "${db_files[$db_name]}" | while IFS= read -r f; do
|
||||
echo "$f"
|
||||
done | xargs -I {} stat -c "%Y {}" {} 2>/dev/null | sort -rn | cut -d' ' -f2-)
|
||||
|
||||
# Track which files to keep
|
||||
declare -A keep_daily
|
||||
declare -A keep_weekly
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
local age_days=$(get_age_days "$file")
|
||||
|
||||
if [[ $age_days -le $KEEP_DB_DAILY_DAYS ]]; then
|
||||
# Keep all files within daily retention
|
||||
((KEPT_COUNT++))
|
||||
|
||||
elif [[ $age_days -le $KEEP_DB_WEEKLY_DAYS ]]; then
|
||||
# Weekly retention: keep one per day
|
||||
local file_date=$(date -d "@$(stat -c %Y "$file")" +"%Y-%m-%d")
|
||||
|
||||
if [[ -z "${keep_daily[$file_date]:-}" ]]; then
|
||||
keep_daily[$file_date]="$file"
|
||||
((KEPT_COUNT++))
|
||||
else
|
||||
rm -f "$file"
|
||||
((DELETED_COUNT++))
|
||||
fi
|
||||
|
||||
elif [[ $age_days -le $KEEP_DB_MONTHLY_DAYS ]]; then
|
||||
# Monthly retention: keep one per week
|
||||
local week_year=$(get_week_year "$file")
|
||||
|
||||
if [[ -z "${keep_weekly[$week_year]:-}" ]]; then
|
||||
keep_weekly[$week_year]="$file"
|
||||
((KEPT_COUNT++))
|
||||
else
|
||||
rm -f "$file"
|
||||
((DELETED_COUNT++))
|
||||
fi
|
||||
|
||||
else
|
||||
# Beyond retention period
|
||||
rm -f "$file"
|
||||
echo " Deleted: $(basename "$file") (${age_days}d > ${KEEP_DB_MONTHLY_DAYS}d)" | tee -a "$LOG_FILE"
|
||||
((DELETED_COUNT++))
|
||||
fi
|
||||
done
|
||||
|
||||
unset keep_daily keep_weekly
|
||||
done
|
||||
|
||||
unset db_files
|
||||
done
|
||||
|
||||
echo "Cleanup completed: ${DELETED_COUNT} deleted, ${KEPT_COUNT} kept" | tee -a "$LOG_FILE"
|
||||
|
||||
# Add cleanup summary to recap file
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "CLEANUP SUMMARY:" >> "$RECAP_FILE"
|
||||
echo " Files deleted: $DELETED_COUNT" >> "$RECAP_FILE"
|
||||
echo " Files kept: $KEPT_COUNT" >> "$RECAP_FILE"
|
||||
}
|
||||
|
||||
# Function to backup a single database (must be defined before use)
|
||||
backup_database() {
|
||||
local database="$1"
|
||||
local timestamp="$(date +%Y%m%d_%H)"
|
||||
local backup_file="$backup_dir/sql/${database}_${timestamp}.sql.gz.enc"
|
||||
|
||||
echo " Backing up database: $database" | tee -a "$LOG_FILE"
|
||||
|
||||
if [[ "$ssh_user" != "root" ]]; then
|
||||
CMD_PREFIX="sudo"
|
||||
else
|
||||
CMD_PREFIX=""
|
||||
fi
|
||||
|
||||
# Execute backup with encryption
|
||||
# First test MySQL connection to get clear error messages (|| true to continue on error)
|
||||
MYSQL_TEST=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- bash -c 'cat > /tmp/d6back.cnf << EOF
|
||||
[client]
|
||||
user=$db_user
|
||||
password=$db_pass
|
||||
host=$db_host
|
||||
EOF
|
||||
chmod 600 /tmp/d6back.cnf
|
||||
mariadb --defaults-extra-file=/tmp/d6back.cnf -e \"SELECT 1\" 2>&1
|
||||
rm -f /tmp/d6back.cnf'" 2>/dev/null || true)
|
||||
|
||||
if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- bash -c 'cat > /tmp/d6back.cnf << EOF
|
||||
[client]
|
||||
user=$db_user
|
||||
password=$db_pass
|
||||
host=$db_host
|
||||
EOF
|
||||
chmod 600 /tmp/d6back.cnf
|
||||
mariadb-dump --defaults-extra-file=/tmp/d6back.cnf --single-transaction --lock-tables=false --add-drop-table --create-options --databases $database 2>/dev/null | sed -e \"/^CREATE DATABASE/s/\\\`$database\\\`/\\\`${database}_${timestamp}\\\`/\" -e \"/^USE/s/\\\`$database\\\`/\\\`${database}_${timestamp}\\\`/\" | gzip
|
||||
rm -f /tmp/d6back.cnf'" | \
|
||||
openssl enc -aes-256-cbc -salt -pass pass:"$ENC_KEY" -pbkdf2 > "$backup_file" 2>/dev/null; then
|
||||
|
||||
# Validate backup file size (encrypted SQL should be > 100 bytes)
|
||||
if [[ -f "$backup_file" ]]; then
|
||||
file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0)
|
||||
if [[ $file_size -lt 100 ]]; then
|
||||
# Analyze MySQL connection test results
|
||||
if [[ "$MYSQL_TEST" == *"Access denied"* ]]; then
|
||||
echo " ERROR: MySQL authentication failed for $database on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
echo " User: $db_user@$db_host - Check password in configuration" | tee -a "$LOG_FILE"
|
||||
elif [[ "$MYSQL_TEST" == *"Unknown database"* ]]; then
|
||||
echo " ERROR: Database '$database' does not exist on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
elif [[ "$MYSQL_TEST" == *"Can't connect"* ]]; then
|
||||
echo " ERROR: Cannot connect to MySQL server at $db_host in $container_name" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo " ERROR: Backup file too small (${file_size} bytes): $database on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
((ERROR_COUNT++))
|
||||
rm -f "$backup_file"
|
||||
else
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved (encrypted): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " SQL: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE"
|
||||
|
||||
# Test backup integrity
|
||||
if ! openssl enc -aes-256-cbc -d -pass pass:"$ENC_KEY" -pbkdf2 -in "$backup_file" | gunzip -t 2>/dev/null; then
|
||||
echo " ERROR: Backup integrity check failed for $database" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Backup file not created: $database" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
# Analyze MySQL connection test for failed backup
|
||||
if [[ "$MYSQL_TEST" == *"Access denied"* ]]; then
|
||||
echo " ERROR: MySQL authentication failed for $database on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
echo " User: $db_user@$db_host - Check password in configuration" | tee -a "$LOG_FILE"
|
||||
elif [[ "$MYSQL_TEST" == *"Unknown database"* ]]; then
|
||||
echo " ERROR: Database '$database' does not exist on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
elif [[ "$MYSQL_TEST" == *"Can't connect"* ]]; then
|
||||
echo " ERROR: Cannot connect to MySQL server at $db_host in $container_name" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo " ERROR: Failed to backup database $database on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
((ERROR_COUNT++))
|
||||
rm -f "$backup_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Process each host
|
||||
host_count=$(yq '.hosts | length' "$CONFIG_FILE")
|
||||
|
||||
for ((i=0; i<$host_count; i++)); do
|
||||
host_name=$(yq ".hosts[$i].name" "$CONFIG_FILE" | tr -d '"')
|
||||
host_ip=$(yq ".hosts[$i].ip" "$CONFIG_FILE" | tr -d '"')
|
||||
ssh_user=$(yq ".hosts[$i].user" "$CONFIG_FILE" | tr -d '"')
|
||||
ssh_key=$(yq ".hosts[$i].key" "$CONFIG_FILE" | tr -d '"')
|
||||
ssh_port=$(yq ".hosts[$i].port // 22" "$CONFIG_FILE" | tr -d '"')
|
||||
|
||||
echo "Processing host: $host_name ($host_ip)" | tee -a "$LOG_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "HOST: $host_name ($host_ip)" >> "$RECAP_FILE"
|
||||
echo "----------------------------" >> "$RECAP_FILE"
|
||||
|
||||
# Test SSH connection
|
||||
if ! ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 -o StrictHostKeyChecking=no "$ssh_user@$host_ip" "true" 2>/dev/null; then
|
||||
echo " ERROR: Cannot connect to $host_name ($host_ip)" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Process containers
|
||||
container_count=$(yq ".hosts[$i].containers | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
|
||||
|
||||
for ((c=0; c<$container_count; c++)); do
|
||||
container_name=$(yq ".hosts[$i].containers[$c].name" "$CONFIG_FILE" | tr -d '"')
|
||||
|
||||
echo " Processing container: $container_name" | tee -a "$LOG_FILE"
|
||||
|
||||
# Add container to recap
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo " Container: $container_name" >> "$RECAP_FILE"
|
||||
|
||||
# Create backup directories
|
||||
backup_dir="$DIR_BACKUP/$host_name/$container_name"
|
||||
mkdir -p "$backup_dir"
|
||||
mkdir -p "$backup_dir/sql"
|
||||
|
||||
# Backup directories (skip if -onlydb mode)
|
||||
if [[ "$ONLY_DB" == "false" ]]; then
|
||||
dir_count=$(yq ".hosts[$i].containers[$c].dirs | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
|
||||
|
||||
for ((d=0; d<$dir_count; d++)); do
|
||||
dir_path=$(yq ".hosts[$i].containers[$c].dirs[$d]" "$CONFIG_FILE" | sed 's/^"\|"$//g')
|
||||
|
||||
# Use sudo if not root
|
||||
if [[ "$ssh_user" != "root" ]]; then
|
||||
CMD_PREFIX="sudo"
|
||||
else
|
||||
CMD_PREFIX=""
|
||||
fi
|
||||
|
||||
# Special handling for /var/www - backup each subdirectory separately
|
||||
if [[ "$dir_path" == "/var/www" ]]; then
|
||||
echo " Backing up subdirectories of $dir_path" | tee -a "$LOG_FILE"
|
||||
|
||||
# Get list of subdirectories
|
||||
subdirs=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- find /var/www -maxdepth 1 -type d ! -path /var/www" 2>/dev/null || echo "")
|
||||
|
||||
for subdir in $subdirs; do
|
||||
subdir_name=$(basename "$subdir" | tr '/' '_')
|
||||
backup_file="$backup_dir/www_${subdir_name}_$(date +%Y%m%d_%H).tar.gz"
|
||||
|
||||
echo " Backing up: $subdir" | tee -a "$LOG_FILE"
|
||||
|
||||
if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- tar czf - $subdir 2>/dev/null" > "$backup_file"; then
|
||||
|
||||
# Validate backup file size (tar.gz should be > 1KB)
|
||||
if [[ -f "$backup_file" ]]; then
|
||||
file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0)
|
||||
if [[ $file_size -lt 1024 ]]; then
|
||||
echo " WARNING: Backup file very small (${file_size} bytes): $subdir" | tee -a "$LOG_FILE"
|
||||
# Keep the file but note it's small
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved (small): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo (WARNING: small)" >> "$RECAP_FILE"
|
||||
else
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved: $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE"
|
||||
fi
|
||||
|
||||
# Test tar integrity
|
||||
if ! tar tzf "$backup_file" >/dev/null 2>&1; then
|
||||
echo " ERROR: Tar integrity check failed" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Backup file not created: $subdir" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Failed to backup $subdir" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
rm -f "$backup_file"
|
||||
fi
|
||||
done
|
||||
else
|
||||
# Normal backup for other directories
|
||||
dir_name=$(basename "$dir_path" | tr '/' '_')
|
||||
backup_file="$backup_dir/${dir_name}_$(date +%Y%m%d_%H).tar.gz"
|
||||
|
||||
echo " Backing up: $dir_path" | tee -a "$LOG_FILE"
|
||||
|
||||
if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- tar czf - $dir_path 2>/dev/null" > "$backup_file"; then
|
||||
|
||||
# Validate backup file size (tar.gz should be > 1KB)
|
||||
if [[ -f "$backup_file" ]]; then
|
||||
file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0)
|
||||
if [[ $file_size -lt 1024 ]]; then
|
||||
echo " WARNING: Backup file very small (${file_size} bytes): $dir_path" | tee -a "$LOG_FILE"
|
||||
# Keep the file but note it's small
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved (small): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo (WARNING: small)" >> "$RECAP_FILE"
|
||||
else
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved: $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE"
|
||||
fi
|
||||
|
||||
# Test tar integrity
|
||||
if ! tar tzf "$backup_file" >/dev/null 2>&1; then
|
||||
echo " ERROR: Tar integrity check failed" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Backup file not created: $dir_path" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Failed to backup $dir_path" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
rm -f "$backup_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi # End of directory backup section
|
||||
|
||||
# Backup databases
|
||||
db_user=$(yq ".hosts[$i].containers[$c].db_user" "$CONFIG_FILE" 2>/dev/null | tr -d '"')
|
||||
db_pass=$(yq ".hosts[$i].containers[$c].db_pass" "$CONFIG_FILE" 2>/dev/null | tr -d '"')
|
||||
db_host=$(yq ".hosts[$i].containers[$c].db_host // \"localhost\"" "$CONFIG_FILE" 2>/dev/null | tr -d '"')
|
||||
|
||||
# Check if we're in onlydb mode
|
||||
if [[ "$ONLY_DB" == "true" ]]; then
|
||||
# Use onlydb list if it exists
|
||||
onlydb_count=$(yq ".hosts[$i].containers[$c].onlydb | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
|
||||
if [[ "$onlydb_count" != "0" ]] && [[ "$onlydb_count" != "null" ]]; then
|
||||
db_count="$onlydb_count"
|
||||
use_onlydb=true
|
||||
else
|
||||
# No onlydb list, skip this container in onlydb mode
|
||||
continue
|
||||
fi
|
||||
else
|
||||
# Normal mode - use databases list
|
||||
db_count=$(yq ".hosts[$i].containers[$c].databases | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
|
||||
use_onlydb=false
|
||||
fi
|
||||
|
||||
if [[ -n "$db_user" ]] && [[ -n "$db_pass" ]] && [[ "$db_count" != "0" ]]; then
|
||||
for ((db=0; db<$db_count; db++)); do
|
||||
if [[ "$use_onlydb" == "true" ]]; then
|
||||
db_name=$(yq ".hosts[$i].containers[$c].onlydb[$db]" "$CONFIG_FILE" | tr -d '"')
|
||||
else
|
||||
db_name=$(yq ".hosts[$i].containers[$c].databases[$db]" "$CONFIG_FILE" | tr -d '"')
|
||||
fi
|
||||
|
||||
if [[ "$db_name" == "ALL" ]]; then
|
||||
echo " Fetching all databases..." | tee -a "$LOG_FILE"
|
||||
|
||||
# Get database list
|
||||
if [[ "$ssh_user" != "root" ]]; then
|
||||
db_list=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"sudo incus exec $container_name -- bash -c 'cat > /tmp/d6back.cnf << EOF
|
||||
[client]
|
||||
user=$db_user
|
||||
password=$db_pass
|
||||
host=$db_host
|
||||
EOF
|
||||
chmod 600 /tmp/d6back.cnf
|
||||
mariadb --defaults-extra-file=/tmp/d6back.cnf -e \"SHOW DATABASES;\" 2>/dev/null
|
||||
rm -f /tmp/d6back.cnf'" | \
|
||||
grep -Ev '^(Database|information_schema|performance_schema|mysql|sys)$' || echo "")
|
||||
else
|
||||
db_list=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"incus exec $container_name -- bash -c 'cat > /tmp/d6back.cnf << EOF
|
||||
[client]
|
||||
user=$db_user
|
||||
password=$db_pass
|
||||
host=$db_host
|
||||
EOF
|
||||
chmod 600 /tmp/d6back.cnf
|
||||
mariadb --defaults-extra-file=/tmp/d6back.cnf -e \"SHOW DATABASES;\" 2>/dev/null
|
||||
rm -f /tmp/d6back.cnf'" | \
|
||||
grep -Ev '^(Database|information_schema|performance_schema|mysql|sys)$' || echo "")
|
||||
fi
|
||||
|
||||
# Backup each database
|
||||
for single_db in $db_list; do
|
||||
backup_database "$single_db"
|
||||
done
|
||||
else
|
||||
backup_database "$db_name"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "=== Backup Completed $(date) ===" | tee -a "$LOG_FILE"
|
||||
|
||||
# Cleanup old backups according to retention policy
|
||||
cleanup_old_backups
|
||||
|
||||
# Show summary
|
||||
total_size=$(du -sh "$DIR_BACKUP" 2>/dev/null | cut -f1)
|
||||
echo "Total backup size: $total_size" | tee -a "$LOG_FILE"
|
||||
|
||||
# Add summary to recap
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "========================================" >> "$RECAP_FILE"
|
||||
|
||||
# Add size details per host/container
|
||||
echo "BACKUP SIZES:" >> "$RECAP_FILE"
|
||||
for host_dir in "$DIR_BACKUP"/*; do
|
||||
if [[ -d "$host_dir" ]]; then
|
||||
host_name=$(basename "$host_dir")
|
||||
host_size=$(du -sh "$host_dir" 2>/dev/null | cut -f1)
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo " $host_name: $host_size" >> "$RECAP_FILE"
|
||||
|
||||
# Size per container
|
||||
for container_dir in "$host_dir"/*; do
|
||||
if [[ -d "$container_dir" ]]; then
|
||||
container_name=$(basename "$container_dir")
|
||||
container_size=$(du -sh "$container_dir" 2>/dev/null | cut -f1)
|
||||
echo " - $container_name: $container_size" >> "$RECAP_FILE"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "TOTAL SIZE: $total_size" >> "$RECAP_FILE"
|
||||
echo "COMPLETED: $(date '+%d.%m.%Y %H:%M')" >> "$RECAP_FILE"
|
||||
|
||||
# Prepare email subject with date format
|
||||
DATE_SUBJECT=$(date '+%d.%m.%Y %H')
|
||||
|
||||
# Send recap email
|
||||
if [[ $ERROR_COUNT -gt 0 ]]; then
|
||||
echo "Total errors: $ERROR_COUNT" | tee -a "$LOG_FILE"
|
||||
|
||||
# Add errors to recap
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "ERRORS DETECTED: $ERROR_COUNT" >> "$RECAP_FILE"
|
||||
echo "----------------------------" >> "$RECAP_FILE"
|
||||
grep -i "ERROR" "$LOG_FILE" >> "$RECAP_FILE"
|
||||
|
||||
# Send email with ERROR in subject
|
||||
echo "Sending ERROR email to $EMAIL_TO (Errors found: $ERROR_COUNT)" | tee -a "$LOG_FILE"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: Backup${BACKUP_SERVER} ERROR $DATE_SUBJECT"
|
||||
echo ""
|
||||
cat "$RECAP_FILE"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
echo "ERROR email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "WARNING: msmtp not found - ERROR email NOT sent" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
else
|
||||
echo "Backup completed successfully with no errors" | tee -a "$LOG_FILE"
|
||||
|
||||
# Send success recap email
|
||||
echo "Sending SUCCESS recap email to $EMAIL_TO" | tee -a "$LOG_FILE"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: Backup${BACKUP_SERVER} $DATE_SUBJECT"
|
||||
echo ""
|
||||
cat "$RECAP_FILE"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
echo "SUCCESS recap email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "WARNING: msmtp not found - SUCCESS recap email NOT sent" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up recap file
|
||||
rm -f "$RECAP_FILE"
|
||||
|
||||
# Exit with error code if there were errors
|
||||
if [[ $ERROR_COUNT -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
112
api/PM7/d6back.yaml
Normal file
112
api/PM7/d6back.yaml
Normal file
@@ -0,0 +1,112 @@
|
||||
# Configuration for MariaDB and directories backup
|
||||
# Backup structure: $dir_backup/$hostname/$containername/ for dirs
|
||||
# $dir_backup/$hostname/$containername/sql/ for databases
|
||||
|
||||
# Global parameters
|
||||
global:
|
||||
backup_server: PM7 # Nom du serveur de backup (PM7, PM1, etc.)
|
||||
email_to: support@unikoffice.com # Email de notification
|
||||
dir_backup: /var/pierre/back # Base backup directory
|
||||
enc_key: /home/pierre/.key_enc # Encryption key for SQL backups
|
||||
keep_dirs: 7d # Garde 7 jours pour les dirs
|
||||
keep_db: 5d,3w,15m # 5 jours complets, 3 semaines (1/jour), 15 mois (1/semaine)
|
||||
|
||||
# Hosts configuration
|
||||
hosts:
|
||||
- name: IN2
|
||||
ip: 145.239.9.105
|
||||
user: debian
|
||||
key: /home/pierre/.ssh/backup_key
|
||||
port: 22
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
containers:
|
||||
- name: nx4
|
||||
db_user: root
|
||||
db_pass: MyDebServer,90b
|
||||
db_host: localhost
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
- /var/www
|
||||
databases:
|
||||
- ALL # Backup all databases
|
||||
onlydb: # Used only with -onlydb parameter (optional)
|
||||
- turing
|
||||
|
||||
- name: IN3
|
||||
ip: 195.154.80.116
|
||||
user: pierre
|
||||
key: /home/pierre/.ssh/backup_key
|
||||
port: 22
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
containers:
|
||||
- name: nx4
|
||||
db_user: root
|
||||
db_pass: MyAlpLocal,90b
|
||||
db_host: localhost
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
- /var/www
|
||||
databases:
|
||||
- ALL # Backup all databases
|
||||
onlydb: # Used only with -onlydb parameter (optional)
|
||||
- geosector
|
||||
|
||||
- name: rca-geo
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
- /var/www
|
||||
|
||||
- name: dva-res
|
||||
db_user: root
|
||||
db_pass: MyAlpineDb.90b
|
||||
db_host: localhost
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
- /var/www
|
||||
databases:
|
||||
- ALL
|
||||
onlydb:
|
||||
- resalice
|
||||
|
||||
- name: dva-front
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
- /var/www
|
||||
|
||||
- name: maria3
|
||||
db_user: root
|
||||
db_pass: MyAlpLocal,90b
|
||||
db_host: localhost
|
||||
dirs:
|
||||
- /etc/my.cnf.d
|
||||
- /var/osm
|
||||
- /var/log
|
||||
databases:
|
||||
- ALL
|
||||
onlydb:
|
||||
- cleo
|
||||
- rca_geo
|
||||
|
||||
- name: IN4
|
||||
ip: 51.159.7.190
|
||||
user: pierre
|
||||
key: /home/pierre/.ssh/backup_key
|
||||
port: 22
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
containers:
|
||||
- name: maria4
|
||||
db_user: root
|
||||
db_pass: MyAlpLocal,90b
|
||||
db_host: localhost
|
||||
dirs:
|
||||
- /etc/my.cnf.d
|
||||
- /var/osm
|
||||
- /var/log
|
||||
databases:
|
||||
- ALL
|
||||
onlydb:
|
||||
- cleo
|
||||
- pra_geo
|
||||
118
api/PM7/decpm7.sh
Normal file
118
api/PM7/decpm7.sh
Normal file
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
CONFIG_FILE="backpm7.yaml"
|
||||
|
||||
# Check if file argument is provided
|
||||
if [ $# -eq 0 ]; then
|
||||
echo -e "${RED}Error: No input file specified${NC}"
|
||||
echo "Usage: $0 <database.sql.gz.enc>"
|
||||
echo "Example: $0 wordpress_20250905_14.sql.gz.enc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT_FILE="$1"
|
||||
|
||||
# Check if input file exists
|
||||
if [ ! -f "$INPUT_FILE" ]; then
|
||||
echo -e "${RED}Error: File not found: $INPUT_FILE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to load encryption key from config
|
||||
load_key_from_config() {
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo -e "${YELLOW}Warning: $CONFIG_FILE not found${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for yq
|
||||
if ! command -v yq &> /dev/null; then
|
||||
echo -e "${RED}Error: yq is required to read config file${NC}"
|
||||
echo "Install with: sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local key_path=$(yq '.global.enc_key' "$CONFIG_FILE" | tr -d '"')
|
||||
|
||||
if [ -z "$key_path" ]; then
|
||||
echo -e "${RED}Error: enc_key not found in $CONFIG_FILE${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$key_path" ]; then
|
||||
echo -e "${RED}Error: Encryption key file not found: $key_path${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
ENC_KEY=$(cat "$key_path")
|
||||
echo -e "${GREEN}Encryption key loaded from: $key_path${NC}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check file type early - accept both old and new naming
|
||||
if [[ "$INPUT_FILE" != *.sql.gz.enc ]] && [[ "$INPUT_FILE" != *.sql.tar.gz.enc ]]; then
|
||||
echo -e "${RED}Error: File must be a .sql.gz.enc or .sql.tar.gz.enc file${NC}"
|
||||
echo "This tool only decrypts SQL backup files created by backpm7.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get encryption key from config
|
||||
if ! load_key_from_config; then
|
||||
echo -e "${RED}Error: Cannot load encryption key${NC}"
|
||||
echo "Make sure $CONFIG_FILE exists and contains enc_key path"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Process SQL backup file
|
||||
echo -e "${BLUE}Decrypting SQL backup: $INPUT_FILE${NC}"
|
||||
|
||||
# Determine output file - extract just the filename and put in current directory
|
||||
BASENAME=$(basename "$INPUT_FILE")
|
||||
if [[ "$BASENAME" == *.sql.tar.gz.enc ]]; then
|
||||
OUTPUT_FILE="${BASENAME%.sql.tar.gz.enc}.sql"
|
||||
else
|
||||
OUTPUT_FILE="${BASENAME%.sql.gz.enc}.sql"
|
||||
fi
|
||||
|
||||
# Decrypt and decompress in one command
|
||||
echo "Decrypting to: $OUTPUT_FILE"
|
||||
|
||||
# Decrypt and decompress in one pipeline
|
||||
if openssl enc -aes-256-cbc -d -salt -pass pass:"$ENC_KEY" -pbkdf2 -in "$INPUT_FILE" | gunzip > "$OUTPUT_FILE" 2>/dev/null; then
|
||||
# Get file size
|
||||
size=$(du -h "$OUTPUT_FILE" | cut -f1)
|
||||
echo -e "${GREEN}✓ Successfully decrypted: $OUTPUT_FILE ($size)${NC}"
|
||||
|
||||
# Show first few lines of SQL
|
||||
echo -e "${BLUE}First 5 lines of SQL:${NC}"
|
||||
head -n 5 "$OUTPUT_FILE"
|
||||
else
|
||||
echo -e "${RED}✗ Decryption failed${NC}"
|
||||
echo "Possible causes:"
|
||||
echo " - Wrong encryption key"
|
||||
echo " - Corrupted file"
|
||||
echo " - File was encrypted differently"
|
||||
|
||||
# Try to help debug
|
||||
echo -e "\n${YELLOW}Debug info:${NC}"
|
||||
echo "File size: $(du -h "$INPUT_FILE" | cut -f1)"
|
||||
echo "First bytes (should start with 'Salted__'):"
|
||||
hexdump -C "$INPUT_FILE" | head -n 1
|
||||
|
||||
# Let's also check what key we're using (first 10 chars)
|
||||
echo "Key begins with: ${ENC_KEY:0:10}..."
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Operation completed successfully${NC}"
|
||||
248
api/PM7/sync_geosector.sh
Normal file
248
api/PM7/sync_geosector.sh
Normal file
@@ -0,0 +1,248 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# sync_geosector.sh - Synchronise les backups geosector depuis PM7 vers maria3 (IN3) et maria4 (IN4)
|
||||
#
|
||||
# Ce script :
|
||||
# 1. Trouve le dernier backup chiffré de geosector sur PM7
|
||||
# 2. Le déchiffre et décompresse localement
|
||||
# 3. Le transfère et l'importe dans IN3/maria3/geosector
|
||||
# 4. Le transfère et l'importe dans IN4/maria4/geosector
|
||||
#
|
||||
# Installation: /var/pierre/bat/sync_geosector.sh
|
||||
# Usage: ./sync_geosector.sh [--force] [--date YYYYMMDD_HH]
|
||||
#
|
||||
|
||||
set -uo pipefail
|
||||
# Note: Removed -e to allow script to continue on sync errors
|
||||
# Errors are handled explicitly with ERROR_COUNT
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_FILE="$SCRIPT_DIR/d6back.yaml"
|
||||
BACKUP_DIR="/var/pierre/back/IN3/nx4/sql"
|
||||
ENC_KEY_FILE="/home/pierre/.key_enc"
|
||||
SSH_KEY="/home/pierre/.ssh/backup_key"
|
||||
TEMP_DIR="/tmp/geosector_sync"
|
||||
LOG_FILE="/var/pierre/bat/logs/sync_geosector.log"
|
||||
RECAP_FILE="/tmp/sync_geosector_recap_$$.txt"
|
||||
|
||||
# Load email config from d6back.yaml
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
EMAIL_TO=$(yq '.global.email_to // "support@unikoffice.com"' "$CONFIG_FILE" | tr -d '"')
|
||||
BACKUP_SERVER=$(yq '.global.backup_server // "BACKUP"' "$CONFIG_FILE" | tr -d '"')
|
||||
else
|
||||
EMAIL_TO="support@unikoffice.com"
|
||||
BACKUP_SERVER="BACKUP"
|
||||
fi
|
||||
|
||||
# Serveurs cibles
|
||||
IN3_HOST="195.154.80.116"
|
||||
IN3_USER="pierre"
|
||||
IN3_CONTAINER="maria3"
|
||||
|
||||
IN4_HOST="51.159.7.190"
|
||||
IN4_USER="pierre"
|
||||
IN4_CONTAINER="maria4"
|
||||
|
||||
# Credentials MariaDB
|
||||
DB_USER="root"
|
||||
IN3_DB_PASS="MyAlpLocal,90b" # maria3
|
||||
IN4_DB_PASS="MyAlpLocal,90b" # maria4
|
||||
DB_NAME="geosector"
|
||||
|
||||
# Fonctions utilitaires
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
error() {
|
||||
log "ERROR: $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -d "$TEMP_DIR" ]]; then
|
||||
log "Nettoyage de $TEMP_DIR"
|
||||
rm -rf "$TEMP_DIR"
|
||||
fi
|
||||
rm -f "$RECAP_FILE"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# Lecture de la clé de chiffrement
|
||||
if [[ ! -f "$ENC_KEY_FILE" ]]; then
|
||||
error "Clé de chiffrement non trouvée: $ENC_KEY_FILE"
|
||||
fi
|
||||
ENC_KEY=$(cat "$ENC_KEY_FILE")
|
||||
|
||||
# Parsing des arguments
|
||||
FORCE=0
|
||||
SPECIFIC_DATE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--force)
|
||||
FORCE=1
|
||||
shift
|
||||
;;
|
||||
--date)
|
||||
SPECIFIC_DATE="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [--force] [--date YYYYMMDD_HH]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Trouver le fichier backup
|
||||
if [[ -n "$SPECIFIC_DATE" ]]; then
|
||||
BACKUP_FILE="$BACKUP_DIR/geosector_${SPECIFIC_DATE}.sql.gz.enc"
|
||||
if [[ ! -f "$BACKUP_FILE" ]]; then
|
||||
error "Backup non trouvé: $BACKUP_FILE"
|
||||
fi
|
||||
else
|
||||
# Chercher le plus récent
|
||||
BACKUP_FILE=$(find "$BACKUP_DIR" -name "geosector_*.sql.gz.enc" -type f -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2-)
|
||||
if [[ -z "$BACKUP_FILE" ]]; then
|
||||
error "Aucun backup geosector trouvé dans $BACKUP_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
BACKUP_BASENAME=$(basename "$BACKUP_FILE")
|
||||
log "Backup sélectionné: $BACKUP_BASENAME"
|
||||
|
||||
# Initialiser le fichier récapitulatif
|
||||
echo "SYNC GEOSECTOR REPORT - $(hostname) - $(date '+%d.%m.%Y %H')h" > "$RECAP_FILE"
|
||||
echo "========================================" >> "$RECAP_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "Backup source: $BACKUP_BASENAME" >> "$RECAP_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
|
||||
# Créer le répertoire temporaire
|
||||
mkdir -p "$TEMP_DIR"
|
||||
DECRYPTED_FILE="$TEMP_DIR/geosector.sql"
|
||||
|
||||
# Étape 1: Déchiffrer et décompresser
|
||||
log "Déchiffrement et décompression du backup..."
|
||||
if ! openssl enc -aes-256-cbc -d -pass pass:"$ENC_KEY" -pbkdf2 -in "$BACKUP_FILE" | gunzip > "$DECRYPTED_FILE"; then
|
||||
error "Échec du déchiffrement/décompression"
|
||||
fi
|
||||
|
||||
FILE_SIZE=$(du -h "$DECRYPTED_FILE" | cut -f1)
|
||||
log "Fichier SQL déchiffré: $FILE_SIZE"
|
||||
|
||||
echo "Decrypted SQL size: $FILE_SIZE" >> "$RECAP_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
|
||||
# Compteur d'erreurs
|
||||
ERROR_COUNT=0
|
||||
|
||||
# Fonction pour synchroniser vers un serveur
|
||||
sync_to_server() {
|
||||
local HOST=$1
|
||||
local USER=$2
|
||||
local CONTAINER=$3
|
||||
local DB_PASS=$4
|
||||
local SERVER_NAME=$5
|
||||
|
||||
log "=== Synchronisation vers $SERVER_NAME ($HOST) ==="
|
||||
echo "TARGET: $SERVER_NAME ($HOST/$CONTAINER)" >> "$RECAP_FILE"
|
||||
|
||||
# Test de connexion SSH
|
||||
if ! ssh -i "$SSH_KEY" -o ConnectTimeout=10 "$USER@$HOST" "echo 'SSH OK'" &>/dev/null; then
|
||||
log "ERROR: Impossible de se connecter à $HOST via SSH"
|
||||
echo " ✗ SSH connection FAILED" >> "$RECAP_FILE"
|
||||
((ERROR_COUNT++))
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Import dans MariaDB
|
||||
log "Import dans $SERVER_NAME/$CONTAINER/geosector..."
|
||||
|
||||
# Drop et recréer la base sur le serveur distant
|
||||
if ! ssh -i "$SSH_KEY" "$USER@$HOST" "incus exec $CONTAINER --project default -- mariadb -u root -p'$DB_PASS' -e 'DROP DATABASE IF EXISTS $DB_NAME; CREATE DATABASE $DB_NAME CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'"; then
|
||||
log "ERROR: Échec de la création de la base sur $SERVER_NAME"
|
||||
echo " ✗ Database creation FAILED" >> "$RECAP_FILE"
|
||||
((ERROR_COUNT++))
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Filtrer et importer le SQL (enlever CREATE DATABASE et USE avec timestamp)
|
||||
log "Filtrage et import du SQL..."
|
||||
if ! sed -e '/^CREATE DATABASE.*geosector_[0-9]/d' \
|
||||
-e '/^USE.*geosector_[0-9]/d' \
|
||||
"$DECRYPTED_FILE" | \
|
||||
ssh -i "$SSH_KEY" "$USER@$HOST" "incus exec $CONTAINER --project default -- mariadb -u root -p'$DB_PASS' $DB_NAME"; then
|
||||
log "ERROR: Échec de l'import sur $SERVER_NAME"
|
||||
echo " ✗ SQL import FAILED" >> "$RECAP_FILE"
|
||||
((ERROR_COUNT++))
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "$SERVER_NAME: Import réussi"
|
||||
echo " ✓ Import SUCCESS" >> "$RECAP_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
}
|
||||
|
||||
# Synchronisation vers IN3/maria3
|
||||
sync_to_server "$IN3_HOST" "$IN3_USER" "$IN3_CONTAINER" "$IN3_DB_PASS" "IN3/maria3"
|
||||
|
||||
# Synchronisation vers IN4/maria4
|
||||
sync_to_server "$IN4_HOST" "$IN4_USER" "$IN4_CONTAINER" "$IN4_DB_PASS" "IN4/maria4"
|
||||
|
||||
# Finaliser le récapitulatif
|
||||
echo "========================================" >> "$RECAP_FILE"
|
||||
echo "COMPLETED: $(date '+%d.%m.%Y %H:%M')" >> "$RECAP_FILE"
|
||||
|
||||
# Préparer le sujet email avec date
|
||||
DATE_SUBJECT=$(date '+%d.%m.%Y %H')
|
||||
|
||||
# Envoyer l'email récapitulatif
|
||||
if [[ $ERROR_COUNT -gt 0 ]]; then
|
||||
log "Total errors: $ERROR_COUNT"
|
||||
|
||||
# Ajouter les erreurs au récap
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "ERRORS DETECTED: $ERROR_COUNT" >> "$RECAP_FILE"
|
||||
echo "----------------------------" >> "$RECAP_FILE"
|
||||
grep -i "ERROR" "$LOG_FILE" | tail -20 >> "$RECAP_FILE"
|
||||
|
||||
# Envoyer email avec ERROR dans le sujet
|
||||
log "Sending ERROR email to $EMAIL_TO (Errors found: $ERROR_COUNT)"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: Sync${BACKUP_SERVER} ERROR $DATE_SUBJECT"
|
||||
echo ""
|
||||
cat "$RECAP_FILE"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
log "ERROR email sent successfully to $EMAIL_TO"
|
||||
else
|
||||
log "WARNING: msmtp not found - ERROR email NOT sent"
|
||||
fi
|
||||
|
||||
log "=== Synchronisation terminée avec des erreurs ==="
|
||||
exit 1
|
||||
else
|
||||
log "=== Synchronisation terminée avec succès ==="
|
||||
log "Les bases geosector sur maria3 et maria4 sont à jour avec le backup $BACKUP_BASENAME"
|
||||
|
||||
# Envoyer email de succès
|
||||
log "Sending SUCCESS recap email to $EMAIL_TO"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: Sync${BACKUP_SERVER} $DATE_SUBJECT"
|
||||
echo ""
|
||||
cat "$RECAP_FILE"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
log "SUCCESS recap email sent successfully to $EMAIL_TO"
|
||||
else
|
||||
log "WARNING: msmtp not found - SUCCESS recap email NOT sent"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
fi
|
||||
2583
api/TODO-API.md
2583
api/TODO-API.md
File diff suppressed because it is too large
Load Diff
@@ -8,13 +8,20 @@
|
||||
"ext-openssl": "*",
|
||||
"ext-pdo": "*",
|
||||
"phpmailer/phpmailer": "^6.8",
|
||||
"phpoffice/phpspreadsheet": "^2.0",
|
||||
"phpoffice/phpspreadsheet": "^5.0",
|
||||
"setasign/fpdf": "^1.8",
|
||||
"setasign/fpdi": "^2.6",
|
||||
"stripe/stripe-php": "^17.6"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"src/"
|
||||
"src/Core/",
|
||||
"src/Config/",
|
||||
"src/Utils/",
|
||||
"src/Controllers/LogController.php"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
|
||||
124
api/composer.lock
generated
124
api/composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "155893f9be89bceda3639efbf19b14d1",
|
||||
"content-hash": "936a7e1a35fde56354a4dea02b309267",
|
||||
"packages": [
|
||||
{
|
||||
"name": "composer/pcre",
|
||||
@@ -87,22 +87,22 @@
|
||||
},
|
||||
{
|
||||
"name": "maennchen/zipstream-php",
|
||||
"version": "3.1.2",
|
||||
"version": "3.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f"
|
||||
"reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
|
||||
"reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"ext-zlib": "*",
|
||||
"php-64bit": "^8.2"
|
||||
"php-64bit": "^8.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^7.7",
|
||||
@@ -111,7 +111,7 @@
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"phpunit/phpunit": "^12.0",
|
||||
"vimeo/psalm": "^6.0"
|
||||
},
|
||||
"suggest": {
|
||||
@@ -153,7 +153,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -161,7 +161,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-01-27T12:07:53+00:00"
|
||||
"time": "2025-07-17T11:15:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "markbaker/complex",
|
||||
@@ -272,16 +272,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v6.10.0",
|
||||
"version": "v6.11.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
|
||||
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d9e3b36b47f04b497a0164c5a20f92acb4593284",
|
||||
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -302,6 +302,7 @@
|
||||
},
|
||||
"suggest": {
|
||||
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
|
||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||
@@ -341,7 +342,7 @@
|
||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.11.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -349,24 +350,24 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-04-24T15:19:31+00:00"
|
||||
"time": "2025-09-30T11:54:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "2.3.8",
|
||||
"version": "5.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "7a700683743bf1c4a21837c84b266916f1aa7d25"
|
||||
"reference": "fd26e45a814e94ae2aad0df757d9d1739c4bf2e0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/7a700683743bf1c4a21837c84b266916f1aa7d25",
|
||||
"reference": "7a700683743bf1c4a21837c84b266916f1aa7d25",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fd26e45a814e94ae2aad0df757d9d1739c4bf2e0",
|
||||
"reference": "fd26e45a814e94ae2aad0df757d9d1739c4bf2e0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer/pcre": "^1 || ^2 || ^3",
|
||||
"composer/pcre": "^1||^2||^3",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
@@ -395,9 +396,10 @@
|
||||
"mitoteam/jpgraph": "^10.3",
|
||||
"mpdf/mpdf": "^8.1.1",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpstan/phpstan": "^1.1",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpunit/phpunit": "^9.6 || ^10.5",
|
||||
"phpstan/phpstan": "^1.1 || ^2.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"tecnickcom/tcpdf": "^6.5"
|
||||
},
|
||||
@@ -452,9 +454,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.3.8"
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.1.0"
|
||||
},
|
||||
"time": "2025-02-08T03:01:45+00:00"
|
||||
"time": "2025-09-04T05:34:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-client",
|
||||
@@ -713,6 +715,78 @@
|
||||
},
|
||||
"time": "2023-06-26T14:44:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "setasign/fpdi",
|
||||
"version": "v2.6.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Setasign/FPDI.git",
|
||||
"reference": "4b53852fde2734ec6a07e458a085db627c60eada"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
|
||||
"reference": "4b53852fde2734ec6a07e458a085db627c60eada",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-zlib": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"setasign/tfpdf": "<1.31"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7",
|
||||
"setasign/fpdf": "~1.8.6",
|
||||
"setasign/tfpdf": "~1.33",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"tecnickcom/tcpdf": "^6.8"
|
||||
},
|
||||
"suggest": {
|
||||
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"setasign\\Fpdi\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jan Slabon",
|
||||
"email": "jan.slabon@setasign.com",
|
||||
"homepage": "https://www.setasign.com"
|
||||
},
|
||||
{
|
||||
"name": "Maximilian Kresse",
|
||||
"email": "maximilian.kresse@setasign.com",
|
||||
"homepage": "https://www.setasign.com"
|
||||
}
|
||||
],
|
||||
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
|
||||
"homepage": "https://www.setasign.com/fpdi",
|
||||
"keywords": [
|
||||
"fpdf",
|
||||
"fpdi",
|
||||
"pdf"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Setasign/FPDI/issues",
|
||||
"source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-05T09:57:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "stripe/stripe-php",
|
||||
"version": "v17.6.0",
|
||||
|
||||
200
api/config/nginx/pra-geo-http-only.conf
Normal file
200
api/config/nginx/pra-geo-http-only.conf
Normal file
@@ -0,0 +1,200 @@
|
||||
# =============================================================================
|
||||
# Configuration NGINX PRODUCTION pour pra-geo (IN4)
|
||||
# Date: 2025-10-07
|
||||
# Environnement: PRODUCTION
|
||||
# Server: Container pra-geo (13.23.34.43)
|
||||
# Port: 80 uniquement (HTTP)
|
||||
# SSL/HTTPS: Géré par le reverse proxy NGINX sur le host IN4
|
||||
# =============================================================================
|
||||
|
||||
# Site principal (web statique)
|
||||
server {
|
||||
listen 80;
|
||||
server_name geosector.fr;
|
||||
|
||||
root /var/www/geosector/web;
|
||||
index index.html;
|
||||
|
||||
# Logs PRODUCTION
|
||||
access_log /var/log/nginx/geosector-web_access.log combined;
|
||||
error_log /var/log/nginx/geosector-web_error.log warn;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Assets statiques avec cache agressif
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|css|js|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Protection des fichiers sensibles
|
||||
location ~ /\.(?!well-known) {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# APPLICATION FLUTTER + API PHP
|
||||
# =============================================================================
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name app3.geosector.fr;
|
||||
|
||||
# Logs PRODUCTION
|
||||
access_log /var/log/nginx/pra-app_access.log combined;
|
||||
error_log /var/log/nginx/pra-app_error.log warn;
|
||||
|
||||
# Récupérer le vrai IP du client depuis le reverse proxy
|
||||
set_real_ip_from 13.23.34.0/24; # Réseau Incus
|
||||
set_real_ip_from 51.159.7.190; # IP publique IN4
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
# Taille maximale des uploads (pour les logos, exports, etc.)
|
||||
client_max_body_size 10M;
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
# Timeouts optimisés pour PRODUCTION
|
||||
client_body_timeout 30s;
|
||||
client_header_timeout 30s;
|
||||
send_timeout 60s;
|
||||
|
||||
# =============================================================================
|
||||
# APPLICATION FLUTTER (contenu statique)
|
||||
# =============================================================================
|
||||
location / {
|
||||
root /var/www/geosector/app;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Cache intelligent pour PRODUCTION
|
||||
# HTML : pas de cache (pour déploiements)
|
||||
location ~* \.html$ {
|
||||
expires -1;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
# Assets Flutter (JS, CSS, fonts) avec hash : cache agressif
|
||||
location ~* \.(js|css|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Images : cache longue durée
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# API PHP (RESTful)
|
||||
# =============================================================================
|
||||
location /api/ {
|
||||
root /var/www/geosector;
|
||||
|
||||
# CORS - Le reverse proxy IN4 ajoute déjà les headers CORS
|
||||
# On les ajoute ici pour les requêtes internes si besoin
|
||||
|
||||
# Cache API : pas de cache (données dynamiques)
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
|
||||
# Rewrite vers index.php
|
||||
try_files $uri $uri/ /api/index.php$is_args$args;
|
||||
|
||||
# Traitement PHP
|
||||
location ~ ^/api/(.+\.php)$ {
|
||||
root /var/www/geosector;
|
||||
|
||||
# FastCGI PHP-FPM
|
||||
fastcgi_pass unix:/run/php-fpm83/php-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $request_filename;
|
||||
|
||||
# Variable d'environnement PRODUCTION
|
||||
fastcgi_param APP_ENV "production";
|
||||
fastcgi_param SERVER_NAME "app3.geosector.fr";
|
||||
|
||||
# Headers transmis à PHP (viennent du reverse proxy)
|
||||
fastcgi_param HTTP_X_REAL_IP $http_x_real_ip;
|
||||
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
|
||||
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
|
||||
fastcgi_param HTTPS $http_x_forwarded_proto;
|
||||
|
||||
# Timeouts pour opérations longues (sync, exports)
|
||||
fastcgi_read_timeout 300;
|
||||
fastcgi_send_timeout 300;
|
||||
fastcgi_connect_timeout 60;
|
||||
|
||||
# Buffers optimisés
|
||||
fastcgi_buffer_size 128k;
|
||||
fastcgi_buffers 256 16k;
|
||||
fastcgi_busy_buffers_size 256k;
|
||||
fastcgi_temp_file_write_size 256k;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# UPLOADS ET MÉDIAS
|
||||
# =============================================================================
|
||||
location /api/uploads/ {
|
||||
alias /var/www/geosector/api/uploads/;
|
||||
|
||||
# Cache pour les médias uploadés
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
|
||||
# Sécurité : empêcher l'exécution de scripts
|
||||
location ~ \.(php|phtml|php3|php4|php5|phps)$ {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SÉCURITÉ
|
||||
# =============================================================================
|
||||
|
||||
# Bloquer l'accès aux fichiers sensibles
|
||||
location ~ /\.(?!well-known) {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Bloquer l'accès aux fichiers de configuration
|
||||
location ~* \.(env|sql|bak|backup|swp|config|conf|ini|log)$ {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Protection contre les requêtes invalides
|
||||
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|PATCH|OPTIONS)$) {
|
||||
return 405;
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MONITORING
|
||||
# =============================================================================
|
||||
|
||||
# Endpoint de health check (accessible en interne)
|
||||
location = /nginx-health {
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
allow 13.23.34.0/24; # Réseau interne Incus
|
||||
deny all;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
290
api/config/nginx/pra-geo-production.conf
Normal file
290
api/config/nginx/pra-geo-production.conf
Normal file
@@ -0,0 +1,290 @@
|
||||
# =============================================================================
|
||||
# Configuration NGINX PRODUCTION pour pra-geo (IN4)
|
||||
# Date: 2025-10-07
|
||||
# Environnement: PRODUCTION
|
||||
# Server: IN4 (51.159.7.190)
|
||||
# =============================================================================
|
||||
|
||||
# Site principal (redirection vers www ou app)
|
||||
server {
|
||||
listen 80;
|
||||
server_name geosector.fr;
|
||||
|
||||
# Redirection permanente vers HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name geosector.fr;
|
||||
|
||||
# Certificats SSL
|
||||
ssl_certificate /etc/letsencrypt/live/geosector.fr/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/geosector.fr/privkey.pem;
|
||||
|
||||
# Configuration SSL optimisée
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305';
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
root /var/www/geosector/web;
|
||||
index index.html;
|
||||
|
||||
# Logs PRODUCTION
|
||||
access_log /var/log/nginx/geosector-web_access.log combined;
|
||||
error_log /var/log/nginx/geosector-web_error.log warn;
|
||||
|
||||
# Headers de sécurité
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Assets statiques avec cache agressif
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|css|js|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Protection des fichiers sensibles
|
||||
location ~ /\.(?!well-known) {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# APPLICATION FLUTTER + API PHP
|
||||
# =============================================================================
|
||||
|
||||
# Redirection HTTP → HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name app3.geosector.fr;
|
||||
|
||||
# Permettre Let's Encrypt validation
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/letsencrypt;
|
||||
allow all;
|
||||
}
|
||||
|
||||
# Redirection permanente vers HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name app3.geosector.fr;
|
||||
|
||||
# Certificats SSL
|
||||
ssl_certificate /etc/letsencrypt/live/app3.geosector.fr/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/app3.geosector.fr/privkey.pem;
|
||||
|
||||
# Configuration SSL optimisée (même que ci-dessus)
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305';
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
# Logs PRODUCTION
|
||||
access_log /var/log/nginx/pra-app_access.log combined;
|
||||
error_log /var/log/nginx/pra-app_error.log warn;
|
||||
|
||||
# Headers de sécurité globaux
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Taille maximale des uploads (pour les logos, exports, etc.)
|
||||
client_max_body_size 10M;
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
# Timeouts optimisés pour PRODUCTION
|
||||
client_body_timeout 30s;
|
||||
client_header_timeout 30s;
|
||||
send_timeout 60s;
|
||||
|
||||
# =============================================================================
|
||||
# APPLICATION FLUTTER (contenu statique)
|
||||
# =============================================================================
|
||||
location / {
|
||||
root /var/www/geosector/app;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Cache intelligent pour PRODUCTION
|
||||
# HTML : pas de cache (pour déploiements)
|
||||
location ~* \.html$ {
|
||||
expires -1;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
# Assets Flutter (JS, CSS, fonts) avec hash : cache agressif
|
||||
location ~* \.(js|css|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Images : cache longue durée
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# API PHP (RESTful)
|
||||
# =============================================================================
|
||||
location /api/ {
|
||||
root /var/www/geosector;
|
||||
|
||||
# CORS - Liste blanche des origines autorisées en PRODUCTION
|
||||
set $cors_origin "";
|
||||
|
||||
# Autoriser uniquement les domaines de production
|
||||
if ($http_origin ~* ^https://(app\.geosector\.fr|geosector\.fr)$) {
|
||||
set $cors_origin $http_origin;
|
||||
}
|
||||
|
||||
# Gestion des preflight requests (OPTIONS)
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' $cors_origin always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
add_header 'Access-Control-Max-Age' 86400;
|
||||
add_header 'Content-Type' 'text/plain; charset=utf-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
# Headers CORS pour les requêtes normales
|
||||
add_header 'Access-Control-Allow-Origin' $cors_origin always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
|
||||
# Cache API : pas de cache (données dynamiques)
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
|
||||
# Headers de sécurité spécifiques API
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
|
||||
# Rewrite vers index.php
|
||||
try_files $uri $uri/ /api/index.php$is_args$args;
|
||||
|
||||
# Traitement PHP
|
||||
location ~ ^/api/(.+\.php)$ {
|
||||
root /var/www/geosector;
|
||||
|
||||
# FastCGI PHP-FPM
|
||||
fastcgi_pass unix:/run/php-fpm83/php-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $request_filename;
|
||||
|
||||
# Variable d'environnement PRODUCTION
|
||||
fastcgi_param APP_ENV "production";
|
||||
fastcgi_param SERVER_NAME "app3.geosector.fr";
|
||||
|
||||
# Headers transmis à PHP
|
||||
fastcgi_param HTTP_X_REAL_IP $remote_addr;
|
||||
fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
|
||||
fastcgi_param HTTP_X_FORWARDED_PROTO $scheme;
|
||||
|
||||
# Timeouts pour opérations longues (sync, exports)
|
||||
fastcgi_read_timeout 300;
|
||||
fastcgi_send_timeout 300;
|
||||
fastcgi_connect_timeout 60;
|
||||
|
||||
# Buffers optimisés
|
||||
fastcgi_buffer_size 128k;
|
||||
fastcgi_buffers 256 16k;
|
||||
fastcgi_busy_buffers_size 256k;
|
||||
fastcgi_temp_file_write_size 256k;
|
||||
|
||||
# Headers CORS pour réponses PHP
|
||||
add_header 'Access-Control-Allow-Origin' $cors_origin always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# UPLOADS ET MÉDIAS
|
||||
# =============================================================================
|
||||
location /api/uploads/ {
|
||||
alias /var/www/geosector/api/uploads/;
|
||||
|
||||
# Cache pour les médias uploadés
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
|
||||
# Sécurité : empêcher l'exécution de scripts
|
||||
location ~ \.(php|phtml|php3|php4|php5|phps)$ {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SÉCURITÉ
|
||||
# =============================================================================
|
||||
|
||||
# Bloquer l'accès aux fichiers sensibles
|
||||
location ~ /\.(?!well-known) {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Bloquer l'accès aux fichiers de configuration
|
||||
location ~* \.(env|sql|bak|backup|swp|config|conf|ini|log)$ {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Bloquer les user-agents malveillants
|
||||
if ($http_user_agent ~* (bot|crawler|spider|scraper|wget|curl)) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
# Protection contre les requêtes invalides
|
||||
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|PATCH|OPTIONS)$) {
|
||||
return 405;
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MONITORING
|
||||
# =============================================================================
|
||||
|
||||
# Endpoint de health check (accessible uniquement en local)
|
||||
location = /nginx-health {
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
allow 13.23.34.0/24; # Réseau interne Incus
|
||||
deny all;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
@@ -75,8 +75,8 @@ cleanup_old_backups() {
|
||||
"pra") prefix="api-pra-" ;;
|
||||
esac
|
||||
|
||||
echo_info "Cleaning old backups (keeping last 10)..."
|
||||
ls -t "${BACKUP_DIR}"/${prefix}*.tar.gz 2>/dev/null | tail -n +11 | xargs -r rm -f && {
|
||||
echo_info "Cleaning old backups (keeping last 5)..."
|
||||
ls -t "${BACKUP_DIR}"/${prefix}*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm -f && {
|
||||
REMAINING_BACKUPS=$(ls "${BACKUP_DIR}"/${prefix}*.tar.gz 2>/dev/null | wc -l)
|
||||
echo_info "Kept ${REMAINING_BACKUPS} backup(s) for ${TARGET_ENV}"
|
||||
}
|
||||
@@ -164,9 +164,12 @@ if [ "$SOURCE_TYPE" = "local_code" ]; then
|
||||
--exclude='.gitignore' \
|
||||
--exclude='.vscode' \
|
||||
--exclude='logs' \
|
||||
--exclude='sessions' \
|
||||
--exclude='opendata' \
|
||||
--exclude='*.template' \
|
||||
--exclude='*.sh' \
|
||||
--exclude='.env' \
|
||||
--exclude='.env_marker' \
|
||||
--exclude='*.log' \
|
||||
--exclude='.DS_Store' \
|
||||
--exclude='README.md' \
|
||||
@@ -193,6 +196,8 @@ elif [ "$SOURCE_TYPE" = "remote_container" ]; then
|
||||
incus exec ${SOURCE_CONTAINER} -- tar \
|
||||
--exclude='logs' \
|
||||
--exclude='uploads' \
|
||||
--exclude='sessions' \
|
||||
--exclude='opendata' \
|
||||
-czf /tmp/${ARCHIVE_NAME} -C ${API_PATH} .
|
||||
" || echo_error "Failed to create archive on remote"
|
||||
|
||||
@@ -254,18 +259,29 @@ if [ "$DEST_HOST" != "local" ]; then
|
||||
|
||||
# Déployer sur le container de destination
|
||||
echo_info "Extracting on destination container..."
|
||||
|
||||
# Déterminer le nom de l'environnement pour le marqueur
|
||||
case $TARGET_ENV in
|
||||
"dev") ENV_MARKER="development" ;;
|
||||
"rca") ENV_MARKER="recette" ;;
|
||||
"pra") ENV_MARKER="production" ;;
|
||||
esac
|
||||
|
||||
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
# Pousser l'archive dans le container
|
||||
incus project switch ${INCUS_PROJECT} &&
|
||||
incus file push /tmp/${ARCHIVE_NAME} ${DEST_CONTAINER}/tmp/${ARCHIVE_NAME} &&
|
||||
|
||||
# Nettoyer sélectivement (préserver logs et uploads)
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH} -mindepth 1 -maxdepth 1 ! -name 'uploads' ! -name 'logs' -exec rm -rf {} \; 2>/dev/null || true &&
|
||||
|
||||
|
||||
# Nettoyer sélectivement (préserver logs, uploads et sessions)
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH} -mindepth 1 -maxdepth 1 ! -name 'uploads' ! -name 'logs' ! -name 'sessions' -exec rm -rf {} \; 2>/dev/null || true &&
|
||||
|
||||
# Extraire l'archive
|
||||
incus exec ${DEST_CONTAINER} -- tar -xzf /tmp/${ARCHIVE_NAME} -C ${API_PATH}/ &&
|
||||
|
||||
# Créer le marqueur d'environnement pour la détection CLI
|
||||
incus exec ${DEST_CONTAINER} -- bash -c 'echo \"${ENV_MARKER}\" > ${API_PATH}/.env_marker' &&
|
||||
|
||||
# Permissions
|
||||
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_GROUP} ${API_PATH} &&
|
||||
@@ -273,24 +289,74 @@ if [ "$DEST_HOST" != "local" ]; then
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH} -type f -exec chmod 644 {} \; &&
|
||||
|
||||
# Permissions spéciales pour logs
|
||||
incus exec ${DEST_CONTAINER} -- test -d ${API_PATH}/logs &&
|
||||
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER_LOGS}:${FINAL_GROUP_LOGS} ${API_PATH}/logs &&
|
||||
incus exec ${DEST_CONTAINER} -- chmod -R 755 ${API_PATH}/logs || true &&
|
||||
incus exec ${DEST_CONTAINER} -- mkdir -p ${API_PATH}/logs/events &&
|
||||
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER_LOGS}:${FINAL_GROUP} ${API_PATH}/logs &&
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH}/logs -type d -exec chmod 750 {} \; &&
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH}/logs -type f -exec chmod 640 {} \; &&
|
||||
|
||||
# Permissions spéciales pour uploads
|
||||
incus exec ${DEST_CONTAINER} -- test -d ${API_PATH}/uploads &&
|
||||
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER_LOGS}:${FINAL_GROUP_LOGS} ${API_PATH}/uploads &&
|
||||
incus exec ${DEST_CONTAINER} -- chmod -R 755 ${API_PATH}/uploads || true &&
|
||||
|
||||
# Composer
|
||||
incus exec ${DEST_CONTAINER} -- bash -c 'cd ${API_PATH} && composer install --no-dev --optimize-autoloader' || echo 'Composer install skipped' &&
|
||||
incus exec ${DEST_CONTAINER} -- mkdir -p ${API_PATH}/uploads &&
|
||||
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER_LOGS}:${FINAL_GROUP} ${API_PATH}/uploads &&
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH}/uploads -type d -exec chmod 750 {} \; &&
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH}/uploads -type f -exec chmod 640 {} \; &&
|
||||
|
||||
# Permissions spéciales pour sessions
|
||||
incus exec ${DEST_CONTAINER} -- mkdir -p ${API_PATH}/sessions &&
|
||||
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER_LOGS}:${FINAL_GROUP} ${API_PATH}/sessions &&
|
||||
incus exec ${DEST_CONTAINER} -- chmod 700 ${API_PATH}/sessions &&
|
||||
|
||||
# Composer (installation stricte - échec bloquant)
|
||||
incus exec ${DEST_CONTAINER} -- bash -c 'cd ${API_PATH} && composer install --no-dev --optimize-autoloader' || { echo 'ERROR: Composer install failed'; exit 1; } &&
|
||||
|
||||
# Nettoyage
|
||||
incus exec ${DEST_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} &&
|
||||
rm -f /tmp/${ARCHIVE_NAME}
|
||||
" || echo_error "Deployment failed on destination"
|
||||
|
||||
|
||||
echo_info "Remote backup saved: ${REMOTE_BACKUP_DIR} on ${DEST_CONTAINER}"
|
||||
|
||||
# Nettoyage des anciens backups sur le container distant
|
||||
echo_info "Cleaning old backup directories on ${DEST_CONTAINER}..."
|
||||
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||
incus exec ${DEST_CONTAINER} -- bash -c 'rm -rf ${API_PATH}_backup_*'
|
||||
" && echo_info "Old backups cleaned" || echo_warning "Could not clean old backups"
|
||||
|
||||
# =====================================
|
||||
# Configuration des tâches CRON
|
||||
# =====================================
|
||||
|
||||
echo_step "Configuring CRON tasks..."
|
||||
|
||||
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||
incus exec ${DEST_CONTAINER} -- bash <<'EOFCRON'
|
||||
# Sauvegarder les crons existants (hors geosector)
|
||||
crontab -l 2>/dev/null | grep -v 'geosector/api/scripts/cron' > /tmp/crontab_backup || true
|
||||
|
||||
# Créer le nouveau crontab avec les tâches CRON pour l'API
|
||||
cat /tmp/crontab_backup > /tmp/new_crontab
|
||||
cat >> /tmp/new_crontab <<'EOF'
|
||||
|
||||
# GEOSECTOR API - Email queue processing (every 5 minutes)
|
||||
*/5 * * * * /usr/bin/php /var/www/geosector/api/scripts/cron/process_email_queue.php >> /var/www/geosector/api/logs/email_queue.log 2>&1
|
||||
|
||||
# GEOSECTOR API - Security data cleanup (daily at 2am)
|
||||
0 2 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_security_data.php >> /var/www/geosector/api/logs/cleanup_security.log 2>&1
|
||||
|
||||
# GEOSECTOR API - Stripe devices update (weekly Sunday at 3am)
|
||||
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
|
||||
EOF
|
||||
|
||||
# Installer le nouveau crontab
|
||||
crontab /tmp/new_crontab
|
||||
|
||||
# Nettoyer
|
||||
rm -f /tmp/crontab_backup /tmp/new_crontab
|
||||
|
||||
# Afficher les crons installés
|
||||
echo 'CRON tasks installed:'
|
||||
crontab -l | grep geosector
|
||||
EOFCRON
|
||||
" && echo_info "CRON tasks configured successfully" || echo_warning "CRON configuration failed"
|
||||
fi
|
||||
|
||||
# L'archive reste dans le dossier de backup, pas de nettoyage nécessaire
|
||||
|
||||
495
api/docs/EVENTS-LOG.md
Normal file
495
api/docs/EVENTS-LOG.md
Normal file
@@ -0,0 +1,495 @@
|
||||
# Système de logs d'événements JSONL
|
||||
|
||||
## 📋 Vue d'ensemble
|
||||
|
||||
Système de traçabilité des événements métier pour statistiques et audit, stocké en fichiers JSONL (JSON Lines) sans impact sur la base de données principale.
|
||||
|
||||
**Créé le :** 26 Octobre 2025
|
||||
**Rétention :** 15 mois
|
||||
**Format :** JSONL (une ligne = un événement JSON)
|
||||
|
||||
## 🎯 Objectifs
|
||||
|
||||
### Événements tracés
|
||||
|
||||
**Authentification**
|
||||
- Connexions réussies (user_id, entity_id, plateforme, IP)
|
||||
- Tentatives échouées (username, raison, IP, nb tentatives)
|
||||
|
||||
**CRUD métier**
|
||||
- **Passages** : création, modification, suppression
|
||||
- **Secteurs** : création, modification, suppression
|
||||
- **Membres** : création, modification, suppression
|
||||
- **Entités** : création, modification, suppression
|
||||
|
||||
### Cas d'usage
|
||||
|
||||
**1. Admin entité**
|
||||
- Stats de son entité : connexions, passages, secteurs sur 1 jour/semaine/mois
|
||||
- Activité des membres de l'entité
|
||||
|
||||
**2. Super-admin**
|
||||
- Stats globales : tous les passages modifiés sur 2 semaines
|
||||
- Événements toutes entités sur période donnée
|
||||
- Détection d'anomalies
|
||||
|
||||
## 📁 Architecture de stockage
|
||||
|
||||
### Structure des répertoires
|
||||
|
||||
```
|
||||
/logs/events/
|
||||
├── 2025-10-26.jsonl # Fichier du jour (écriture append)
|
||||
├── 2025-10-25.jsonl
|
||||
├── 2025-10-24.jsonl
|
||||
├── 2025-09-30.jsonl
|
||||
├── 2025-09-29.jsonl.gz # Compression auto après 30 jours
|
||||
└── archive/
|
||||
├── 2025-09.jsonl.gz # Archive mensuelle
|
||||
├── 2025-08.jsonl.gz
|
||||
└── 2024-07.jsonl.gz # Supprimé auto après 15 mois
|
||||
```
|
||||
|
||||
### Cycle de vie des fichiers
|
||||
|
||||
| Âge | État | Taille estimée | Accès |
|
||||
|-----|------|----------------|-------|
|
||||
| 0-30 jours | `.jsonl` non compressé | 1-10 MB/jour | Lecture directe rapide |
|
||||
| 30 jours-15 mois | `.jsonl.gz` compressé | ~100 KB/jour | Décompression à la volée |
|
||||
| > 15 mois | Supprimé automatiquement | - | - |
|
||||
|
||||
### Rotation et rétention
|
||||
|
||||
**CRON mensuel** : `scripts/cron/rotate_event_logs.php`
|
||||
- **Fréquence** : 1er du mois à 3h00
|
||||
- **Actions** :
|
||||
1. Compresser les fichiers `.jsonl` de plus de 30 jours en `.jsonl.gz`
|
||||
2. Supprimer les fichiers `.jsonl.gz` de plus de 15 mois
|
||||
3. Logger le résumé de rotation
|
||||
|
||||
**Commande manuelle** :
|
||||
```bash
|
||||
php scripts/cron/rotate_event_logs.php
|
||||
```
|
||||
|
||||
## 📊 Format des événements
|
||||
|
||||
### Structure commune
|
||||
|
||||
Tous les événements partagent ces champs :
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-10-26T14:32:15Z", // ISO 8601 UTC
|
||||
"event": "nom_evenement", // Type d'événement
|
||||
"user_id": 123, // ID utilisateur (si authentifié)
|
||||
"entity_id": 5, // ID entité (si applicable)
|
||||
"ip": "192.168.1.100", // IP client
|
||||
"platform": "ios|android|web", // Plateforme
|
||||
"app_version": "3.3.6" // Version app (mobile uniquement)
|
||||
}
|
||||
```
|
||||
|
||||
### Événements d'authentification
|
||||
|
||||
#### Login réussi
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T14:32:15Z","event":"login_success","user_id":123,"entity_id":5,"platform":"ios","app_version":"3.3.6","ip":"192.168.1.100","username":"user123"}
|
||||
```
|
||||
|
||||
#### Login échoué
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T14:35:22Z","event":"login_failed","username":"test","reason":"invalid_password","ip":"192.168.1.101","attempt":3,"platform":"web"}
|
||||
```
|
||||
|
||||
**Raisons possibles** : `invalid_password`, `user_not_found`, `account_inactive`, `blocked_ip`
|
||||
|
||||
#### Logout
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T16:45:00Z","event":"logout","user_id":123,"entity_id":5,"platform":"android","session_duration":7800}
|
||||
```
|
||||
|
||||
### Événements Passages
|
||||
|
||||
#### Création
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T14:40:10Z","event":"passage_created","passage_id":45678,"user_id":123,"entity_id":5,"operation_id":789,"sector_id":12,"amount":50.00,"payment_type":"cash","platform":"android"}
|
||||
```
|
||||
|
||||
#### Modification
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T14:42:05Z","event":"passage_updated","passage_id":45678,"user_id":123,"entity_id":5,"changes":{"amount":{"old":50.00,"new":75.00},"payment_type":{"old":"cash","new":"stripe"}},"platform":"ios"}
|
||||
```
|
||||
|
||||
#### Suppression
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T14:45:30Z","event":"passage_deleted","passage_id":45678,"user_id":123,"entity_id":5,"operation_id":789,"deleted_by":123,"soft_delete":true,"platform":"web"}
|
||||
```
|
||||
|
||||
### Événements Secteurs
|
||||
|
||||
#### Création
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:10:00Z","event":"sector_created","sector_id":456,"operation_id":789,"entity_id":5,"user_id":123,"sector_name":"Secteur A","platform":"web"}
|
||||
```
|
||||
|
||||
#### Modification
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:12:00Z","event":"sector_updated","sector_id":456,"operation_id":789,"entity_id":5,"user_id":123,"changes":{"sector_name":{"old":"Secteur A","new":"Secteur Alpha"}},"platform":"web"}
|
||||
```
|
||||
|
||||
#### Suppression
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:15:00Z","event":"sector_deleted","sector_id":456,"operation_id":789,"entity_id":5,"user_id":123,"deleted_by":123,"soft_delete":true,"platform":"web"}
|
||||
```
|
||||
|
||||
### Événements Membres (Users)
|
||||
|
||||
#### Création
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:20:00Z","event":"user_created","new_user_id":789,"entity_id":5,"created_by":123,"role_id":1,"username":"newuser","platform":"web"}
|
||||
```
|
||||
|
||||
#### Modification
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:25:00Z","event":"user_updated","user_id":789,"entity_id":5,"updated_by":123,"changes":{"role_id":{"old":1,"new":2},"encrypted_phone":true},"platform":"web"}
|
||||
```
|
||||
|
||||
**Note** : Les champs chiffrés sont indiqués par un booléen `true` sans exposer les valeurs
|
||||
|
||||
#### Suppression
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:30:00Z","event":"user_deleted","user_id":789,"entity_id":5,"deleted_by":123,"soft_delete":true,"platform":"web"}
|
||||
```
|
||||
|
||||
### Événements Entités
|
||||
|
||||
#### Création
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:35:00Z","event":"entity_created","entity_id":25,"created_by":1,"entity_type_id":1,"postal_code":"75001","platform":"web"}
|
||||
```
|
||||
|
||||
#### Modification
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:40:00Z","event":"entity_updated","entity_id":25,"user_id":123,"updated_by":123,"changes":{"encrypted_name":true,"encrypted_email":true,"chk_stripe":{"old":0,"new":1}},"platform":"web"}
|
||||
```
|
||||
|
||||
#### Suppression (rare)
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:45:00Z","event":"entity_deleted","entity_id":25,"deleted_by":1,"soft_delete":true,"reason":"duplicate","platform":"web"}
|
||||
```
|
||||
|
||||
### Événements Opérations
|
||||
|
||||
#### Création
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T16:00:00Z","event":"operation_created","operation_id":999,"entity_id":5,"created_by":123,"date_start":"2025-11-01","date_end":"2025-11-30","platform":"web"}
|
||||
```
|
||||
|
||||
#### Modification
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T16:05:00Z","event":"operation_updated","operation_id":999,"entity_id":5,"updated_by":123,"changes":{"date_end":{"old":"2025-11-30","new":"2025-12-15"},"chk_active":{"old":0,"new":1}},"platform":"web"}
|
||||
```
|
||||
|
||||
#### Suppression
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T16:10:00Z","event":"operation_deleted","operation_id":999,"entity_id":5,"deleted_by":123,"soft_delete":true,"platform":"web"}
|
||||
```
|
||||
|
||||
## 🛠️ Implémentation
|
||||
|
||||
### Service EventLogService.php
|
||||
|
||||
**Emplacement** : `src/Services/EventLogService.php`
|
||||
|
||||
**Méthodes publiques** :
|
||||
```php
|
||||
EventLogService::logLoginSuccess($userId, $entityId, $username)
|
||||
EventLogService::logLoginFailed($username, $reason, $attempt)
|
||||
EventLogService::logLogout($userId, $entityId, $sessionDuration)
|
||||
|
||||
EventLogService::logPassageCreated($passageId, $operationId, $sectorId, $amount, $paymentType)
|
||||
EventLogService::logPassageUpdated($passageId, $changes)
|
||||
EventLogService::logPassageDeleted($passageId, $operationId, $softDelete)
|
||||
|
||||
EventLogService::logSectorCreated($sectorId, $operationId, $sectorName)
|
||||
EventLogService::logSectorUpdated($sectorId, $operationId, $changes)
|
||||
EventLogService::logSectorDeleted($sectorId, $operationId, $softDelete)
|
||||
|
||||
EventLogService::logUserCreated($newUserId, $entityId, $roleId, $username)
|
||||
EventLogService::logUserUpdated($userId, $changes)
|
||||
EventLogService::logUserDeleted($userId, $softDelete)
|
||||
|
||||
EventLogService::logEntityCreated($entityId, $entityTypeId, $postalCode)
|
||||
EventLogService::logEntityUpdated($entityId, $changes)
|
||||
EventLogService::logEntityDeleted($entityId, $reason)
|
||||
|
||||
EventLogService::logOperationCreated($operationId, $dateStart, $dateEnd)
|
||||
EventLogService::logOperationUpdated($operationId, $changes)
|
||||
EventLogService::logOperationDeleted($operationId, $softDelete)
|
||||
```
|
||||
|
||||
**Enrichissement automatique** :
|
||||
- `timestamp` : Généré automatiquement (UTC)
|
||||
- `user_id`, `entity_id` : Récupérés depuis `Session`
|
||||
- `ip` : Récupérée via `ClientDetector`
|
||||
- `platform` : Détecté via `ClientDetector` (ios/android/web)
|
||||
- `app_version` : Extrait du User-Agent pour mobile
|
||||
|
||||
### Intégration dans les Controllers
|
||||
|
||||
**Exemple dans PassageController** :
|
||||
```php
|
||||
public function createPassage(Request $request, Response $response): void {
|
||||
// ... validation et création ...
|
||||
|
||||
$passageId = $db->lastInsertId();
|
||||
|
||||
// Log de l'événement
|
||||
EventLogService::logPassageCreated(
|
||||
$passageId,
|
||||
$data['fk_operation'],
|
||||
$data['fk_sector'],
|
||||
$data['montant'],
|
||||
$data['fk_type_reglement']
|
||||
);
|
||||
|
||||
// ... suite du code ...
|
||||
}
|
||||
```
|
||||
|
||||
### Scripts d'analyse
|
||||
|
||||
#### 1. Stats entité
|
||||
|
||||
**Fichier** : `scripts/stats/entity_stats.php`
|
||||
|
||||
**Usage** :
|
||||
```bash
|
||||
# Stats entité 5 sur 7 derniers jours
|
||||
php scripts/stats/entity_stats.php --entity-id=5 --days=7
|
||||
|
||||
# Stats entité 5 entre deux dates
|
||||
php scripts/stats/entity_stats.php --entity-id=5 --from=2025-10-01 --to=2025-10-26
|
||||
|
||||
# Résultat JSON
|
||||
{
|
||||
"entity_id": 5,
|
||||
"period": {"from": "2025-10-20", "to": "2025-10-26"},
|
||||
"stats": {
|
||||
"logins": {"success": 45, "failed": 2},
|
||||
"passages": {"created": 120, "updated": 15, "deleted": 3},
|
||||
"sectors": {"created": 2, "updated": 8, "deleted": 0},
|
||||
"users": {"created": 1, "updated": 5, "deleted": 0}
|
||||
},
|
||||
"top_users": [
|
||||
{"user_id": 123, "actions": 85},
|
||||
{"user_id": 456, "actions": 42}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Stats globales super-admin
|
||||
|
||||
**Fichier** : `scripts/stats/global_stats.php`
|
||||
|
||||
**Usage** :
|
||||
```bash
|
||||
# Tous les passages modifiés sur 2 semaines
|
||||
php scripts/stats/global_stats.php --event=passage_updated --days=14
|
||||
|
||||
# Toutes les connexions échouées du mois
|
||||
php scripts/stats/global_stats.php --event=login_failed --month=2025-10
|
||||
|
||||
# Résultat JSON
|
||||
{
|
||||
"event": "passage_updated",
|
||||
"period": {"from": "2025-10-13", "to": "2025-10-26"},
|
||||
"total_events": 342,
|
||||
"by_entity": [
|
||||
{"entity_id": 5, "count": 120},
|
||||
{"entity_id": 12, "count": 85},
|
||||
{"entity_id": 18, "count": 67}
|
||||
],
|
||||
"by_day": {
|
||||
"2025-10-26": 45,
|
||||
"2025-10-25": 38,
|
||||
"2025-10-24": 52
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Export CSV pour analyse externe
|
||||
|
||||
**Fichier** : `scripts/stats/export_events_csv.php`
|
||||
|
||||
**Usage** :
|
||||
```bash
|
||||
# Exporter toutes les connexions du mois en CSV
|
||||
php scripts/stats/export_events_csv.php \
|
||||
--event=login_success \
|
||||
--month=2025-10 \
|
||||
--output=/tmp/logins_october.csv
|
||||
```
|
||||
|
||||
### CRON de rotation
|
||||
|
||||
**Fichier** : `scripts/cron/rotate_event_logs.php`
|
||||
|
||||
**Configuration crontab** :
|
||||
```cron
|
||||
# Rotation des logs d'événements - 1er du mois à 3h
|
||||
0 3 1 * * cd /var/www/geosector/api && php scripts/cron/rotate_event_logs.php
|
||||
```
|
||||
|
||||
**Actions** :
|
||||
1. Compresser fichiers > 30 jours : `gzip logs/events/2025-09-*.jsonl`
|
||||
2. Supprimer archives > 15 mois : `rm logs/events/*-2024-06-*.jsonl.gz`
|
||||
3. Logger résumé dans `logs/rotation.log`
|
||||
|
||||
## 📈 Performances et volumétrie
|
||||
|
||||
### Estimations
|
||||
|
||||
**Volume quotidien moyen** (pour 50 entités actives) :
|
||||
- 500 connexions/jour = 500 lignes
|
||||
- 2000 passages créés/modifiés = 2000 lignes
|
||||
- 100 autres événements = 100 lignes
|
||||
- **Total : ~2600 événements/jour**
|
||||
|
||||
**Taille fichier** :
|
||||
- 1 événement ≈ 200-400 bytes JSON
|
||||
- 2600 événements ≈ 0.8-1 MB/jour non compressé
|
||||
- Compression gzip : ratio ~10:1 → **~100 KB/jour compressé**
|
||||
|
||||
**Rétention 15 mois** :
|
||||
- Non compressé (30 jours) : 30 MB
|
||||
- Compressé (14.5 mois) : 45 MB
|
||||
- **Total stockage : ~75 MB** pour 15 mois
|
||||
|
||||
### Optimisation lecture
|
||||
|
||||
**Lecture mono-fichier** : < 50ms pour analyser 1 jour (2600 événements)
|
||||
|
||||
**Lecture période 7 jours** :
|
||||
- 7 fichiers × 1 MB = 7 MB à lire
|
||||
- Filtrage `jq` ou PHP : ~200-300ms
|
||||
|
||||
**Lecture période 2 semaines (super-admin)** :
|
||||
- 14 fichiers × 1 MB = 14 MB à lire
|
||||
- Filtrage sur type événement : ~500ms
|
||||
|
||||
**Lecture archive compressée** :
|
||||
- Décompression à la volée : +100-200ms
|
||||
- Total : ~700-800ms pour 1 mois compressé
|
||||
|
||||
## 🔒 Sécurité et confidentialité
|
||||
|
||||
### Données sensibles
|
||||
|
||||
**❌ Jamais loggé en clair** :
|
||||
- Mots de passe
|
||||
- Contenu chiffré (noms, emails, téléphones, IBAN)
|
||||
- Tokens d'authentification
|
||||
|
||||
**✅ Loggé** :
|
||||
- IDs (user_id, entity_id, passage_id, etc.)
|
||||
- Montants financiers
|
||||
- Dates et timestamps
|
||||
- Types de modifications (indicateur booléen pour champs chiffrés)
|
||||
|
||||
### Exemple champ chiffré
|
||||
```json
|
||||
{
|
||||
"event": "user_updated",
|
||||
"changes": {
|
||||
"encrypted_name": true, // Indique modification sans valeur
|
||||
"encrypted_email": true,
|
||||
"role_id": {"old": 1, "new": 2} // Champ non sensible = valeurs OK
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Permissions d'accès
|
||||
|
||||
**Fichiers logs** :
|
||||
- Propriétaire : `nginx:nginx`
|
||||
- Permissions : `0640` (lecture nginx, écriture nginx, aucun autre)
|
||||
- Dossier `/logs/events/` : `0750`
|
||||
|
||||
**Scripts d'analyse** :
|
||||
- Exécution : root ou nginx uniquement
|
||||
- Pas d'accès direct via endpoints API (pour l'instant)
|
||||
|
||||
## 🚀 Roadmap et évolutions futures
|
||||
|
||||
### Phase 1 - MVP (actuel) ✅
|
||||
- [x] Architecture JSONL quotidienne
|
||||
- [x] Service EventLogService.php
|
||||
- [x] Intégration dans controllers (LoginController, PassageController, UserController, SectorController, OperationController, EntiteController)
|
||||
- [ ] CRON de rotation 15 mois
|
||||
- [ ] Scripts d'analyse de base
|
||||
|
||||
### Phase 2 - Dashboards (Q1 2026)
|
||||
- [ ] Endpoints API : `GET /api/stats/entity/{id}`, `GET /api/stats/global`
|
||||
- [ ] Interface web admin : graphiques connexions, passages
|
||||
- [ ] Filtres avancés (période, plateforme, utilisateur)
|
||||
|
||||
### Phase 3 - Alertes (Q2 2026)
|
||||
- [ ] Détection anomalies (pics de connexions échouées)
|
||||
- [ ] Alertes email super-admins
|
||||
- [ ] Seuils configurables par entité
|
||||
|
||||
### Phase 4 - Migration TimescaleDB (si besoin)
|
||||
- [ ] Évaluation volume : si > 50k événements/jour
|
||||
- [ ] Import JSONL → TimescaleDB
|
||||
- [ ] Rétention hybride : 90j TimescaleDB, archives JSONL
|
||||
|
||||
## 📝 Statut implémentation
|
||||
|
||||
**Date : 28 Octobre 2025**
|
||||
|
||||
### ✅ Terminé
|
||||
- Service `EventLogService.php` créé avec toutes les méthodes de logging
|
||||
- Intégration complète dans les 6 controllers principaux :
|
||||
- **LoginController** : login réussi/échoué, logout
|
||||
- **PassageController** : création, modification, suppression passages
|
||||
- **UserController** : création, modification, suppression utilisateurs
|
||||
- **SectorController** : création, modification, suppression secteurs
|
||||
- **OperationController** : création, modification, suppression opérations
|
||||
- **EntiteController** : création, modification entités
|
||||
- Enrichissement automatique : timestamp UTC, user_id, entity_id, IP, platform, app_version
|
||||
- Sécurité : champs sensibles loggés en booléen uniquement (pas de valeurs chiffrées)
|
||||
- Script de déploiement `deploy-api.sh` crée automatiquement `/logs/events/` avec permissions 0750
|
||||
|
||||
### 🔄 En attente
|
||||
- Scripts d'analyse (`entity_stats.php`, `global_stats.php`, `export_events_csv.php`)
|
||||
- CRON de rotation 15 mois (`rotate_event_logs.php`)
|
||||
- Tests en environnement DEV
|
||||
|
||||
## 📝 Checklist déploiement
|
||||
|
||||
### Environnement DEV (dva-geo)
|
||||
- [x] Créer dossier `/logs/events/` (permissions 0750) - Intégré dans deploy-api.sh
|
||||
- [x] Déployer `EventLogService.php`
|
||||
- [ ] Déployer scripts stats et rotation
|
||||
- [ ] Configurer CRON rotation
|
||||
- [ ] Tests : générer événements manuellement
|
||||
- [ ] Valider format JSONL et rotation
|
||||
|
||||
### Environnement RECETTE (rca-geo)
|
||||
- [ ] Déployer depuis DEV validé
|
||||
- [ ] Tests de charge : 10k événements/jour
|
||||
- [ ] Valider performances scripts d'analyse
|
||||
- [ ] Valider compression et suppression auto
|
||||
|
||||
### Environnement PRODUCTION (pra-geo)
|
||||
- [ ] Déployer depuis RECETTE validée
|
||||
- [ ] Monitoring volumétrie
|
||||
- [ ] Backups quotidiens `/logs/events/` (via CRON général)
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour :** 28 Octobre 2025
|
||||
**Version :** 1.1
|
||||
**Statut :** ✅ Service implémenté et intégré - Scripts d'analyse à développer
|
||||
@@ -24,24 +24,78 @@ Ce document décrit le système de gestion des secteurs dans l'API Geosector, in
|
||||
- Contient toutes les tables de l'application
|
||||
- Tables concernées : `ope_sectors`, `sectors_adresses`, `ope_pass`, `ope_users_sectors`, `x_departements_contours`
|
||||
|
||||
2. **Base adresses** (dans conteneurs Incus séparés)
|
||||
- DVA : `dva-maria` (13.23.33.46) - base `adresses`
|
||||
- RCA : `rca-maria` (13.23.33.36) - base `adresses`
|
||||
- PRA : `pra-maria` (13.23.33.26) - base `adresses`
|
||||
- Credentials : `adr_geo_user` / `d66,AdrGeoDev.User`
|
||||
2. **Base adresses** (dans conteneurs maria3/maria4)
|
||||
- **DVA** : maria3 (13.23.33.4) - base `adresses`
|
||||
- User : `adr_geo_user` / `d66,AdrGeoDev.User`
|
||||
- **RCA** : maria3 (13.23.33.4) - base `adresses`
|
||||
- User : `adr_geo_user` / `d66,AdrGeoRec.User`
|
||||
- **PROD** : maria4 (13.23.33.4) - base `adresses`
|
||||
- User : `adr_geo_user` / `d66,AdrGeoPrd.User`
|
||||
- Tables par département : `cp22`, `cp23`, etc.
|
||||
|
||||
3. **Base bâtiments** (dans conteneurs maria3/maria4)
|
||||
- **DVA** : maria3 (13.23.33.4) - base `batiments`
|
||||
- User : `adr_geo_user` / `d66,AdrGeoDev.User`
|
||||
- **RCA** : maria3 (13.23.33.4) - base `batiments`
|
||||
- User : `adr_geo_user` / `d66,AdrGeoRec.User`
|
||||
- **PROD** : maria4 (13.23.33.4) - base `batiments`
|
||||
- User : `adr_geo_user` / `d66,AdrGeoPrd.User`
|
||||
- Tables par département : `bat22`, `bat23`, etc.
|
||||
- Colonnes principales : `batiment_groupe_id`, `cle_interop_adr`, `nb_log`, `nb_niveau`, `residence`, `altitude_sol_mean`
|
||||
- Lien avec adresses : `bat{dept}.cle_interop_adr = cp{dept}.id`
|
||||
|
||||
### Configuration
|
||||
|
||||
Dans `src/Config/AppConfig.php` :
|
||||
|
||||
```php
|
||||
// DÉVELOPPEMENT
|
||||
'addresses_database' => [
|
||||
'host' => '13.23.33.46', // Varie selon l'environnement
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'adresses',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoDev.User',
|
||||
],
|
||||
|
||||
// RECETTE
|
||||
'addresses_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'adresses',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoRec.User',
|
||||
],
|
||||
|
||||
// PRODUCTION
|
||||
'addresses_database' => [
|
||||
'host' => '13.23.33.4', // Container maria4 sur IN4
|
||||
'name' => 'adresses',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoPrd.User',
|
||||
],
|
||||
|
||||
// DÉVELOPPEMENT - Bâtiments
|
||||
'buildings_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'batiments',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoDev.User',
|
||||
],
|
||||
|
||||
// RECETTE - Bâtiments
|
||||
'buildings_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'batiments',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoRec.User',
|
||||
],
|
||||
|
||||
// PRODUCTION - Bâtiments
|
||||
'buildings_database' => [
|
||||
'host' => '13.23.33.4', // Container maria4 sur IN4
|
||||
'name' => 'batiments',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoPrd.User',
|
||||
],
|
||||
```
|
||||
|
||||
## Gestion des contours départementaux
|
||||
@@ -100,7 +154,7 @@ Vérifie les limites départementales des secteurs :
|
||||
class DepartmentBoundaryService {
|
||||
// Vérifie si un secteur est contenu dans un département
|
||||
public function checkSectorInDepartment(array $sectorCoordinates, string $departmentCode): array
|
||||
|
||||
|
||||
// Liste tous les départements touchés par un secteur
|
||||
public function getDepartmentsForSector(array $sectorCoordinates): array
|
||||
}
|
||||
@@ -118,6 +172,46 @@ class DepartmentBoundaryService {
|
||||
]
|
||||
```
|
||||
|
||||
### BuildingService
|
||||
|
||||
Enrichit les adresses avec les données bâtiments :
|
||||
|
||||
```php
|
||||
namespace App\Services;
|
||||
|
||||
class BuildingService {
|
||||
// Enrichit une liste d'adresses avec les métadonnées des bâtiments
|
||||
public function enrichAddresses(array $addresses): array
|
||||
}
|
||||
```
|
||||
|
||||
**Fonctionnement** :
|
||||
- Connexion à la base `batiments` externe
|
||||
- Interrogation des tables `bat{dept}` par département
|
||||
- JOIN sur `bat{dept}.cle_interop_adr = cp{dept}.id`
|
||||
- Ajout des métadonnées : `fk_batiment`, `fk_habitat`, `nb_niveau`, `nb_log`, `residence`, `alt_sol`
|
||||
- Fallback : `fk_habitat=1` (maison individuelle) si pas de bâtiment trouvé
|
||||
|
||||
**Données retournées** :
|
||||
```php
|
||||
[
|
||||
'id' => 'cp22.123456',
|
||||
'numero' => '10',
|
||||
'voie' => 'Rue Victor Hugo',
|
||||
'code_postal' => '22000',
|
||||
'commune' => 'Saint-Brieuc',
|
||||
'latitude' => 48.5149,
|
||||
'longitude' => -2.7658,
|
||||
// Données bâtiment enrichies :
|
||||
'fk_batiment' => 'BAT_123456', // null si maison
|
||||
'fk_habitat' => 2, // 1=individuel, 2=collectif
|
||||
'nb_niveau' => 4, // null si maison
|
||||
'nb_log' => 12, // null si maison
|
||||
'residence' => 'Résidence Les Pins', // '' si maison
|
||||
'alt_sol' => 25.5 // null si maison
|
||||
]
|
||||
```
|
||||
|
||||
## Processus de création de secteur
|
||||
|
||||
### 1. Structure du payload
|
||||
@@ -150,13 +244,77 @@ class DepartmentBoundaryService {
|
||||
- Recherche des passages avec `fk_sector = 0` dans le polygone
|
||||
- Mise à jour de leur `fk_sector` vers le nouveau secteur
|
||||
- Exclusion des passages ayant déjà une `fk_adresse`
|
||||
7. **Récupération** des adresses via `AddressService`
|
||||
8. **Stockage** des adresses dans `sectors_adresses`
|
||||
9. **Création** des passages dans `ope_pass` pour chaque adresse :
|
||||
7. **Récupération** des adresses via `AddressService::getAddressesInPolygon()`
|
||||
8. **Enrichissement** avec données bâtiments via `AddressService::enrichAddressesWithBuildings()`
|
||||
9. **Stockage** des adresses dans `sectors_adresses` avec colonnes bâtiment :
|
||||
- `fk_batiment`, `fk_habitat`, `nb_niveau`, `nb_log`, `residence`, `alt_sol`
|
||||
10. **Création** des passages dans `ope_pass` :
|
||||
- **Maisons individuelles** (fk_habitat=1) : 1 passage par adresse
|
||||
- **Immeubles** (fk_habitat=2) : nb_log passages par adresse (1 par appartement)
|
||||
- Champs ajoutés : `residence`, `appt` (numéro 1 à nb_log), `fk_habitat`
|
||||
- Affectés au premier utilisateur de la liste
|
||||
- Avec toutes les FK nécessaires (entité, opération, secteur, user)
|
||||
- Données d'adresse complètes
|
||||
10. **Commit** de la transaction ou **rollback** en cas d'erreur
|
||||
11. **Commit** de la transaction ou **rollback** en cas d'erreur
|
||||
|
||||
## Processus de modification de secteur
|
||||
|
||||
### 1. Structure du payload UPDATE
|
||||
|
||||
```json
|
||||
{
|
||||
"libelle": "Secteur Centre-Ville Modifié",
|
||||
"color": "#00FF00",
|
||||
"sector": "48.117266/-1.6777926#48.118500/-1.6750000#...",
|
||||
"users": [12, 34],
|
||||
"chk_adresses_change": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Paramètre chk_adresses_change
|
||||
|
||||
**Valeurs** :
|
||||
- `0` : Ne pas recalculer les adresses et passages (modification simple)
|
||||
- `1` : Recalculer les adresses et passages (défaut)
|
||||
|
||||
**Cas d'usage** :
|
||||
|
||||
#### chk_adresses_change = 0
|
||||
Modification rapide sans toucher aux adresses/passages :
|
||||
- ✅ Modification du libellé
|
||||
- ✅ Modification de la couleur
|
||||
- ✅ Modification des coordonnées du polygone (visuel uniquement)
|
||||
- ✅ Modification des membres affectés
|
||||
- ❌ Pas de recalcul des adresses dans sectors_adresses
|
||||
- ❌ Pas de mise à jour des passages (orphelins, créés, supprimés)
|
||||
- ❌ **Réponse sans passages_sector** (tableau vide)
|
||||
|
||||
**Utilité** : Permet aux admins de corriger rapidement un libellé, une couleur, ou d'ajuster légèrement le périmètre visuel sans déclencher un recalcul complet qui pourrait prendre plusieurs secondes.
|
||||
|
||||
**Réponse API** :
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Secteur modifié avec succès",
|
||||
"sector": { "id": 123, "libelle": "...", "color": "...", "sector": "..." },
|
||||
"passages_sector": [], // Vide car chk_adresses_change = 0
|
||||
"passages_orphaned": 0,
|
||||
"passages_deleted": 0,
|
||||
"passages_updated": 0,
|
||||
"passages_created": 0,
|
||||
"passages_total": 0,
|
||||
"users_sectors": [...]
|
||||
}
|
||||
```
|
||||
|
||||
#### chk_adresses_change = 1 (défaut)
|
||||
Modification complète avec recalcul :
|
||||
- ✅ Modification du libellé/couleur/polygone
|
||||
- ✅ Modification des membres
|
||||
- ✅ Suppression et recréation de sectors_adresses
|
||||
- ✅ Application des règles de gestion des bâtiments
|
||||
- ✅ Mise en orphelin des passages hors périmètre
|
||||
- ✅ Création de nouveaux passages pour nouvelles adresses
|
||||
|
||||
### 3. Réponse API pour CREATE
|
||||
|
||||
@@ -287,14 +445,28 @@ $coordinates = [
|
||||
|
||||
### sectors_adresses
|
||||
- `fk_sector` : Lien vers le secteur
|
||||
- `fk_address` : ID de l'adresse dans la base externe
|
||||
- `numero`, `voie`, `code_postal`, `commune`
|
||||
- `latitude`, `longitude`
|
||||
- `fk_adresse` : ID de l'adresse dans la base externe
|
||||
- `numero`, `rue`, `rue_bis`, `cp`, `ville`
|
||||
- `gps_lat`, `gps_lng`
|
||||
- **Colonnes bâtiment** :
|
||||
- `fk_batiment` : ID bâtiment (VARCHAR 50, null si maison)
|
||||
- `fk_habitat` : 1=individuel, 2=collectif (TINYINT UNSIGNED)
|
||||
- `nb_niveau` : Nombre d'étages (INT, null)
|
||||
- `nb_log` : Nombre de logements (INT, null)
|
||||
- `residence` : Nom résidence/copropriété (VARCHAR 75)
|
||||
- `alt_sol` : Altitude sol en mètres (DECIMAL 10,2, null)
|
||||
|
||||
### ope_pass (passages)
|
||||
- `fk_entite`, `fk_operation`, `fk_sector`, `fk_user`
|
||||
- `numero`, `voie`, `code_postal`, `commune`
|
||||
- `latitude`, `longitude`
|
||||
- `fk_operation`, `fk_sector`, `fk_user`, `fk_adresse`
|
||||
- `numero`, `rue`, `rue_bis`, `ville`
|
||||
- `gps_lat`, `gps_lng`
|
||||
- **Colonnes bâtiment** :
|
||||
- `residence` : Nom résidence (VARCHAR 75)
|
||||
- `appt` : Numéro appartement (VARCHAR 10, saisie libre)
|
||||
- `niveau` : Étage (VARCHAR 10, saisie libre)
|
||||
- `fk_habitat` : 1=individuel, 2=collectif (TINYINT UNSIGNED)
|
||||
- `fk_type` : Type passage (2=à faire, autres valeurs pour fait/refus)
|
||||
- `encrypted_name`, `encrypted_email`, `encrypted_phone` : Données cryptées
|
||||
- `created_at`, `fk_user_creat`, `chk_active`
|
||||
|
||||
### ope_users_sectors
|
||||
@@ -303,6 +475,103 @@ $coordinates = [
|
||||
- `fk_sector` : Lien vers le secteur
|
||||
- `created_at`, `fk_user_creat`, `chk_active`
|
||||
|
||||
## Règles de gestion des bâtiments lors de l'UPDATE
|
||||
|
||||
### Principe général
|
||||
|
||||
Lors de la mise à jour d'un secteur, le système applique une logique intelligente pour gérer les passages en fonction du type d'habitat (maison/immeuble) et du nombre de logements.
|
||||
|
||||
### Clé d'identification unique
|
||||
|
||||
**Tous les passages** sont identifiés par la clé : `numero|rue|rue_bis|ville`
|
||||
|
||||
Cette clé ne contient **pas** `residence` ni `appt` car ces champs sont en **saisie libre** par l'utilisateur.
|
||||
|
||||
### Cas 1 : Maison individuelle (fk_habitat=1)
|
||||
|
||||
#### Si 0 passage existant :
|
||||
```
|
||||
→ INSERT 1 nouveau passage
|
||||
- fk_habitat = 1
|
||||
- residence = ''
|
||||
- appt = ''
|
||||
```
|
||||
|
||||
#### Si 1+ passages existants :
|
||||
```
|
||||
→ UPDATE le premier passage
|
||||
- fk_habitat = 1
|
||||
- residence = ''
|
||||
→ Les autres passages restent INTACTS
|
||||
(peuvent correspondre à plusieurs habitants saisis manuellement)
|
||||
```
|
||||
|
||||
### Cas 2 : Immeuble (fk_habitat=2)
|
||||
|
||||
#### Étape 1 : UPDATE systématique
|
||||
```
|
||||
→ UPDATE TOUS les passages existants à cette adresse
|
||||
- fk_habitat = 2
|
||||
- residence = sectors_adresses.residence (si non vide)
|
||||
```
|
||||
|
||||
#### Étape 2a : Si nb_existants < nb_log (ex: 3 passages, nb_log=6)
|
||||
```
|
||||
→ INSERT (nb_log - nb_existants) nouveaux passages
|
||||
- fk_habitat = 2
|
||||
- residence = sectors_adresses.residence
|
||||
- appt = '' (pas de numéro prédéfini)
|
||||
- fk_type = 2 (à faire)
|
||||
|
||||
Résultat : 6 passages total (3 conservés + 3 créés)
|
||||
```
|
||||
|
||||
#### Étape 2b : Si nb_existants > nb_log (ex: 10 passages, nb_log=6)
|
||||
```
|
||||
→ DELETE max (nb_existants - nb_log) passages
|
||||
Conditions de suppression :
|
||||
- fk_type = 2 (à faire)
|
||||
- ET encrypted_name vide (non visité)
|
||||
- Tri par created_at ASC (les plus anciens d'abord)
|
||||
|
||||
Résultat : Entre 6 et 10 passages (selon combien sont visités)
|
||||
```
|
||||
|
||||
### Points importants
|
||||
|
||||
✅ **Préservation des données utilisateur** :
|
||||
- `appt` et `niveau` ne sont **JAMAIS modifiés** (saisie libre conservée)
|
||||
- Les passages visités (encrypted_name rempli) ne sont **JAMAIS supprimés**
|
||||
|
||||
✅ **Mise à jour conditionnelle** :
|
||||
- `residence` est mis à jour **uniquement si non vide** dans sectors_adresses
|
||||
- Permet de conserver une saisie manuelle si la base bâtiments n'a pas l'info
|
||||
|
||||
✅ **Gestion des transitions** :
|
||||
- Une adresse peut passer de maison (fk_habitat=1) à immeuble (fk_habitat=2) ou inversement
|
||||
- La logique s'adapte automatiquement au nouveau type d'habitat
|
||||
|
||||
✅ **Uniformisation GPS** :
|
||||
- **Tous les passages d'une même adresse partagent les mêmes coordonnées GPS** (gps_lat, gps_lng)
|
||||
- Ces coordonnées proviennent de `sectors_adresses` (enrichies depuis la base externe `adresses`)
|
||||
- Cette règle s'applique lors de la **création** et de la **mise à jour** avec `chk_adresses_change=1`
|
||||
- Garantit la cohérence géographique pour tous les passages d'un même immeuble
|
||||
|
||||
### Exemple concret
|
||||
|
||||
**Situation initiale** :
|
||||
- Adresse : "10 rue Victor Hugo, 22000 Saint-Brieuc"
|
||||
- 8 passages existants (dont 3 visités)
|
||||
- nb_log passe de 8 à 5
|
||||
|
||||
**Actions** :
|
||||
1. UPDATE les 8 passages → fk_habitat=2, residence="Les Chênes"
|
||||
2. Tentative suppression de (8-5) = 3 passages
|
||||
3. Recherche des passages avec fk_type=2 ET encrypted_name vide
|
||||
4. Suppose 5 passages non visités trouvés
|
||||
5. Suppression des 3 plus anciens non visités
|
||||
6. **Résultat** : 5 passages restants (3 visités + 2 non visités)
|
||||
|
||||
## Logs et monitoring
|
||||
|
||||
Le système génère des logs détaillés pour :
|
||||
|
||||
464
api/docs/STRIPE-BACKEND-MIGRATION.md
Normal file
464
api/docs/STRIPE-BACKEND-MIGRATION.md
Normal file
@@ -0,0 +1,464 @@
|
||||
# 🔧 Migration Backend Stripe - Option A (Tout en 1)
|
||||
|
||||
## 📋 Objectif
|
||||
|
||||
Optimiser la création de compte Stripe Connect en **1 seule requête** côté Flutter qui crée :
|
||||
1. Le compte Stripe Connect
|
||||
2. La Location Terminal
|
||||
3. Le lien d'onboarding
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ 1. Modification de la base de données
|
||||
|
||||
### **Ajouter la colonne `stripe_location_id`**
|
||||
|
||||
```sql
|
||||
ALTER TABLE amicales
|
||||
ADD COLUMN stripe_location_id VARCHAR(255) NULL
|
||||
AFTER stripe_id;
|
||||
```
|
||||
|
||||
**Vérification** :
|
||||
```sql
|
||||
DESCRIBE amicales;
|
||||
```
|
||||
|
||||
Doit afficher :
|
||||
```
|
||||
+-------------------+--------------+------+-----+---------+-------+
|
||||
| Field | Type | Null | Key | Default | Extra |
|
||||
+-------------------+--------------+------+-----+---------+-------+
|
||||
| stripe_id | varchar(255) | YES | | NULL | |
|
||||
| stripe_location_id| varchar(255) | YES | | NULL | |
|
||||
+-------------------+--------------+------+-----+---------+-------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 2. Modification de l'endpoint `POST /stripe/accounts`
|
||||
|
||||
### **Fichier** : `app/Http/Controllers/StripeController.php` (ou similaire)
|
||||
|
||||
### **Méthode** : `createAccount()` ou `store()`
|
||||
|
||||
### **Code proposé** :
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\Amicale;
|
||||
|
||||
/**
|
||||
* Créer un compte Stripe Connect avec Location Terminal et lien d'onboarding
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function createStripeAccount(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'fk_entite' => 'required|integer|exists:amicales,id',
|
||||
'return_url' => 'required|string|url',
|
||||
'refresh_url' => 'required|string|url',
|
||||
]);
|
||||
|
||||
$fkEntite = $request->fk_entite;
|
||||
$amicale = Amicale::findOrFail($fkEntite);
|
||||
|
||||
// Vérifier si un compte existe déjà
|
||||
if (!empty($amicale->stripe_id)) {
|
||||
return $this->handleExistingAccount($amicale, $request);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// Configurer la clé Stripe (selon environnement)
|
||||
\Stripe\Stripe::setApiKey(config('services.stripe.secret'));
|
||||
|
||||
// 1️⃣ Créer le compte Stripe Connect Express
|
||||
$account = \Stripe\Account::create([
|
||||
'type' => 'express',
|
||||
'country' => 'FR',
|
||||
'email' => $amicale->email,
|
||||
'business_type' => 'non_profit', // ou 'company' selon le cas
|
||||
'business_profile' => [
|
||||
'name' => $amicale->name,
|
||||
'url' => config('app.url'),
|
||||
],
|
||||
'capabilities' => [
|
||||
'card_payments' => ['requested' => true],
|
||||
'transfers' => ['requested' => true],
|
||||
],
|
||||
]);
|
||||
|
||||
\Log::info('Stripe account created', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'account_id' => $account->id,
|
||||
]);
|
||||
|
||||
// 2️⃣ Créer la Location Terminal pour Tap to Pay
|
||||
$location = \Stripe\Terminal\Location::create([
|
||||
'display_name' => $amicale->name,
|
||||
'address' => [
|
||||
'line1' => $amicale->adresse1 ?: 'Non renseigné',
|
||||
'line2' => $amicale->adresse2,
|
||||
'city' => $amicale->ville ?: 'Non renseigné',
|
||||
'postal_code' => $amicale->code_postal ?: '00000',
|
||||
'country' => 'FR',
|
||||
],
|
||||
], [
|
||||
'stripe_account' => $account->id, // ← Important : Connect account
|
||||
]);
|
||||
|
||||
\Log::info('Stripe Terminal Location created', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'location_id' => $location->id,
|
||||
]);
|
||||
|
||||
// 3️⃣ Créer le lien d'onboarding
|
||||
$accountLink = \Stripe\AccountLink::create([
|
||||
'account' => $account->id,
|
||||
'refresh_url' => $request->refresh_url,
|
||||
'return_url' => $request->return_url,
|
||||
'type' => 'account_onboarding',
|
||||
]);
|
||||
|
||||
\Log::info('Stripe onboarding link created', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'account_id' => $account->id,
|
||||
]);
|
||||
|
||||
// 4️⃣ Sauvegarder TOUT en base de données
|
||||
$amicale->stripe_id = $account->id;
|
||||
$amicale->stripe_location_id = $location->id;
|
||||
$amicale->chk_stripe = true;
|
||||
$amicale->save();
|
||||
|
||||
DB::commit();
|
||||
|
||||
// 5️⃣ Retourner TOUTES les informations
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'account_id' => $account->id,
|
||||
'location_id' => $location->id,
|
||||
'onboarding_url' => $accountLink->url,
|
||||
'charges_enabled' => $account->charges_enabled,
|
||||
'payouts_enabled' => $account->payouts_enabled,
|
||||
'existing' => false,
|
||||
'message' => 'Compte Stripe Connect créé avec succès',
|
||||
], 201);
|
||||
|
||||
} catch (\Stripe\Exception\ApiErrorException $e) {
|
||||
DB::rollBack();
|
||||
|
||||
\Log::error('Stripe API error', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'error' => $e->getMessage(),
|
||||
'type' => get_class($e),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Erreur Stripe : ' . $e->getMessage(),
|
||||
], 500);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
\Log::error('Stripe account creation failed', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Erreur lors de la création du compte Stripe',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gérer le cas d'un compte Stripe existant
|
||||
*/
|
||||
private function handleExistingAccount(Amicale $amicale, Request $request)
|
||||
{
|
||||
try {
|
||||
\Stripe\Stripe::setApiKey(config('services.stripe.secret'));
|
||||
|
||||
// Récupérer les infos du compte existant
|
||||
$account = \Stripe\Account::retrieve($amicale->stripe_id);
|
||||
|
||||
// Si pas de location_id, la créer maintenant
|
||||
if (empty($amicale->stripe_location_id)) {
|
||||
$location = \Stripe\Terminal\Location::create([
|
||||
'display_name' => $amicale->name,
|
||||
'address' => [
|
||||
'line1' => $amicale->adresse1 ?: 'Non renseigné',
|
||||
'city' => $amicale->ville ?: 'Non renseigné',
|
||||
'postal_code' => $amicale->code_postal ?: '00000',
|
||||
'country' => 'FR',
|
||||
],
|
||||
], [
|
||||
'stripe_account' => $amicale->stripe_id,
|
||||
]);
|
||||
|
||||
$amicale->stripe_location_id = $location->id;
|
||||
$amicale->save();
|
||||
|
||||
\Log::info('Location created for existing account', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'location_id' => $location->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Si le compte est déjà complètement configuré
|
||||
if ($account->charges_enabled && $account->payouts_enabled) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'account_id' => $amicale->stripe_id,
|
||||
'location_id' => $amicale->stripe_location_id,
|
||||
'onboarding_url' => null,
|
||||
'charges_enabled' => true,
|
||||
'payouts_enabled' => true,
|
||||
'existing' => true,
|
||||
'message' => 'Compte Stripe déjà configuré et actif',
|
||||
]);
|
||||
}
|
||||
|
||||
// Compte existant mais configuration incomplète : générer un nouveau lien
|
||||
$accountLink = \Stripe\AccountLink::create([
|
||||
'account' => $amicale->stripe_id,
|
||||
'refresh_url' => $request->refresh_url,
|
||||
'return_url' => $request->return_url,
|
||||
'type' => 'account_onboarding',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'account_id' => $amicale->stripe_id,
|
||||
'location_id' => $amicale->stripe_location_id,
|
||||
'onboarding_url' => $accountLink->url,
|
||||
'charges_enabled' => $account->charges_enabled,
|
||||
'payouts_enabled' => $account->payouts_enabled,
|
||||
'existing' => true,
|
||||
'message' => 'Compte existant, configuration à finaliser',
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error handling existing account', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Erreur lors de la vérification du compte existant',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📡 3. Modification de l'endpoint `GET /stripe/accounts/{id}/status`
|
||||
|
||||
Ajouter `location_id` dans la réponse :
|
||||
|
||||
```php
|
||||
public function checkAccountStatus($amicaleId)
|
||||
{
|
||||
$amicale = Amicale::findOrFail($amicaleId);
|
||||
|
||||
if (empty($amicale->stripe_id)) {
|
||||
return response()->json([
|
||||
'has_account' => false,
|
||||
'account_id' => null,
|
||||
'location_id' => null,
|
||||
'charges_enabled' => false,
|
||||
'payouts_enabled' => false,
|
||||
'onboarding_completed' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
\Stripe\Stripe::setApiKey(config('services.stripe.secret'));
|
||||
$account = \Stripe\Account::retrieve($amicale->stripe_id);
|
||||
|
||||
return response()->json([
|
||||
'has_account' => true,
|
||||
'account_id' => $amicale->stripe_id,
|
||||
'location_id' => $amicale->stripe_location_id, // ← Ajouté
|
||||
'charges_enabled' => $account->charges_enabled,
|
||||
'payouts_enabled' => $account->payouts_enabled,
|
||||
'onboarding_completed' => $account->details_submitted,
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'has_account' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗑️ 4. Endpoint à SUPPRIMER (devenu inutile)
|
||||
|
||||
### **❌ `POST /stripe/locations`**
|
||||
|
||||
Cet endpoint n'est plus nécessaire car la Location est créée automatiquement dans `POST /stripe/accounts`.
|
||||
|
||||
**Option 1** : Supprimer complètement
|
||||
**Option 2** : Le garder pour compatibilité temporaire (si utilisé ailleurs)
|
||||
|
||||
---
|
||||
|
||||
## 📝 5. Modification du modèle Eloquent
|
||||
|
||||
### **Fichier** : `app/Models/Amicale.php`
|
||||
|
||||
Ajouter le champ `stripe_location_id` :
|
||||
|
||||
```php
|
||||
protected $fillable = [
|
||||
// ... autres champs
|
||||
'stripe_id',
|
||||
'stripe_location_id', // ← Ajouté
|
||||
'chk_stripe',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'chk_stripe' => 'boolean',
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 6. Tests à effectuer
|
||||
|
||||
### **Test 1 : Nouvelle amicale**
|
||||
```bash
|
||||
curl -X POST http://localhost/api/stripe/accounts \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-d '{
|
||||
"fk_entite": 123,
|
||||
"return_url": "https://app.geosector.fr/stripe/success",
|
||||
"refresh_url": "https://app.geosector.fr/stripe/refresh"
|
||||
}'
|
||||
```
|
||||
|
||||
**Réponse attendue** :
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"account_id": "acct_xxxxxxxxxxxxx",
|
||||
"location_id": "tml_xxxxxxxxxxxxx",
|
||||
"onboarding_url": "https://connect.stripe.com/setup/...",
|
||||
"charges_enabled": false,
|
||||
"payouts_enabled": false,
|
||||
"existing": false,
|
||||
"message": "Compte Stripe Connect créé avec succès"
|
||||
}
|
||||
```
|
||||
|
||||
### **Test 2 : Amicale avec compte existant**
|
||||
```bash
|
||||
curl -X POST http://localhost/api/stripe/accounts \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-d '{
|
||||
"fk_entite": 456,
|
||||
"return_url": "https://app.geosector.fr/stripe/success",
|
||||
"refresh_url": "https://app.geosector.fr/stripe/refresh"
|
||||
}'
|
||||
```
|
||||
|
||||
**Réponse attendue** :
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"account_id": "acct_xxxxxxxxxxxxx",
|
||||
"location_id": "tml_xxxxxxxxxxxxx",
|
||||
"onboarding_url": null,
|
||||
"charges_enabled": true,
|
||||
"payouts_enabled": true,
|
||||
"existing": true,
|
||||
"message": "Compte Stripe déjà configuré et actif"
|
||||
}
|
||||
```
|
||||
|
||||
### **Test 3 : Vérifier la BDD**
|
||||
```sql
|
||||
SELECT id, name, stripe_id, stripe_location_id, chk_stripe
|
||||
FROM amicales
|
||||
WHERE id = 123;
|
||||
```
|
||||
|
||||
**Résultat attendu** :
|
||||
```
|
||||
+-----+------------------+-------------------+-------------------+------------+
|
||||
| id | name | stripe_id | stripe_location_id| chk_stripe |
|
||||
+-----+------------------+-------------------+-------------------+------------+
|
||||
| 123 | Pompiers Paris15 | acct_xxxxxxxxxxxxx| tml_xxxxxxxxxxxxx | 1 |
|
||||
+-----+------------------+-------------------+-------------------+------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 7. Déploiement
|
||||
|
||||
### **Étapes** :
|
||||
1. ✅ Appliquer la migration SQL
|
||||
2. ✅ Déployer le code Backend modifié
|
||||
3. ✅ Tester avec Postman/curl
|
||||
4. ✅ Déployer le code Flutter modifié
|
||||
5. ✅ Tester le flow complet depuis l'app
|
||||
|
||||
---
|
||||
|
||||
## 📊 Comparaison Avant/Après
|
||||
|
||||
| Aspect | Avant | Après |
|
||||
|--------|-------|-------|
|
||||
| **Appels API Flutter → Backend** | 3 | 1 |
|
||||
| **Appels Backend → Stripe** | 3 | 3 (mais atomiques) |
|
||||
| **Latence totale** | ~3-5s | ~1-2s |
|
||||
| **Gestion erreurs** | Complexe | Simplifié avec transaction |
|
||||
| **Atomicité** | ❌ Non | ✅ Oui (DB transaction) |
|
||||
| **Location ID sauvegardé** | ❌ Non | ✅ Oui |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Bénéfices
|
||||
|
||||
1. ✅ **Performance** : Latence divisée par 2-3
|
||||
2. ✅ **Fiabilité** : Transaction BDD garantit la cohérence
|
||||
3. ✅ **Simplicité** : Code Flutter plus simple
|
||||
4. ✅ **Maintenance** : Moins de code à maintenir
|
||||
5. ✅ **Traçabilité** : Logs centralisés côté Backend
|
||||
6. ✅ **Tap to Pay prêt** : `location_id` disponible immédiatement
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Points d'attention
|
||||
|
||||
1. **Rollback** : Si la transaction échoue, rien n'est sauvegardé (bon comportement)
|
||||
2. **Logs** : Bien logger chaque étape pour le debug
|
||||
3. **Stripe Connect limitations** : Respecter les rate limits Stripe
|
||||
4. **Tests** : Tester avec des comptes Stripe de test d'abord
|
||||
|
||||
---
|
||||
|
||||
## 📚 Ressources
|
||||
|
||||
- [Stripe Connect Express Accounts](https://stripe.com/docs/connect/express-accounts)
|
||||
- [Stripe Terminal Locations](https://stripe.com/docs/terminal/fleet/locations)
|
||||
- [Stripe Account Links](https://stripe.com/docs/connect/account-links)
|
||||
1482
api/docs/TECHBOOK.md
1482
api/docs/TECHBOOK.md
File diff suppressed because it is too large
Load Diff
@@ -1,476 +0,0 @@
|
||||
-- -------------------------------------------------------------
|
||||
-- TablePlus 6.4.8(608)
|
||||
--
|
||||
-- https://tableplus.com/
|
||||
--
|
||||
-- Database: geo_app
|
||||
-- Generation Time: 2025-06-09 18:03:43.5140
|
||||
-- -------------------------------------------------------------
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
|
||||
-- Tables préfixées "chat_"
|
||||
CREATE TABLE chat_rooms (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
title VARCHAR(255),
|
||||
type ENUM('private', 'group', 'broadcast'),
|
||||
created_at TIMESTAMP,
|
||||
created_by INT
|
||||
);
|
||||
|
||||
CREATE TABLE chat_messages (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
room_id VARCHAR(36),
|
||||
content TEXT,
|
||||
sender_id INT,
|
||||
sent_at TIMESTAMP,
|
||||
FOREIGN KEY (room_id) REFERENCES chat_rooms(id)
|
||||
);
|
||||
|
||||
CREATE TABLE chat_participants (
|
||||
room_id VARCHAR(36),
|
||||
user_id INT,
|
||||
role INT,
|
||||
entite_id INT,
|
||||
joined_at TIMESTAMP,
|
||||
PRIMARY KEY (room_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE chat_read_receipts (
|
||||
message_id VARCHAR(36),
|
||||
user_id INT,
|
||||
read_at TIMESTAMP,
|
||||
PRIMARY KEY (message_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE `email_counter` (
|
||||
`id` int(10) unsigned NOT NULL DEFAULT 1,
|
||||
`hour_start` timestamp NULL DEFAULT NULL,
|
||||
`count` int(10) unsigned DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `email_queue` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_pass` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`to_email` varchar(255) DEFAULT NULL,
|
||||
`subject` varchar(255) DEFAULT NULL,
|
||||
`body` text DEFAULT NULL,
|
||||
`headers` text DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||
`status` enum('pending','sent','failed') DEFAULT 'pending',
|
||||
`sent_at` timestamp NULL DEFAULT NULL,
|
||||
`attempts` int(10) unsigned DEFAULT 0,
|
||||
`error_message` text DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `entites` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`encrypted_name` varchar(255) DEFAULT NULL,
|
||||
`adresse1` varchar(45) DEFAULT '',
|
||||
`adresse2` varchar(45) DEFAULT '',
|
||||
`code_postal` varchar(5) DEFAULT '',
|
||||
`ville` varchar(45) DEFAULT '',
|
||||
`fk_region` int(10) unsigned DEFAULT NULL,
|
||||
`fk_type` int(10) unsigned DEFAULT 1,
|
||||
`encrypted_phone` varchar(128) DEFAULT '',
|
||||
`encrypted_mobile` varchar(128) DEFAULT '',
|
||||
`encrypted_email` varchar(255) DEFAULT '',
|
||||
`gps_lat` varchar(20) NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) NOT NULL DEFAULT '',
|
||||
`chk_stripe` tinyint(1) unsigned DEFAULT 0,
|
||||
`encrypted_stripe_id` varchar(255) DEFAULT '',
|
||||
`encrypted_iban` varchar(255) DEFAULT '',
|
||||
`encrypted_bic` varchar(128) DEFAULT '',
|
||||
`chk_demo` tinyint(1) unsigned DEFAULT 1,
|
||||
`chk_mdp_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'Gestion des mots de passe manuelle (1) ou automatique (0)',
|
||||
`chk_username_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'Gestion des usernames manuelle (1) ou automatique (0)',
|
||||
`chk_copie_mail_recu` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`chk_accept_sms` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `entites_ibfk_1` (`fk_region`),
|
||||
KEY `entites_ibfk_2` (`fk_type`),
|
||||
CONSTRAINT `entites_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `entites_ibfk_2` FOREIGN KEY (`fk_type`) REFERENCES `x_entites_types` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1230 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `medias` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`support` varchar(45) NOT NULL DEFAULT '' COMMENT 'Type de support (entite, user, operation, passage)',
|
||||
`support_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'ID de élément associé',
|
||||
`fichier` varchar(250) NOT NULL DEFAULT '' COMMENT 'Nom du fichier stocké',
|
||||
`file_type` varchar(50) DEFAULT NULL COMMENT 'Extension du fichier (pdf, jpg, xlsx, etc.)',
|
||||
`file_category` varchar(50) DEFAULT NULL COMMENT 'export, logo, carte, etc.',
|
||||
`file_size` int(10) unsigned DEFAULT NULL COMMENT 'Taille du fichier en octets',
|
||||
`mime_type` varchar(100) DEFAULT NULL COMMENT 'Type MIME du fichier',
|
||||
`original_name` varchar(255) DEFAULT NULL COMMENT 'Nom original du fichier uploadé',
|
||||
`fk_entite` int(10) unsigned DEFAULT NULL COMMENT 'ID de entité propriétaire',
|
||||
`fk_operation` int(10) unsigned DEFAULT NULL COMMENT 'ID de opération (pour passages)',
|
||||
`file_path` varchar(500) DEFAULT NULL COMMENT 'Chemin complet du fichier',
|
||||
`original_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur originale de image',
|
||||
`original_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur originale de image',
|
||||
`processed_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur après traitement',
|
||||
`processed_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur après traitement',
|
||||
`is_processed` tinyint(1) unsigned DEFAULT 0 COMMENT 'Image redimensionnée (1) ou originale (0)',
|
||||
`description` varchar(100) NOT NULL DEFAULT '' COMMENT 'Description du fichier',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
|
||||
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `idx_entite` (`fk_entite`),
|
||||
KEY `idx_operation` (`fk_operation`),
|
||||
KEY `idx_support_type` (`support`, `support_id`),
|
||||
KEY `idx_file_type` (`file_type`),
|
||||
KEY `idx_file_category` (`file_category`),
|
||||
CONSTRAINT `fk_medias_entite` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_medias_operation` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `ope_pass` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_sector` int(10) unsigned DEFAULT 0,
|
||||
`fk_user` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_adresse` varchar(25) DEFAULT '' COMMENT 'adresses.cp??.id',
|
||||
`passed_at` timestamp NULL DEFAULT NULL COMMENT 'Date du passage',
|
||||
`fk_type` int(10) unsigned DEFAULT 0,
|
||||
`numero` varchar(10) NOT NULL DEFAULT '',
|
||||
`rue` varchar(75) NOT NULL DEFAULT '',
|
||||
`rue_bis` varchar(1) NOT NULL DEFAULT '',
|
||||
`ville` varchar(75) NOT NULL DEFAULT '',
|
||||
`fk_habitat` int(10) unsigned DEFAULT 1,
|
||||
`appt` varchar(5) DEFAULT '',
|
||||
`niveau` varchar(5) DEFAULT '',
|
||||
`residence` varchar(75) DEFAULT '',
|
||||
`gps_lat` varchar(20) NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) NOT NULL DEFAULT '',
|
||||
`encrypted_name` varchar(255) NOT NULL DEFAULT '',
|
||||
`montant` decimal(7,2) NOT NULL DEFAULT 0.00,
|
||||
`fk_type_reglement` int(10) unsigned DEFAULT 1,
|
||||
`remarque` text DEFAULT '',
|
||||
`encrypted_email` varchar(255) DEFAULT '',
|
||||
`nom_recu` varchar(50) DEFAULT NULL,
|
||||
`date_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de réception',
|
||||
`date_creat_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de création du reçu',
|
||||
`date_sent_recu` timestamp NULL DEFAULT NULL COMMENT 'Date envoi du reçu',
|
||||
`email_erreur` varchar(30) DEFAULT '',
|
||||
`chk_email_sent` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`encrypted_phone` varchar(128) NOT NULL DEFAULT '',
|
||||
`is_striped` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`docremis` tinyint(1) unsigned DEFAULT 0,
|
||||
`date_repasser` timestamp NULL DEFAULT NULL COMMENT 'Date prévue pour repasser',
|
||||
`nb_passages` int(11) DEFAULT 1 COMMENT 'Nb passages pour les a repasser',
|
||||
`chk_gps_maj` tinyint(1) unsigned DEFAULT 0,
|
||||
`chk_map_create` tinyint(1) unsigned DEFAULT 0,
|
||||
`chk_mobile` tinyint(1) unsigned DEFAULT 0,
|
||||
`chk_synchro` tinyint(1) unsigned DEFAULT 1 COMMENT 'chk synchro entre web et appli',
|
||||
`chk_api_adresse` tinyint(1) unsigned DEFAULT 0,
|
||||
`chk_maj_adresse` tinyint(1) unsigned DEFAULT 0,
|
||||
`anomalie` tinyint(1) unsigned DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_operation` (`fk_operation`),
|
||||
KEY `fk_sector` (`fk_sector`),
|
||||
KEY `fk_user` (`fk_user`),
|
||||
KEY `fk_type` (`fk_type`),
|
||||
KEY `fk_type_reglement` (`fk_type_reglement`),
|
||||
KEY `email` (`encrypted_email`),
|
||||
CONSTRAINT `ope_pass_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_pass_ibfk_2` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_pass_ibfk_3` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_pass_ibfk_4` FOREIGN KEY (`fk_type_reglement`) REFERENCES `x_types_reglements` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=19499566 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `ope_pass_histo` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_pass` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`date_histo` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date historique',
|
||||
`sujet` varchar(50) DEFAULT NULL,
|
||||
`remarque` varchar(250) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `ope_pass_histo_fk_pass_IDX` (`fk_pass`) USING BTREE,
|
||||
KEY `ope_pass_histo_date_histo_IDX` (`date_histo`) USING BTREE,
|
||||
CONSTRAINT `ope_pass_histo_ibfk_1` FOREIGN KEY (`fk_pass`) REFERENCES `ope_pass` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=6752 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `ope_sectors` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_old_sector` int(10) unsigned DEFAULT NULL,
|
||||
`libelle` varchar(75) NOT NULL DEFAULT '',
|
||||
`sector` text NOT NULL DEFAULT '',
|
||||
`color` varchar(7) NOT NULL DEFAULT '#4B77BE',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id` (`id`),
|
||||
KEY `fk_operation` (`fk_operation`),
|
||||
CONSTRAINT `ope_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=27675 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `ope_users` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_user` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `ope_users_ibfk_1` (`fk_operation`),
|
||||
KEY `ope_users_ibfk_2` (`fk_user`),
|
||||
CONSTRAINT `ope_users_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_users_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=199006 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `ope_users_sectors` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_user` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_sector` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id` (`id`),
|
||||
KEY `fk_operation` (`fk_operation`),
|
||||
KEY `fk_user` (`fk_user`),
|
||||
KEY `fk_sector` (`fk_sector`),
|
||||
CONSTRAINT `ope_users_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_users_sectors_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_users_sectors_ibfk_3` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=48082 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `operations` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_entite` int(10) unsigned NOT NULL DEFAULT 1,
|
||||
`libelle` varchar(75) NOT NULL DEFAULT '',
|
||||
`date_deb` date NOT NULL DEFAULT '0000-00-00',
|
||||
`date_fin` date NOT NULL DEFAULT '0000-00-00',
|
||||
`chk_distinct_sectors` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_entite` (`fk_entite`),
|
||||
KEY `date_deb` (`date_deb`),
|
||||
CONSTRAINT `operations_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3121 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `params` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(35) NOT NULL DEFAULT '',
|
||||
`valeur` varchar(255) NOT NULL DEFAULT '',
|
||||
`aide` varchar(150) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `sectors_adresses` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_adresse` varchar(25) DEFAULT NULL COMMENT 'adresses.cp??.id',
|
||||
`osm_id` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_sector` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`osm_name` varchar(50) NOT NULL DEFAULT '',
|
||||
`numero` varchar(5) NOT NULL DEFAULT '',
|
||||
`rue_bis` varchar(5) NOT NULL DEFAULT '',
|
||||
`rue` varchar(60) NOT NULL DEFAULT '',
|
||||
`cp` varchar(5) NOT NULL DEFAULT '',
|
||||
`ville` varchar(60) NOT NULL DEFAULT '',
|
||||
`gps_lat` varchar(20) NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) NOT NULL DEFAULT '',
|
||||
`osm_date_creat` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `sectors_adresses_fk_sector_index` (`fk_sector`),
|
||||
KEY `sectors_adresses_numero_index` (`numero`),
|
||||
KEY `sectors_adresses_rue_index` (`rue`),
|
||||
KEY `sectors_adresses_ville_index` (`ville`),
|
||||
CONSTRAINT `sectors_adresses_ibfk_1` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1562946 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `users` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_entite` int(10) unsigned DEFAULT 1,
|
||||
`fk_role` int(10) unsigned DEFAULT 1,
|
||||
`fk_titre` int(10) unsigned DEFAULT 1,
|
||||
`encrypted_name` varchar(255) DEFAULT NULL,
|
||||
`first_name` varchar(45) DEFAULT NULL,
|
||||
`sect_name` varchar(60) DEFAULT '',
|
||||
`encrypted_user_name` varchar(128) DEFAULT '',
|
||||
`user_pass_hash` varchar(60) DEFAULT NULL,
|
||||
`encrypted_phone` varchar(128) DEFAULT NULL,
|
||||
`encrypted_mobile` varchar(128) DEFAULT NULL,
|
||||
`encrypted_email` varchar(255) DEFAULT '',
|
||||
`chk_alert_email` tinyint(1) unsigned DEFAULT 1,
|
||||
`chk_suivi` tinyint(1) unsigned DEFAULT 0,
|
||||
`date_naissance` date DEFAULT NULL,
|
||||
`date_embauche` date DEFAULT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_entite` (`fk_entite`),
|
||||
KEY `username` (`encrypted_user_name`),
|
||||
KEY `users_ibfk_2` (`fk_role`),
|
||||
KEY `users_ibfk_3` (`fk_titre`),
|
||||
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `users_ibfk_2` FOREIGN KEY (`fk_role`) REFERENCES `x_users_roles` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `users_ibfk_3` FOREIGN KEY (`fk_titre`) REFERENCES `x_users_titres` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10027748 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_departements` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(3) DEFAULT NULL,
|
||||
`fk_region` int(10) unsigned DEFAULT 1,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `x_departements_ibfk_1` (`fk_region`),
|
||||
CONSTRAINT `x_departements_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_devises` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(3) DEFAULT NULL,
|
||||
`symbole` varchar(6) DEFAULT NULL,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_entites_types` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_pays` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(3) DEFAULT NULL,
|
||||
`fk_continent` int(10) unsigned DEFAULT NULL,
|
||||
`fk_devise` int(10) unsigned DEFAULT 1,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `x_pays_ibfk_1` (`fk_devise`),
|
||||
CONSTRAINT `x_pays_ibfk_1` FOREIGN KEY (`fk_devise`) REFERENCES `x_devises` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Table des pays avec leurs codes' `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_regions` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_pays` int(10) unsigned DEFAULT 1,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`libelle_long` varchar(45) DEFAULT NULL,
|
||||
`table_osm` varchar(45) DEFAULT NULL,
|
||||
`departements` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `x_regions_ibfk_1` (`fk_pays`),
|
||||
CONSTRAINT `x_regions_ibfk_1` FOREIGN KEY (`fk_pays`) REFERENCES `x_pays` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_types_passages` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(10) DEFAULT NULL,
|
||||
`color_button` varchar(15) DEFAULT NULL,
|
||||
`color_mark` varchar(15) DEFAULT NULL,
|
||||
`color_table` varchar(15) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_types_reglements` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_users_roles` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents rôles des utilisateurs' `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_users_titres` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents titres des utilisateurs' `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_villes` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_departement` int(10) unsigned DEFAULT 1,
|
||||
`libelle` varchar(65) DEFAULT NULL,
|
||||
`code_postal` varchar(5) DEFAULT NULL,
|
||||
`code_insee` varchar(5) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `x_villes_ibfk_1` (`fk_departement`),
|
||||
CONSTRAINT `x_villes_ibfk_1` FOREIGN KEY (`fk_departement`) REFERENCES `x_departements` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=38950 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `z_sessions` (
|
||||
`sid` text NOT NULL,
|
||||
`fk_user` int(11) NOT NULL,
|
||||
`role` varchar(10) DEFAULT NULL,
|
||||
`date_modified` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`ip` varchar(50) NOT NULL,
|
||||
`browser` varchar(150) NOT NULL,
|
||||
`data` mediumtext DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `chat_conversations_unread` AS select `r`.`id` AS `id`,`r`.`type` AS `type`,`r`.`title` AS `title`,`r`.`date_creation` AS `date_creation`,`r`.`fk_user` AS `fk_user`,`r`.`fk_entite` AS `fk_entite`,`r`.`statut` AS `statut`,`r`.`description` AS `description`,`r`.`reply_permission` AS `reply_permission`,`r`.`is_pinned` AS `is_pinned`,`r`.`expiry_date` AS `expiry_date`,`r`.`updated_at` AS `updated_at`,count(distinct `m`.`id`) AS `total_messages`,count(distinct `rm`.`id`) AS `read_messages`,count(distinct `m`.`id`) - count(distinct `rm`.`id`) AS `unread_messages`,(select `geo_app`.`chat_messages`.`date_sent` from `chat_messages` where `geo_app`.`chat_messages`.`fk_room` = `r`.`id` order by `geo_app`.`chat_messages`.`date_sent` desc limit 1) AS `last_message_date` from ((`chat_rooms` `r` left join `chat_messages` `m` on(`r`.`id` = `m`.`fk_room`)) left join `chat_read_messages` `rm` on(`m`.`id` = `rm`.`fk_message`)) group by `r`.`id`;
|
||||
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
@@ -1,621 +0,0 @@
|
||||
-- Création de la base de données geo_app si elle n'existe pas
|
||||
DROP DATABASE IF EXISTS `geo_app`;
|
||||
CREATE DATABASE IF NOT EXISTS `geo_app` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- Création de l'utilisateur et attribution des droits
|
||||
CREATE USER IF NOT EXISTS 'geo_app_user'@'localhost' IDENTIFIED BY 'QO:96df*?k{4W6m';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON `geo_app`.* TO 'geo_app_user'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
USE geo_app;
|
||||
|
||||
--
|
||||
-- Table structure for table `email_counter`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `email_counter`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `email_counter` (
|
||||
`id` int unsigned NOT NULL DEFAULT '1',
|
||||
`hour_start` timestamp NULL DEFAULT NULL,
|
||||
`count` int unsigned DEFAULT '0',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `x_devises`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_devises` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(3) DEFAULT NULL,
|
||||
`symbole` varchar(6) DEFAULT NULL,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_entites_types`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_entites_types`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_entites_types` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_types_passages`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_types_passages`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_types_passages` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`color_button` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`color_mark` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`color_table` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT '1',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_types_reglements`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_types_reglements`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_types_reglements` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_users_roles`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_users_roles`;
|
||||
|
||||
CREATE TABLE `x_users_roles` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents rôles des utilisateurs';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `x_users_titres`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_users_titres` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents titres des utilisateurs';
|
||||
|
||||
DROP TABLE IF EXISTS `x_pays`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_pays` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(3) DEFAULT NULL,
|
||||
`fk_continent` int unsigned DEFAULT NULL,
|
||||
`fk_devise` int unsigned DEFAULT '1',
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
CONSTRAINT `x_pays_ibfk_1` FOREIGN KEY (`fk_devise`) REFERENCES `x_devises` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Table des pays avec leurs codes';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `x_regions`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_regions` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_pays` int unsigned DEFAULT '1',
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`libelle_long` varchar(45) DEFAULT NULL,
|
||||
`table_osm` varchar(45) DEFAULT NULL,
|
||||
`departements` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
CONSTRAINT `x_regions_ibfk_1` FOREIGN KEY (`fk_pays`) REFERENCES `x_pays` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `x_departements`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_departements` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(3) DEFAULT NULL,
|
||||
`fk_region` int unsigned DEFAULT '1',
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
CONSTRAINT `x_departements_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `entites`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `entites` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`encrypted_name` varchar(255) DEFAULT NULL,
|
||||
`adresse1` varchar(45) DEFAULT '',
|
||||
`adresse2` varchar(45) DEFAULT '',
|
||||
`code_postal` varchar(5) DEFAULT '',
|
||||
`ville` varchar(45) DEFAULT '',
|
||||
`fk_region` int unsigned DEFAULT NULL,
|
||||
`fk_type` int unsigned DEFAULT '1',
|
||||
`encrypted_phone` varchar(128) DEFAULT '',
|
||||
`encrypted_mobile` varchar(128) DEFAULT '',
|
||||
`encrypted_email` varchar(255) DEFAULT '',
|
||||
`gps_lat` varchar(20) NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) NOT NULL DEFAULT '',
|
||||
`encrypted_stripe_id` varchar(255) DEFAULT '',
|
||||
`encrypted_iban` varchar(255) DEFAULT '',
|
||||
`encrypted_bic` varchar(128) DEFAULT '',
|
||||
`chk_demo` tinyint(1) unsigned DEFAULT '1',
|
||||
`chk_mdp_manuel` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT 'Gestion des mots de passe manuelle O/N',
|
||||
`chk_copie_mail_recu` tinyint(1) unsigned NOT NULL DEFAULT '0',
|
||||
`chk_accept_sms` tinyint(1) unsigned NOT NULL DEFAULT '0',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`fk_user_creat` int unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
CONSTRAINT `entites_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `entites_ibfk_2` FOREIGN KEY (`fk_type`) REFERENCES `x_entites_types` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `x_villes`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_villes` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_departement` int unsigned DEFAULT '1',
|
||||
`libelle` varchar(65) DEFAULT NULL,
|
||||
`cp` varchar(5) DEFAULT NULL,
|
||||
`code_insee` varchar(5) DEFAULT NULL,
|
||||
`departement` varchar(65) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
CONSTRAINT `x_villes_ibfk_1` FOREIGN KEY (`fk_departement`) REFERENCES `x_departements` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `users` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_entite` int unsigned DEFAULT '1',
|
||||
`fk_role` int unsigned DEFAULT '1',
|
||||
`fk_titre` int unsigned DEFAULT '1',
|
||||
`encrypted_name` varchar(255) DEFAULT NULL,
|
||||
`first_name` varchar(45) DEFAULT NULL,
|
||||
`sect_name` varchar(60) DEFAULT '',
|
||||
`encrypted_user_name` varchar(128) DEFAULT '',
|
||||
`user_pass_hash` varchar(60) DEFAULT NULL,
|
||||
`encrypted_phone` varchar(128) DEFAULT NULL,
|
||||
`encrypted_mobile` varchar(128) DEFAULT NULL,
|
||||
`encrypted_email` varchar(255) DEFAULT '',
|
||||
`chk_alert_email` tinyint(1) unsigned DEFAULT '1',
|
||||
`chk_suivi` tinyint(1) unsigned DEFAULT '0',
|
||||
`date_naissance` date DEFAULT NULL,
|
||||
`date_embauche` date DEFAULT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`fk_user_creat` int unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_entite` (`fk_entite`),
|
||||
KEY `username` (`encrypted_user_name`),
|
||||
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `users_ibfk_2` FOREIGN KEY (`fk_role`) REFERENCES `x_users_roles` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `users_ibfk_3` FOREIGN KEY (`fk_titre`) REFERENCES `x_users_titres` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `operations`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `operations` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_entite` int unsigned NOT NULL DEFAULT '1',
|
||||
`libelle` varchar(75) NOT NULL DEFAULT '',
|
||||
`date_deb` date NOT NULL DEFAULT '0000-00-00',
|
||||
`date_fin` date NOT NULL DEFAULT '0000-00-00',
|
||||
`chk_distinct_sectors` tinyint(1) unsigned NOT NULL DEFAULT '0',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`fk_user_creat` int unsigned NOT NULL DEFAULT '0',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int unsigned NOT NULL DEFAULT '0',
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_entite` (`fk_entite`),
|
||||
KEY `date_deb` (`date_deb`),
|
||||
CONSTRAINT `operations_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `ope_sectors`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ope_sectors` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_old_sector` int unsigned NOT NULL DEFAULT '0',
|
||||
`libelle` varchar(75) NOT NULL DEFAULT '',
|
||||
`sector` text NOT NULL DEFAULT '',
|
||||
`color` varchar(7) NOT NULL DEFAULT '#4B77BE',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`fk_user_creat` int unsigned NOT NULL DEFAULT '0',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int unsigned NOT NULL DEFAULT '0',
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id` (`id`),
|
||||
KEY `fk_operation` (`fk_operation`),
|
||||
CONSTRAINT `ope_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `ope_users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ope_users` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_user` int unsigned NOT NULL DEFAULT '0',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`fk_user_creat` int unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
CONSTRAINT `ope_users_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_users_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `email_queue`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `email_queue` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_pass` int unsigned NOT NULL DEFAULT '0',
|
||||
`to_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`subject` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`body` text COLLATE utf8mb4_unicode_ci,
|
||||
`headers` text COLLATE utf8mb4_unicode_ci,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`status` enum('pending','sent','failed') COLLATE utf8mb4_unicode_ci DEFAULT 'pending',
|
||||
`sent_at` timestamp NULL DEFAULT NULL,
|
||||
`attempts` int unsigned DEFAULT '0',
|
||||
`error_message` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `ope_users_sectors`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ope_users_sectors` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_user` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_sector` int unsigned NOT NULL DEFAULT '0',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`fk_user_creat` int unsigned NOT NULL DEFAULT '0',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id` (`id`),
|
||||
KEY `fk_operation` (`fk_operation`),
|
||||
KEY `fk_user` (`fk_user`),
|
||||
KEY `fk_sector` (`fk_sector`),
|
||||
CONSTRAINT `ope_users_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_users_sectors_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_users_sectors_ibfk_3` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `ope_users_suivis`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ope_users_suivis` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_user` int unsigned NOT NULL DEFAULT '0',
|
||||
`date_suivi` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date du suivi',
|
||||
`gps_lat` varchar(20) NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) NOT NULL DEFAULT '',
|
||||
`vitesse` varchar(20) NOT NULL DEFAULT '',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`fk_user_creat` int unsigned NOT NULL DEFAULT '0',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int unsigned NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `sectors_adresses`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `sectors_adresses` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_adresse` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'adresses.cp??.id',
|
||||
`osm_id` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_sector` int unsigned NOT NULL DEFAULT '0',
|
||||
`osm_name` varchar(50) NOT NULL DEFAULT '',
|
||||
`numero` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`rue_bis` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`rue` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`cp` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`ville` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`gps_lat` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`osm_date_creat` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `sectors_adresses_fk_sector_index` (`fk_sector`),
|
||||
KEY `sectors_adresses_numero_index` (`numero`),
|
||||
KEY `sectors_adresses_rue_index` (`rue`),
|
||||
KEY `sectors_adresses_ville_index` (`ville`),
|
||||
CONSTRAINT `sectors_adresses_ibfk_1` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `ope_pass`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ope_pass` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_sector` int unsigned DEFAULT '0',
|
||||
`fk_user` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_adresse` varchar(25) DEFAULT '' COMMENT 'adresses.cp??.id',
|
||||
`passed_at` timestamp NULL DEFAULT NULL COMMENT 'Date du passage',
|
||||
`fk_type` int unsigned DEFAULT '0',
|
||||
`numero` varchar(10) NOT NULL DEFAULT '',
|
||||
`rue` varchar(75) NOT NULL DEFAULT '',
|
||||
`rue_bis` varchar(1) NOT NULL DEFAULT '',
|
||||
`ville` varchar(75) NOT NULL DEFAULT '',
|
||||
`fk_habitat` int unsigned DEFAULT '1',
|
||||
`appt` varchar(5) DEFAULT '',
|
||||
`niveau` varchar(5) DEFAULT '',
|
||||
`residence` varchar(75) DEFAULT '',
|
||||
`gps_lat` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`encrypted_name` varchar(255) NOT NULL DEFAULT '',
|
||||
`montant` decimal(7,2) NOT NULL DEFAULT '0.00',
|
||||
`fk_type_reglement` int unsigned DEFAULT '1',
|
||||
`remarque` text DEFAULT '',
|
||||
`encrypted_email` varchar(255) DEFAULT '',
|
||||
`nom_recu` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`date_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de réception',
|
||||
`date_creat_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de création du reçu',
|
||||
`date_sent_recu` timestamp NULL DEFAULT NULL COMMENT 'Date envoi du reçu',
|
||||
`email_erreur` varchar(30) DEFAULT '',
|
||||
`chk_email_sent` tinyint(1) unsigned NOT NULL DEFAULT '0',
|
||||
`encrypted_phone` varchar(128) NOT NULL DEFAULT '',
|
||||
`chk_striped` tinyint(1) unsigned DEFAULT '0',
|
||||
`docremis` tinyint(1) unsigned DEFAULT '0',
|
||||
`date_repasser` timestamp NULL DEFAULT NULL COMMENT 'Date prévue pour repasser',
|
||||
`nb_passages` int DEFAULT '1' COMMENT 'Nb passages pour les a repasser',
|
||||
`chk_gps_maj` tinyint(1) unsigned DEFAULT '0',
|
||||
`chk_map_create` tinyint(1) unsigned DEFAULT '0',
|
||||
`chk_mobile` tinyint(1) unsigned DEFAULT '0',
|
||||
`chk_synchro` tinyint(1) unsigned DEFAULT '1' COMMENT 'chk synchro entre web et appli',
|
||||
`chk_api_adresse` tinyint(1) unsigned DEFAULT '0',
|
||||
`chk_maj_adresse` tinyint(1) unsigned DEFAULT '0',
|
||||
`anomalie` tinyint(1) unsigned DEFAULT '0',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`fk_user_creat` int unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_operation` (`fk_operation`),
|
||||
KEY `fk_sector` (`fk_sector`),
|
||||
KEY `fk_user` (`fk_user`),
|
||||
KEY `fk_type` (`fk_type`),
|
||||
KEY `fk_type_reglement` (`fk_type_reglement`),
|
||||
KEY `email` (`email`),
|
||||
CONSTRAINT `ope_pass_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_pass_ibfk_2` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_pass_ibfk_3` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_pass_ibfk_4` FOREIGN KEY (`fk_type_reglement`) REFERENCES `x_types_reglements` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `ope_pass_histo`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ope_pass_histo` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_pass` int unsigned NOT NULL DEFAULT '0',
|
||||
`date_histo` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date historique',
|
||||
`sujet` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`remarque` varchar(250) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `ope_pass_histo_fk_pass_IDX` (`fk_pass`) USING BTREE,
|
||||
KEY `ope_pass_histo_date_histo_IDX` (`date_histo`) USING BTREE,
|
||||
CONSTRAINT `ope_pass_histo_ibfk_1` FOREIGN KEY (`fk_pass`) REFERENCES `ope_pass` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `medias`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `medias` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`support` varchar(45) NOT NULL DEFAULT '',
|
||||
`support_id` int unsigned NOT NULL DEFAULT '0',
|
||||
`fichier` varchar(250) NOT NULL DEFAULT '',
|
||||
`description` varchar(100) NOT NULL DEFAULT '',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`fk_user_creat` int unsigned NOT NULL DEFAULT '0',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`fk_user_modif` int unsigned NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
-- Création des tables pour le système de chat
|
||||
DROP TABLE IF EXISTS `chat_rooms`;
|
||||
-- Table des salles de discussion
|
||||
CREATE TABLE chat_rooms (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type ENUM('privee', 'groupe', 'liste_diffusion') NOT NULL,
|
||||
date_creation timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
fk_user INT UNSIGNED NOT NULL,
|
||||
fk_entite INT UNSIGNED,
|
||||
statut ENUM('active', 'archive') NOT NULL DEFAULT 'active',
|
||||
description TEXT,
|
||||
INDEX idx_user (fk_user),
|
||||
INDEX idx_entite (fk_entite)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `chat_participants`;
|
||||
-- Table des participants aux salles de discussion
|
||||
CREATE TABLE chat_participants (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
id_room INT UNSIGNED NOT NULL,
|
||||
id_user INT UNSIGNED NOT NULL,
|
||||
role ENUM('administrateur', 'participant', 'en_lecture_seule') NOT NULL DEFAULT 'participant',
|
||||
date_ajout timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date ajout',
|
||||
notification_activee BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
INDEX idx_room (id_room),
|
||||
INDEX idx_user (id_user),
|
||||
CONSTRAINT uc_room_user UNIQUE (id_room, id_user),
|
||||
FOREIGN KEY (id_room) REFERENCES chat_rooms(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `chat_messages`;
|
||||
-- Table des messages
|
||||
CREATE TABLE chat_messages (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_room INT UNSIGNED NOT NULL,
|
||||
fk_user INT UNSIGNED NOT NULL,
|
||||
content TEXT,
|
||||
date_sent timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date envoi',
|
||||
type ENUM('texte', 'media', 'systeme') NOT NULL DEFAULT 'texte',
|
||||
statut ENUM('envoye', 'livre', 'lu') NOT NULL DEFAULT 'envoye',
|
||||
INDEX idx_room (fk_room),
|
||||
INDEX idx_user (fk_user),
|
||||
INDEX idx_date (date_sent),
|
||||
FOREIGN KEY (fk_room) REFERENCES chat_rooms(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `chat_listes_diffusion`;
|
||||
-- Table des listes de diffusion
|
||||
CREATE TABLE chat_listes_diffusion (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_room INT UNSIGNED NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
fk_user INT UNSIGNED NOT NULL,
|
||||
date_creation timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
INDEX idx_room (fk_room),
|
||||
INDEX idx_user (fk_user),
|
||||
FOREIGN KEY (fk_room) REFERENCES chat_rooms(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `chat_read_messages`;
|
||||
-- Table pour suivre la lecture des messages
|
||||
CREATE TABLE chat_read_messages (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_message INT UNSIGNED NOT NULL,
|
||||
fk_user INT UNSIGNED NOT NULL,
|
||||
date_read timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de lecture',
|
||||
INDEX idx_message (fk_message),
|
||||
INDEX idx_user (fk_user),
|
||||
CONSTRAINT uc_message_user UNIQUE (fk_message, fk_user),
|
||||
FOREIGN KEY (fk_message) REFERENCES chat_messages(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `chat_notifications`;
|
||||
-- Table des notifications
|
||||
CREATE TABLE chat_notifications (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_user INT UNSIGNED NOT NULL,
|
||||
fk_message INT UNSIGNED,
|
||||
fk_room INT UNSIGNED,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
contenu TEXT,
|
||||
date_creation timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
date_lecture timestamp NULL DEFAULT NULL COMMENT 'Date de lecture',
|
||||
statut ENUM('non_lue', 'lue') NOT NULL DEFAULT 'non_lue',
|
||||
INDEX idx_user (fk_user),
|
||||
INDEX idx_message (fk_message),
|
||||
INDEX idx_room (fk_room),
|
||||
FOREIGN KEY (fk_message) REFERENCES chat_messages(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (fk_room) REFERENCES chat_rooms(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `z_params`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `params` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(35) NOT NULL DEFAULT '',
|
||||
`valeur` varchar(255) NOT NULL DEFAULT '',
|
||||
`aide` varchar(150) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `z_sessions`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `z_sessions` (
|
||||
`sid` text NOT NULL,
|
||||
`fk_user` int NOT NULL,
|
||||
`role` varchar(10) DEFAULT NULL,
|
||||
`date_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`ip` varchar(50) NOT NULL,
|
||||
`browser` varchar(150) NOT NULL,
|
||||
`data` mediumtext
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
BIN
api/docs/recu_13718.pdf
Normal file
BIN
api/docs/recu_13718.pdf
Normal file
Binary file not shown.
193
api/docs/traite_batiments.sql
Normal file
193
api/docs/traite_batiments.sql
Normal file
@@ -0,0 +1,193 @@
|
||||
USE batiments;
|
||||
|
||||
-- Table temp pour FFO (nb_niveau, nb_log)
|
||||
DROP TABLE IF EXISTS tmp_ffo_999;
|
||||
CREATE TABLE tmp_ffo_999 (
|
||||
batiment_groupe_id VARCHAR(50),
|
||||
code_departement_insee VARCHAR(5),
|
||||
nb_niveau INT,
|
||||
annee_construction INT,
|
||||
usage_niveau_1_txt VARCHAR(100),
|
||||
mat_mur_txt VARCHAR(100),
|
||||
mat_toit_txt VARCHAR(100),
|
||||
nb_log INT,
|
||||
KEY (batiment_groupe_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOAD DATA LOCAL INFILE '/var/osm/csv/batiment_groupe_ffo_bat.csv'
|
||||
INTO TABLE tmp_ffo_999
|
||||
CHARACTER SET 'UTF8mb4'
|
||||
FIELDS TERMINATED BY ','
|
||||
OPTIONALLY ENCLOSED BY '"'
|
||||
IGNORE 1 LINES;
|
||||
|
||||
-- Table temp pour Adresse (lien BAN)
|
||||
DROP TABLE IF EXISTS tmp_adr_999;
|
||||
CREATE TABLE tmp_adr_999 (
|
||||
wkt TEXT,
|
||||
batiment_groupe_id VARCHAR(50),
|
||||
cle_interop_adr VARCHAR(50),
|
||||
code_departement_insee VARCHAR(5),
|
||||
classe VARCHAR(50),
|
||||
lien_valide TINYINT,
|
||||
origine VARCHAR(50),
|
||||
KEY (batiment_groupe_id),
|
||||
KEY (cle_interop_adr)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOAD DATA LOCAL INFILE '/var/osm/csv/rel_batiment_groupe_adresse.csv'
|
||||
INTO TABLE tmp_adr_999
|
||||
CHARACTER SET 'UTF8mb4'
|
||||
FIELDS TERMINATED BY ','
|
||||
OPTIONALLY ENCLOSED BY '"'
|
||||
IGNORE 1 LINES;
|
||||
|
||||
-- Table temp pour RNC (copropriétés)
|
||||
DROP TABLE IF EXISTS tmp_rnc_999;
|
||||
CREATE TABLE tmp_rnc_999 (
|
||||
batiment_groupe_id VARCHAR(50),
|
||||
code_departement_insee VARCHAR(5),
|
||||
numero_immat_principal VARCHAR(50),
|
||||
periode_construction_max VARCHAR(50),
|
||||
l_annee_construction VARCHAR(100),
|
||||
nb_lot_garpark INT,
|
||||
nb_lot_tot INT,
|
||||
nb_log INT,
|
||||
nb_lot_tertiaire INT,
|
||||
l_nom_copro VARCHAR(200),
|
||||
l_siret VARCHAR(50),
|
||||
copro_dans_pvd TINYINT,
|
||||
KEY (batiment_groupe_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOAD DATA LOCAL INFILE '/var/osm/csv/batiment_groupe_rnc.csv'
|
||||
INTO TABLE tmp_rnc_999
|
||||
CHARACTER SET 'UTF8mb4'
|
||||
FIELDS TERMINATED BY ','
|
||||
OPTIONALLY ENCLOSED BY '"'
|
||||
IGNORE 1 LINES;
|
||||
|
||||
-- Table temp pour BDTOPO (altitude)
|
||||
DROP TABLE IF EXISTS tmp_topo_999;
|
||||
CREATE TABLE tmp_topo_999 (
|
||||
batiment_groupe_id VARCHAR(50),
|
||||
code_departement_insee VARCHAR(5),
|
||||
l_nature VARCHAR(200),
|
||||
l_usage_1 VARCHAR(200),
|
||||
l_usage_2 VARCHAR(200),
|
||||
l_etat VARCHAR(100),
|
||||
hauteur_mean DECIMAL(10,2),
|
||||
max_hauteur DECIMAL(10,2),
|
||||
altitude_sol_mean DECIMAL(10,2),
|
||||
KEY (batiment_groupe_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOAD DATA LOCAL INFILE '/var/osm/csv/batiment_groupe_bdtopo_bat.csv'
|
||||
INTO TABLE tmp_topo_999
|
||||
CHARACTER SET 'UTF8mb4'
|
||||
FIELDS TERMINATED BY ','
|
||||
OPTIONALLY ENCLOSED BY '"'
|
||||
IGNORE 1 LINES;
|
||||
|
||||
-- Table temp pour Usage principal
|
||||
DROP TABLE IF EXISTS tmp_usage_999;
|
||||
CREATE TABLE tmp_usage_999 (
|
||||
batiment_groupe_id VARCHAR(50),
|
||||
code_departement_insee VARCHAR(5),
|
||||
usage_principal_bdnb_open VARCHAR(100),
|
||||
KEY (batiment_groupe_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOAD DATA LOCAL INFILE '/var/osm/csv/batiment_groupe_synthese_propriete_usage.csv'
|
||||
INTO TABLE tmp_usage_999
|
||||
CHARACTER SET 'UTF8mb4'
|
||||
FIELDS TERMINATED BY ','
|
||||
OPTIONALLY ENCLOSED BY '"'
|
||||
IGNORE 1 LINES;
|
||||
|
||||
-- Table temp pour DLE Enedis (compteurs électriques)
|
||||
DROP TABLE IF EXISTS tmp_dle_999;
|
||||
CREATE TABLE tmp_dle_999 (
|
||||
batiment_groupe_id VARCHAR(50),
|
||||
code_departement_insee VARCHAR(5),
|
||||
millesime VARCHAR(10),
|
||||
nb_pdl_res INT,
|
||||
nb_pdl_pro INT,
|
||||
nb_pdl_tot INT,
|
||||
conso_res DECIMAL(12,2),
|
||||
conso_pro DECIMAL(12,2),
|
||||
conso_tot DECIMAL(12,2),
|
||||
conso_res_par_pdl DECIMAL(12,2),
|
||||
conso_pro_par_pdl DECIMAL(12,2),
|
||||
conso_tot_par_pdl DECIMAL(12,2),
|
||||
KEY (batiment_groupe_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOAD DATA LOCAL INFILE '/var/osm/csv/batiment_groupe_dle_elec_multimillesime.csv'
|
||||
INTO TABLE tmp_dle_999
|
||||
CHARACTER SET 'UTF8mb4'
|
||||
FIELDS TERMINATED BY ','
|
||||
OPTIONALLY ENCLOSED BY '"'
|
||||
IGNORE 1 LINES;
|
||||
|
||||
-- Création de la table finale avec jointure et filtre
|
||||
DROP TABLE IF EXISTS bat999;
|
||||
CREATE TABLE bat999 (
|
||||
batiment_groupe_id VARCHAR(50) PRIMARY KEY,
|
||||
code_departement_insee VARCHAR(5),
|
||||
cle_interop_adr VARCHAR(50),
|
||||
nb_niveau INT,
|
||||
nb_log INT,
|
||||
nb_pdl_tot INT,
|
||||
annee_construction INT,
|
||||
residence VARCHAR(200),
|
||||
usage_principal VARCHAR(100),
|
||||
altitude_sol_mean DECIMAL(10,2),
|
||||
gps_lat DECIMAL(10,7),
|
||||
gps_lng DECIMAL(10,7),
|
||||
KEY (cle_interop_adr),
|
||||
KEY (usage_principal),
|
||||
KEY (nb_log)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO bat999
|
||||
SELECT
|
||||
f.batiment_groupe_id,
|
||||
f.code_departement_insee,
|
||||
a.cle_interop_adr,
|
||||
f.nb_niveau,
|
||||
f.nb_log,
|
||||
d.nb_pdl_tot,
|
||||
f.annee_construction,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(r.l_nom_copro, '[', ''), ']', ''), '"', ''), ' ', ' ') as residence,
|
||||
u.usage_principal_bdnb_open as usage_principal,
|
||||
t.altitude_sol_mean,
|
||||
NULL as gps_lat,
|
||||
NULL as gps_lng
|
||||
FROM tmp_ffo_999 f
|
||||
INNER JOIN tmp_adr_999 a ON f.batiment_groupe_id = a.batiment_groupe_id AND a.lien_valide = 1
|
||||
LEFT JOIN tmp_rnc_999 r ON f.batiment_groupe_id = r.batiment_groupe_id
|
||||
LEFT JOIN tmp_topo_999 t ON f.batiment_groupe_id = t.batiment_groupe_id
|
||||
LEFT JOIN tmp_usage_999 u ON f.batiment_groupe_id = u.batiment_groupe_id
|
||||
LEFT JOIN tmp_dle_999 d ON f.batiment_groupe_id = d.batiment_groupe_id
|
||||
WHERE u.usage_principal_bdnb_open IN ('Résidentiel individuel', 'Résidentiel collectif', 'Secondaire', 'Tertiaire')
|
||||
AND f.nb_log > 1
|
||||
AND a.cle_interop_adr IS NOT NULL
|
||||
GROUP BY f.batiment_groupe_id;
|
||||
|
||||
-- Mise à jour des coordonnées GPS depuis la base adresses
|
||||
UPDATE bat999 b
|
||||
JOIN adresses.cp999 a ON b.cle_interop_adr = a.id
|
||||
SET b.gps_lat = a.gps_lat, b.gps_lng = a.gps_lng
|
||||
WHERE b.cle_interop_adr IS NOT NULL;
|
||||
|
||||
-- Nettoyage des tables temporaires
|
||||
DROP TABLE IF EXISTS tmp_ffo_999;
|
||||
DROP TABLE IF EXISTS tmp_adr_999;
|
||||
DROP TABLE IF EXISTS tmp_rnc_999;
|
||||
DROP TABLE IF EXISTS tmp_topo_999;
|
||||
DROP TABLE IF EXISTS tmp_usage_999;
|
||||
DROP TABLE IF EXISTS tmp_dle_999;
|
||||
|
||||
-- Historique
|
||||
INSERT INTO _histo SET date_import=NOW(), dept='999', nb_batiments=(SELECT COUNT(*) FROM bat999);
|
||||
@@ -42,6 +42,8 @@ require_once __DIR__ . '/src/Controllers/ChatController.php';
|
||||
require_once __DIR__ . '/src/Controllers/SecurityController.php';
|
||||
require_once __DIR__ . '/src/Controllers/StripeController.php';
|
||||
require_once __DIR__ . '/src/Controllers/StripeWebhookController.php';
|
||||
require_once __DIR__ . '/src/Controllers/MigrationController.php';
|
||||
require_once __DIR__ . '/src/Controllers/HealthController.php';
|
||||
|
||||
// Initialiser la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
|
||||
290
api/scripts/CORRECTIONS_MIGRATE.md
Normal file
290
api/scripts/CORRECTIONS_MIGRATE.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# 🔧 CORRECTIONS CRITIQUES - migrate_from_backup.php
|
||||
|
||||
## ❌ ERREURS DÉTECTÉES
|
||||
|
||||
### 1. **migrateUsers** (ligne 456)
|
||||
```sql
|
||||
-- ERREUR
|
||||
u.nom, u.prenom, u.nom_sect, u.username, u.password, u.phone, u.mobile
|
||||
|
||||
-- CORRECTION (noms réels dans geosector.users)
|
||||
u.libelle, u.prenom, u.nom_tournee, u.username, u.userpass, u.telephone, u.mobile
|
||||
```
|
||||
|
||||
### 2. **migrateOpePass** (ligne 1043)
|
||||
```sql
|
||||
-- ERREUR
|
||||
p.passed_at, p.libelle, p.email, p.phone
|
||||
|
||||
-- CORRECTION (noms réels dans geosector.ope_pass)
|
||||
p.date_eve AS passed_at, p.libelle AS encrypted_name, p.email, p.phone
|
||||
```
|
||||
|
||||
### 3. **migrateSectorsAdresses** (ligne 777)
|
||||
```sql
|
||||
-- ERREUR
|
||||
sa.osm_id, sa.osm_name, sa.osm_date_creat
|
||||
|
||||
-- CORRECTION (ces champs n'existent PAS dans geosector.sectors_adresses)
|
||||
-- Ces champs doivent être mis à 0 ou NULL dans la cible
|
||||
0 AS osm_id, '' AS osm_name, NULL AS osm_date_creat
|
||||
```
|
||||
|
||||
### 4. **migrateOpeUsersSectors** (ligne 955)
|
||||
```sql
|
||||
-- ERREUR
|
||||
ous.date_creat, ous.fk_user_creat, ous.date_modif, ous.fk_user_modif
|
||||
|
||||
-- CORRECTION (geosector.ope_users_sectors n'a PAS ces champs)
|
||||
NULL AS created_at, NULL AS fk_user_creat, NULL AS updated_at, NULL AS fk_user_modif
|
||||
```
|
||||
|
||||
### 5. **migrateMedias** (à vérifier)
|
||||
```sql
|
||||
-- ERREUR potentielle
|
||||
m.support_rowid
|
||||
|
||||
-- CORRECTION
|
||||
m.support_rowid AS support_id
|
||||
```
|
||||
|
||||
### 6. **migrateOperations** (erreur NOT NULL)
|
||||
```sql
|
||||
-- PROBLÈME: Column 'fk_user_modif' cannot be null
|
||||
-- CORRECTION: Utiliser 0 au lieu de NULL
|
||||
'fk_user_modif' => $row['fk_user_modif'] ?? 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ SOLUTION RAPIDE
|
||||
|
||||
Créez un script `HOTFIX_migrate.sql` pour corriger rapidement :
|
||||
|
||||
```sql
|
||||
-- Permettre NULL sur les champs problématiques
|
||||
ALTER TABLE operations MODIFY COLUMN fk_user_modif INT(10) UNSIGNED NULL DEFAULT NULL;
|
||||
ALTER TABLE ope_sectors MODIFY COLUMN fk_user_modif INT(10) UNSIGNED NULL DEFAULT NULL;
|
||||
ALTER TABLE ope_users MODIFY COLUMN fk_user_creat INT(10) UNSIGNED NULL DEFAULT NULL;
|
||||
ALTER TABLE ope_users MODIFY COLUMN fk_user_modif INT(10) UNSIGNED NULL DEFAULT NULL;
|
||||
ALTER TABLE ope_users_sectors MODIFY COLUMN fk_user_creat INT(10) UNSIGNED NULL DEFAULT NULL;
|
||||
ALTER TABLE ope_users_sectors MODIFY COLUMN fk_user_modif INT(10) UNSIGNED NULL DEFAULT NULL;
|
||||
```
|
||||
|
||||
OU utiliser `0` à la place de `NULL` systématiquement dans le script PHP.
|
||||
|
||||
---
|
||||
|
||||
## 📋 STATUT DES CORRECTIONS (10/10/2025)
|
||||
|
||||
1. ✅ **migrateEntites** - CORRIGÉ (cp, tel1, tel2, demo)
|
||||
2. ✅ **migrateUsers** - CORRIGÉ (libelle, nom_tournee, telephone, userpass, alert_email) - Lignes 455-537
|
||||
3. ✅ **migrateOperations** - CORRIGÉ (fk_user_modif ?? 0, fk_user_creat ?? 0) - Lignes 614-625
|
||||
4. ✅ **migrateOpeSectors** - CORRIGÉ (fk_user_modif ?? 0, fk_user_creat ?? 0) - Lignes 727-738
|
||||
5. ✅ **migrateSectorsAdresses** - CORRIGÉ (osm_id=0, osm_name='', osm_date_creat=null, created_at/updated_at=null) - Lignes 776-855
|
||||
6. ✅ **migrateOpeUsers** - CORRIGÉ (vérification existence user dans TARGET avant insertion) - Lignes 960-1020
|
||||
7. ✅ **migrateOpeUsersSectors** - CORRIGÉ (date_creat, fk_user_creat, date_modif, fk_user_modif = null + vérification user) - Lignes 1054-1135
|
||||
8. ✅ **migrateOpePass** - CORRIGÉ (date_eve, libelle, recu + fk_type_reglement forcé à 4 si invalide + vérification user) - Lignes 1215-1330
|
||||
9. ✅ **migrateMedias** - CORRIGÉ (support_rowid, type_fichier, hauteur/largeur) - Lignes 1281-1343
|
||||
10. ✅ **countTargetRows()** - CORRIGÉ (requêtes SQL spécifiques par table avec JOINs corrects) - Lignes 303-355
|
||||
|
||||
---
|
||||
|
||||
## ✅ CORRECTIONS APPLIQUÉES
|
||||
|
||||
**Toutes les erreurs ont été corrigées dans `migrate_from_backup.php`.**
|
||||
|
||||
Les corrections incluent :
|
||||
- Utilisation des vrais noms de colonnes SOURCE (`geosector-structure.sql`)
|
||||
- Gestion des champs manquants dans SOURCE avec valeurs par défaut
|
||||
- Utilisation de `?? 0` au lieu de `?? null` pour les FK NOT NULL
|
||||
- Suppression des champs inexistants dans les requêtes SELECT
|
||||
|
||||
**ATTENTION** : Les noms de colonnes TARGET n'ont PAS été vérifiés contre `geo_app_structure.sql`.
|
||||
Le script utilise peut-être les mauvais noms TARGET (à vérifier avec `migrate_users.php` et autres `migrate_*.php` de référence).
|
||||
|
||||
---
|
||||
|
||||
## 🔧 CORRECTIONS RÉCENTES (Session actuelle)
|
||||
|
||||
### 10. **Vérification FK users** (lignes 1008-1015, 1117-1125, 1257-1266)
|
||||
**Problème** : Violations de contraintes FK car certains `fk_user` référencent des utilisateurs absents dans TARGET.
|
||||
|
||||
**Solution** : Ajout de vérification d'existence avant insertion :
|
||||
```php
|
||||
// Vérifier que fk_user existe dans users de la TARGET
|
||||
$checkUser = $this->targetDb->prepare("SELECT id FROM users WHERE id = ?");
|
||||
$checkUser->execute([$row['fk_user']]);
|
||||
if (!$checkUser->fetch()) {
|
||||
$this->log(" ⚠ Record {$row['rowid']}: user {$row['fk_user']} non trouvé, ignoré", 'WARNING');
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
**Appliqué sur** :
|
||||
- `migrateOpeUsers()` - ligne 1008
|
||||
- `migrateOpeUsersSectors()` - ligne 1117
|
||||
- `migrateOpePass()` - ligne 1257
|
||||
|
||||
**Résultat** : Les enregistrements avec FK invalides sont ignorés avec un WARNING au lieu de provoquer une erreur fatale.
|
||||
|
||||
### 11. **countTargetRows() - Requêtes SQL spécifiques** (lignes 303-355)
|
||||
**Problème** : Erreurs SQL car toutes les tables n'ont pas les mêmes colonnes/relations :
|
||||
- `Unknown column 'fk_entite' in 'WHERE'` pour `entites`
|
||||
- `Unknown column 't.fk_operation' in 'ON'` pour `operations`, `ope_pass_histo`, `medias`
|
||||
|
||||
**Solution** : Requêtes SQL personnalisées par table :
|
||||
```php
|
||||
// Pour entites : pas de FK, juste l'ID
|
||||
if ($tableName === 'entites') {
|
||||
$sql = "SELECT COUNT(*) as count FROM $tableName WHERE id = :entity_id";
|
||||
}
|
||||
// Pour operations : FK directe vers entites
|
||||
else if ($tableName === 'operations') {
|
||||
$sql = "SELECT COUNT(*) as count FROM $tableName WHERE fk_entite = :entity_id";
|
||||
}
|
||||
// Pour sectors_adresses : JOIN via ope_sectors -> operations
|
||||
else if ($tableName === 'sectors_adresses') {
|
||||
$sql = "SELECT COUNT(*) as count FROM $tableName sa
|
||||
INNER JOIN ope_sectors s ON sa.fk_sector = s.id
|
||||
INNER JOIN operations o ON s.fk_operation = o.id
|
||||
WHERE o.fk_entite = :entity_id";
|
||||
}
|
||||
// Pour tables avec fk_operation directe
|
||||
else if (in_array($tableName, ['ope_sectors', 'ope_users', 'ope_users_sectors', 'ope_pass', 'ope_pass_histo', 'medias'])) {
|
||||
$sql = "SELECT COUNT(*) as count FROM $tableName t
|
||||
INNER JOIN operations o ON t.fk_operation = o.id
|
||||
WHERE o.fk_entite = :entity_id";
|
||||
}
|
||||
```
|
||||
|
||||
**Résultat** : Comptages TARGET précis et sans erreurs SQL pour toutes les tables.
|
||||
|
||||
### 12. **fk_type_reglement validation** (lignes 1237-1241)
|
||||
**Problème** : FK violations car certains `fk_type_reglement` référencent des IDs inexistants dans `x_types_reglements` (IDs valides : 1, 2, 3).
|
||||
|
||||
**Solution** : Forcer à 4 ("-") si valeur invalide (comme dans `migrate_ope_pass.php`) :
|
||||
```php
|
||||
// Vérification et correction du type de règlement
|
||||
$fkTypeReglement = $row['fk_type_reglement'] ?? 1;
|
||||
if (!in_array($fkTypeReglement, [1, 2, 3])) {
|
||||
$fkTypeReglement = 4; // Forcer à 4 ("-") si différent de 1, 2 ou 3
|
||||
}
|
||||
```
|
||||
|
||||
**Résultat** : Tous les `ope_pass` sont migrés sans violation de FK sur `fk_type_reglement`.
|
||||
|
||||
### 13. **Limitation aux 3 dernières opérations** (lignes 646-647) ⚠️ IMPORTANT
|
||||
**Problème** : Migration de TOUTES les opérations au lieu des 3 dernières uniquement.
|
||||
|
||||
**Solution** : Ajout de `ORDER BY rowid DESC LIMIT 3` dans la requête :
|
||||
```php
|
||||
// Ne migrer que les 3 dernières opérations (plus récentes)
|
||||
$sql .= " ORDER BY rowid DESC LIMIT 3";
|
||||
```
|
||||
|
||||
**Résultat** : Seules les 3 opérations les plus récentes (par rowid DESC) sont migrées par entité.
|
||||
**Impact** : Réduit considérablement le volume de données migrées et toutes les tables liées (ope_sectors, ope_users, ope_users_sectors, ope_pass, medias, sectors_adresses).
|
||||
|
||||
### 14. **Option de suppression avant migration** (lignes 127-200, 1692, 1722, 1776) ⭐ NOUVELLE FONCTIONNALITÉ
|
||||
**Besoin** : Permettre de supprimer les données existantes d'une entité dans TARGET avant migration pour repartir à zéro.
|
||||
|
||||
**Solution** : Ajout du paramètre `--delete-before` :
|
||||
|
||||
**Script bash** (lignes 174-183) :
|
||||
```bash
|
||||
# Demander si suppression des données de l'entité avant migration
|
||||
echo -ne "${YELLOW}3️⃣ Supprimer les données existantes de cette entité dans la TARGET avant migration ? (y/N): ${NC}"
|
||||
read -r DELETE_BEFORE
|
||||
DELETE_FLAG=""
|
||||
if [[ $DELETE_BEFORE =~ ^[Yy]$ ]]; then
|
||||
echo -e "${GREEN}✓${NC} Les données seront supprimées avant migration"
|
||||
DELETE_FLAG="--delete-before"
|
||||
fi
|
||||
```
|
||||
|
||||
**Script PHP** - Méthode `deleteEntityData()` (lignes 127-200) :
|
||||
```php
|
||||
private function deleteEntityData($entityId) {
|
||||
// Ordre de suppression inverse pour respecter les FK
|
||||
$deletionOrder = [
|
||||
'medias', 'ope_pass_histo', 'ope_pass', 'ope_users_sectors',
|
||||
'ope_users', 'sectors_adresses', 'ope_sectors', 'operations', 'users'
|
||||
];
|
||||
|
||||
foreach ($deletionOrder as $table) {
|
||||
// Suppression via JOIN avec operations pour respecter FK
|
||||
DELETE t FROM $table t
|
||||
INNER JOIN operations o ON t.fk_operation = o.id
|
||||
WHERE o.fk_entite = ?
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Résultat** :
|
||||
- En mode interactif, l'utilisateur peut choisir de supprimer les données existantes avant migration
|
||||
- Suppression propre dans l'ordre inverse des FK (pas d'erreur de contrainte)
|
||||
- L'entité elle-même n'est PAS supprimée (car peut avoir d'autres données liées)
|
||||
- Transaction avec rollback en cas d'erreur
|
||||
|
||||
**Usage** :
|
||||
```bash
|
||||
# Interactif
|
||||
./scripts/migrate_batch.sh
|
||||
# Choisir option d) puis répondre 'y' à la question de suppression
|
||||
|
||||
# Direct
|
||||
php migrate_from_backup.php --source-db=geosector_20251008 --mode=entity --entity-id=5 --delete-before
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 RÉSULTATS MIGRATION TEST (Entité #5)
|
||||
|
||||
Dernière exécution avec toutes les corrections :
|
||||
- ✅ **Entités** : 1 SOURCE → 1 TARGET
|
||||
- ✅ **Users** : 21 SOURCE → 21 TARGET (100%)
|
||||
- ✅ **Operations** : 4 SOURCE → 4 TARGET (100%)
|
||||
- ✅ **Ope_sectors** : 64 SOURCE → 64 TARGET (100%)
|
||||
- ⚠️ **Sectors_adresses** : 1975 SOURCE → 1040 TARGET (différence de -935, à investiguer)
|
||||
- ✅ **Ope_users** : 20 migrés (0 erreurs après vérification FK)
|
||||
- ✅ **Ope_users_sectors** : 20 migrés (0 erreurs après vérification FK)
|
||||
- ⚠️ **Ope_pass** : 466 erreurs (users manquants - comportement attendu avec validation FK)
|
||||
- ✅ **Medias** : Migration réussie
|
||||
|
||||
### 15. **Ajout de contraintes UNIQUE pour éviter les doublons** (10/10/2025) ⭐ CONTRAINTES MANQUANTES
|
||||
**Problème** : Les tables `ope_users` et `ope_users_sectors` n'avaient PAS de contrainte UNIQUE sur leurs combinaisons de FK, permettant des doublons massifs.
|
||||
|
||||
**Diagnostic** :
|
||||
- Table `ope_users` : 186+ doublons pour la même paire (fk_operation, fk_user)
|
||||
- Table `ope_users_sectors` : Risque de doublons sur (fk_operation, fk_user, fk_sector)
|
||||
- Le `ON DUPLICATE KEY UPDATE` ne fonctionnait pas car aucune contrainte UNIQUE n'existait
|
||||
|
||||
**Solution** : Création du script `scripts/sql/add_unique_constraints_ope_tables.sql` qui :
|
||||
1. Supprime les doublons existants (garde la première occurrence, supprime les duplicatas)
|
||||
2. Ajoute `UNIQUE KEY idx_operation_user (fk_operation, fk_user)` sur `ope_users`
|
||||
3. Ajoute `UNIQUE KEY idx_operation_user_sector (fk_operation, fk_user, fk_sector)` sur `ope_users_sectors`
|
||||
4. Vérifie les contraintes et compte les doublons restants
|
||||
|
||||
**Fichiers modifiés** :
|
||||
- `scripts/sql/add_unique_constraints_ope_tables.sql` - Script SQL d'ajout des contraintes
|
||||
- `scripts/php/geo_app_structure.sql` - Documentation de la structure cible avec contraintes
|
||||
|
||||
**À exécuter AVANT la prochaine migration** :
|
||||
```bash
|
||||
mysql -u root -p pra_geo < scripts/sql/add_unique_constraints_ope_tables.sql
|
||||
```
|
||||
|
||||
**Puis re-migrer l'entité** :
|
||||
```bash
|
||||
php migrate_from_backup.php --source-db=geosector_20251008 --mode=entity --entity-id=5 --delete-before
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Prochaines étapes** :
|
||||
1. ✅ Exécuter le script SQL pour ajouter les contraintes UNIQUE
|
||||
2. ✅ Re-migrer l'entité #5 avec `--delete-before` pour vérifier l'absence de doublons
|
||||
3. Investiguer la différence de -935 sur `sectors_adresses`
|
||||
4. Analyser les 466 erreurs sur `ope_pass` (probablement des références à des users d'autres entités)
|
||||
5. Tester sur une autre entité pour valider la stabilité des corrections
|
||||
350
api/scripts/MIGRATION_PATCH_INSTRUCTIONS.md
Normal file
350
api/scripts/MIGRATION_PATCH_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# Instructions de modification des scripts de migration
|
||||
|
||||
## Modifications à effectuer
|
||||
|
||||
### 1. migrate_from_backup.php
|
||||
|
||||
#### A. Remplacer les lignes 31-50 (configuration DB)
|
||||
|
||||
**ANCIEN** :
|
||||
```php
|
||||
private $sourceDbName;
|
||||
private $targetDbName;
|
||||
private $sourceDb;
|
||||
private $targetDb;
|
||||
private $mode;
|
||||
private $entityId;
|
||||
private $logFile;
|
||||
private $deleteBefore;
|
||||
|
||||
// Configuration MariaDB (maria4 sur IN4)
|
||||
// pra-geo se connecte à maria4 via l'IP du container
|
||||
private const DB_HOST = '13.23.33.4'; // maria4 sur IN4
|
||||
private const DB_PORT = 3306;
|
||||
private const DB_USER = 'pra_geo_user';
|
||||
private const DB_PASS = 'd2jAAGGWi8fxFrWgXjOA';
|
||||
|
||||
// Pour la base source (backup), on utilise pra_geo_user (avec SELECT sur geosector_*)
|
||||
// L'utilisateur root n'est pas accessible depuis pra-geo (13.23.33.22)
|
||||
private const DB_USER_ROOT = 'pra_geo_user';
|
||||
private const DB_PASS_ROOT = 'd2jAAGGWi8fxFrWgXjOA';
|
||||
```
|
||||
|
||||
**NOUVEAU** :
|
||||
```php
|
||||
private $sourceDbName;
|
||||
private $targetDbName;
|
||||
private $sourceDb;
|
||||
private $targetDb;
|
||||
private $mode;
|
||||
private $entityId;
|
||||
private $logFile;
|
||||
private $deleteBefore;
|
||||
private $env;
|
||||
|
||||
// Configuration multi-environnement
|
||||
private const ENVIRONMENTS = [
|
||||
'rca' => [
|
||||
'host' => '13.23.33.3', // maria3 sur IN3
|
||||
'port' => 3306,
|
||||
'user' => 'rca_geo_user',
|
||||
'pass' => 'UPf3C0cQ805LypyM71iW',
|
||||
'target_db' => 'rca_geo',
|
||||
'source_db' => 'geosector' // Base synchronisée par PM7
|
||||
],
|
||||
'pra' => [
|
||||
'host' => '13.23.33.4', // maria4 sur IN4
|
||||
'port' => 3306,
|
||||
'user' => 'pra_geo_user',
|
||||
'pass' => 'd2jAAGGWi8fxFrWgXjOA',
|
||||
'target_db' => 'pra_geo',
|
||||
'source_db' => 'geosector' // Base synchronisée par PM7
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
#### B. Modifier le constructeur (ligne 67)
|
||||
|
||||
**ANCIEN** :
|
||||
```php
|
||||
public function __construct($sourceDbName, $targetDbName, $mode = 'global', $entityId = null, $logFile = null, $deleteBefore = false) {
|
||||
$this->sourceDbName = $sourceDbName;
|
||||
$this->targetDbName = $targetDbName;
|
||||
$this->mode = $mode;
|
||||
$this->entityId = $entityId;
|
||||
$this->logFile = $logFile ?? '/var/back/migration_' . date('Ymd_His') . '.log';
|
||||
$this->deleteBefore = $deleteBefore;
|
||||
|
||||
$this->log("=== Migration depuis backup PM7 ===");
|
||||
$this->log("Source: {$sourceDbName}");
|
||||
$this->log("Cible: {$targetDbName}");
|
||||
$this->log("Mode: {$mode}");
|
||||
```
|
||||
|
||||
**NOUVEAU** :
|
||||
```php
|
||||
public function __construct($env, $mode = 'global', $entityId = null, $logFile = null, $deleteBefore = false) {
|
||||
// Validation de l'environnement
|
||||
if (!isset(self::ENVIRONMENTS[$env])) {
|
||||
throw new Exception("Invalid environment: $env. Use 'rca' or 'pra'");
|
||||
}
|
||||
|
||||
$this->env = $env;
|
||||
$config = self::ENVIRONMENTS[$env];
|
||||
$this->sourceDbName = $config['source_db'];
|
||||
$this->targetDbName = $config['target_db'];
|
||||
$this->mode = $mode;
|
||||
$this->entityId = $entityId;
|
||||
$this->logFile = $logFile ?? '/var/back/migration_' . date('Ymd_His') . '.log';
|
||||
$this->deleteBefore = $deleteBefore;
|
||||
|
||||
$this->log("=== Migration depuis backup PM7 ===");
|
||||
$this->log("Environment: {$env}");
|
||||
$this->log("Source: {$this->sourceDbName} → Cible: {$this->targetDbName}");
|
||||
$this->log("Mode: {$mode}");
|
||||
```
|
||||
|
||||
#### C. Modifier connect() (lignes 90-112)
|
||||
|
||||
**Remplacer toutes les constantes** :
|
||||
- `self::DB_HOST` → `self::ENVIRONMENTS[$this->env]['host']`
|
||||
- `self::DB_PORT` → `self::ENVIRONMENTS[$this->env]['port']`
|
||||
- `self::DB_USER_ROOT` → `self::ENVIRONMENTS[$this->env]['user']`
|
||||
- `self::DB_PASS_ROOT` → `self::ENVIRONMENTS[$this->env]['pass']`
|
||||
- `self::DB_USER` → `self::ENVIRONMENTS[$this->env]['user']`
|
||||
- `self::DB_PASS` → `self::ENVIRONMENTS[$this->env]['pass']`
|
||||
|
||||
#### D. Modifier parseArguments() (vers la fin du fichier)
|
||||
|
||||
**ANCIEN** :
|
||||
```php
|
||||
$args = [
|
||||
'source-db' => null,
|
||||
'target-db' => 'pra_geo',
|
||||
'mode' => 'global',
|
||||
'entity-id' => null,
|
||||
'log' => null,
|
||||
'delete-before' => true,
|
||||
'help' => false
|
||||
];
|
||||
```
|
||||
|
||||
**NOUVEAU** :
|
||||
```php
|
||||
$args = [
|
||||
'env' => 'rca', // Défaut: recette
|
||||
'mode' => 'global',
|
||||
'entity-id' => null,
|
||||
'log' => null,
|
||||
'delete-before' => true,
|
||||
'help' => false
|
||||
];
|
||||
```
|
||||
|
||||
#### E. Modifier showHelp()
|
||||
|
||||
**ANCIEN** :
|
||||
```php
|
||||
--source-db=NAME Nom de la base source (backup restauré, ex: geosector_20251007) [REQUIS]
|
||||
--target-db=NAME Nom de la base cible (défaut: pra_geo)
|
||||
```
|
||||
|
||||
**NOUVEAU** :
|
||||
```php
|
||||
--env=ENV Environment: 'rca' (recette) ou 'pra' (production) [défaut: rca]
|
||||
```
|
||||
|
||||
**ANCIEN** (exemples) :
|
||||
```php
|
||||
php migrate_from_backup.php --source-db=geosector_20251007 --target-db=pra_geo --mode=global
|
||||
```
|
||||
|
||||
**NOUVEAU** :
|
||||
```php
|
||||
php migrate_from_backup.php --env=pra --mode=global
|
||||
php migrate_from_backup.php --env=rca --mode=entity --entity-id=45
|
||||
```
|
||||
|
||||
#### F. Modifier validation des arguments
|
||||
|
||||
**ANCIEN** :
|
||||
```php
|
||||
if (!$args['source-db']) {
|
||||
echo "Erreur: --source-db est requis\n\n";
|
||||
showHelp();
|
||||
exit(1);
|
||||
}
|
||||
```
|
||||
|
||||
**NOUVEAU** :
|
||||
```php
|
||||
if (!in_array($args['env'], ['rca', 'pra'])) {
|
||||
echo "Erreur: --env doit être 'rca' ou 'pra'\n\n";
|
||||
showHelp();
|
||||
exit(1);
|
||||
}
|
||||
```
|
||||
|
||||
#### G. Modifier instanciation BackupMigration
|
||||
|
||||
**ANCIEN** :
|
||||
```php
|
||||
$migration = new BackupMigration(
|
||||
$args['source-db'],
|
||||
$args['target-db'],
|
||||
$args['mode'],
|
||||
$args['entity-id'],
|
||||
$args['log'],
|
||||
(bool)$args['delete-before']
|
||||
);
|
||||
```
|
||||
|
||||
**NOUVEAU** :
|
||||
```php
|
||||
$migration = new BackupMigration(
|
||||
$args['env'],
|
||||
$args['mode'],
|
||||
$args['entity-id'],
|
||||
$args['log'],
|
||||
(bool)$args['delete-before']
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. migrate_batch.sh
|
||||
|
||||
#### A. Ajouter détection automatique de l'environnement (après ligne 22)
|
||||
|
||||
**AJOUTER** :
|
||||
```bash
|
||||
# Détection automatique de l'environnement
|
||||
if [ -f "/etc/hostname" ]; then
|
||||
CONTAINER_NAME=$(cat /etc/hostname)
|
||||
case $CONTAINER_NAME in
|
||||
rca-geo)
|
||||
ENV="rca"
|
||||
;;
|
||||
pra-geo)
|
||||
ENV="pra"
|
||||
;;
|
||||
*)
|
||||
ENV="rca" # Défaut
|
||||
;;
|
||||
esac
|
||||
else
|
||||
ENV="rca" # Défaut
|
||||
fi
|
||||
```
|
||||
|
||||
#### B. Remplacer lignes 26-27
|
||||
|
||||
**ANCIEN** :
|
||||
```bash
|
||||
SOURCE_DB="geosector_20251013_13"
|
||||
TARGET_DB="pra_geo"
|
||||
```
|
||||
|
||||
**NOUVEAU** :
|
||||
```bash
|
||||
# SOURCE_DB et TARGET_DB ne sont plus utilisés
|
||||
# Ils sont déduits de --env dans migrate_from_backup.php
|
||||
```
|
||||
|
||||
#### C. Ajouter option --env dans le parsing (ligne 68)
|
||||
|
||||
**AJOUTER avant `--interactive|-i)` ** :
|
||||
```bash
|
||||
--env)
|
||||
ENV="$2"
|
||||
shift 2
|
||||
;;
|
||||
```
|
||||
|
||||
#### D. Modifier les appels PHP - ligne 200-206
|
||||
|
||||
**ANCIEN** :
|
||||
```bash
|
||||
php "$MIGRATION_SCRIPT" \
|
||||
--source-db="$SOURCE_DB" \
|
||||
--target-db="$TARGET_DB" \
|
||||
--mode=entity \
|
||||
--entity-id="$SPECIFIC_ENTITY_ID" \
|
||||
--log="$ENTITY_LOG" \
|
||||
$DELETE_FLAG
|
||||
```
|
||||
|
||||
**NOUVEAU** :
|
||||
```bash
|
||||
php "$MIGRATION_SCRIPT" \
|
||||
--env="$ENV" \
|
||||
--mode=entity \
|
||||
--entity-id="$SPECIFIC_ENTITY_ID" \
|
||||
--log="$ENTITY_LOG" \
|
||||
$DELETE_FLAG
|
||||
```
|
||||
|
||||
#### E. Modifier les appels PHP - ligne 374-379
|
||||
|
||||
**ANCIEN** :
|
||||
```bash
|
||||
php "$MIGRATION_SCRIPT" \
|
||||
--source-db="$SOURCE_DB" \
|
||||
--target-db="$TARGET_DB" \
|
||||
--mode=entity \
|
||||
--entity-id="$ENTITY_ID" \
|
||||
--log="$ENTITY_LOG" > /tmp/migration_output_$$.txt 2>&1
|
||||
```
|
||||
|
||||
**NOUVEAU** :
|
||||
```bash
|
||||
php "$MIGRATION_SCRIPT" \
|
||||
--env="$ENV" \
|
||||
--mode=entity \
|
||||
--entity-id="$ENTITY_ID" \
|
||||
--log="$ENTITY_LOG" > /tmp/migration_output_$$.txt 2>&1
|
||||
```
|
||||
|
||||
#### F. Modifier les messages de log (lignes 289-291)
|
||||
|
||||
**ANCIEN** :
|
||||
```bash
|
||||
log "📅 Date: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
log "📁 Source: $SOURCE_DB"
|
||||
log "📁 Cible: $TARGET_DB"
|
||||
```
|
||||
|
||||
**NOUVEAU** :
|
||||
```bash
|
||||
log "📅 Date: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
log "🌍 Environment: $ENV"
|
||||
log "📁 Source: geosector → Target: (déduit de \$ENV)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nouveaux usages
|
||||
|
||||
### Sur rca-geo (IN3)
|
||||
```bash
|
||||
# Détection automatique
|
||||
./migrate_batch.sh
|
||||
|
||||
# Ou explicite
|
||||
./migrate_batch.sh --env=rca
|
||||
|
||||
# Migration PHP directe
|
||||
php php/migrate_from_backup.php --env=rca --mode=entity --entity-id=45
|
||||
```
|
||||
|
||||
### Sur pra-geo (IN4)
|
||||
```bash
|
||||
# Détection automatique
|
||||
./migrate_batch.sh
|
||||
|
||||
# Ou explicite
|
||||
./migrate_batch.sh --env=pra
|
||||
|
||||
# Migration PHP directe
|
||||
php php/migrate_from_backup.php --env=pra --mode=entity --entity-id=45
|
||||
```
|
||||
1925
api/scripts/README-migration.md
Normal file
1925
api/scripts/README-migration.md
Normal file
File diff suppressed because it is too large
Load Diff
273
api/scripts/cron/CRON.md
Normal file
273
api/scripts/cron/CRON.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# Documentation des tâches CRON - API Geosector
|
||||
|
||||
Ce dossier contient les scripts automatisés de maintenance et de traitement pour l'API Geosector.
|
||||
|
||||
## Scripts disponibles
|
||||
|
||||
### 1. `process_email_queue.php`
|
||||
|
||||
**Fonction** : Traite la queue d'emails en attente (reçus fiscaux, notifications)
|
||||
|
||||
**Caractéristiques** :
|
||||
|
||||
- Traite 50 emails maximum par exécution
|
||||
- 3 tentatives maximum par email
|
||||
- Lock file pour éviter l'exécution simultanée
|
||||
- Nettoyage automatique des emails envoyés de plus de 30 jours
|
||||
|
||||
**Fréquence recommandée** : Toutes les 5 minutes
|
||||
|
||||
**Ligne crontab** :
|
||||
|
||||
```bash
|
||||
*/5 * * * * /usr/bin/php /var/www/geosector/api/scripts/cron/process_email_queue.php >> /var/www/geosector/api/logs/email_queue.log 2>&1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `cleanup_security_data.php`
|
||||
|
||||
**Fonction** : Purge les données de sécurité obsolètes selon la politique de rétention
|
||||
|
||||
**Données nettoyées** :
|
||||
|
||||
- Métriques de performance : 30 jours
|
||||
- Tentatives de login échouées : 7 jours
|
||||
- Alertes résolues : 90 jours
|
||||
- IPs expirées : Déblocage immédiat
|
||||
|
||||
**Fréquence recommandée** : Quotidien à 2h du matin
|
||||
|
||||
**Ligne crontab** :
|
||||
|
||||
```bash
|
||||
0 2 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_security_data.php >> /var/www/geosector/api/logs/cleanup_security.log 2>&1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `cleanup_logs.php`
|
||||
|
||||
**Fonction** : Supprime les fichiers de logs de plus de 10 jours
|
||||
|
||||
**Caractéristiques** :
|
||||
|
||||
- Cible tous les fichiers `*.log` dans `/api/logs/`
|
||||
- Exclut le dossier `/logs/events/` (rétention 15 mois)
|
||||
- Rétention : 10 jours
|
||||
- Logs détaillés des fichiers supprimés et taille libérée
|
||||
- Lock file pour éviter l'exécution simultanée
|
||||
|
||||
**Fréquence recommandée** : Quotidien à 3h du matin
|
||||
|
||||
**Ligne crontab** :
|
||||
|
||||
```bash
|
||||
0 3 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_logs.php >> /var/www/geosector/api/logs/cleanup_logs.log 2>&1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. `rotate_event_logs.php`
|
||||
|
||||
**Fonction** : Rotation des logs d'événements JSONL (système EventLogService)
|
||||
|
||||
**Politique de rétention (15 mois)** :
|
||||
|
||||
- 0-15 mois : fichiers `.jsonl` conservés (non compressés pour accès API)
|
||||
- > 15 mois : suppression automatique
|
||||
|
||||
**Caractéristiques** :
|
||||
|
||||
- Suppression des fichiers > 15 mois
|
||||
- Pas de compression (fichiers accessibles par l'API)
|
||||
- Logs détaillés des suppressions
|
||||
- Lock file pour éviter l'exécution simultanée
|
||||
|
||||
**Fréquence recommandée** : Mensuel le 1er à 3h du matin
|
||||
|
||||
**Ligne crontab** :
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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`
|
||||
|
||||
**Fonction** : Synchronise les bases de données entre environnements
|
||||
|
||||
**Note** : Ce script est spécifique à un cas d'usage particulier. Vérifier son utilité avant activation.
|
||||
|
||||
**Fréquence recommandée** : À définir selon les besoins
|
||||
|
||||
**Ligne crontab** :
|
||||
|
||||
```bash
|
||||
# À configurer selon les besoins
|
||||
# 0 4 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/sync_databases.php >> /var/www/geosector/api/logs/sync_databases.log 2>&1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation sur les containers Incus
|
||||
|
||||
### 1. Déployer les scripts sur les environnements
|
||||
|
||||
```bash
|
||||
# DEV (dva-geo sur IN3)
|
||||
./deploy-api.sh
|
||||
|
||||
# RECETTE (rca-geo sur IN3)
|
||||
./deploy-api.sh rca
|
||||
|
||||
# PRODUCTION (pra-geo sur IN4)
|
||||
./deploy-api.sh pra
|
||||
```
|
||||
|
||||
### 2. Configurer le crontab sur chaque container
|
||||
|
||||
```bash
|
||||
# Se connecter au container
|
||||
incus exec dva-geo -- sh # ou rca-geo, pra-geo
|
||||
|
||||
# Éditer le crontab
|
||||
crontab -e
|
||||
|
||||
# Ajouter les lignes ci-dessous (adapter les chemins si nécessaire)
|
||||
```
|
||||
|
||||
### 3. Configuration complète recommandée
|
||||
|
||||
```bash
|
||||
# Traitement de la queue d'emails (toutes les 5 minutes)
|
||||
*/5 * * * * /usr/bin/php /var/www/geosector/api/scripts/cron/process_email_queue.php >> /var/www/geosector/api/logs/email_queue.log 2>&1
|
||||
|
||||
# Nettoyage des données de sécurité (quotidien à 2h)
|
||||
0 2 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_security_data.php >> /var/www/geosector/api/logs/cleanup_security.log 2>&1
|
||||
|
||||
# Nettoyage des anciens logs (quotidien à 3h)
|
||||
0 3 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_logs.php >> /var/www/geosector/api/logs/cleanup_logs.log 2>&1
|
||||
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# Lister les CRONs configurés
|
||||
crontab -l
|
||||
|
||||
# Vérifier les logs pour s'assurer qu'ils s'exécutent
|
||||
tail -f /var/www/geosector/api/logs/email_queue.log
|
||||
tail -f /var/www/geosector/api/logs/cleanup_logs.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Surveillance et monitoring
|
||||
|
||||
### Emplacement des logs
|
||||
|
||||
Tous les logs CRON sont stockés dans `/var/www/geosector/api/logs/` :
|
||||
|
||||
- `email_queue.log` : Traitement de la queue d'emails
|
||||
- `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
|
||||
|
||||
```bash
|
||||
# Voir les dernières exécutions du processeur d'emails
|
||||
tail -n 50 /var/www/geosector/api/logs/email_queue.log
|
||||
|
||||
# Voir les derniers nettoyages de logs
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes importantes
|
||||
|
||||
1. **Détection d'environnement** : Tous les scripts détectent automatiquement l'environnement via `gethostname()` :
|
||||
|
||||
- `pra-geo` → Production (app3.geosector.fr)
|
||||
- `rca-geo` → Recette (rapp.geosector.fr)
|
||||
- `dva-geo` → Développement (dapp.geosector.fr)
|
||||
|
||||
2. **Lock files** : Les scripts critiques utilisent des fichiers de lock dans `/tmp/` pour éviter l'exécution simultanée
|
||||
|
||||
3. **Permissions** : Les scripts doivent être exécutables (`chmod +x script.php`)
|
||||
|
||||
4. **Logs** : Tous les scripts loggent via `LogService` pour traçabilité complète
|
||||
|
||||
---
|
||||
|
||||
## Dépannage
|
||||
|
||||
### Le CRON ne s'exécute pas
|
||||
|
||||
```bash
|
||||
# Vérifier que le service cron est actif
|
||||
rc-service crond status # Alpine Linux
|
||||
|
||||
# Relancer le service si nécessaire
|
||||
rc-service crond restart
|
||||
```
|
||||
|
||||
### Erreur de permissions
|
||||
|
||||
```bash
|
||||
# Vérifier les permissions du script
|
||||
ls -l /var/www/geosector/api/scripts/cron/
|
||||
|
||||
# Rendre exécutable si nécessaire
|
||||
chmod +x /var/www/geosector/api/scripts/cron/*.php
|
||||
|
||||
# Vérifier les permissions du dossier logs
|
||||
ls -ld /var/www/geosector/api/logs/
|
||||
```
|
||||
|
||||
### Lock file bloqué
|
||||
|
||||
```bash
|
||||
# Si un script semble bloqué, supprimer le lock file
|
||||
rm /tmp/process_email_queue.lock
|
||||
rm /tmp/cleanup_logs.lock
|
||||
```
|
||||
165
api/scripts/cron/cleanup_logs.php
Executable file
165
api/scripts/cron/cleanup_logs.php
Executable file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script CRON pour nettoyer les anciens fichiers de logs
|
||||
* Supprime les fichiers .log de plus de 10 jours dans le dossier /logs/
|
||||
*
|
||||
* À exécuter quotidiennement via crontab :
|
||||
* 0 3 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_logs.php >> /var/www/geosector/api/logs/cleanup_logs.log 2>&1
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Configuration
|
||||
define('LOG_RETENTION_DAYS', 10);
|
||||
define('LOCK_FILE', '/tmp/cleanup_logs.lock');
|
||||
|
||||
// Empêcher l'exécution multiple simultanée
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
$lockTime = filemtime(LOCK_FILE);
|
||||
// Si le lock a plus de 30 minutes, on le supprime (processus probablement bloqué)
|
||||
if (time() - $lockTime > 1800) {
|
||||
unlink(LOCK_FILE);
|
||||
} else {
|
||||
die("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') {
|
||||
// Détecter l'environnement basé sur le hostname
|
||||
$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'; // DVA par défaut
|
||||
}
|
||||
|
||||
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
|
||||
// Définir getallheaders si elle n'existe pas (CLI)
|
||||
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/Services/LogService.php';
|
||||
|
||||
try {
|
||||
// Initialisation de la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$environment = $appConfig->getEnvironment();
|
||||
|
||||
// Définir le chemin du dossier logs
|
||||
$logDir = __DIR__ . '/../../logs';
|
||||
|
||||
if (!is_dir($logDir)) {
|
||||
echo "Le dossier de logs n'existe pas : {$logDir}\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Date limite (10 jours en arrière)
|
||||
$cutoffDate = time() - (LOG_RETENTION_DAYS * 24 * 60 * 60);
|
||||
|
||||
// Lister tous les fichiers .log (exclure le dossier events/)
|
||||
$logFiles = glob($logDir . '/*.log');
|
||||
|
||||
// Exclure explicitement les logs du sous-dossier events/
|
||||
$logFiles = array_filter($logFiles, function($file) {
|
||||
return strpos($file, '/events/') === false;
|
||||
});
|
||||
|
||||
if (empty($logFiles)) {
|
||||
echo "Aucun fichier .log trouvé dans {$logDir}\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$deletedCount = 0;
|
||||
$deletedSize = 0;
|
||||
$deletedFiles = [];
|
||||
|
||||
foreach ($logFiles as $file) {
|
||||
$fileTime = filemtime($file);
|
||||
|
||||
// Vérifier si le fichier est plus vieux que la date limite
|
||||
if ($fileTime < $cutoffDate) {
|
||||
$fileSize = filesize($file);
|
||||
$fileName = basename($file);
|
||||
|
||||
if (unlink($file)) {
|
||||
$deletedCount++;
|
||||
$deletedSize += $fileSize;
|
||||
$deletedFiles[] = $fileName;
|
||||
echo "Supprimé : {$fileName} (" . number_format($fileSize / 1024, 2) . " KB)\n";
|
||||
} else {
|
||||
echo "ERREUR : Impossible de supprimer {$fileName}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logger le résumé
|
||||
if ($deletedCount > 0) {
|
||||
$message = sprintf(
|
||||
"Nettoyage des logs terminé - %d fichier(s) supprimé(s) - %.2f MB libérés",
|
||||
$deletedCount,
|
||||
$deletedSize / (1024 * 1024)
|
||||
);
|
||||
|
||||
LogService::log($message, [
|
||||
'level' => 'info',
|
||||
'script' => 'cleanup_logs.php',
|
||||
'environment' => $environment,
|
||||
'deleted_count' => $deletedCount,
|
||||
'deleted_size_mb' => round($deletedSize / (1024 * 1024), 2),
|
||||
'deleted_files' => $deletedFiles
|
||||
]);
|
||||
|
||||
echo "\n" . $message . "\n";
|
||||
} else {
|
||||
echo "Aucun fichier à supprimer (tous les logs ont moins de " . LOG_RETENTION_DAYS . " jours)\n";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$errorMsg = 'Erreur lors du nettoyage des logs : ' . $e->getMessage();
|
||||
|
||||
LogService::log($errorMsg, [
|
||||
'level' => 'error',
|
||||
'script' => 'cleanup_logs.php',
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
echo $errorMsg . "\n";
|
||||
|
||||
// Supprimer le lock en cas d'erreur
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
unlink(LOCK_FILE);
|
||||
}
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Supprimer le lock
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
unlink(LOCK_FILE);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
@@ -41,14 +41,14 @@ register_shutdown_function(function() {
|
||||
if (php_sapi_name() === 'cli') {
|
||||
// Détecter l'environnement basé sur le hostname ou un paramètre
|
||||
$hostname = gethostname();
|
||||
if (strpos($hostname, 'prod') !== false) {
|
||||
$_SERVER['SERVER_NAME'] = 'app.geosector.fr';
|
||||
} elseif (strpos($hostname, 'rec') !== false || strpos($hostname, 'rapp') !== false) {
|
||||
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'] = 'app.geo.dev'; // DVA par défaut
|
||||
$_SERVER['SERVER_NAME'] = 'dapp.geosector.fr'; // DVA par défaut
|
||||
}
|
||||
|
||||
|
||||
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
|
||||
@@ -69,6 +69,7 @@ require_once __DIR__ . '/../../src/Services/LogService.php';
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
use App\Services\LogService;
|
||||
|
||||
try {
|
||||
// Initialisation de la configuration
|
||||
|
||||
169
api/scripts/cron/rotate_event_logs.php
Normal file
169
api/scripts/cron/rotate_event_logs.php
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script CRON pour rotation des logs d'événements JSONL
|
||||
*
|
||||
* Politique de rétention : 15 mois
|
||||
* - 0-15 mois : fichiers .jsonl conservés (non compressés pour accès API)
|
||||
* - > 15 mois : suppression
|
||||
*
|
||||
* À exécuter mensuellement via crontab (1er du mois à 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
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Configuration
|
||||
define('RETENTION_MONTHS', 15); // Conserver 15 mois
|
||||
define('LOCK_FILE', '/tmp/rotate_event_logs.lock');
|
||||
|
||||
// Empêcher l'exécution multiple simultanée
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
$lockTime = filemtime(LOCK_FILE);
|
||||
// Si le lock a plus de 2 heures, on le supprime (processus probablement bloqué)
|
||||
if (time() - $lockTime > 7200) {
|
||||
unlink(LOCK_FILE);
|
||||
} else {
|
||||
die("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') {
|
||||
// Détecter l'environnement basé sur le hostname
|
||||
$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'; // DVA par défaut
|
||||
}
|
||||
|
||||
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
|
||||
// Définir getallheaders si elle n'existe pas (CLI)
|
||||
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/Services/LogService.php';
|
||||
|
||||
try {
|
||||
// Initialisation de la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$environment = $appConfig->getEnvironment();
|
||||
|
||||
// Définir le chemin du dossier des logs événements
|
||||
$eventLogDir = __DIR__ . '/../../logs/events';
|
||||
|
||||
if (!is_dir($eventLogDir)) {
|
||||
echo "Le dossier de logs événements n'existe pas : {$eventLogDir}\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Date limite de suppression
|
||||
$deletionDate = strtotime('-' . RETENTION_MONTHS . ' months');
|
||||
|
||||
// Lister tous les fichiers .jsonl
|
||||
$jsonlFiles = glob($eventLogDir . '/*.jsonl');
|
||||
|
||||
if (empty($jsonlFiles)) {
|
||||
echo "Aucun fichier .jsonl trouvé dans {$eventLogDir}\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$deletedCount = 0;
|
||||
$deletedSize = 0;
|
||||
$deletedFiles = [];
|
||||
|
||||
// ========================================
|
||||
// Suppression des fichiers > 15 mois
|
||||
// ========================================
|
||||
foreach ($jsonlFiles as $file) {
|
||||
$fileTime = filemtime($file);
|
||||
|
||||
// Vérifier si le fichier est plus vieux que la date de rétention
|
||||
if ($fileTime < $deletionDate) {
|
||||
$fileSize = filesize($file);
|
||||
$fileName = basename($file);
|
||||
|
||||
if (unlink($file)) {
|
||||
$deletedCount++;
|
||||
$deletedSize += $fileSize;
|
||||
$deletedFiles[] = $fileName;
|
||||
echo "Supprimé : {$fileName} (> " . RETENTION_MONTHS . " mois, " .
|
||||
number_format($fileSize / 1024, 2) . " KB)\n";
|
||||
} else {
|
||||
echo "ERREUR : Impossible de supprimer {$fileName}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// RÉSUMÉ ET LOGGING
|
||||
// ========================================
|
||||
if ($deletedCount > 0) {
|
||||
$message = sprintf(
|
||||
"Rotation des logs événements terminée - %d fichier(s) supprimé(s) - %.2f MB libérés",
|
||||
$deletedCount,
|
||||
$deletedSize / (1024 * 1024)
|
||||
);
|
||||
|
||||
LogService::log($message, [
|
||||
'level' => 'info',
|
||||
'script' => 'rotate_event_logs.php',
|
||||
'environment' => $environment,
|
||||
'deleted_count' => $deletedCount,
|
||||
'deleted_size_mb' => round($deletedSize / (1024 * 1024), 2),
|
||||
'deleted_files' => $deletedFiles
|
||||
]);
|
||||
|
||||
echo "\n" . $message . "\n";
|
||||
} else {
|
||||
echo "Aucune rotation nécessaire - Tous les fichiers .jsonl ont moins de " . RETENTION_MONTHS . " mois\n";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$errorMsg = 'Erreur lors de la rotation des logs événements : ' . $e->getMessage();
|
||||
|
||||
LogService::log($errorMsg, [
|
||||
'level' => 'error',
|
||||
'script' => 'rotate_event_logs.php',
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
echo $errorMsg . "\n";
|
||||
|
||||
// Supprimer le lock en cas d'erreur
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
unlink(LOCK_FILE);
|
||||
}
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Supprimer le lock
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
unlink(LOCK_FILE);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
@@ -1,186 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script de test pour vérifier le processeur de queue d'emails
|
||||
* Affiche les emails en attente sans les envoyer
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Simuler l'environnement web pour AppConfig en CLI
|
||||
if (php_sapi_name() === 'cli') {
|
||||
$_SERVER['SERVER_NAME'] = $_SERVER['SERVER_NAME'] ?? 'app.geo.dev'; // DVA par défaut
|
||||
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
|
||||
// Définir getallheaders si elle n'existe pas (CLI)
|
||||
if (!function_exists('getallheaders')) {
|
||||
function getallheaders() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||
|
||||
try {
|
||||
// Initialiser la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$dbConfig = $appConfig->getDatabaseConfig();
|
||||
|
||||
// Initialiser la base de données avec la configuration
|
||||
Database::init($dbConfig);
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "=== TEST DE LA QUEUE D'EMAILS ===\n\n";
|
||||
|
||||
// Statistiques générales
|
||||
$stmt = $db->query('
|
||||
SELECT
|
||||
status,
|
||||
COUNT(*) as count,
|
||||
MIN(created_at) as oldest,
|
||||
MAX(created_at) as newest
|
||||
FROM email_queue
|
||||
GROUP BY status
|
||||
');
|
||||
|
||||
$stats = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo "STATISTIQUES:\n";
|
||||
echo "-------------\n";
|
||||
foreach ($stats as $stat) {
|
||||
echo sprintf(
|
||||
"Status: %s - Nombre: %d (Plus ancien: %s, Plus récent: %s)\n",
|
||||
$stat['status'],
|
||||
$stat['count'],
|
||||
$stat['oldest'] ?? 'N/A',
|
||||
$stat['newest'] ?? 'N/A'
|
||||
);
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
|
||||
// Emails en attente
|
||||
$stmt = $db->prepare('
|
||||
SELECT
|
||||
eq.id,
|
||||
eq.fk_pass,
|
||||
eq.to_email,
|
||||
eq.subject,
|
||||
eq.created_at,
|
||||
eq.attempts,
|
||||
eq.status,
|
||||
p.fk_type,
|
||||
p.montant,
|
||||
p.nom_recu
|
||||
FROM email_queue eq
|
||||
LEFT JOIN ope_pass p ON eq.fk_pass = p.id
|
||||
WHERE eq.status = ?
|
||||
ORDER BY eq.created_at DESC
|
||||
LIMIT 10
|
||||
');
|
||||
|
||||
$stmt->execute(['pending']);
|
||||
$pendingEmails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($pendingEmails)) {
|
||||
echo "Aucun email en attente.\n";
|
||||
} else {
|
||||
echo "EMAILS EN ATTENTE (10 plus récents):\n";
|
||||
echo "------------------------------------\n";
|
||||
foreach ($pendingEmails as $email) {
|
||||
echo sprintf(
|
||||
"ID: %d | Passage: %d | Destinataire: %s\n",
|
||||
$email['id'],
|
||||
$email['fk_pass'],
|
||||
$email['to_email']
|
||||
);
|
||||
echo sprintf(
|
||||
" Sujet: %s\n",
|
||||
$email['subject']
|
||||
);
|
||||
echo sprintf(
|
||||
" Créé le: %s | Tentatives: %d\n",
|
||||
$email['created_at'],
|
||||
$email['attempts']
|
||||
);
|
||||
if ($email['fk_pass'] > 0) {
|
||||
echo sprintf(
|
||||
" Passage - Type: %s | Montant: %.2f€ | Reçu: %s\n",
|
||||
$email['fk_type'] == 1 ? 'DON' : 'Autre',
|
||||
$email['montant'] ?? 0,
|
||||
$email['nom_recu'] ?? 'Non généré'
|
||||
);
|
||||
}
|
||||
echo "---\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Emails échoués
|
||||
$stmt = $db->prepare('
|
||||
SELECT
|
||||
id,
|
||||
fk_pass,
|
||||
to_email,
|
||||
subject,
|
||||
created_at,
|
||||
attempts,
|
||||
error_message
|
||||
FROM email_queue
|
||||
WHERE status = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5
|
||||
');
|
||||
|
||||
$stmt->execute(['failed']);
|
||||
$failedEmails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($failedEmails)) {
|
||||
echo "\nEMAILS ÉCHOUÉS (5 plus récents):\n";
|
||||
echo "--------------------------------\n";
|
||||
foreach ($failedEmails as $email) {
|
||||
echo sprintf(
|
||||
"ID: %d | Passage: %d | Destinataire: %s\n",
|
||||
$email['id'],
|
||||
$email['fk_pass'],
|
||||
$email['to_email']
|
||||
);
|
||||
echo sprintf(
|
||||
" Sujet: %s\n",
|
||||
$email['subject']
|
||||
);
|
||||
echo sprintf(
|
||||
" Tentatives: %d | Erreur: %s\n",
|
||||
$email['attempts'],
|
||||
$email['error_message'] ?? 'Non spécifiée'
|
||||
);
|
||||
echo "---\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier la configuration SMTP
|
||||
echo "\nCONFIGURATION SMTP:\n";
|
||||
echo "-------------------\n";
|
||||
|
||||
$smtpConfig = $appConfig->getSmtpConfig();
|
||||
$emailConfig = $appConfig->getEmailConfig();
|
||||
|
||||
echo "Host: " . ($smtpConfig['host'] ?? 'Non configuré') . "\n";
|
||||
echo "Port: " . ($smtpConfig['port'] ?? 'Non configuré') . "\n";
|
||||
echo "Username: " . ($smtpConfig['user'] ?? 'Non configuré') . "\n";
|
||||
echo "Password: " . (isset($smtpConfig['pass']) ? '***' : 'Non configuré') . "\n";
|
||||
echo "Encryption: " . ($smtpConfig['secure'] ?? 'Non configuré') . "\n";
|
||||
echo "From Email: " . ($emailConfig['from'] ?? 'Non configuré') . "\n";
|
||||
echo "Contact Email: " . ($emailConfig['contact'] ?? 'Non configuré') . "\n";
|
||||
|
||||
echo "\n=== FIN DU TEST ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "ERREUR: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
@@ -42,7 +42,7 @@ register_shutdown_function(function() {
|
||||
if (php_sapi_name() === 'cli') {
|
||||
$hostname = gethostname();
|
||||
if (strpos($hostname, 'prod') !== false || strpos($hostname, 'pra') !== false) {
|
||||
$_SERVER['SERVER_NAME'] = 'app.geosector.fr';
|
||||
$_SERVER['SERVER_NAME'] = 'app3.geosector.fr';
|
||||
} elseif (strpos($hostname, 'rec') !== false || strpos($hostname, 'rca') !== false) {
|
||||
$_SERVER['SERVER_NAME'] = 'rapp.geosector.fr';
|
||||
} else {
|
||||
@@ -67,6 +67,8 @@ 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";
|
||||
|
||||
|
||||
467
api/scripts/migrate_batch.sh
Executable file
467
api/scripts/migrate_batch.sh
Executable file
@@ -0,0 +1,467 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Script de migration en batch des entités depuis geosector_20251008
|
||||
#
|
||||
# Usage: ./migrate_batch.sh [options]
|
||||
#
|
||||
# Options:
|
||||
# --start N Commencer à partir de l'entité N (défaut: 1)
|
||||
# --limit N Migrer seulement N entités (défaut: toutes)
|
||||
# --dry-run Simuler sans exécuter
|
||||
# --continue Continuer après une erreur (défaut: s'arrêter)
|
||||
# --interactive Mode interactif (défaut si aucune option)
|
||||
#
|
||||
# Exemple:
|
||||
# ./migrate_batch.sh --start 10 --limit 5
|
||||
# ./migrate_batch.sh --continue
|
||||
# ./migrate_batch.sh --interactive
|
||||
###############################################################################
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
JSON_FILE="${SCRIPT_DIR}/migrations_entites.json"
|
||||
LOG_DIR="/var/www/geosector/api/logs/migrations"
|
||||
MIGRATION_SCRIPT="${SCRIPT_DIR}/php/migrate_from_backup.php"
|
||||
SOURCE_DB="geosector_20251013_13"
|
||||
TARGET_DB="pra_geo"
|
||||
|
||||
# Paramètres par défaut
|
||||
START_INDEX=1
|
||||
LIMIT=0
|
||||
DRY_RUN=0
|
||||
CONTINUE_ON_ERROR=0
|
||||
INTERACTIVE_MODE=0
|
||||
SPECIFIC_ENTITY_ID=""
|
||||
SPECIFIC_CP=""
|
||||
|
||||
# Couleurs
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Sauvegarder le nombre d'arguments avant le parsing
|
||||
INITIAL_ARGS=$#
|
||||
|
||||
# Parse des arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--start)
|
||||
START_INDEX="$2"
|
||||
shift 2
|
||||
;;
|
||||
--limit)
|
||||
LIMIT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
--continue)
|
||||
CONTINUE_ON_ERROR=1
|
||||
shift
|
||||
;;
|
||||
--interactive|-i)
|
||||
INTERACTIVE_MODE=1
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
grep "^#" "$0" | grep -v "^#!/bin/bash" | sed 's/^# //'
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Option inconnue: $1"
|
||||
echo "Utilisez --help pour l'aide"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Activer le mode interactif si aucun argument n'a été fourni
|
||||
if [ $INITIAL_ARGS -eq 0 ]; then
|
||||
INTERACTIVE_MODE=1
|
||||
fi
|
||||
|
||||
# Vérifications préalables
|
||||
if [ ! -f "$JSON_FILE" ]; then
|
||||
echo -e "${RED}❌ Fichier JSON introuvable: $JSON_FILE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$MIGRATION_SCRIPT" ]; then
|
||||
echo -e "${RED}❌ Script de migration introuvable: $MIGRATION_SCRIPT${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Créer le répertoire de logs
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# Fichiers de log
|
||||
BATCH_LOG="${LOG_DIR}/batch_$(date +%Y%m%d_%H%M%S).log"
|
||||
SUCCESS_LOG="${LOG_DIR}/success.log"
|
||||
ERROR_LOG="${LOG_DIR}/errors.log"
|
||||
|
||||
# MODE INTERACTIF
|
||||
if [ $INTERACTIVE_MODE -eq 1 ]; then
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} 🔧 Mode interactif - Migration d'entités GeoSector${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# Question 1: Migration globale ou ciblée ?
|
||||
echo -e "${YELLOW}1️⃣ Type de migration :${NC}"
|
||||
echo -e " ${CYAN}a)${NC} Migration globale (toutes les entités)"
|
||||
echo -e " ${CYAN}b)${NC} Migration par lot (plage d'entités)"
|
||||
echo -e " ${CYAN}c)${NC} Migration par code postal"
|
||||
echo -e " ${CYAN}d)${NC} Migration d'une entité spécifique (ID)"
|
||||
echo ""
|
||||
echo -ne "${YELLOW}Votre choix (a/b/c/d) : ${NC}"
|
||||
read -r MIGRATION_TYPE
|
||||
echo ""
|
||||
|
||||
case $MIGRATION_TYPE in
|
||||
a|A)
|
||||
# Migration globale - garder les valeurs par défaut
|
||||
START_INDEX=1
|
||||
LIMIT=0
|
||||
echo -e "${GREEN}✓${NC} Migration globale sélectionnée"
|
||||
;;
|
||||
b|B)
|
||||
# Migration par lot
|
||||
echo -e "${YELLOW}2️⃣ Configuration du lot :${NC}"
|
||||
echo -ne " Première entité (index, défaut=1) : "
|
||||
read -r USER_START
|
||||
if [ -n "$USER_START" ]; then
|
||||
START_INDEX=$USER_START
|
||||
fi
|
||||
|
||||
echo -ne " Limite (nombre d'entités, défaut=toutes) : "
|
||||
read -r USER_LIMIT
|
||||
if [ -n "$USER_LIMIT" ]; then
|
||||
LIMIT=$USER_LIMIT
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${GREEN}✓${NC} Migration par lot : de l'index $START_INDEX, limite de $LIMIT entités"
|
||||
;;
|
||||
c|C)
|
||||
# Migration par code postal
|
||||
echo -ne "${YELLOW}2️⃣ Code postal à migrer : ${NC}"
|
||||
read -r SPECIFIC_CP
|
||||
echo ""
|
||||
if [ -z "$SPECIFIC_CP" ]; then
|
||||
echo -e "${RED}❌ Code postal requis${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓${NC} Migration pour le code postal : $SPECIFIC_CP"
|
||||
;;
|
||||
d|D)
|
||||
# Migration d'une entité spécifique - bypass complet du JSON
|
||||
echo -ne "${YELLOW}2️⃣ ID de l'entité à migrer : ${NC}"
|
||||
read -r SPECIFIC_ENTITY_ID
|
||||
echo ""
|
||||
if [ -z "$SPECIFIC_ENTITY_ID" ]; then
|
||||
echo -e "${RED}❌ ID d'entité requis${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓${NC} Migration de l'entité ID : $SPECIFIC_ENTITY_ID"
|
||||
echo ""
|
||||
|
||||
# Demander si suppression des données de l'entité avant migration
|
||||
echo -ne "${YELLOW}3️⃣ Supprimer les données existantes de cette entité dans la TARGET avant migration ? (y/N): ${NC}"
|
||||
read -r DELETE_BEFORE
|
||||
DELETE_FLAG=""
|
||||
if [[ $DELETE_BEFORE =~ ^[Yy]$ ]]; then
|
||||
echo -e "${GREEN}✓${NC} Les données seront supprimées avant migration"
|
||||
DELETE_FLAG="--delete-before"
|
||||
else
|
||||
echo -e "${BLUE}ℹ${NC} Les données seront conservées (ON DUPLICATE KEY UPDATE)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Confirmer la migration
|
||||
echo -ne "${YELLOW}⚠️ Confirmer la migration de l'entité #${SPECIFIC_ENTITY_ID} ? (y/N): ${NC}"
|
||||
read -r CONFIRM
|
||||
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
|
||||
echo -e "${RED}❌ Migration annulée${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Exécuter directement la migration sans passer par le JSON
|
||||
ENTITY_LOG="${LOG_DIR}/entity_${SPECIFIC_ENTITY_ID}_$(date +%Y%m%d_%H%M%S).log"
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}⏳ Migration de l'entité #${SPECIFIC_ENTITY_ID} en cours...${NC}"
|
||||
|
||||
php "$MIGRATION_SCRIPT" \
|
||||
--source-db="$SOURCE_DB" \
|
||||
--target-db="$TARGET_DB" \
|
||||
--mode=entity \
|
||||
--entity-id="$SPECIFIC_ENTITY_ID" \
|
||||
--log="$ENTITY_LOG" \
|
||||
$DELETE_FLAG
|
||||
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo -e "${GREEN}✅ Entité #${SPECIFIC_ENTITY_ID} migrée avec succès${NC}"
|
||||
echo -e "${BLUE}📋 Log détaillé : $ENTITY_LOG${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Erreur lors de la migration de l'entité #${SPECIFIC_ENTITY_ID}${NC}"
|
||||
echo -e "${RED}📋 Voir le log : $ENTITY_LOG${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}❌ Choix invalide${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Fonctions utilitaires
|
||||
log() {
|
||||
echo -e "$1" | tee -a "$BATCH_LOG"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo "$1" >> "$SUCCESS_LOG"
|
||||
log "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "$1" >> "$ERROR_LOG"
|
||||
log "${RED}✗${NC} $1"
|
||||
}
|
||||
|
||||
# Extraire les entity_id du JSON (compatible sans jq)
|
||||
get_entity_ids() {
|
||||
if [ -n "$SPECIFIC_ENTITY_ID" ]; then
|
||||
# Entité spécifique par ID - chercher exactement "entity_id" : ID,
|
||||
grep "\"entity_id\" : ${SPECIFIC_ENTITY_ID}," "$JSON_FILE" | sed 's/.*: \([0-9]*\).*/\1/'
|
||||
elif [ -n "$SPECIFIC_CP" ]; then
|
||||
# Entités par code postal
|
||||
grep -B 2 "\"code_postal\" : \"$SPECIFIC_CP\"" "$JSON_FILE" | grep '"entity_id"' | sed 's/.*: \([0-9]*\).*/\1/'
|
||||
else
|
||||
# Toutes les entités
|
||||
grep '"entity_id"' "$JSON_FILE" | sed 's/.*: \([0-9]*\).*/\1/'
|
||||
fi
|
||||
}
|
||||
|
||||
# Compter le nombre total d'entités
|
||||
TOTAL_ENTITIES=$(get_entity_ids | wc -l)
|
||||
|
||||
# Vérifier si des entités ont été trouvées
|
||||
if [ $TOTAL_ENTITIES -eq 0 ]; then
|
||||
if [ -n "$SPECIFIC_ENTITY_ID" ]; then
|
||||
echo -e "${RED}❌ Entité #${SPECIFIC_ENTITY_ID} introuvable dans le fichier JSON${NC}"
|
||||
elif [ -n "$SPECIFIC_CP" ]; then
|
||||
echo -e "${RED}❌ Aucune entité trouvée pour le code postal ${SPECIFIC_CP}${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Aucune entité trouvée${NC}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Calculer le nombre d'entités à migrer
|
||||
if [ $LIMIT -gt 0 ]; then
|
||||
END_INDEX=$((START_INDEX + LIMIT - 1))
|
||||
if [ $END_INDEX -gt $TOTAL_ENTITIES ]; then
|
||||
END_INDEX=$TOTAL_ENTITIES
|
||||
fi
|
||||
else
|
||||
END_INDEX=$TOTAL_ENTITIES
|
||||
fi
|
||||
|
||||
# Bannière de démarrage
|
||||
echo ""
|
||||
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
||||
log "${BLUE} Migration en batch des entités GeoSector${NC}"
|
||||
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
||||
log "📅 Date: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
log "📁 Source: $SOURCE_DB"
|
||||
log "📁 Cible: $TARGET_DB"
|
||||
|
||||
# Afficher les informations selon le mode
|
||||
if [ -n "$SPECIFIC_ENTITY_ID" ]; then
|
||||
log "🎯 Mode: Migration d'une entité spécifique"
|
||||
log "📊 Entité ID: $SPECIFIC_ENTITY_ID"
|
||||
elif [ -n "$SPECIFIC_CP" ]; then
|
||||
log "🎯 Mode: Migration par code postal"
|
||||
log "📮 Code postal: $SPECIFIC_CP"
|
||||
log "📊 Entités trouvées: $TOTAL_ENTITIES"
|
||||
else
|
||||
TOTAL_AVAILABLE=$(grep '"entity_id"' "$JSON_FILE" | wc -l)
|
||||
log "📊 Total entités disponibles: $TOTAL_AVAILABLE"
|
||||
log "🎯 Entités à migrer: $START_INDEX à $END_INDEX"
|
||||
fi
|
||||
|
||||
if [ $DRY_RUN -eq 1 ]; then
|
||||
log "${YELLOW}🔍 Mode DRY-RUN (simulation)${NC}"
|
||||
fi
|
||||
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# Confirmation utilisateur
|
||||
if [ $DRY_RUN -eq 0 ]; then
|
||||
echo -ne "${YELLOW}⚠️ Confirmer la migration ? (y/N): ${NC}"
|
||||
read -r CONFIRM
|
||||
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
|
||||
log "❌ Migration annulée par l'utilisateur"
|
||||
exit 0
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Compteurs
|
||||
SUCCESS_COUNT=0
|
||||
ERROR_COUNT=0
|
||||
SKIPPED_COUNT=0
|
||||
CURRENT_INDEX=0
|
||||
|
||||
# Début de la migration
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
# Lire les entity_id et migrer
|
||||
get_entity_ids | while read -r ENTITY_ID; do
|
||||
CURRENT_INDEX=$((CURRENT_INDEX + 1))
|
||||
|
||||
# Filtrer par index
|
||||
if [ $CURRENT_INDEX -lt $START_INDEX ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ $CURRENT_INDEX -gt $END_INDEX ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# Récupérer les détails de l'entité depuis le JSON (match exact avec la virgule)
|
||||
ENTITY_INFO=$(grep -A 8 "\"entity_id\" : ${ENTITY_ID}," "$JSON_FILE")
|
||||
ENTITY_NAME=$(echo "$ENTITY_INFO" | grep '"nom"' | sed 's/.*: "\(.*\)".*/\1/')
|
||||
ENTITY_CP=$(echo "$ENTITY_INFO" | grep '"code_postal"' | sed 's/.*: "\(.*\)".*/\1/')
|
||||
NB_USERS=$(echo "$ENTITY_INFO" | grep '"nb_users"' | sed 's/.*: \([0-9]*\).*/\1/')
|
||||
NB_PASSAGES=$(echo "$ENTITY_INFO" | grep '"nb_passages"' | sed 's/.*: \([0-9]*\).*/\1/')
|
||||
|
||||
# Afficher la progression
|
||||
PROGRESS=$((CURRENT_INDEX - START_INDEX + 1))
|
||||
TOTAL=$((END_INDEX - START_INDEX + 1))
|
||||
PERCENT=$((PROGRESS * 100 / TOTAL))
|
||||
|
||||
log ""
|
||||
log "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
log "${BLUE}[$PROGRESS/$TOTAL - ${PERCENT}%]${NC} Entité #${ENTITY_ID}: ${ENTITY_NAME} (${ENTITY_CP})"
|
||||
log " 👥 Users: ${NB_USERS} | 📍 Passages: ${NB_PASSAGES}"
|
||||
|
||||
# Mode dry-run
|
||||
if [ $DRY_RUN -eq 1 ]; then
|
||||
log "${YELLOW} 🔍 [DRY-RUN] Simulation de la migration${NC}"
|
||||
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Exécuter la migration
|
||||
ENTITY_LOG="${LOG_DIR}/entity_${ENTITY_ID}_$(date +%Y%m%d_%H%M%S).log"
|
||||
|
||||
log " ⏳ Migration en cours..."
|
||||
php "$MIGRATION_SCRIPT" \
|
||||
--source-db="$SOURCE_DB" \
|
||||
--target-db="$TARGET_DB" \
|
||||
--mode=entity \
|
||||
--entity-id="$ENTITY_ID" \
|
||||
--log="$ENTITY_LOG" > /tmp/migration_output_$$.txt 2>&1
|
||||
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
# Succès
|
||||
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||
log_success "Entité #${ENTITY_ID} (${ENTITY_NAME}) migrée avec succès"
|
||||
|
||||
# Afficher un résumé du log avec détails
|
||||
if [ -f "$ENTITY_LOG" ]; then
|
||||
# Chercher la ligne avec les marqueurs #STATS#
|
||||
STATS_LINE=$(grep "#STATS#" "$ENTITY_LOG" 2>/dev/null)
|
||||
|
||||
if [ -n "$STATS_LINE" ]; then
|
||||
# Extraire chaque compteur
|
||||
OPE=$(echo "$STATS_LINE" | grep -oE 'OPE:[0-9]+' | cut -d: -f2)
|
||||
USERS=$(echo "$STATS_LINE" | grep -oE 'USER:[0-9]+' | cut -d: -f2)
|
||||
SECTORS=$(echo "$STATS_LINE" | grep -oE 'SECTOR:[0-9]+' | cut -d: -f2)
|
||||
PASSAGES=$(echo "$STATS_LINE" | grep -oE 'PASS:[0-9]+' | cut -d: -f2)
|
||||
|
||||
# Valeurs par défaut si extraction échoue
|
||||
OPE=${OPE:-0}
|
||||
USERS=${USERS:-0}
|
||||
SECTORS=${SECTORS:-0}
|
||||
PASSAGES=${PASSAGES:-0}
|
||||
|
||||
log " 📊 ope: ${OPE} | users: ${USERS} | sectors: ${SECTORS} | passages: ${PASSAGES}"
|
||||
else
|
||||
log " 📊 Statistiques non disponibles"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Erreur
|
||||
ERROR_COUNT=$((ERROR_COUNT + 1))
|
||||
log_error "Entité #${ENTITY_ID} (${ENTITY_NAME}) - Erreur code $EXIT_CODE"
|
||||
|
||||
# Afficher les dernières lignes du log d'erreur
|
||||
if [ -f "/tmp/migration_output_$$.txt" ]; then
|
||||
log "${RED} 📋 Dernières erreurs:${NC}"
|
||||
tail -5 "/tmp/migration_output_$$.txt" | sed 's/^/ /' | tee -a "$BATCH_LOG"
|
||||
fi
|
||||
|
||||
# Arrêter ou continuer ?
|
||||
if [ $CONTINUE_ON_ERROR -eq 0 ]; then
|
||||
log ""
|
||||
log "${RED}❌ Migration interrompue suite à une erreur${NC}"
|
||||
log " Utilisez --continue pour continuer malgré les erreurs"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Nettoyage
|
||||
rm -f "/tmp/migration_output_$$.txt"
|
||||
|
||||
# Pause entre les migrations (pour éviter de surcharger)
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Fin de la migration
|
||||
END_TIME=$(date +%s)
|
||||
DURATION=$((END_TIME - START_TIME))
|
||||
HOURS=$((DURATION / 3600))
|
||||
MINUTES=$(((DURATION % 3600) / 60))
|
||||
SECONDS=$((DURATION % 60))
|
||||
|
||||
# Résumé final
|
||||
log ""
|
||||
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
||||
log "${BLUE} Résumé de la migration${NC}"
|
||||
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
||||
log "✅ Succès: ${GREEN}${SUCCESS_COUNT}${NC}"
|
||||
log "❌ Erreurs: ${RED}${ERROR_COUNT}${NC}"
|
||||
log "⏭️ Ignorées: ${YELLOW}${SKIPPED_COUNT}${NC}"
|
||||
log "⏱️ Durée: ${HOURS}h ${MINUTES}m ${SECONDS}s"
|
||||
log ""
|
||||
log "📋 Logs détaillés:"
|
||||
log " - Batch: $BATCH_LOG"
|
||||
log " - Succès: $SUCCESS_LOG"
|
||||
log " - Erreurs: $ERROR_LOG"
|
||||
log " - Individuels: $LOG_DIR/entity_*.log"
|
||||
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
||||
|
||||
# Code de sortie
|
||||
if [ $ERROR_COUNT -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
410
api/scripts/migration2/README.md
Normal file
410
api/scripts/migration2/README.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# Migration v2 - Architecture modulaire
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Cette nouvelle architecture simplifie la migration en utilisant :
|
||||
- **Source fixe** : `geosector` (synchronisée 2x/jour par PM7 depuis nx4)
|
||||
- **Multi-environnement** : `--env=dva` (développement), `--env=rca` (recette) ou `--env=pra` (production)
|
||||
- **Auto-détection** : L'environnement est détecté automatiquement selon le serveur
|
||||
- **Classes réutilisables** : Configuration, Logger, Connexion
|
||||
|
||||
## Structure modulaire
|
||||
|
||||
```
|
||||
migration2/
|
||||
├── README.md # Ce fichier
|
||||
├── logs/ # Logs de migration (auto-créé)
|
||||
│ └── .gitignore
|
||||
├── php/
|
||||
│ ├── migrate_from_backup.php # Script principal orchestrateur
|
||||
│ └── lib/
|
||||
│ ├── DatabaseConfig.php # Configuration multi-env
|
||||
│ ├── MigrationLogger.php # Gestion des logs
|
||||
│ ├── DatabaseConnection.php # Connexions PDO
|
||||
│ ├── OperationMigrator.php # Migration des opérations
|
||||
│ ├── UserMigrator.php # Migration des ope_users
|
||||
│ ├── SectorMigrator.php # Migration des secteurs
|
||||
│ └── PassageMigrator.php # Migration des passages
|
||||
```
|
||||
|
||||
**Architecture modulaire** : Chaque type de données a son propre migrator spécialisé, orchestré par le script principal.
|
||||
|
||||
## ⚠️ AVERTISSEMENT IMPORTANT
|
||||
|
||||
**Par défaut, le script SUPPRIME toutes les données de l'entité dans la base cible avant la migration.**
|
||||
|
||||
Cela inclut :
|
||||
- ✅ Toutes les opérations de l'entité
|
||||
- ✅ Tous les utilisateurs de l'entité
|
||||
- ✅ Tous les secteurs et passages
|
||||
- ✅ Tous les médias associés
|
||||
- ℹ️ L'entité elle-même est conservée (seules les données liées sont supprimées)
|
||||
|
||||
Pour **désactiver** la suppression et conserver les données existantes :
|
||||
```bash
|
||||
php php/migrate_from_backup.php --mode=entity --entity-id=45 --delete-before=false
|
||||
```
|
||||
|
||||
⚠️ **Attention** : Sans suppression préalable, risque de doublons si les données existent déjà.
|
||||
|
||||
---
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Migration d'une entité spécifique
|
||||
|
||||
#### Sur dva-geo (IN3)
|
||||
```bash
|
||||
# Auto-détection de l'environnement (recommandé)
|
||||
php php/migrate_from_backup.php --mode=entity --entity-id=45
|
||||
|
||||
# Ou avec environnement explicite
|
||||
php php/migrate_from_backup.php --env=dva --mode=entity --entity-id=45
|
||||
```
|
||||
|
||||
#### Sur rca-geo (IN3)
|
||||
```bash
|
||||
# Auto-détection de l'environnement (recommandé)
|
||||
php php/migrate_from_backup.php --mode=entity --entity-id=45
|
||||
|
||||
# Ou avec environnement explicite
|
||||
php php/migrate_from_backup.php --env=rca --mode=entity --entity-id=45
|
||||
```
|
||||
|
||||
#### Sur pra-geo (IN4)
|
||||
```bash
|
||||
# Auto-détection de l'environnement (recommandé)
|
||||
php php/migrate_from_backup.php --mode=entity --entity-id=45
|
||||
|
||||
# Ou avec environnement explicite
|
||||
php php/migrate_from_backup.php --env=pra --mode=entity --entity-id=45
|
||||
```
|
||||
|
||||
### Migration globale (toutes les entités)
|
||||
|
||||
```bash
|
||||
# Sur dva-geo, rca-geo ou pra-geo
|
||||
php php/migrate_from_backup.php --mode=global
|
||||
```
|
||||
|
||||
### Options disponibles
|
||||
|
||||
```bash
|
||||
--env=ENV # 'dva' (développement), 'rca' (recette) ou 'pra' (production)
|
||||
# Par défaut : auto-détection selon le hostname
|
||||
--mode=MODE # 'global' ou 'entity' (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 (défaut: true)
|
||||
--help # Afficher l'aide complète
|
||||
```
|
||||
|
||||
### Exemples d'utilisation
|
||||
|
||||
```bash
|
||||
# Migration STANDARD (avec suppression des données existantes - recommandé)
|
||||
php php/migrate_from_backup.php --mode=entity --entity-id=45
|
||||
|
||||
# Migration SANS suppression (pour ajout/mise à jour uniquement - risque de doublons)
|
||||
php php/migrate_from_backup.php --mode=entity --entity-id=45 --delete-before=false
|
||||
|
||||
# Migration avec log personnalisé
|
||||
php php/migrate_from_backup.php --mode=entity --entity-id=45 --log=/custom/path/entity_45.log
|
||||
|
||||
# Afficher l'aide complète
|
||||
php php/migrate_from_backup.php --help
|
||||
```
|
||||
|
||||
## Différences avec l'ancienne version
|
||||
|
||||
| Aspect | Ancien | Nouveau |
|
||||
|--------|--------|---------|
|
||||
| **Source** | `--source-db=geosector_YYYYMMDD_HH` | Toujours `geosector` (fixe) |
|
||||
| **Cible** | `--target-db=pra_geo` | Déduite de `--env` ou auto-détectée (dva_geo, rca_geo, pra_geo) |
|
||||
| **Config** | Constantes hardcodées | Classes configurables |
|
||||
| **Environnement** | Manuel | Auto-détection par hostname (dva-geo, rca-geo, pra-geo) |
|
||||
| **Arguments** | 2 arguments DB requis | 1 seul `--env` (optionnel) |
|
||||
|
||||
## Avantages
|
||||
|
||||
✅ **Plus simple** : Plus besoin de spécifier les noms de bases
|
||||
✅ **Plus sûr** : Moins de risques d'erreurs de saisie
|
||||
✅ **Plus flexible** : Fonctionne sur dva-geo, rca-geo et pra-geo sans modification
|
||||
✅ **Plus maintenable** : Configuration centralisée dans DatabaseConfig
|
||||
✅ **Meilleurs logs** : Séparateurs, niveaux (info/warning/error/success)
|
||||
|
||||
## Déploiement
|
||||
|
||||
### Copier vers dva-geo (IN3)
|
||||
```bash
|
||||
scp -r migration2 root@195.154.80.116:/tmp/
|
||||
ssh root@195.154.80.116 "incus file push -r /tmp/migration2 dva-geo/var/www/geosector/api/scripts/"
|
||||
```
|
||||
|
||||
### Copier vers rca-geo (IN3)
|
||||
```bash
|
||||
scp -r migration2 root@195.154.80.116:/tmp/
|
||||
ssh root@195.154.80.116 "incus file push -r /tmp/migration2 rca-geo/var/www/geosector/api/scripts/"
|
||||
```
|
||||
|
||||
### Copier vers pra-geo (IN4)
|
||||
```bash
|
||||
scp -r migration2 root@51.159.7.190:/tmp/
|
||||
ssh root@51.159.7.190 "incus file push -r /tmp/migration2 pra-geo/var/www/geosector/api/scripts/"
|
||||
```
|
||||
|
||||
### Test après déploiement
|
||||
|
||||
```bash
|
||||
# Se connecter au container
|
||||
incus exec dva-geo -- bash # ou rca-geo, ou pra-geo
|
||||
|
||||
# Tester avec une entité
|
||||
cd /var/www/geosector/api/scripts/migration2
|
||||
php php/migrate_from_backup.php --mode=entity --entity-id=45
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
Les logs sont enregistrés par défaut dans :
|
||||
```
|
||||
scripts/migration2/logs/migration_[MODE]_YYYYMMDD_HHMMSS.log
|
||||
```
|
||||
|
||||
**Nommage automatique selon le mode :**
|
||||
- Migration globale : `migration_global_20251021_143045.log`
|
||||
- Migration d'une entité : `migration_entite_45_20251021_143045.log`
|
||||
|
||||
Format des logs :
|
||||
- `[INFO]` : Informations générales
|
||||
- `[SUCCESS]` : Opérations réussies
|
||||
- `[WARNING]` : Avertissements
|
||||
- `[ERROR]` : Erreurs
|
||||
|
||||
Le dossier `logs/` est créé automatiquement si nécessaire.
|
||||
|
||||
**Note :** Vous pouvez toujours spécifier un fichier de log personnalisé avec l'option `--log=PATH`.
|
||||
|
||||
## Récapitulatif de migration
|
||||
|
||||
À la fin de chaque migration, un **récapitulatif détaillé** est automatiquement affiché et enregistré dans le fichier de log.
|
||||
|
||||
### Format du récapitulatif
|
||||
|
||||
```
|
||||
========================================
|
||||
📊 RÉCAPITULATIF DE LA MIGRATION
|
||||
========================================
|
||||
Entité: Nom de l'entité (ID: XX)
|
||||
Date: YYYY-MM-DD HH:MM:SS
|
||||
|
||||
Opérations migrées: 3
|
||||
|
||||
Opération #1: "Adhésions 2024" (ID: 850)
|
||||
├─ Utilisateurs: 12
|
||||
├─ Secteurs: 5
|
||||
├─ Passages totaux: 245
|
||||
└─ Détail par secteur:
|
||||
├─ Centre-ville (ID: 5400)
|
||||
│ ├─ Utilisateurs affectés: 3
|
||||
│ └─ Passages: 67
|
||||
├─ Quartier Est (ID: 5401)
|
||||
│ ├─ Utilisateurs affectés: 5
|
||||
│ └─ Passages: 98
|
||||
└─ Nord (ID: 5402)
|
||||
├─ Utilisateurs affectés: 4
|
||||
└─ Passages: 80
|
||||
|
||||
Opération #2: "Collecte Printemps" (ID: 851)
|
||||
├─ Utilisateurs: 8
|
||||
├─ Secteurs: 3
|
||||
├─ Passages totaux: 156
|
||||
└─ Détail par secteur:
|
||||
[...]
|
||||
|
||||
========================================
|
||||
```
|
||||
|
||||
### Informations fournies
|
||||
|
||||
Le récapitulatif inclut pour chaque migration :
|
||||
|
||||
**Au niveau de l'entité :**
|
||||
- Nom et ID de l'entité
|
||||
- Date et heure de la migration
|
||||
- Nombre total d'opérations migrées
|
||||
|
||||
**Pour chaque opération :**
|
||||
- Nom et nouvel ID
|
||||
- Nombre d'utilisateurs migrés
|
||||
- Nombre de secteurs migrés
|
||||
- Nombre total de passages migrés
|
||||
|
||||
**Pour chaque secteur :**
|
||||
- Nom et nouvel ID
|
||||
- Nombre d'utilisateurs affectés au secteur
|
||||
- Nombre de passages effectués dans le secteur
|
||||
|
||||
### Utilisation du récapitulatif
|
||||
|
||||
Ce récapitulatif permet de :
|
||||
- ✅ Vérifier rapidement que toutes les données ont été migrées
|
||||
- ✅ Comparer avec les données source pour validation
|
||||
- ✅ Identifier d'éventuelles anomalies (secteurs vides, passages manquants)
|
||||
- ✅ Documenter précisément ce qui a été migré
|
||||
- ✅ Tracer les migrations pour audit
|
||||
|
||||
Le récapitulatif est présent à la fois :
|
||||
- **À l'écran** (stdout) en temps réel
|
||||
- **Dans le fichier de log** pour conservation
|
||||
|
||||
## Dépannage
|
||||
|
||||
### Erreur "env doit être 'dva', 'rca' ou 'pra'"
|
||||
L'auto-détection a échoué. Spécifiez manuellement :
|
||||
```bash
|
||||
php php/migrate_from_backup.php --env=dva --mode=entity --entity-id=45
|
||||
```
|
||||
|
||||
### Erreur de connexion
|
||||
Vérifiez que vous êtes bien dans le bon container (dva-geo, rca-geo ou pra-geo).
|
||||
|
||||
### Données dupliquées après migration
|
||||
Si vous avez des doublons, c'est que vous avez utilisé `--delete-before=false` sur des données existantes.
|
||||
|
||||
**Solution** : Refaire la migration avec suppression (défaut) :
|
||||
```bash
|
||||
php php/migrate_from_backup.php --mode=entity --entity-id=45
|
||||
```
|
||||
|
||||
### Vérifier ce qui sera supprimé avant migration
|
||||
Consultez la section "Ordre de suppression" ci-dessous pour voir exactement quelles tables seront affectées.
|
||||
|
||||
### Logs non créés
|
||||
Vérifiez les permissions du dossier `logs/` :
|
||||
```bash
|
||||
ls -la scripts/migration2/logs/
|
||||
```
|
||||
|
||||
## Détails techniques
|
||||
|
||||
### Architecture hiérarchique de migration
|
||||
|
||||
La migration fonctionne par **opération** avec une hiérarchie complète :
|
||||
|
||||
```
|
||||
Pour chaque opération de l'entité:
|
||||
migrateOperation($oldOperationId)
|
||||
├── Créer operation
|
||||
├── Migrer ope_users (DISTINCT depuis ope_users_sectors)
|
||||
│ └── Mapper oldUserId → newOpeUserId
|
||||
├── Pour chaque secteur DISTINCT de l'opération:
|
||||
│ └── migrateSector($oldOperationId, $newOperationId, $oldSectorId)
|
||||
│ ├── Créer ope_sectors
|
||||
│ ├── Mapper "opId_sectId" → newOpeSectorId
|
||||
│ ├── Migrer sectors_adresses (fk_sector = newOpeSectorId)
|
||||
│ ├── Migrer ope_users_sectors (avec mappings users + sector)
|
||||
│ ├── Migrer ope_pass (avec mappings users + sector)
|
||||
│ │ └── Pour chaque passage:
|
||||
│ │ └── migratePassageHisto($oldPassId, $newPassId)
|
||||
│ └── Migrer médias des passages
|
||||
└── Migrer médias de l'opération
|
||||
```
|
||||
|
||||
### Changement d'organisation des données : Exemple concret
|
||||
|
||||
#### Contexte : Opération de collecte des adhésions 2024
|
||||
|
||||
**Ancienne organisation** (base geosector - partagée) :
|
||||
- 1 opération "Adhésions 2024" avec ID 450
|
||||
- 3 utilisateurs affectés : Jean (ID 100), Marie (ID 101), Paul (ID 102)
|
||||
- 2 secteurs utilisés : Centre-ville (ID 1004) et Quartier Est (ID 1005)
|
||||
- Jean travaille sur Centre-ville, Marie et Paul sur Quartier Est
|
||||
|
||||
Dans l'ancienne base :
|
||||
- Les 3 users existent UNE SEULE FOIS dans la table centrale `users`
|
||||
- Les 2 secteurs existent UNE SEULE FOIS dans la table centrale `sectors`
|
||||
- Les liens entre users et secteurs sont dans `ope_users_sectors`
|
||||
- Les passages font référence directement aux users (ID 100, 101, 102)
|
||||
|
||||
**Nouvelle organisation** (base rca_geo/pra_geo - isolée par opération) :
|
||||
|
||||
Après migration, **CHAQUE opération devient autonome** :
|
||||
- L'opération "Adhésions 2024" reçoit un nouvel ID (exemple : 850)
|
||||
- Les 3 utilisateurs sont **dupliqués** dans `ope_users` avec de nouveaux IDs :
|
||||
- Jean → ope_users.id = 2500 (avec fk_user = 100 et fk_operation = 850)
|
||||
- Marie → ope_users.id = 2501 (avec fk_user = 101 et fk_operation = 850)
|
||||
- Paul → ope_users.id = 2502 (avec fk_user = 102 et fk_operation = 850)
|
||||
- Les 2 secteurs sont **dupliqués** dans `ope_sectors` :
|
||||
- Centre-ville → ope_sectors.id = 5400 (avec fk_operation = 850)
|
||||
- Quartier Est → ope_sectors.id = 5401 (avec fk_operation = 850)
|
||||
- Tous les passages sont mis à jour pour référencer les NOUVEAUX IDs (2500, 2501, 2502)
|
||||
|
||||
**Pourquoi cette duplication ?**
|
||||
|
||||
✅ **Isolation complète** : Si l'opération est supprimée, tout part avec (secteurs, users, passages)
|
||||
✅ **Performance** : Pas de jointures complexes entre opérations
|
||||
✅ **Historique** : Les données de l'opération restent figées dans le temps
|
||||
✅ **Simplicité** : Chaque opération est indépendante
|
||||
|
||||
**Impact pour un utilisateur qui travaille sur 3 opérations différentes** :
|
||||
- Il existera 1 seule fois dans la table centrale `users` (ID 100)
|
||||
- Il existera 3 fois dans `ope_users` (1 enregistrement par opération)
|
||||
- Chaque enregistrement `ope_users` garde la référence vers `users.id = 100`
|
||||
|
||||
Cette architecture permet de **fermer** une opération complètement sans impacter les autres.
|
||||
|
||||
### Sélection des opérations à migrer
|
||||
|
||||
Pour chaque entité, **maximum 3 opérations** sont migrées :
|
||||
1. **1 opération active** (`active = 1`)
|
||||
2. **2 dernières opérations inactives** (`active = 0`) ayant au moins **10 passages effectués** (`fk_type = 1`)
|
||||
|
||||
### Ordre de suppression (si --delete-before=true)
|
||||
|
||||
Les données sont supprimées dans cet ordre pour respecter les contraintes de clés étrangères :
|
||||
|
||||
1. `medias` - Médias associés à l'entité ou aux opérations
|
||||
2. `ope_pass_histo` - Historique des passages
|
||||
3. `ope_pass` - Passages
|
||||
4. `ope_users_sectors` - Associations utilisateurs/secteurs
|
||||
5. `ope_users` - Utilisateurs d'opérations
|
||||
6. `sectors_adresses` - Adresses de secteurs
|
||||
7. `ope_sectors` - Secteurs d'opérations
|
||||
8. `operations` - Opérations
|
||||
9. `users` - Utilisateurs de l'entité
|
||||
|
||||
⚠️ **L'entité elle-même** (`entites`) **n'est jamais supprimée**.
|
||||
|
||||
### Tables de référence non migrées
|
||||
|
||||
Les tables suivantes ne sont **pas** migrées car déjà remplies dans la cible :
|
||||
- `x_*` - Tables de référence (secteurs, adresses, etc.)
|
||||
|
||||
## Notes importantes
|
||||
|
||||
1. **Configuration centralisée** : Les paramètres de connexion DB sont récupérés depuis `AppConfig.php` - pas de duplication
|
||||
2. **Chiffrement** : ApiService est toujours utilisé pour les mots de passe
|
||||
3. **Logique métier** : Inchangée (migrateEntites, migrateUsers, etc.)
|
||||
4. **Mappings** : Secteurs et adresses sont toujours mappés automatiquement
|
||||
5. **Backup** : Un backup de l'ancien script est disponible dans `migrate_from_backup.php.backup`
|
||||
6. **Suppression par défaut** : Activée pour éviter les doublons et garantir une migration propre
|
||||
|
||||
## Statut
|
||||
|
||||
**Architecture modulaire v2** :
|
||||
- ✅ DatabaseConfig.php - Configuration multi-environnement
|
||||
- ✅ MigrationLogger.php - Gestion des logs
|
||||
- ✅ DatabaseConnection.php - Connexions PDO
|
||||
- ✅ OperationMigrator.php - Migration hiérarchique des opérations
|
||||
- ✅ UserMigrator.php - Migration des utilisateurs par opération
|
||||
- ✅ SectorMigrator.php - Migration des secteurs par opération
|
||||
- ✅ PassageMigrator.php - Migration des passages et historiques
|
||||
- ✅ migrate_from_backup.php - Script principal orchestrateur
|
||||
- ⏳ Tests sur rca-geo
|
||||
- ⏳ Tests sur pra-geo
|
||||
|
||||
## Support
|
||||
|
||||
En cas de problème, consulter les logs détaillés ou contacter l'équipe technique.
|
||||
1199
api/scripts/migration2/geo_app_structure.sql
Normal file
1199
api/scripts/migration2/geo_app_structure.sql
Normal file
File diff suppressed because it is too large
Load Diff
1088
api/scripts/migration2/geosector-structure.sql
Normal file
1088
api/scripts/migration2/geosector-structure.sql
Normal file
File diff suppressed because it is too large
Load Diff
1
api/scripts/migration2/logs/.gitignore
vendored
Normal file
1
api/scripts/migration2/logs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.log
|
||||
467
api/scripts/migration2/migrate_batch.sh
Executable file
467
api/scripts/migration2/migrate_batch.sh
Executable file
@@ -0,0 +1,467 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Script de migration en batch des entités depuis geosector_20251008
|
||||
#
|
||||
# Usage: ./migrate_batch.sh [options]
|
||||
#
|
||||
# Options:
|
||||
# --start N Commencer à partir de l'entité N (défaut: 1)
|
||||
# --limit N Migrer seulement N entités (défaut: toutes)
|
||||
# --dry-run Simuler sans exécuter
|
||||
# --continue Continuer après une erreur (défaut: s'arrêter)
|
||||
# --interactive Mode interactif (défaut si aucune option)
|
||||
#
|
||||
# Exemple:
|
||||
# ./migrate_batch.sh --start 10 --limit 5
|
||||
# ./migrate_batch.sh --continue
|
||||
# ./migrate_batch.sh --interactive
|
||||
###############################################################################
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
JSON_FILE="${SCRIPT_DIR}/migrations_entites.json"
|
||||
LOG_DIR="/var/www/geosector/api/logs/migrations"
|
||||
MIGRATION_SCRIPT="${SCRIPT_DIR}/php/migrate_from_backup.php"
|
||||
SOURCE_DB="geosector_20251013_13"
|
||||
TARGET_DB="pra_geo"
|
||||
|
||||
# Paramètres par défaut
|
||||
START_INDEX=1
|
||||
LIMIT=0
|
||||
DRY_RUN=0
|
||||
CONTINUE_ON_ERROR=0
|
||||
INTERACTIVE_MODE=0
|
||||
SPECIFIC_ENTITY_ID=""
|
||||
SPECIFIC_CP=""
|
||||
|
||||
# Couleurs
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Sauvegarder le nombre d'arguments avant le parsing
|
||||
INITIAL_ARGS=$#
|
||||
|
||||
# Parse des arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--start)
|
||||
START_INDEX="$2"
|
||||
shift 2
|
||||
;;
|
||||
--limit)
|
||||
LIMIT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
--continue)
|
||||
CONTINUE_ON_ERROR=1
|
||||
shift
|
||||
;;
|
||||
--interactive|-i)
|
||||
INTERACTIVE_MODE=1
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
grep "^#" "$0" | grep -v "^#!/bin/bash" | sed 's/^# //'
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Option inconnue: $1"
|
||||
echo "Utilisez --help pour l'aide"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Activer le mode interactif si aucun argument n'a été fourni
|
||||
if [ $INITIAL_ARGS -eq 0 ]; then
|
||||
INTERACTIVE_MODE=1
|
||||
fi
|
||||
|
||||
# Vérifications préalables
|
||||
if [ ! -f "$JSON_FILE" ]; then
|
||||
echo -e "${RED}❌ Fichier JSON introuvable: $JSON_FILE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$MIGRATION_SCRIPT" ]; then
|
||||
echo -e "${RED}❌ Script de migration introuvable: $MIGRATION_SCRIPT${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Créer le répertoire de logs
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# Fichiers de log
|
||||
BATCH_LOG="${LOG_DIR}/batch_$(date +%Y%m%d_%H%M%S).log"
|
||||
SUCCESS_LOG="${LOG_DIR}/success.log"
|
||||
ERROR_LOG="${LOG_DIR}/errors.log"
|
||||
|
||||
# MODE INTERACTIF
|
||||
if [ $INTERACTIVE_MODE -eq 1 ]; then
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} 🔧 Mode interactif - Migration d'entités GeoSector${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# Question 1: Migration globale ou ciblée ?
|
||||
echo -e "${YELLOW}1️⃣ Type de migration :${NC}"
|
||||
echo -e " ${CYAN}a)${NC} Migration globale (toutes les entités)"
|
||||
echo -e " ${CYAN}b)${NC} Migration par lot (plage d'entités)"
|
||||
echo -e " ${CYAN}c)${NC} Migration par code postal"
|
||||
echo -e " ${CYAN}d)${NC} Migration d'une entité spécifique (ID)"
|
||||
echo ""
|
||||
echo -ne "${YELLOW}Votre choix (a/b/c/d) : ${NC}"
|
||||
read -r MIGRATION_TYPE
|
||||
echo ""
|
||||
|
||||
case $MIGRATION_TYPE in
|
||||
a|A)
|
||||
# Migration globale - garder les valeurs par défaut
|
||||
START_INDEX=1
|
||||
LIMIT=0
|
||||
echo -e "${GREEN}✓${NC} Migration globale sélectionnée"
|
||||
;;
|
||||
b|B)
|
||||
# Migration par lot
|
||||
echo -e "${YELLOW}2️⃣ Configuration du lot :${NC}"
|
||||
echo -ne " Première entité (index, défaut=1) : "
|
||||
read -r USER_START
|
||||
if [ -n "$USER_START" ]; then
|
||||
START_INDEX=$USER_START
|
||||
fi
|
||||
|
||||
echo -ne " Limite (nombre d'entités, défaut=toutes) : "
|
||||
read -r USER_LIMIT
|
||||
if [ -n "$USER_LIMIT" ]; then
|
||||
LIMIT=$USER_LIMIT
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${GREEN}✓${NC} Migration par lot : de l'index $START_INDEX, limite de $LIMIT entités"
|
||||
;;
|
||||
c|C)
|
||||
# Migration par code postal
|
||||
echo -ne "${YELLOW}2️⃣ Code postal à migrer : ${NC}"
|
||||
read -r SPECIFIC_CP
|
||||
echo ""
|
||||
if [ -z "$SPECIFIC_CP" ]; then
|
||||
echo -e "${RED}❌ Code postal requis${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓${NC} Migration pour le code postal : $SPECIFIC_CP"
|
||||
;;
|
||||
d|D)
|
||||
# Migration d'une entité spécifique - bypass complet du JSON
|
||||
echo -ne "${YELLOW}2️⃣ ID de l'entité à migrer : ${NC}"
|
||||
read -r SPECIFIC_ENTITY_ID
|
||||
echo ""
|
||||
if [ -z "$SPECIFIC_ENTITY_ID" ]; then
|
||||
echo -e "${RED}❌ ID d'entité requis${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓${NC} Migration de l'entité ID : $SPECIFIC_ENTITY_ID"
|
||||
echo ""
|
||||
|
||||
# Demander si suppression des données de l'entité avant migration
|
||||
echo -ne "${YELLOW}3️⃣ Supprimer les données existantes de cette entité dans la TARGET avant migration ? (y/N): ${NC}"
|
||||
read -r DELETE_BEFORE
|
||||
DELETE_FLAG=""
|
||||
if [[ $DELETE_BEFORE =~ ^[Yy]$ ]]; then
|
||||
echo -e "${GREEN}✓${NC} Les données seront supprimées avant migration"
|
||||
DELETE_FLAG="--delete-before"
|
||||
else
|
||||
echo -e "${BLUE}ℹ${NC} Les données seront conservées (ON DUPLICATE KEY UPDATE)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Confirmer la migration
|
||||
echo -ne "${YELLOW}⚠️ Confirmer la migration de l'entité #${SPECIFIC_ENTITY_ID} ? (y/N): ${NC}"
|
||||
read -r CONFIRM
|
||||
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
|
||||
echo -e "${RED}❌ Migration annulée${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Exécuter directement la migration sans passer par le JSON
|
||||
ENTITY_LOG="${LOG_DIR}/entity_${SPECIFIC_ENTITY_ID}_$(date +%Y%m%d_%H%M%S).log"
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}⏳ Migration de l'entité #${SPECIFIC_ENTITY_ID} en cours...${NC}"
|
||||
|
||||
php "$MIGRATION_SCRIPT" \
|
||||
--source-db="$SOURCE_DB" \
|
||||
--target-db="$TARGET_DB" \
|
||||
--mode=entity \
|
||||
--entity-id="$SPECIFIC_ENTITY_ID" \
|
||||
--log="$ENTITY_LOG" \
|
||||
$DELETE_FLAG
|
||||
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo -e "${GREEN}✅ Entité #${SPECIFIC_ENTITY_ID} migrée avec succès${NC}"
|
||||
echo -e "${BLUE}📋 Log détaillé : $ENTITY_LOG${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Erreur lors de la migration de l'entité #${SPECIFIC_ENTITY_ID}${NC}"
|
||||
echo -e "${RED}📋 Voir le log : $ENTITY_LOG${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}❌ Choix invalide${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Fonctions utilitaires
|
||||
log() {
|
||||
echo -e "$1" | tee -a "$BATCH_LOG"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo "$1" >> "$SUCCESS_LOG"
|
||||
log "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "$1" >> "$ERROR_LOG"
|
||||
log "${RED}✗${NC} $1"
|
||||
}
|
||||
|
||||
# Extraire les entity_id du JSON (compatible sans jq)
|
||||
get_entity_ids() {
|
||||
if [ -n "$SPECIFIC_ENTITY_ID" ]; then
|
||||
# Entité spécifique par ID - chercher exactement "entity_id" : ID,
|
||||
grep "\"entity_id\" : ${SPECIFIC_ENTITY_ID}," "$JSON_FILE" | sed 's/.*: \([0-9]*\).*/\1/'
|
||||
elif [ -n "$SPECIFIC_CP" ]; then
|
||||
# Entités par code postal
|
||||
grep -B 2 "\"code_postal\" : \"$SPECIFIC_CP\"" "$JSON_FILE" | grep '"entity_id"' | sed 's/.*: \([0-9]*\).*/\1/'
|
||||
else
|
||||
# Toutes les entités
|
||||
grep '"entity_id"' "$JSON_FILE" | sed 's/.*: \([0-9]*\).*/\1/'
|
||||
fi
|
||||
}
|
||||
|
||||
# Compter le nombre total d'entités
|
||||
TOTAL_ENTITIES=$(get_entity_ids | wc -l)
|
||||
|
||||
# Vérifier si des entités ont été trouvées
|
||||
if [ $TOTAL_ENTITIES -eq 0 ]; then
|
||||
if [ -n "$SPECIFIC_ENTITY_ID" ]; then
|
||||
echo -e "${RED}❌ Entité #${SPECIFIC_ENTITY_ID} introuvable dans le fichier JSON${NC}"
|
||||
elif [ -n "$SPECIFIC_CP" ]; then
|
||||
echo -e "${RED}❌ Aucune entité trouvée pour le code postal ${SPECIFIC_CP}${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Aucune entité trouvée${NC}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Calculer le nombre d'entités à migrer
|
||||
if [ $LIMIT -gt 0 ]; then
|
||||
END_INDEX=$((START_INDEX + LIMIT - 1))
|
||||
if [ $END_INDEX -gt $TOTAL_ENTITIES ]; then
|
||||
END_INDEX=$TOTAL_ENTITIES
|
||||
fi
|
||||
else
|
||||
END_INDEX=$TOTAL_ENTITIES
|
||||
fi
|
||||
|
||||
# Bannière de démarrage
|
||||
echo ""
|
||||
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
||||
log "${BLUE} Migration en batch des entités GeoSector${NC}"
|
||||
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
||||
log "📅 Date: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
log "📁 Source: $SOURCE_DB"
|
||||
log "📁 Cible: $TARGET_DB"
|
||||
|
||||
# Afficher les informations selon le mode
|
||||
if [ -n "$SPECIFIC_ENTITY_ID" ]; then
|
||||
log "🎯 Mode: Migration d'une entité spécifique"
|
||||
log "📊 Entité ID: $SPECIFIC_ENTITY_ID"
|
||||
elif [ -n "$SPECIFIC_CP" ]; then
|
||||
log "🎯 Mode: Migration par code postal"
|
||||
log "📮 Code postal: $SPECIFIC_CP"
|
||||
log "📊 Entités trouvées: $TOTAL_ENTITIES"
|
||||
else
|
||||
TOTAL_AVAILABLE=$(grep '"entity_id"' "$JSON_FILE" | wc -l)
|
||||
log "📊 Total entités disponibles: $TOTAL_AVAILABLE"
|
||||
log "🎯 Entités à migrer: $START_INDEX à $END_INDEX"
|
||||
fi
|
||||
|
||||
if [ $DRY_RUN -eq 1 ]; then
|
||||
log "${YELLOW}🔍 Mode DRY-RUN (simulation)${NC}"
|
||||
fi
|
||||
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# Confirmation utilisateur
|
||||
if [ $DRY_RUN -eq 0 ]; then
|
||||
echo -ne "${YELLOW}⚠️ Confirmer la migration ? (y/N): ${NC}"
|
||||
read -r CONFIRM
|
||||
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
|
||||
log "❌ Migration annulée par l'utilisateur"
|
||||
exit 0
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Compteurs
|
||||
SUCCESS_COUNT=0
|
||||
ERROR_COUNT=0
|
||||
SKIPPED_COUNT=0
|
||||
CURRENT_INDEX=0
|
||||
|
||||
# Début de la migration
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
# Lire les entity_id et migrer
|
||||
get_entity_ids | while read -r ENTITY_ID; do
|
||||
CURRENT_INDEX=$((CURRENT_INDEX + 1))
|
||||
|
||||
# Filtrer par index
|
||||
if [ $CURRENT_INDEX -lt $START_INDEX ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ $CURRENT_INDEX -gt $END_INDEX ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# Récupérer les détails de l'entité depuis le JSON (match exact avec la virgule)
|
||||
ENTITY_INFO=$(grep -A 8 "\"entity_id\" : ${ENTITY_ID}," "$JSON_FILE")
|
||||
ENTITY_NAME=$(echo "$ENTITY_INFO" | grep '"nom"' | sed 's/.*: "\(.*\)".*/\1/')
|
||||
ENTITY_CP=$(echo "$ENTITY_INFO" | grep '"code_postal"' | sed 's/.*: "\(.*\)".*/\1/')
|
||||
NB_USERS=$(echo "$ENTITY_INFO" | grep '"nb_users"' | sed 's/.*: \([0-9]*\).*/\1/')
|
||||
NB_PASSAGES=$(echo "$ENTITY_INFO" | grep '"nb_passages"' | sed 's/.*: \([0-9]*\).*/\1/')
|
||||
|
||||
# Afficher la progression
|
||||
PROGRESS=$((CURRENT_INDEX - START_INDEX + 1))
|
||||
TOTAL=$((END_INDEX - START_INDEX + 1))
|
||||
PERCENT=$((PROGRESS * 100 / TOTAL))
|
||||
|
||||
log ""
|
||||
log "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
log "${BLUE}[$PROGRESS/$TOTAL - ${PERCENT}%]${NC} Entité #${ENTITY_ID}: ${ENTITY_NAME} (${ENTITY_CP})"
|
||||
log " 👥 Users: ${NB_USERS} | 📍 Passages: ${NB_PASSAGES}"
|
||||
|
||||
# Mode dry-run
|
||||
if [ $DRY_RUN -eq 1 ]; then
|
||||
log "${YELLOW} 🔍 [DRY-RUN] Simulation de la migration${NC}"
|
||||
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Exécuter la migration
|
||||
ENTITY_LOG="${LOG_DIR}/entity_${ENTITY_ID}_$(date +%Y%m%d_%H%M%S).log"
|
||||
|
||||
log " ⏳ Migration en cours..."
|
||||
php "$MIGRATION_SCRIPT" \
|
||||
--source-db="$SOURCE_DB" \
|
||||
--target-db="$TARGET_DB" \
|
||||
--mode=entity \
|
||||
--entity-id="$ENTITY_ID" \
|
||||
--log="$ENTITY_LOG" > /tmp/migration_output_$$.txt 2>&1
|
||||
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
# Succès
|
||||
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||
log_success "Entité #${ENTITY_ID} (${ENTITY_NAME}) migrée avec succès"
|
||||
|
||||
# Afficher un résumé du log avec détails
|
||||
if [ -f "$ENTITY_LOG" ]; then
|
||||
# Chercher la ligne avec les marqueurs #STATS#
|
||||
STATS_LINE=$(grep "#STATS#" "$ENTITY_LOG" 2>/dev/null)
|
||||
|
||||
if [ -n "$STATS_LINE" ]; then
|
||||
# Extraire chaque compteur
|
||||
OPE=$(echo "$STATS_LINE" | grep -oE 'OPE:[0-9]+' | cut -d: -f2)
|
||||
USERS=$(echo "$STATS_LINE" | grep -oE 'USER:[0-9]+' | cut -d: -f2)
|
||||
SECTORS=$(echo "$STATS_LINE" | grep -oE 'SECTOR:[0-9]+' | cut -d: -f2)
|
||||
PASSAGES=$(echo "$STATS_LINE" | grep -oE 'PASS:[0-9]+' | cut -d: -f2)
|
||||
|
||||
# Valeurs par défaut si extraction échoue
|
||||
OPE=${OPE:-0}
|
||||
USERS=${USERS:-0}
|
||||
SECTORS=${SECTORS:-0}
|
||||
PASSAGES=${PASSAGES:-0}
|
||||
|
||||
log " 📊 ope: ${OPE} | users: ${USERS} | sectors: ${SECTORS} | passages: ${PASSAGES}"
|
||||
else
|
||||
log " 📊 Statistiques non disponibles"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Erreur
|
||||
ERROR_COUNT=$((ERROR_COUNT + 1))
|
||||
log_error "Entité #${ENTITY_ID} (${ENTITY_NAME}) - Erreur code $EXIT_CODE"
|
||||
|
||||
# Afficher les dernières lignes du log d'erreur
|
||||
if [ -f "/tmp/migration_output_$$.txt" ]; then
|
||||
log "${RED} 📋 Dernières erreurs:${NC}"
|
||||
tail -5 "/tmp/migration_output_$$.txt" | sed 's/^/ /' | tee -a "$BATCH_LOG"
|
||||
fi
|
||||
|
||||
# Arrêter ou continuer ?
|
||||
if [ $CONTINUE_ON_ERROR -eq 0 ]; then
|
||||
log ""
|
||||
log "${RED}❌ Migration interrompue suite à une erreur${NC}"
|
||||
log " Utilisez --continue pour continuer malgré les erreurs"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Nettoyage
|
||||
rm -f "/tmp/migration_output_$$.txt"
|
||||
|
||||
# Pause entre les migrations (pour éviter de surcharger)
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Fin de la migration
|
||||
END_TIME=$(date +%s)
|
||||
DURATION=$((END_TIME - START_TIME))
|
||||
HOURS=$((DURATION / 3600))
|
||||
MINUTES=$(((DURATION % 3600) / 60))
|
||||
SECONDS=$((DURATION % 60))
|
||||
|
||||
# Résumé final
|
||||
log ""
|
||||
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
||||
log "${BLUE} Résumé de la migration${NC}"
|
||||
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
||||
log "✅ Succès: ${GREEN}${SUCCESS_COUNT}${NC}"
|
||||
log "❌ Erreurs: ${RED}${ERROR_COUNT}${NC}"
|
||||
log "⏭️ Ignorées: ${YELLOW}${SKIPPED_COUNT}${NC}"
|
||||
log "⏱️ Durée: ${HOURS}h ${MINUTES}m ${SECONDS}s"
|
||||
log ""
|
||||
log "📋 Logs détaillés:"
|
||||
log " - Batch: $BATCH_LOG"
|
||||
log " - Succès: $SUCCESS_LOG"
|
||||
log " - Erreurs: $ERROR_LOG"
|
||||
log " - Individuels: $LOG_DIR/entity_*.log"
|
||||
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
|
||||
|
||||
# Code de sortie
|
||||
if [ $ERROR_COUNT -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
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
@@ -1,57 +0,0 @@
|
||||
-- Migration : Ajout des champs manquants dans email_queue
|
||||
-- Date : 2025-01-06
|
||||
-- Description : Ajoute sent_at et error_message pour le bon fonctionnement du CRON
|
||||
|
||||
USE geo_app;
|
||||
|
||||
-- Vérifier si les champs existent déjà avant de les ajouter
|
||||
SET @db_name = DATABASE();
|
||||
SET @table_name = 'email_queue';
|
||||
|
||||
-- Ajouter sent_at si n'existe pas
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name
|
||||
AND TABLE_NAME = @table_name
|
||||
AND COLUMN_NAME = 'sent_at'
|
||||
);
|
||||
|
||||
SET @sql = IF(@column_exists = 0,
|
||||
'ALTER TABLE email_queue ADD COLUMN sent_at TIMESTAMP NULL DEFAULT NULL AFTER status',
|
||||
'SELECT "Column sent_at already exists" AS message'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Ajouter error_message si n'existe pas
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name
|
||||
AND TABLE_NAME = @table_name
|
||||
AND COLUMN_NAME = 'error_message'
|
||||
);
|
||||
|
||||
SET @sql = IF(@column_exists = 0,
|
||||
'ALTER TABLE email_queue ADD COLUMN error_message TEXT NULL DEFAULT NULL AFTER attempts',
|
||||
'SELECT "Column error_message already exists" AS message'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Vérifier le résultat
|
||||
SELECT
|
||||
'Migration terminée' AS status,
|
||||
COLUMN_NAME,
|
||||
COLUMN_TYPE,
|
||||
IS_NULLABLE,
|
||||
COLUMN_DEFAULT
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name
|
||||
AND TABLE_NAME = @table_name
|
||||
AND COLUMN_NAME IN ('sent_at', 'error_message');
|
||||
@@ -1,94 +0,0 @@
|
||||
-- =====================================================
|
||||
-- Migration Stripe : is_striped → stripe_payment_id
|
||||
-- Date : Janvier 2025
|
||||
-- Description : Refactoring pour simplifier la gestion des paiements Stripe
|
||||
-- =====================================================
|
||||
|
||||
-- 1. Modifier la table ope_pass
|
||||
-- ------------------------------
|
||||
ALTER TABLE `ope_pass` DROP COLUMN IF EXISTS `chk_striped`;
|
||||
ALTER TABLE `ope_pass` ADD COLUMN `stripe_payment_id` VARCHAR(50) DEFAULT NULL COMMENT 'ID du PaymentIntent Stripe (pi_xxx)';
|
||||
ALTER TABLE `ope_pass` ADD INDEX `idx_stripe_payment` (`stripe_payment_id`);
|
||||
|
||||
-- 2. Modifier stripe_payment_history pour la rendre indépendante
|
||||
-- ----------------------------------------------------------------
|
||||
-- Supprimer la clé étrangère vers stripe_payment_intents
|
||||
ALTER TABLE `stripe_payment_history`
|
||||
DROP FOREIGN KEY IF EXISTS `stripe_payment_history_ibfk_1`;
|
||||
|
||||
-- Modifier la colonne pour stocker directement l'ID Stripe (totalement indépendante)
|
||||
ALTER TABLE `stripe_payment_history`
|
||||
DROP INDEX IF EXISTS `idx_fk_payment_intent`,
|
||||
CHANGE COLUMN `fk_payment_intent` `stripe_payment_intent_id` VARCHAR(255) DEFAULT NULL COMMENT 'ID du PaymentIntent Stripe',
|
||||
ADD INDEX `idx_stripe_payment_intent_id` (`stripe_payment_intent_id`);
|
||||
|
||||
-- 3. Modifier stripe_refunds pour la rendre indépendante
|
||||
-- --------------------------------------------------------
|
||||
ALTER TABLE `stripe_refunds`
|
||||
DROP FOREIGN KEY IF EXISTS `stripe_refunds_ibfk_1`;
|
||||
|
||||
-- Modifier la colonne pour stocker directement l'ID Stripe (totalement indépendante)
|
||||
ALTER TABLE `stripe_refunds`
|
||||
DROP INDEX IF EXISTS `idx_fk_payment_intent`,
|
||||
CHANGE COLUMN `fk_payment_intent` `stripe_payment_intent_id` VARCHAR(255) NOT NULL COMMENT 'ID du PaymentIntent Stripe',
|
||||
ADD INDEX `idx_stripe_payment_intent_id` (`stripe_payment_intent_id`);
|
||||
|
||||
-- 4. Supprimer la vue qui dépend de stripe_payment_intents
|
||||
-- ----------------------------------------------------------
|
||||
DROP VIEW IF EXISTS `v_stripe_payment_stats`;
|
||||
|
||||
-- 5. Supprimer la table stripe_payment_intents
|
||||
-- ---------------------------------------------
|
||||
DROP TABLE IF EXISTS `stripe_payment_intents`;
|
||||
|
||||
-- 6. Créer une nouvelle vue basée sur ope_pass
|
||||
-- ----------------------------------------------
|
||||
CREATE OR REPLACE VIEW `v_stripe_payment_stats` AS
|
||||
SELECT
|
||||
o.fk_entite,
|
||||
e.encrypted_name as entite_name,
|
||||
p.fk_user,
|
||||
CONCAT(u.first_name, ' ', u.sect_name) as user_name,
|
||||
COUNT(DISTINCT p.id) as total_ventes,
|
||||
COUNT(DISTINCT CASE WHEN p.stripe_payment_id IS NOT NULL THEN p.id END) as ventes_stripe,
|
||||
SUM(CASE WHEN p.stripe_payment_id IS NOT NULL THEN p.montant ELSE 0 END) as montant_stripe,
|
||||
SUM(CASE WHEN p.stripe_payment_id IS NULL THEN p.montant ELSE 0 END) as montant_autres,
|
||||
DATE(p.created_at) as date_vente
|
||||
FROM ope_pass p
|
||||
LEFT JOIN operations o ON p.fk_operation = o.id
|
||||
LEFT JOIN entites e ON o.fk_entite = e.id
|
||||
LEFT JOIN users u ON p.fk_user = u.id
|
||||
WHERE p.fk_type = 2 -- Type vente calendrier
|
||||
GROUP BY o.fk_entite, p.fk_user, DATE(p.created_at);
|
||||
|
||||
-- 7. Vue pour les statistiques par entité uniquement
|
||||
-- ----------------------------------------------------
|
||||
CREATE OR REPLACE VIEW `v_stripe_entite_stats` AS
|
||||
SELECT
|
||||
e.id as entite_id,
|
||||
e.encrypted_name as entite_name,
|
||||
sa.stripe_account_id,
|
||||
sa.charges_enabled,
|
||||
sa.payouts_enabled,
|
||||
COUNT(DISTINCT p.id) as total_passages,
|
||||
COUNT(DISTINCT CASE WHEN p.stripe_payment_id IS NOT NULL THEN p.id END) as passages_stripe,
|
||||
SUM(CASE WHEN p.stripe_payment_id IS NOT NULL THEN p.montant ELSE 0 END) as revenue_stripe,
|
||||
SUM(p.montant) as revenue_total
|
||||
FROM entites e
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
LEFT JOIN operations o ON e.id = o.fk_entite
|
||||
LEFT JOIN ope_pass p ON o.id = p.fk_operation
|
||||
GROUP BY e.id, e.encrypted_name, sa.stripe_account_id;
|
||||
|
||||
-- 8. Fonction helper pour vérifier si un passage a un paiement Stripe
|
||||
-- ---------------------------------------------------------------------
|
||||
-- NOTE: Si vous exécutez en copier/coller, cette fonction est optionnelle
|
||||
-- Vous pouvez l'ignorer ou l'exécuter séparément avec DELIMITER
|
||||
|
||||
-- =====================================================
|
||||
-- FIN DE LA MIGRATION
|
||||
-- =====================================================
|
||||
-- Tables supprimées : stripe_payment_intents
|
||||
-- Tables modifiées : ope_pass, stripe_payment_history, stripe_refunds
|
||||
-- Tables conservées : stripe_accounts, stripe_terminal_readers, etc.
|
||||
-- =====================================================
|
||||
@@ -1,197 +0,0 @@
|
||||
-- =============================================================
|
||||
-- Tables pour l'intégration Stripe Connect + Terminal
|
||||
-- Date: 2025-09-01
|
||||
-- Version: 1.0.0
|
||||
-- Préfixe: stripe_
|
||||
-- =============================================================
|
||||
|
||||
-- Table pour stocker les comptes Stripe Connect des amicales
|
||||
CREATE TABLE IF NOT EXISTS stripe_accounts (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
fk_entite INT(10) UNSIGNED NOT NULL,
|
||||
stripe_account_id VARCHAR(255) UNIQUE,
|
||||
stripe_location_id VARCHAR(255),
|
||||
charges_enabled BOOLEAN DEFAULT FALSE,
|
||||
payouts_enabled BOOLEAN DEFAULT FALSE,
|
||||
onboarding_completed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (fk_entite) REFERENCES entites(id) ON DELETE CASCADE,
|
||||
INDEX idx_fk_entite (fk_entite),
|
||||
INDEX idx_stripe_account_id (stripe_account_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Table pour stocker les intentions de paiement
|
||||
CREATE TABLE IF NOT EXISTS stripe_payment_intents (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
stripe_payment_intent_id VARCHAR(255) UNIQUE,
|
||||
fk_entite INT(10) UNSIGNED NOT NULL,
|
||||
fk_user INT(10) UNSIGNED NOT NULL,
|
||||
amount INT NOT NULL COMMENT 'Montant en centimes',
|
||||
currency VARCHAR(3) DEFAULT 'eur',
|
||||
status VARCHAR(50),
|
||||
application_fee INT COMMENT 'Commission en centimes',
|
||||
metadata JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (fk_entite) REFERENCES entites(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (fk_user) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_fk_entite (fk_entite),
|
||||
INDEX idx_fk_user (fk_user),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Table pour les readers Terminal (Tap to Pay virtuel)
|
||||
CREATE TABLE IF NOT EXISTS stripe_terminal_readers (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
stripe_reader_id VARCHAR(255) UNIQUE,
|
||||
fk_entite INT(10) UNSIGNED NOT NULL,
|
||||
label VARCHAR(255),
|
||||
location VARCHAR(255),
|
||||
status VARCHAR(50),
|
||||
device_type VARCHAR(50) COMMENT 'ios_tap_to_pay, android_tap_to_pay',
|
||||
device_info JSON COMMENT 'Infos sur le device (modèle, OS, etc)',
|
||||
last_seen_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (fk_entite) REFERENCES entites(id) ON DELETE CASCADE,
|
||||
INDEX idx_fk_entite (fk_entite),
|
||||
INDEX idx_device_type (device_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Table pour les appareils Android certifiés Tap to Pay
|
||||
CREATE TABLE IF NOT EXISTS stripe_android_certified_devices (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
manufacturer VARCHAR(100),
|
||||
model VARCHAR(200),
|
||||
model_identifier VARCHAR(200),
|
||||
tap_to_pay_certified BOOLEAN DEFAULT FALSE,
|
||||
certification_date DATE,
|
||||
min_android_version INT,
|
||||
country VARCHAR(2) DEFAULT 'FR',
|
||||
notes TEXT,
|
||||
last_verified TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_manufacturer_model (manufacturer, model),
|
||||
INDEX idx_certified (tap_to_pay_certified, country),
|
||||
UNIQUE KEY unique_device (manufacturer, model, model_identifier)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Table pour l'historique des paiements (pour audit et réconciliation)
|
||||
CREATE TABLE IF NOT EXISTS stripe_payment_history (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
fk_payment_intent INT(10) UNSIGNED,
|
||||
event_type VARCHAR(50) COMMENT 'created, processing, succeeded, failed, refunded',
|
||||
event_data JSON,
|
||||
webhook_id VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (fk_payment_intent) REFERENCES stripe_payment_intents(id) ON DELETE CASCADE,
|
||||
INDEX idx_fk_payment_intent (fk_payment_intent),
|
||||
INDEX idx_event_type (event_type),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Table pour les remboursements
|
||||
CREATE TABLE IF NOT EXISTS stripe_refunds (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
stripe_refund_id VARCHAR(255) UNIQUE,
|
||||
fk_payment_intent INT(10) UNSIGNED NOT NULL,
|
||||
amount INT NOT NULL COMMENT 'Montant remboursé en centimes',
|
||||
reason VARCHAR(100) COMMENT 'duplicate, fraudulent, requested_by_customer',
|
||||
status VARCHAR(50),
|
||||
metadata JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (fk_payment_intent) REFERENCES stripe_payment_intents(id) ON DELETE CASCADE,
|
||||
INDEX idx_fk_payment_intent (fk_payment_intent),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Table pour les webhooks reçus (pour éviter les doublons et debug)
|
||||
CREATE TABLE IF NOT EXISTS stripe_webhooks (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
stripe_event_id VARCHAR(255) UNIQUE,
|
||||
event_type VARCHAR(100),
|
||||
livemode BOOLEAN DEFAULT FALSE,
|
||||
payload JSON,
|
||||
processed BOOLEAN DEFAULT FALSE,
|
||||
error_message TEXT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
processed_at TIMESTAMP NULL,
|
||||
INDEX idx_event_type (event_type),
|
||||
INDEX idx_processed (processed),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Insertion des appareils Android certifiés pour Tap to Pay en France
|
||||
INSERT INTO stripe_android_certified_devices (manufacturer, model, model_identifier, tap_to_pay_certified, min_android_version, certification_date) VALUES
|
||||
-- Samsung
|
||||
('Samsung', 'Galaxy S21', 'SM-G991B', TRUE, 11, '2023-01-01'),
|
||||
('Samsung', 'Galaxy S21+', 'SM-G996B', TRUE, 11, '2023-01-01'),
|
||||
('Samsung', 'Galaxy S21 Ultra', 'SM-G998B', TRUE, 11, '2023-01-01'),
|
||||
('Samsung', 'Galaxy S22', 'SM-S901B', TRUE, 12, '2023-01-01'),
|
||||
('Samsung', 'Galaxy S22+', 'SM-S906B', TRUE, 12, '2023-01-01'),
|
||||
('Samsung', 'Galaxy S22 Ultra', 'SM-S908B', TRUE, 12, '2023-01-01'),
|
||||
('Samsung', 'Galaxy S23', 'SM-S911B', TRUE, 13, '2023-06-01'),
|
||||
('Samsung', 'Galaxy S23+', 'SM-S916B', TRUE, 13, '2023-06-01'),
|
||||
('Samsung', 'Galaxy S23 Ultra', 'SM-S918B', TRUE, 13, '2023-06-01'),
|
||||
('Samsung', 'Galaxy S24', 'SM-S921B', TRUE, 14, '2024-01-01'),
|
||||
('Samsung', 'Galaxy S24+', 'SM-S926B', TRUE, 14, '2024-01-01'),
|
||||
('Samsung', 'Galaxy S24 Ultra', 'SM-S928B', TRUE, 14, '2024-01-01'),
|
||||
-- Google Pixel
|
||||
('Google', 'Pixel 6', 'oriole', TRUE, 12, '2023-01-01'),
|
||||
('Google', 'Pixel 6 Pro', 'raven', TRUE, 12, '2023-01-01'),
|
||||
('Google', 'Pixel 6a', 'bluejay', TRUE, 12, '2023-03-01'),
|
||||
('Google', 'Pixel 7', 'panther', TRUE, 13, '2023-03-01'),
|
||||
('Google', 'Pixel 7 Pro', 'cheetah', TRUE, 13, '2023-03-01'),
|
||||
('Google', 'Pixel 7a', 'lynx', TRUE, 13, '2023-06-01'),
|
||||
('Google', 'Pixel 8', 'shiba', TRUE, 14, '2023-10-01'),
|
||||
('Google', 'Pixel 8 Pro', 'husky', TRUE, 14, '2023-10-01'),
|
||||
('Google', 'Pixel Fold', 'felix', TRUE, 13, '2023-07-01'),
|
||||
-- OnePlus
|
||||
('OnePlus', '9', 'LE2113', TRUE, 11, '2023-03-01'),
|
||||
('OnePlus', '9 Pro', 'LE2123', TRUE, 11, '2023-03-01'),
|
||||
('OnePlus', '10 Pro', 'NE2213', TRUE, 12, '2023-06-01'),
|
||||
('OnePlus', '11', 'CPH2449', TRUE, 13, '2023-09-01'),
|
||||
-- Xiaomi
|
||||
('Xiaomi', 'Mi 11', 'M2011K2G', TRUE, 11, '2023-06-01'),
|
||||
('Xiaomi', '12', '2201123G', TRUE, 12, '2023-09-01'),
|
||||
('Xiaomi', '12 Pro', '2201122G', TRUE, 12, '2023-09-01'),
|
||||
('Xiaomi', '13', '2211133G', TRUE, 13, '2024-01-01'),
|
||||
('Xiaomi', '13 Pro', '2210132G', TRUE, 13, '2024-01-01');
|
||||
|
||||
-- Vue pour faciliter les requêtes de statistiques
|
||||
CREATE OR REPLACE VIEW v_stripe_payment_stats AS
|
||||
SELECT
|
||||
spi.fk_entite,
|
||||
e.encrypted_name AS entite_name,
|
||||
spi.fk_user,
|
||||
u.encrypted_name AS user_nom,
|
||||
u.first_name AS user_prenom,
|
||||
COUNT(CASE WHEN spi.status = 'succeeded' THEN 1 END) as total_ventes,
|
||||
SUM(CASE WHEN spi.status = 'succeeded' THEN spi.amount ELSE 0 END) as total_montant,
|
||||
SUM(CASE WHEN spi.status = 'succeeded' THEN spi.application_fee ELSE 0 END) as total_commissions,
|
||||
DATE(spi.created_at) as date_vente
|
||||
FROM stripe_payment_intents spi
|
||||
LEFT JOIN entites e ON spi.fk_entite = e.id
|
||||
LEFT JOIN users u ON spi.fk_user = u.id
|
||||
GROUP BY spi.fk_entite, spi.fk_user, DATE(spi.created_at);
|
||||
|
||||
-- Vue pour le dashboard des amicales
|
||||
CREATE OR REPLACE VIEW v_stripe_amicale_dashboard AS
|
||||
SELECT
|
||||
sa.fk_entite,
|
||||
e.encrypted_name AS entite_name,
|
||||
sa.stripe_account_id,
|
||||
sa.charges_enabled,
|
||||
sa.payouts_enabled,
|
||||
COUNT(DISTINCT spi.id) as total_transactions,
|
||||
SUM(CASE WHEN spi.status = 'succeeded' THEN spi.amount ELSE 0 END) as total_revenus,
|
||||
SUM(CASE WHEN spi.status = 'succeeded' THEN spi.application_fee ELSE 0 END) as total_frais_plateforme,
|
||||
MAX(spi.created_at) as derniere_transaction
|
||||
FROM stripe_accounts sa
|
||||
LEFT JOIN entites e ON sa.fk_entite = e.id
|
||||
LEFT JOIN stripe_payment_intents spi ON sa.fk_entite = spi.fk_entite
|
||||
GROUP BY sa.fk_entite, sa.stripe_account_id;
|
||||
4469
api/scripts/migrations_entites.json
Normal file
4469
api/scripts/migrations_entites.json
Normal file
File diff suppressed because it is too large
Load Diff
473
api/scripts/orga/TODO-ISOLATION-OPERATIONS.md
Normal file
473
api/scripts/orga/TODO-ISOLATION-OPERATIONS.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# TODO - Isolation complète des opérations
|
||||
|
||||
## 🎯 Objectif
|
||||
|
||||
Mettre en place une **isolation complète par opération** où chaque opération est totalement autonome et peut être supprimée indépendamment sans impacter les autres opérations ou la table centrale `users`.
|
||||
|
||||
## 📊 Architecture cible
|
||||
|
||||
```
|
||||
operations (id: 850)
|
||||
├── ope_users (id: 2500, fk_operation: 850, fk_user: 100)
|
||||
│ ├── ope_users_sectors (fk_user: 2500 ← ope_users.id, fk_sector: 5400)
|
||||
│ └── ope_pass (fk_user: 2500 ← ope_users.id, fk_sector: 5400)
|
||||
└── ope_sectors (id: 5400, fk_operation: 850)
|
||||
|
||||
users (id: 100) ← table centrale (conservée même si opération supprimée)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Tâche 1 : Modification du schéma SQL
|
||||
|
||||
### 📁 Fichier : `scripts/orga/fix_fk_constraints.sql`
|
||||
|
||||
### Actions
|
||||
|
||||
- [ ] **1.1** Tester le script SQL sur **dva_geo** (DEV)
|
||||
```bash
|
||||
incus exec dva-geo -- mysql rca_geo < /var/www/geosector/api/scripts/orga/fix_fk_constraints.sql
|
||||
```
|
||||
|
||||
- [ ] **1.2** Vérifier les contraintes après exécution :
|
||||
```sql
|
||||
SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = 'rca_geo'
|
||||
AND TABLE_NAME IN ('ope_users_sectors', 'ope_pass')
|
||||
AND COLUMN_NAME = 'fk_user';
|
||||
```
|
||||
Résultat attendu :
|
||||
- `ope_users_sectors.fk_user → ope_users.id`
|
||||
- `ope_pass.fk_user → ope_users.id`
|
||||
|
||||
- [ ] **1.3** Appliquer sur **rca_geo** (RECETTE) après validation sur dva_geo
|
||||
|
||||
- [ ] **1.4** Appliquer sur **pra_geo** (PRODUCTION) après validation sur rca_geo
|
||||
|
||||
### ⚠️ Important
|
||||
|
||||
- Les données existantes doivent être **nettoyées avant** d'appliquer le script
|
||||
- Ou bien : recréer toutes les données avec la nouvelle migration
|
||||
- Les FK `ON DELETE CASCADE` supprimeront automatiquement `ope_users_sectors` et `ope_pass` quand `ope_users` est supprimé
|
||||
|
||||
---
|
||||
|
||||
## ✅ Tâche 2 : Correction du script de migration2
|
||||
|
||||
### 📁 Fichiers concernés
|
||||
|
||||
1. `scripts/migration2/php/lib/SectorMigrator.php`
|
||||
2. `scripts/migration2/php/lib/PassageMigrator.php`
|
||||
|
||||
### Actions
|
||||
|
||||
#### 2.1 SectorMigrator.php - Migration de ope_users_sectors
|
||||
|
||||
- [ ] **Ligne 253** : Changer de `users.id` vers `ope_users.id`
|
||||
|
||||
```php
|
||||
// ❌ AVANT
|
||||
':fk_user' => $us['fk_user'], // ID de users (table centrale)
|
||||
|
||||
// ✅ APRÈS
|
||||
':fk_user' => $userMapping[$us['fk_user']], // ID de ope_users (mapping)
|
||||
```
|
||||
|
||||
#### 2.2 PassageMigrator.php - Migration de ope_pass
|
||||
|
||||
- [ ] **Ligne 64-67** : Vérifier le mapping existe
|
||||
- [ ] **Ligne 77** : Passer `ope_users.id` au lieu de `users.id`
|
||||
|
||||
```php
|
||||
// ❌ AVANT (ligne 77)
|
||||
$newPassId = $this->insertPassage($passage, $newOperationId, $newOpeSectorId, $passage['fk_user']);
|
||||
|
||||
// ✅ APRÈS
|
||||
$newOpeUserId = $userMapping[$passage['fk_user']];
|
||||
$newPassId = $this->insertPassage($passage, $newOperationId, $newOpeSectorId, $newOpeUserId);
|
||||
```
|
||||
|
||||
- [ ] **Ligne 164** : Utiliser le paramètre `$userId` qui sera maintenant `ope_users.id`
|
||||
|
||||
```php
|
||||
// ❌ AVANT
|
||||
':fk_user' => $userId, // ID de users (table centrale)
|
||||
|
||||
// ✅ APRÈS (le paramètre $userId contiendra déjà ope_users.id)
|
||||
':fk_user' => $userId, // ID de ope_users
|
||||
```
|
||||
|
||||
- [ ] **Ligne 71** : Corriger `verifyUserSectorAssociation` pour vérifier avec `ope_users.id`
|
||||
|
||||
```php
|
||||
// ❌ AVANT
|
||||
if (!$this->verifyUserSectorAssociation($newOperationId, $passage['fk_user'], $newOpeSectorId)) {
|
||||
|
||||
// ✅ APRÈS
|
||||
if (!$this->verifyUserSectorAssociation($newOperationId, $newOpeUserId, $newOpeSectorId)) {
|
||||
```
|
||||
|
||||
#### 2.3 Tester la migration complète
|
||||
|
||||
- [ ] **Sur dva_geo** : Vider les données d'une entité et relancer la migration
|
||||
```bash
|
||||
php php/migrate_from_backup.php --mode=entity --entity-id=5
|
||||
```
|
||||
|
||||
- [ ] **Vérifier** dans la base que :
|
||||
- `ope_users_sectors.fk_user` contient des IDs de `ope_users.id`
|
||||
- `ope_pass.fk_user` contient des IDs de `ope_users.id`
|
||||
- Les valeurs correspondent bien au mapping
|
||||
|
||||
- [ ] **Vérifier** qu'on peut supprimer une opération et que tout part avec (CASCADE)
|
||||
```sql
|
||||
DELETE FROM operations WHERE id = 850;
|
||||
-- Doit supprimer automatiquement :
|
||||
-- - ope_users (ON DELETE CASCADE depuis operations)
|
||||
-- - ope_users_sectors (ON DELETE CASCADE depuis ope_users)
|
||||
-- - ope_pass (ON DELETE CASCADE depuis ope_users)
|
||||
-- - ope_sectors (ON DELETE CASCADE depuis operations)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Tâche 3 : Vérifications API
|
||||
|
||||
### Impact sur les endpoints API
|
||||
|
||||
#### 3.1 Vérifier les requêtes utilisant `ope_pass.fk_user`
|
||||
|
||||
- [ ] **Rechercher** tous les endpoints qui lisent `ope_pass.fk_user`
|
||||
```bash
|
||||
grep -r "ope_pass.*fk_user" src/Controllers/
|
||||
grep -r "fk_user.*ope_pass" src/Controllers/
|
||||
```
|
||||
|
||||
- [ ] **Vérifier** que ces endpoints :
|
||||
- Font-ils des JOIN avec `users` via `ope_pass.fk_user` ?
|
||||
- Si OUI : Ajouter un JOIN via `ope_users` :
|
||||
```sql
|
||||
-- ❌ AVANT
|
||||
SELECT op.*, u.encrypted_name
|
||||
FROM ope_pass op
|
||||
JOIN users u ON op.fk_user = u.id
|
||||
|
||||
-- ✅ APRÈS
|
||||
SELECT op.*, u.encrypted_name
|
||||
FROM ope_pass op
|
||||
JOIN ope_users ou ON op.fk_user = ou.id
|
||||
JOIN users u ON ou.fk_user = u.id
|
||||
```
|
||||
|
||||
#### 3.2 Vérifier les requêtes utilisant `ope_users_sectors.fk_user`
|
||||
|
||||
- [ ] **Rechercher** tous les endpoints qui lisent `ope_users_sectors.fk_user`
|
||||
```bash
|
||||
grep -r "ope_users_sectors.*fk_user" src/Controllers/
|
||||
```
|
||||
|
||||
- [ ] **Vérifier** la même chose : si JOIN avec `users`, ajouter passage par `ope_users`
|
||||
|
||||
#### 3.3 Endpoints probablement concernés
|
||||
|
||||
À vérifier :
|
||||
- [ ] `OperationController` - Liste des utilisateurs d'une opération
|
||||
- [ ] `PassageController` - Liste/détails des passages
|
||||
- [ ] `SectorController` - Liste des secteurs avec utilisateurs affectés
|
||||
- [ ] Tout endpoint retournant des statistiques par utilisateur
|
||||
|
||||
---
|
||||
|
||||
## ✅ Tâche 4 : Corrections API - Response JSON Login
|
||||
|
||||
### Impact sur la réponse JSON du login
|
||||
|
||||
#### 4.1 Groupe `users_sectors` - Ajouter `ope_user_id`
|
||||
|
||||
**Problème identifié** : Flutter reçoit `users_sectors` avec `id` (users.id) mais les `passages` ont `fk_user` (ope_users.id). Le mapping est impossible.
|
||||
|
||||
**Solution** : Modifier la requête dans `LoginController.php` (lignes 426 et 1181) pour retourner les deux IDs :
|
||||
|
||||
```sql
|
||||
-- ✅ APRÈS
|
||||
SELECT DISTINCT
|
||||
u.id as user_id, -- users.id (table centrale, pour gestion membres)
|
||||
ou.id as ope_user_id, -- ope_users.id (pour lier avec passages/sectors)
|
||||
ou.first_name,
|
||||
u.encrypted_name,
|
||||
u.sect_name,
|
||||
us.fk_sector
|
||||
FROM users u
|
||||
JOIN ope_users ou ON u.id = ou.fk_user
|
||||
JOIN ope_users_sectors us ON ou.id = us.fk_user AND ou.fk_operation = us.fk_operation
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
AND u.chk_active = 1
|
||||
AND u.id != ?
|
||||
```
|
||||
|
||||
**Résultat JSON attendu** :
|
||||
```json
|
||||
{
|
||||
"user_id": 123, // users.id (pour gestion des membres dans l'interface)
|
||||
"ope_user_id": 50, // ope_users.id (pour lier avec passages.fk_user et sectors)
|
||||
"first_name": "Jane",
|
||||
"name": "Jane Smith",
|
||||
"sect_name": "Smith",
|
||||
"fk_sector": 456
|
||||
}
|
||||
```
|
||||
|
||||
**Usage Flutter** :
|
||||
```dart
|
||||
// Trouver les passages d'un utilisateur
|
||||
passages.where((p) => p.fkUser == usersSectors[i].opeUserId) // ✅ OK
|
||||
```
|
||||
|
||||
- [ ] **Modifier** `LoginController.php` ligne 426 (méthode `login()`)
|
||||
- [ ] **Modifier** `LoginController.php` ligne 1181 (méthode `checkSession()`)
|
||||
- [ ] **Tester** la réponse JSON du login en mode admin
|
||||
|
||||
---
|
||||
|
||||
## ✅ Tâche 5 : Vérifications Flutter - Gestion des IDs
|
||||
|
||||
### Impact sur l'application mobile
|
||||
|
||||
#### 5.1 Modèles de données
|
||||
|
||||
- [ ] **Vérifier** le modèle `UserSector` (ou équivalent)
|
||||
- Ajouter le champ `opeUserId` (int) pour stocker `ope_users.id`
|
||||
- Conserver `userId` (int) pour stocker `users.id`
|
||||
|
||||
- [ ] **Vérifier** le modèle `Passage` (ou équivalent)
|
||||
- Le champ `fkUser` pointe maintenant vers `ope_users.id`
|
||||
|
||||
#### 5.2 Gestion des secteurs (Mode Admin)
|
||||
|
||||
- [ ] **Création de secteur**
|
||||
- L'API crée dans `ope_sectors`
|
||||
- Attribution des users : utiliser `ope_user_id` (pas `user_id`)
|
||||
- Endpoint : `POST /api/sectors`
|
||||
- Body : `{ ..., users: [50, 51, 52] }` ← IDs de `ope_users`
|
||||
|
||||
- [ ] **Modification de secteur**
|
||||
- Attribution des users : utiliser `ope_user_id`
|
||||
- Endpoint : `PUT /api/sectors/:id`
|
||||
- Body : `{ ..., users: [50, 51, 52] }` ← IDs de `ope_users`
|
||||
|
||||
- [ ] **Suppression de secteur**
|
||||
- L'API supprime dans `ope_pass`, `ope_users_sectors` et `ope_sectors`
|
||||
- CASCADE gère automatiquement les dépendances
|
||||
- Endpoint : `DELETE /api/sectors/:id`
|
||||
|
||||
#### 5.3 Gestion des membres (Mode Admin)
|
||||
|
||||
- [ ] **Création de membre**
|
||||
- L'API crée dans `users` (table centrale)
|
||||
- L'API crée aussi dans `ope_users` pour l'opération active
|
||||
- **Réponse attendue** :
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"user": {
|
||||
"id": 123, // users.id
|
||||
"ope_user_id": 50, // ope_users.id (nouveau)
|
||||
"first_name": "John",
|
||||
"name": "John Doe",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
- Endpoint : `POST /api/users`
|
||||
- Flutter stocke les 2 IDs : `userId` et `opeUserId`
|
||||
|
||||
- [ ] **Modification de membre**
|
||||
- L'API met à jour `users` (table centrale)
|
||||
- L'API met à jour aussi `ope_users` pour l'opération active
|
||||
- Endpoint : `PUT /api/users/:id`
|
||||
|
||||
- [ ] **Suppression de membre**
|
||||
- L'API supprime de `ope_users` (opération active)
|
||||
- L'API supprime de `users` (table centrale)
|
||||
- CASCADE supprime automatiquement `ope_users_sectors` et `ope_pass`
|
||||
- Endpoint : `DELETE /api/users/:id?transfer_to=XX`
|
||||
|
||||
#### 5.4 Gestion des passages (Mode Admin & User)
|
||||
|
||||
- [ ] **Création de passage**
|
||||
- Attribution automatique du `ope_sectors.id` le plus proche
|
||||
- Attribution du `ope_users.id` (utilisateur connecté ou sélectionné)
|
||||
- Endpoint : `POST /api/passages`
|
||||
- Body : `{ ..., fk_user: 50, fk_sector: 456 }` ← IDs de `ope_users` et `ope_sectors`
|
||||
|
||||
- [ ] **Modification de passage**
|
||||
- Attribution du `ope_users.id` si changement d'utilisateur
|
||||
- Endpoint : `PUT /api/passages/:id`
|
||||
- Body : `{ ..., fk_user: 50 }` ← ID de `ope_users`
|
||||
|
||||
- [ ] **Suppression de passage**
|
||||
- L'API supprime dans `ope_pass`
|
||||
- Endpoint : `DELETE /api/passages/:id`
|
||||
|
||||
#### 5.5 Interface Flutter - Mapping des IDs
|
||||
|
||||
**Scénarios à gérer** :
|
||||
|
||||
1. **Affichage des secteurs avec utilisateurs affectés** :
|
||||
```dart
|
||||
// Utiliser usersSectors[i].opeUserId pour lier avec passages
|
||||
final userPassages = passages.where((p) =>
|
||||
p.fkUser == usersSectors[i].opeUserId &&
|
||||
p.fkSector == sector.id
|
||||
).toList();
|
||||
```
|
||||
|
||||
2. **Attribution d'un passage à un utilisateur** :
|
||||
```dart
|
||||
// Envoyer ope_user_id dans la requête API
|
||||
await apiService.createPassage({
|
||||
...passageData,
|
||||
'fk_user': userSector.opeUserId, // ope_users.id
|
||||
'fk_sector': sector.id
|
||||
});
|
||||
```
|
||||
|
||||
3. **Affichage du nom d'un utilisateur depuis un passage** :
|
||||
```dart
|
||||
// Chercher dans usersSectors avec ope_user_id
|
||||
final userSector = usersSectors.firstWhere(
|
||||
(us) => us.opeUserId == passage.fkUser,
|
||||
orElse: () => null
|
||||
);
|
||||
final userName = userSector?.name ?? 'Inconnu';
|
||||
```
|
||||
|
||||
4. **Gestion des membres** :
|
||||
```dart
|
||||
// Conserver les 2 IDs lors de la création
|
||||
final newMember = await apiService.createUser(userData);
|
||||
membres.add(Member(
|
||||
userId: newMember['id'], // users.id
|
||||
opeUserId: newMember['ope_user_id'], // ope_users.id
|
||||
...
|
||||
));
|
||||
```
|
||||
|
||||
#### 5.6 Tests d'affichage
|
||||
|
||||
- [ ] Tester l'affichage des passages avec noms d'utilisateurs
|
||||
- [ ] Tester l'affichage des secteurs avec utilisateurs affectés
|
||||
- [ ] Tester la création d'un membre (vérifier que les 2 IDs sont reçus)
|
||||
- [ ] Tester la suppression d'un membre (vérifier le transfert de passages)
|
||||
- [ ] Tester la création d'un secteur avec attribution d'utilisateurs
|
||||
- [ ] Tester la création d'un passage avec attribution d'utilisateur
|
||||
- [ ] Tester la suppression d'une opération (doit tout nettoyer)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Ordre d'exécution recommandé
|
||||
|
||||
1. ✅ **Corriger le code de migration2** (PHP)
|
||||
2. ✅ **Tester sur dva_geo** avec schéma modifié
|
||||
3. ✅ **Vérifier l'API** sur dva_geo
|
||||
4. ✅ **Vérifier Flutter** avec dva_geo
|
||||
5. 🚀 **Déployer le schéma SQL** sur rca_geo
|
||||
6. 🚀 **Déployer le code** sur rca_geo
|
||||
7. ✅ **Tester en recette**
|
||||
8. 🚀 **Déployer en production** (pra_geo)
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Requêtes SQL utiles pour vérification
|
||||
|
||||
### Vérifier les contraintes FK actuelles
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
COLUMN_NAME,
|
||||
CONSTRAINT_NAME,
|
||||
REFERENCED_TABLE_NAME,
|
||||
REFERENCED_COLUMN_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND (TABLE_NAME = 'ope_pass' OR TABLE_NAME = 'ope_users_sectors')
|
||||
AND COLUMN_NAME = 'fk_user';
|
||||
```
|
||||
|
||||
### Vérifier l'intégrité des données après migration
|
||||
|
||||
```sql
|
||||
-- Vérifier que tous les fk_user de ope_pass existent dans ope_users
|
||||
SELECT COUNT(*) as orphans
|
||||
FROM ope_pass op
|
||||
LEFT JOIN ope_users ou ON op.fk_user = ou.id
|
||||
WHERE ou.id IS NULL;
|
||||
-- Résultat attendu : 0
|
||||
|
||||
-- Vérifier que tous les fk_user de ope_users_sectors existent dans ope_users
|
||||
SELECT COUNT(*) as orphans
|
||||
FROM ope_users_sectors ous
|
||||
LEFT JOIN ope_users ou ON ous.fk_user = ou.id
|
||||
WHERE ou.id IS NULL;
|
||||
-- Résultat attendu : 0
|
||||
```
|
||||
|
||||
### Tester la suppression en cascade
|
||||
|
||||
```sql
|
||||
-- Compter avant suppression
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM ope_users WHERE fk_operation = 850) as ope_users_count,
|
||||
(SELECT COUNT(*) FROM ope_users_sectors WHERE fk_operation = 850) as ope_users_sectors_count,
|
||||
(SELECT COUNT(*) FROM ope_pass WHERE fk_operation = 850) as ope_pass_count,
|
||||
(SELECT COUNT(*) FROM ope_sectors WHERE fk_operation = 850) as ope_sectors_count;
|
||||
|
||||
-- Supprimer l'opération
|
||||
DELETE FROM operations WHERE id = 850;
|
||||
|
||||
-- Vérifier que tout a été supprimé (doit retourner 0 partout)
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM ope_users WHERE fk_operation = 850) as ope_users_count,
|
||||
(SELECT COUNT(*) FROM ope_users_sectors WHERE fk_operation = 850) as ope_users_sectors_count,
|
||||
(SELECT COUNT(*) FROM ope_pass WHERE fk_operation = 850) as ope_pass_count,
|
||||
(SELECT COUNT(*) FROM ope_sectors WHERE fk_operation = 850) as ope_sectors_count;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes importantes
|
||||
|
||||
### Avantages de cette architecture
|
||||
|
||||
✅ **Isolation complète** : Supprimer une opération supprime tout (ope_users, secteurs, passages)
|
||||
✅ **Performance** : Pas de jointures complexes avec la table centrale `users`
|
||||
✅ **Historique** : Les données d'une opération sont figées dans le temps
|
||||
✅ **Simplicité** : Requêtes plus simples, moins de risques d'incohérences
|
||||
|
||||
### Implications
|
||||
|
||||
⚠️ **Duplication** : Un utilisateur travaillant sur 3 opérations aura 3 entrées dans `ope_users`
|
||||
⚠️ **Taille** : La table `ope_users` sera plus volumineuse
|
||||
⚠️ **Jointures** : Pour remonter aux infos de la table `users`, il faut passer par `ope_users.fk_user`
|
||||
|
||||
### Rétrocompatibilité
|
||||
|
||||
❌ Ce changement **CASSE** la compatibilité avec les données existantes
|
||||
✅ Nécessite une **re-migration complète** de toutes les entités après modification du schéma
|
||||
✅ Ou bien : script de transformation des données existantes (plus complexe)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Statut
|
||||
|
||||
- [ ] Schéma SQL modifié sur dva_geo
|
||||
- [ ] Code migration2 corrigé
|
||||
- [ ] API vérifiée et corrigée
|
||||
- [ ] Flutter vérifié et corrigé
|
||||
- [ ] Tests complets sur dva_geo
|
||||
- [ ] Déploiement rca_geo
|
||||
- [ ] Déploiement pra_geo
|
||||
65
api/scripts/orga/fix_fk_constraints.sql
Normal file
65
api/scripts/orga/fix_fk_constraints.sql
Normal file
@@ -0,0 +1,65 @@
|
||||
-- ================================================================================
|
||||
-- Script de migration : Correction des contraintes FK pour isolation par opération
|
||||
-- ================================================================================
|
||||
--
|
||||
-- Ce script modifie les contraintes de clés étrangères pour que :
|
||||
-- - ope_users_sectors.fk_user → pointe vers ope_users.id (au lieu de users.id)
|
||||
-- - ope_pass.fk_user → pointe vers ope_users.id (au lieu de users.id)
|
||||
--
|
||||
-- Cela permet une isolation complète des opérations : supprimer une opération
|
||||
-- supprime automatiquement tous ses ope_users, ope_sectors, ope_users_sectors et ope_pass.
|
||||
--
|
||||
-- ORDRE D'EXÉCUTION :
|
||||
-- 1. dva_geo (DEV) - test
|
||||
-- 2. rca_geo (RECETTE)
|
||||
-- 3. pra_geo (PRODUCTION)
|
||||
--
|
||||
-- ================================================================================
|
||||
|
||||
USE dva_geo; -- Adapter selon l'environnement (dva_geo, rca_geo, pra_geo)
|
||||
|
||||
-- ================================================================================
|
||||
-- 1. Modification de ope_users_sectors.fk_user
|
||||
-- ================================================================================
|
||||
|
||||
-- Supprimer l'ancienne contrainte FK
|
||||
ALTER TABLE ope_users_sectors
|
||||
DROP FOREIGN KEY ope_users_sectors_ibfk_2;
|
||||
|
||||
-- Recréer la contrainte FK vers ope_users.id
|
||||
ALTER TABLE ope_users_sectors
|
||||
ADD CONSTRAINT ope_users_sectors_ibfk_2
|
||||
FOREIGN KEY (fk_user) REFERENCES ope_users (id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- ================================================================================
|
||||
-- 2. Modification de ope_pass.fk_user
|
||||
-- ================================================================================
|
||||
|
||||
-- Supprimer l'ancienne contrainte FK
|
||||
ALTER TABLE ope_pass
|
||||
DROP FOREIGN KEY ope_pass_ibfk_3;
|
||||
|
||||
-- Recréer la contrainte FK vers ope_users.id
|
||||
ALTER TABLE ope_pass
|
||||
ADD CONSTRAINT ope_pass_ibfk_3
|
||||
FOREIGN KEY (fk_user) REFERENCES ope_users (id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- ================================================================================
|
||||
-- Vérification finale
|
||||
-- ================================================================================
|
||||
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
COLUMN_NAME,
|
||||
CONSTRAINT_NAME,
|
||||
REFERENCED_TABLE_NAME,
|
||||
REFERENCED_COLUMN_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME IN ('ope_users_sectors', 'ope_pass')
|
||||
AND COLUMN_NAME = 'fk_user'
|
||||
ORDER BY TABLE_NAME;
|
||||
|
||||
-- Résultat attendu :
|
||||
-- ope_pass | fk_user | ope_pass_ibfk_3 | ope_users | id
|
||||
-- ope_users_sectors | fk_user | ope_users_sectors_ibfk_2 | ope_users | id
|
||||
121
api/scripts/orga/fix_fk_constraints_safe.sql
Normal file
121
api/scripts/orga/fix_fk_constraints_safe.sql
Normal file
@@ -0,0 +1,121 @@
|
||||
-- ================================================================================
|
||||
-- Script de migration SÉCURISÉ : Correction des contraintes FK pour isolation par opération
|
||||
-- ================================================================================
|
||||
--
|
||||
-- Ce script modifie les contraintes de clés étrangères pour que :
|
||||
-- - ope_users_sectors.fk_user → pointe vers ope_users.id (au lieu de users.id)
|
||||
-- - ope_pass.fk_user → pointe vers ope_users.id (au lieu de users.id)
|
||||
--
|
||||
-- Version SÉCURISÉE : Vérifie l'existence des contraintes avant de les supprimer
|
||||
--
|
||||
-- ================================================================================
|
||||
|
||||
USE dva_geo;
|
||||
|
||||
-- ================================================================================
|
||||
-- Afficher les contraintes FK actuelles
|
||||
-- ================================================================================
|
||||
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
COLUMN_NAME,
|
||||
CONSTRAINT_NAME,
|
||||
REFERENCED_TABLE_NAME,
|
||||
REFERENCED_COLUMN_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = 'dva_geo'
|
||||
AND TABLE_NAME IN ('ope_users_sectors', 'ope_pass')
|
||||
AND COLUMN_NAME = 'fk_user'
|
||||
ORDER BY TABLE_NAME;
|
||||
|
||||
-- ================================================================================
|
||||
-- 1. Modification de ope_users_sectors.fk_user
|
||||
-- ================================================================================
|
||||
|
||||
-- Supprimer l'ancienne contrainte FK si elle existe
|
||||
SET @constraint_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = 'dva_geo'
|
||||
AND TABLE_NAME = 'ope_users_sectors'
|
||||
AND COLUMN_NAME = 'fk_user'
|
||||
AND CONSTRAINT_NAME LIKE '%ibfk%'
|
||||
);
|
||||
|
||||
SET @sql = IF(@constraint_exists > 0,
|
||||
CONCAT('ALTER TABLE ope_users_sectors DROP FOREIGN KEY ',
|
||||
(SELECT CONSTRAINT_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = 'dva_geo'
|
||||
AND TABLE_NAME = 'ope_users_sectors'
|
||||
AND COLUMN_NAME = 'fk_user'
|
||||
AND CONSTRAINT_NAME LIKE '%ibfk%'
|
||||
LIMIT 1)),
|
||||
'SELECT "Aucune contrainte FK à supprimer sur ope_users_sectors" AS message'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Recréer la contrainte FK vers ope_users.id
|
||||
ALTER TABLE ope_users_sectors
|
||||
ADD CONSTRAINT ope_users_sectors_ibfk_2
|
||||
FOREIGN KEY (fk_user) REFERENCES ope_users (id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- ================================================================================
|
||||
-- 2. Modification de ope_pass.fk_user
|
||||
-- ================================================================================
|
||||
|
||||
-- Supprimer l'ancienne contrainte FK si elle existe
|
||||
SET @constraint_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = 'dva_geo'
|
||||
AND TABLE_NAME = 'ope_pass'
|
||||
AND COLUMN_NAME = 'fk_user'
|
||||
AND CONSTRAINT_NAME LIKE '%ibfk%'
|
||||
);
|
||||
|
||||
SET @sql = IF(@constraint_exists > 0,
|
||||
CONCAT('ALTER TABLE ope_pass DROP FOREIGN KEY ',
|
||||
(SELECT CONSTRAINT_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = 'dva_geo'
|
||||
AND TABLE_NAME = 'ope_pass'
|
||||
AND COLUMN_NAME = 'fk_user'
|
||||
AND CONSTRAINT_NAME LIKE '%ibfk%'
|
||||
LIMIT 1)),
|
||||
'SELECT "Aucune contrainte FK à supprimer sur ope_pass" AS message'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Recréer la contrainte FK vers ope_users.id
|
||||
ALTER TABLE ope_pass
|
||||
ADD CONSTRAINT ope_pass_ibfk_3
|
||||
FOREIGN KEY (fk_user) REFERENCES ope_users (id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- ================================================================================
|
||||
-- Vérification finale
|
||||
-- ================================================================================
|
||||
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
COLUMN_NAME,
|
||||
CONSTRAINT_NAME,
|
||||
REFERENCED_TABLE_NAME,
|
||||
REFERENCED_COLUMN_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = 'dva_geo'
|
||||
AND TABLE_NAME IN ('ope_users_sectors', 'ope_pass')
|
||||
AND COLUMN_NAME = 'fk_user'
|
||||
ORDER BY TABLE_NAME;
|
||||
|
||||
-- Résultat attendu :
|
||||
-- ope_pass | fk_user | ope_pass_ibfk_3 | ope_users | id
|
||||
-- ope_users_sectors | fk_user | ope_users_sectors_ibfk_2 | ope_users | id
|
||||
|
||||
SELECT '✓ Contraintes FK modifiées avec succès !' AS status;
|
||||
93
api/scripts/orga/truncate_all_tables.sql
Normal file
93
api/scripts/orga/truncate_all_tables.sql
Normal file
@@ -0,0 +1,93 @@
|
||||
-- ================================================================================
|
||||
-- Script de nettoyage complet des tables - DVA_GEO
|
||||
-- ================================================================================
|
||||
--
|
||||
-- Ce script vide toutes les tables pour repartir à zéro.
|
||||
-- ATTENTION : Toutes les données seront perdues !
|
||||
--
|
||||
-- Usage : À exécuter sur dva_geo UNIQUEMENT (environnement de développement)
|
||||
--
|
||||
-- ================================================================================
|
||||
|
||||
USE dva_geo;
|
||||
|
||||
-- Désactiver temporairement les vérifications de clés étrangères
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ================================================================================
|
||||
-- 1. Tables dépendantes (dans l'ordre des dépendances)
|
||||
-- ================================================================================
|
||||
|
||||
TRUNCATE TABLE ope_pass_histo;
|
||||
TRUNCATE TABLE ope_pass;
|
||||
TRUNCATE TABLE ope_users_sectors;
|
||||
TRUNCATE TABLE sectors_adresses;
|
||||
TRUNCATE TABLE ope_sectors;
|
||||
TRUNCATE TABLE ope_users;
|
||||
TRUNCATE TABLE medias;
|
||||
TRUNCATE TABLE operations;
|
||||
|
||||
-- ================================================================================
|
||||
-- 2. Tables liées aux utilisateurs
|
||||
-- ================================================================================
|
||||
|
||||
TRUNCATE TABLE user_devices;
|
||||
|
||||
-- ================================================================================
|
||||
-- 3. Tables de chat
|
||||
-- ================================================================================
|
||||
|
||||
TRUNCATE TABLE chat_messages;
|
||||
TRUNCATE TABLE chat_participants;
|
||||
TRUNCATE TABLE chat_read_receipts;
|
||||
TRUNCATE TABLE chat_rooms;
|
||||
|
||||
-- ================================================================================
|
||||
-- 4. Tables principales
|
||||
-- ================================================================================
|
||||
|
||||
TRUNCATE TABLE users;
|
||||
TRUNCATE TABLE entites;
|
||||
|
||||
-- Réactiver les vérifications de clés étrangères
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- ================================================================================
|
||||
-- Vérification : Compter les lignes restantes
|
||||
-- ================================================================================
|
||||
|
||||
SELECT
|
||||
'ope_pass_histo' AS table_name, COUNT(*) AS rows_count FROM ope_pass_histo
|
||||
UNION ALL
|
||||
SELECT 'ope_pass', COUNT(*) FROM ope_pass
|
||||
UNION ALL
|
||||
SELECT 'ope_users_sectors', COUNT(*) FROM ope_users_sectors
|
||||
UNION ALL
|
||||
SELECT 'sectors_adresses', COUNT(*) FROM sectors_adresses
|
||||
UNION ALL
|
||||
SELECT 'ope_sectors', COUNT(*) FROM ope_sectors
|
||||
UNION ALL
|
||||
SELECT 'ope_users', COUNT(*) FROM ope_users
|
||||
UNION ALL
|
||||
SELECT 'medias', COUNT(*) FROM medias
|
||||
UNION ALL
|
||||
SELECT 'operations', COUNT(*) FROM operations
|
||||
UNION ALL
|
||||
SELECT 'user_devices', COUNT(*) FROM user_devices
|
||||
UNION ALL
|
||||
SELECT 'chat_messages', COUNT(*) FROM chat_messages
|
||||
UNION ALL
|
||||
SELECT 'chat_participants', COUNT(*) FROM chat_participants
|
||||
UNION ALL
|
||||
SELECT 'chat_read_receipts', COUNT(*) FROM chat_read_receipts
|
||||
UNION ALL
|
||||
SELECT 'chat_rooms', COUNT(*) FROM chat_rooms
|
||||
UNION ALL
|
||||
SELECT 'users', COUNT(*) FROM users
|
||||
UNION ALL
|
||||
SELECT 'entites', COUNT(*) FROM entites
|
||||
ORDER BY table_name;
|
||||
|
||||
-- Résultat attendu : 0 partout
|
||||
|
||||
SELECT '✓ Toutes les tables ont été vidées avec succès !' AS status;
|
||||
150
api/scripts/orga/verify_isolation.sql
Normal file
150
api/scripts/orga/verify_isolation.sql
Normal file
@@ -0,0 +1,150 @@
|
||||
-- ================================================================================
|
||||
-- Script de vérification : Isolation complète des opérations
|
||||
-- ================================================================================
|
||||
--
|
||||
-- Ce script vérifie que l'isolation par opération fonctionne correctement
|
||||
--
|
||||
-- ================================================================================
|
||||
|
||||
USE dva_geo;
|
||||
|
||||
-- ================================================================================
|
||||
-- 1. Vérifier les contraintes FK
|
||||
-- ================================================================================
|
||||
|
||||
SELECT '=== VÉRIFICATION DES CONTRAINTES FK ===' AS '';
|
||||
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
COLUMN_NAME,
|
||||
CONSTRAINT_NAME,
|
||||
REFERENCED_TABLE_NAME,
|
||||
REFERENCED_COLUMN_NAME
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = 'dva_geo'
|
||||
AND TABLE_NAME IN ('ope_users_sectors', 'ope_pass')
|
||||
AND COLUMN_NAME = 'fk_user'
|
||||
ORDER BY TABLE_NAME;
|
||||
|
||||
-- Résultat attendu :
|
||||
-- ope_pass | fk_user | ope_pass_ibfk_3 | ope_users | id
|
||||
-- ope_users_sectors | fk_user | ope_users_sectors_ibfk_2 | ope_users | id
|
||||
|
||||
-- ================================================================================
|
||||
-- 2. Vérifier l'intégrité des données (pas d'orphelins)
|
||||
-- ================================================================================
|
||||
|
||||
SELECT '=== VÉRIFICATION INTÉGRITÉ DES DONNÉES ===' AS '';
|
||||
|
||||
-- Vérifier que tous les fk_user de ope_pass existent dans ope_users
|
||||
SELECT
|
||||
'ope_pass → ope_users' AS verification,
|
||||
COUNT(*) as orphelins
|
||||
FROM ope_pass op
|
||||
LEFT JOIN ope_users ou ON op.fk_user = ou.id
|
||||
WHERE ou.id IS NULL;
|
||||
-- Résultat attendu : 0
|
||||
|
||||
-- Vérifier que tous les fk_user de ope_users_sectors existent dans ope_users
|
||||
SELECT
|
||||
'ope_users_sectors → ope_users' AS verification,
|
||||
COUNT(*) as orphelins
|
||||
FROM ope_users_sectors ous
|
||||
LEFT JOIN ope_users ou ON ous.fk_user = ou.id
|
||||
WHERE ou.id IS NULL;
|
||||
-- Résultat attendu : 0
|
||||
|
||||
-- ================================================================================
|
||||
-- 3. Statistiques de migration
|
||||
-- ================================================================================
|
||||
|
||||
SELECT '=== STATISTIQUES DE MIGRATION ===' AS '';
|
||||
|
||||
-- Nombre d'entités
|
||||
SELECT 'Entités' AS table_name, COUNT(*) AS count FROM entites
|
||||
UNION ALL
|
||||
-- Nombre d'opérations
|
||||
SELECT 'Opérations' AS table_name, COUNT(*) AS count FROM operations
|
||||
UNION ALL
|
||||
-- Nombre d'utilisateurs dans la table centrale
|
||||
SELECT 'Users (centrale)' AS table_name, COUNT(*) AS count FROM users
|
||||
UNION ALL
|
||||
-- Nombre d'utilisateurs dans les opérations
|
||||
SELECT 'ope_users' AS table_name, COUNT(*) AS count FROM ope_users
|
||||
UNION ALL
|
||||
-- Nombre de secteurs
|
||||
SELECT 'ope_sectors' AS table_name, COUNT(*) AS count FROM ope_sectors
|
||||
UNION ALL
|
||||
-- Nombre d'associations user-secteur
|
||||
SELECT 'ope_users_sectors' AS table_name, COUNT(*) AS count FROM ope_users_sectors
|
||||
UNION ALL
|
||||
-- Nombre de passages
|
||||
SELECT 'ope_pass' AS table_name, COUNT(*) AS count FROM ope_pass
|
||||
UNION ALL
|
||||
-- Nombre d'historiques de passage
|
||||
SELECT 'ope_pass_histo' AS table_name, COUNT(*) AS count FROM ope_pass_histo;
|
||||
|
||||
-- ================================================================================
|
||||
-- 4. Détail par opération
|
||||
-- ================================================================================
|
||||
|
||||
SELECT '=== DÉTAIL PAR OPÉRATION ===' AS '';
|
||||
|
||||
SELECT
|
||||
o.id AS operation_id,
|
||||
o.libelle AS operation_name,
|
||||
(SELECT COUNT(*) FROM ope_users WHERE fk_operation = o.id) AS nb_users,
|
||||
(SELECT COUNT(*) FROM ope_sectors WHERE fk_operation = o.id) AS nb_sectors,
|
||||
(SELECT COUNT(*) FROM ope_users_sectors WHERE fk_operation = o.id) AS nb_user_sector_links,
|
||||
(SELECT COUNT(*) FROM ope_pass WHERE fk_operation = o.id) AS nb_passages
|
||||
FROM operations o
|
||||
ORDER BY o.id;
|
||||
|
||||
-- ================================================================================
|
||||
-- 5. Vérifier la relation users → ope_users
|
||||
-- ================================================================================
|
||||
|
||||
SELECT '=== RELATION users → ope_users ===' AS '';
|
||||
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.first_name,
|
||||
u.sect_name,
|
||||
COUNT(DISTINCT ou.fk_operation) AS nb_operations,
|
||||
GROUP_CONCAT(DISTINCT ou.fk_operation ORDER BY ou.fk_operation) AS operations_ids
|
||||
FROM users u
|
||||
LEFT JOIN ope_users ou ON u.id = ou.fk_user
|
||||
GROUP BY u.id, u.first_name, u.sect_name
|
||||
ORDER BY u.id;
|
||||
|
||||
-- ================================================================================
|
||||
-- 6. TEST DE SUPPRESSION (commenté pour sécurité)
|
||||
-- ================================================================================
|
||||
|
||||
SELECT '=== INSTRUCTIONS POUR TEST DE SUPPRESSION ===' AS '';
|
||||
SELECT 'Pour tester la suppression en CASCADE, décommentez la section ci-dessous' AS instruction;
|
||||
|
||||
-- Compter avant suppression (remplacer [ID_OPERATION] par un ID réel)
|
||||
/*
|
||||
SET @operation_id = [ID_OPERATION];
|
||||
|
||||
SELECT
|
||||
CONCAT('Opération ID: ', @operation_id) AS info,
|
||||
(SELECT COUNT(*) FROM ope_users WHERE fk_operation = @operation_id) as ope_users_count,
|
||||
(SELECT COUNT(*) FROM ope_users_sectors WHERE fk_operation = @operation_id) as ope_users_sectors_count,
|
||||
(SELECT COUNT(*) FROM ope_pass WHERE fk_operation = @operation_id) as ope_pass_count,
|
||||
(SELECT COUNT(*) FROM ope_sectors WHERE fk_operation = @operation_id) as ope_sectors_count;
|
||||
|
||||
-- Supprimer l'opération
|
||||
DELETE FROM operations WHERE id = @operation_id;
|
||||
|
||||
-- Vérifier que tout a été supprimé (doit retourner 0 partout)
|
||||
SELECT
|
||||
CONCAT('Après suppression de l''opération ID: ', @operation_id) AS info,
|
||||
(SELECT COUNT(*) FROM ope_users WHERE fk_operation = @operation_id) as ope_users_count,
|
||||
(SELECT COUNT(*) FROM ope_users_sectors WHERE fk_operation = @operation_id) as ope_users_sectors_count,
|
||||
(SELECT COUNT(*) FROM ope_pass WHERE fk_operation = @operation_id) as ope_pass_count,
|
||||
(SELECT COUNT(*) FROM ope_sectors WHERE fk_operation = @operation_id) as ope_sectors_count;
|
||||
*/
|
||||
|
||||
SELECT '✓ Vérifications terminées avec succès !' AS status;
|
||||
182
api/scripts/patch_migration_scripts.sh
Normal file
182
api/scripts/patch_migration_scripts.sh
Normal file
@@ -0,0 +1,182 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script de patch pour adapter migrate_from_backup.php et migrate_batch.sh
|
||||
# pour fonctionner avec --env=rca|pra et source=geosector
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PHP_SCRIPT="$SCRIPT_DIR/php/migrate_from_backup.php"
|
||||
BATCH_SCRIPT="$SCRIPT_DIR/migrate_batch.sh"
|
||||
|
||||
echo "=== Patching migration scripts ==="
|
||||
echo ""
|
||||
|
||||
# Backup des fichiers originaux
|
||||
echo "Creating backups..."
|
||||
cp "$PHP_SCRIPT" "$PHP_SCRIPT.backup"
|
||||
cp "$BATCH_SCRIPT" "$BATCH_SCRIPT.backup"
|
||||
echo "✓ Backups created"
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# PATCH 1: migrate_from_backup.php - Configuration multi-env
|
||||
# ============================================================
|
||||
|
||||
echo "Patching migrate_from_backup.php..."
|
||||
|
||||
# Étape 1: Remplacer les constantes DB par configuration multi-env
|
||||
sed -i '31,50s/.*/ \/\/ REPLACED BY PATCH - see below/' "$PHP_SCRIPT"
|
||||
|
||||
# Insérer la nouvelle configuration après la ligne 38
|
||||
sed -i '38a\
|
||||
private $env;\
|
||||
\
|
||||
\/\/ Configuration multi-environnement\
|
||||
private const ENVIRONMENTS = [\
|
||||
'\''rca'\'' => [\
|
||||
'\''host'\'' => '\''13.23.33.3'\'', \/\/ maria3 sur IN3\
|
||||
'\''port'\'' => 3306,\
|
||||
'\''user'\'' => '\''rca_geo_user'\'',\
|
||||
'\''pass'\'' => '\''UPf3C0cQ805LypyM71iW'\'',\
|
||||
'\''target_db'\'' => '\''rca_geo'\'',\
|
||||
'\''source_db'\'' => '\''geosector'\'' \/\/ Base synchronisée par PM7\
|
||||
],\
|
||||
'\''pra'\'' => [\
|
||||
'\''host'\'' => '\''13.23.33.4'\'', \/\/ maria4 sur IN4\
|
||||
'\''port'\'' => 3306,\
|
||||
'\''user'\'' => '\''pra_geo_user'\'',\
|
||||
'\''pass'\'' => '\''d2jAAGGWi8fxFrWgXjOA'\'',\
|
||||
'\''target_db'\'' => '\''pra_geo'\'',\
|
||||
'\''source_db'\'' => '\''geosector'\'' \/\/ Base synchronisée par PM7\
|
||||
]\
|
||||
];' "$PHP_SCRIPT"
|
||||
|
||||
# Étape 2: Modifier le constructeur pour accepter $env
|
||||
sed -i 's/public function __construct($sourceDbName, $targetDbName, $mode/public function __construct($env, $mode/' "$PHP_SCRIPT"
|
||||
|
||||
# Étape 3: Adapter le corps du constructeur
|
||||
sed -i '/public function __construct/,/^ }$/{
|
||||
s/\$this->sourceDbName = \$sourceDbName;/\$this->env = \$env;\n if (!isset(self::ENVIRONMENTS[\$env])) {\n throw new Exception("Invalid environment: \$env. Use '\''rca'\'' or '\''pra'\''");\n }\n \$config = self::ENVIRONMENTS[\$env];\n \$this->sourceDbName = \$config['\''source_db'\''];\n \$this->targetDbName = \$config['\''target_db'\''];/
|
||||
s/\$this->targetDbName = \$targetDbName;//
|
||||
s/Source: {\$sourceDbName}/Environment: \$env/
|
||||
s/Cible: {\$targetDbName}/Source: {\$this->sourceDbName} → Target: {\$this->targetDbName}/
|
||||
}' "$PHP_SCRIPT"
|
||||
|
||||
# Étape 4: Modifier connect() pour utiliser la config de l'env
|
||||
sed -i '/public function connect()/,/^ }$/{
|
||||
s/self::DB_HOST/self::ENVIRONMENTS[\$this->env]['\''host'\'']/g
|
||||
s/self::DB_PORT/self::ENVIRONMENTS[\$this->env]['\''port'\'']/g
|
||||
s/self::DB_USER_ROOT/self::ENVIRONMENTS[\$this->env]['\''user'\'']/g
|
||||
s/self::DB_PASS_ROOT/self::ENVIRONMENTS[\$this->env]['\''pass'\'']/g
|
||||
s/self::DB_USER/self::ENVIRONMENTS[\$this->env]['\''user'\'']/g
|
||||
s/self::DB_PASS/self::ENVIRONMENTS[\$this->env]['\''pass'\'']/g
|
||||
}' "$PHP_SCRIPT"
|
||||
|
||||
# Étape 5: Modifier parseArguments() - supprimer source-db et target-db, ajouter env
|
||||
sed -i '/function parseArguments/,/^}$/{
|
||||
s/'\''source-db'\'' => null,/'\''env'\'' => '\''rca'\'',/
|
||||
s/'\''target-db'\'' => '\''pra_geo'\'',//
|
||||
}' "$PHP_SCRIPT"
|
||||
|
||||
# Étape 6: Modifier showHelp()
|
||||
sed -i '/function showHelp/,/^}$/{
|
||||
s/--source-db=NAME.*\[REQUIS\]/--env=ENV Environment: '\''rca'\'' (recette) ou '\''pra'\'' (production) [défaut: rca]/
|
||||
s/--target-db=NAME.*/ (supprimé - déduit automatiquement de --env)/
|
||||
s/--source-db=geosector_20251007/--env=rca/g
|
||||
s/--target-db=pra_geo//g
|
||||
s/--target-db=rca_geo//g
|
||||
}' "$PHP_SCRIPT"
|
||||
|
||||
# Étape 7: Modifier la validation des arguments
|
||||
sed -i '/Validation des arguments/,/exit(1);/{
|
||||
s/if (!$args\['\''source-db'\''\])/if (!isset(self::ENVIRONMENTS[\$args['\''env'\'']]))/
|
||||
s/--source-db est requis/--env doit être '\''rca'\'' ou '\''pra'\''/
|
||||
}' "$PHP_SCRIPT"
|
||||
|
||||
# Étape 8: Modifier l'instanciation de BackupMigration
|
||||
sed -i '/new BackupMigration/,/);/{
|
||||
s/\$args\['\''source-db'\''\],/\$args['\''env'\''],/
|
||||
s/\$args\['\''target-db'\''\],//
|
||||
}' "$PHP_SCRIPT"
|
||||
|
||||
echo "✓ migrate_from_backup.php patched"
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# PATCH 2: migrate_batch.sh - Adapter pour env rca/pra
|
||||
# ============================================================
|
||||
|
||||
echo "Patching migrate_batch.sh..."
|
||||
|
||||
# Étape 1: Détecter l'environnement automatiquement ou via paramètre
|
||||
sed -i '/# Configuration/a\
|
||||
\
|
||||
# Détection automatique de l'\''environnement\
|
||||
if [ -f "/etc/hostname" ]; then\
|
||||
CONTAINER_NAME=$(cat /etc/hostname)\
|
||||
case $CONTAINER_NAME in\
|
||||
rca-geo)\
|
||||
ENV="rca"\
|
||||
;;\
|
||||
pra-geo)\
|
||||
ENV="pra"\
|
||||
;;\
|
||||
*)\
|
||||
ENV="rca" # Défaut\
|
||||
;;\
|
||||
esac\
|
||||
else\
|
||||
ENV="rca" # Défaut\
|
||||
fi' "$BATCH_SCRIPT"
|
||||
|
||||
# Étape 2: Remplacer SOURCE_DB et TARGET_DB
|
||||
sed -i 's/SOURCE_DB="geosector_20251013_13"/# SOURCE_DB removed - always "geosector" (deduced from --env)/' "$BATCH_SCRIPT"
|
||||
sed -i 's/TARGET_DB="pra_geo"/# TARGET_DB removed - deduced from --env/' "$BATCH_SCRIPT"
|
||||
|
||||
# Étape 3: Ajouter option --env dans le parsing
|
||||
sed -i '/--interactive|-i)/i\
|
||||
--env)\
|
||||
ENV="$2"\
|
||||
shift 2\
|
||||
;;' "$BATCH_SCRIPT"
|
||||
|
||||
# Étape 4: Modifier les appels à migrate_from_backup.php - ligne 200
|
||||
sed -i '200,210s/--source-db="\$SOURCE_DB"/--env="$ENV"/' "$BATCH_SCRIPT"
|
||||
sed -i '200,210s/--target-db="\$TARGET_DB"//' "$BATCH_SCRIPT"
|
||||
|
||||
# Étape 5: Modifier les appels dans la boucle - ligne 374
|
||||
sed -i '374,380s/--source-db="\$SOURCE_DB"/--env="$ENV"/' "$BATCH_SCRIPT"
|
||||
sed -i '374,380s/--target-db="\$TARGET_DB"//' "$BATCH_SCRIPT"
|
||||
|
||||
# Étape 6: Mettre à jour les messages de log
|
||||
sed -i 's/📁 Source: \$SOURCE_DB/🌍 Environment: $ENV/' "$BATCH_SCRIPT"
|
||||
sed -i 's/📁 Cible: \$TARGET_DB/📁 Source: geosector → Target: (déduit de $ENV)/' "$BATCH_SCRIPT"
|
||||
|
||||
echo "✓ migrate_batch.sh patched"
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# Résumé
|
||||
# ============================================================
|
||||
|
||||
echo "=== Patch completed ==="
|
||||
echo ""
|
||||
echo "Backups saved:"
|
||||
echo " - $PHP_SCRIPT.backup"
|
||||
echo " - $BATCH_SCRIPT.backup"
|
||||
echo ""
|
||||
echo "New usage:"
|
||||
echo " # Sur rca-geo (détection auto)"
|
||||
echo " ./migrate_batch.sh"
|
||||
echo ""
|
||||
echo " # Sur pra-geo avec --env explicite"
|
||||
echo " ./migrate_batch.sh --env=pra"
|
||||
echo ""
|
||||
echo " # Migration d'une entité spécifique"
|
||||
echo " php php/migrate_from_backup.php --env=rca --mode=entity --entity-id=45"
|
||||
echo ""
|
||||
echo "To restore backups:"
|
||||
echo " cp $PHP_SCRIPT.backup $PHP_SCRIPT"
|
||||
echo " cp $BATCH_SCRIPT.backup $BATCH_SCRIPT"
|
||||
240
api/scripts/php/create_missing_stripe_locations.php
Executable file
240
api/scripts/php/create_missing_stripe_locations.php
Executable file
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Script : Créer les Stripe Terminal Locations manquantes
|
||||
*
|
||||
* Raison : Certains comptes Stripe Connect ont été créés avant l'implémentation
|
||||
* de la création automatique de Location. Ce script crée les Locations
|
||||
* manquantes pour tous les comptes existants.
|
||||
*
|
||||
* Date : 2025-11-03
|
||||
* Auteur : Migration automatique
|
||||
*/
|
||||
|
||||
// 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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Charger l'autoloader Composer (pour Stripe SDK)
|
||||
require_once dirname(dirname(__DIR__)) . '/vendor/autoload.php';
|
||||
|
||||
// Charger les classes nécessaires explicitement
|
||||
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/ApiService.php';
|
||||
require_once dirname(dirname(__DIR__)) . '/src/Services/LogService.php';
|
||||
require_once dirname(dirname(__DIR__)) . '/src/Services/StripeService.php';
|
||||
|
||||
use App\Services\StripeService;
|
||||
|
||||
// Initialiser la configuration
|
||||
$config = AppConfig::getInstance();
|
||||
$env = $config->getEnvironment();
|
||||
$dbConfig = $config->getDatabaseConfig();
|
||||
|
||||
echo "\n";
|
||||
echo "=============================================================================\n";
|
||||
echo " Création des Stripe Terminal Locations manquantes\n";
|
||||
echo "=============================================================================\n";
|
||||
echo "Environnement : " . strtoupper($env) . "\n";
|
||||
echo "Base de données : " . $dbConfig['name'] . "\n";
|
||||
echo "\n";
|
||||
|
||||
try {
|
||||
// Initialiser la base de données avec la configuration
|
||||
Database::init($dbConfig);
|
||||
$db = Database::getInstance();
|
||||
|
||||
// StripeService est un singleton
|
||||
$stripeService = StripeService::getInstance();
|
||||
|
||||
// 1. Identifier les comptes sans Location
|
||||
echo "📋 Recherche des comptes Stripe sans Location...\n\n";
|
||||
|
||||
$stmt = $db->query("
|
||||
SELECT
|
||||
sa.id,
|
||||
sa.fk_entite,
|
||||
sa.stripe_account_id,
|
||||
sa.stripe_location_id,
|
||||
e.encrypted_name,
|
||||
e.adresse1,
|
||||
e.adresse2,
|
||||
e.code_postal,
|
||||
e.ville
|
||||
FROM stripe_accounts sa
|
||||
INNER JOIN entites e ON sa.fk_entite = e.id
|
||||
WHERE sa.stripe_account_id IS NOT NULL
|
||||
AND (sa.stripe_location_id IS NULL OR sa.stripe_location_id = '')
|
||||
AND e.chk_active = 1
|
||||
");
|
||||
|
||||
$accountsWithoutLocation = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$total = count($accountsWithoutLocation);
|
||||
|
||||
if ($total === 0) {
|
||||
echo "✅ Aucun compte sans Location trouvé. Tous les comptes sont à jour !\n\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
echo "ℹ️ Trouvé $total compte(s) sans Location :\n\n";
|
||||
|
||||
foreach ($accountsWithoutLocation as $account) {
|
||||
$name = !empty($account['encrypted_name'])
|
||||
? ApiService::decryptData($account['encrypted_name'])
|
||||
: 'Amicale #' . $account['fk_entite'];
|
||||
|
||||
echo " - Entité #{$account['fk_entite']} : $name\n";
|
||||
echo " Stripe Account : {$account['stripe_account_id']}\n";
|
||||
echo " Adresse : {$account['adresse1']}, {$account['code_postal']} {$account['ville']}\n\n";
|
||||
}
|
||||
|
||||
// Demander confirmation
|
||||
echo "⚠️ Voulez-vous créer les Locations manquantes ? (yes/no) : ";
|
||||
$handle = fopen("php://stdin", "r");
|
||||
$line = trim(fgets($handle));
|
||||
fclose($handle);
|
||||
|
||||
if ($line !== 'yes') {
|
||||
echo "❌ Opération annulée.\n\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
echo "\n🚀 Création des Locations...\n\n";
|
||||
|
||||
// Initialiser Stripe avec la bonne clé selon le mode
|
||||
$stripeConfig = $config->getStripeConfig();
|
||||
$stripeMode = $stripeConfig['mode'] ?? 'test';
|
||||
$stripeSecretKey = ($stripeMode === 'live')
|
||||
? $stripeConfig['secret_key_live']
|
||||
: $stripeConfig['secret_key_test'];
|
||||
|
||||
\Stripe\Stripe::setApiKey($stripeSecretKey);
|
||||
echo "ℹ️ Mode Stripe : " . strtoupper($stripeMode) . "\n\n";
|
||||
|
||||
$success = 0;
|
||||
$errors = 0;
|
||||
|
||||
// 2. Créer les Locations manquantes
|
||||
foreach ($accountsWithoutLocation as $account) {
|
||||
$entiteId = $account['fk_entite'];
|
||||
$stripeAccountId = $account['stripe_account_id'];
|
||||
|
||||
$name = !empty($account['encrypted_name'])
|
||||
? ApiService::decryptData($account['encrypted_name'])
|
||||
: 'Amicale #' . $entiteId;
|
||||
|
||||
echo "🔧 Entité #{$entiteId} : $name\n";
|
||||
|
||||
try {
|
||||
// Construire l'adresse
|
||||
$adresse1 = !empty($account['adresse1']) ? $account['adresse1'] : 'Adresse non renseignée';
|
||||
$ville = !empty($account['ville']) ? $account['ville'] : 'Ville';
|
||||
$codePostal = !empty($account['code_postal']) ? $account['code_postal'] : '00000';
|
||||
|
||||
// Construire l'adresse pour Stripe (ne pas envoyer line2 si vide)
|
||||
$addressData = [
|
||||
'line1' => $adresse1,
|
||||
'city' => $ville,
|
||||
'postal_code' => $codePostal,
|
||||
'country' => 'FR',
|
||||
];
|
||||
|
||||
// Ajouter line2 seulement s'il n'est pas vide
|
||||
if (!empty($account['adresse2'])) {
|
||||
$addressData['line2'] = $account['adresse2'];
|
||||
}
|
||||
|
||||
// Créer la Location via Stripe API
|
||||
$location = \Stripe\Terminal\Location::create([
|
||||
'display_name' => $name,
|
||||
'address' => $addressData,
|
||||
'metadata' => [
|
||||
'entite_id' => $entiteId,
|
||||
'type' => 'tap_to_pay',
|
||||
'created_by' => 'migration_script'
|
||||
]
|
||||
], [
|
||||
'stripe_account' => $stripeAccountId
|
||||
]);
|
||||
|
||||
$locationId = $location->id;
|
||||
|
||||
// Mettre à jour la base de données
|
||||
$updateStmt = $db->prepare("
|
||||
UPDATE stripe_accounts
|
||||
SET stripe_location_id = :location_id,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id
|
||||
");
|
||||
|
||||
$updateStmt->execute([
|
||||
'location_id' => $locationId,
|
||||
'id' => $account['id']
|
||||
]);
|
||||
|
||||
echo " ✅ Location créée : $locationId\n\n";
|
||||
$success++;
|
||||
|
||||
} catch (\Stripe\Exception\ApiErrorException $e) {
|
||||
echo " ❌ Erreur Stripe : " . $e->getMessage() . "\n\n";
|
||||
$errors++;
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ Erreur : " . $e->getMessage() . "\n\n";
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Résumé
|
||||
echo "\n";
|
||||
echo "=============================================================================\n";
|
||||
echo " Résumé de l'opération\n";
|
||||
echo "=============================================================================\n";
|
||||
echo "✅ Locations créées avec succès : $success\n";
|
||||
echo "❌ Erreurs : $errors\n";
|
||||
echo "📊 Total traité : $total\n";
|
||||
echo "\n";
|
||||
|
||||
// 4. Vérification finale
|
||||
echo "🔍 Vérification finale...\n";
|
||||
$stmt = $db->query("
|
||||
SELECT COUNT(*) as remaining
|
||||
FROM stripe_accounts sa
|
||||
WHERE sa.stripe_account_id IS NOT NULL
|
||||
AND (sa.stripe_location_id IS NULL OR sa.stripe_location_id = '')
|
||||
");
|
||||
$remaining = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
echo " ℹ️ Comptes restants sans Location : " . $remaining['remaining'] . "\n\n";
|
||||
|
||||
if ($remaining['remaining'] == 0) {
|
||||
echo "🎉 Tous les comptes Stripe ont maintenant une Location !\n\n";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "\n";
|
||||
echo "=============================================================================\n";
|
||||
echo " ❌ ERREUR\n";
|
||||
echo "=============================================================================\n";
|
||||
echo "Message : " . $e->getMessage() . "\n";
|
||||
echo "Fichier : " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||
echo "\n";
|
||||
exit(1);
|
||||
}
|
||||
2047
api/scripts/php/migrate_from_backup.php
Executable file
2047
api/scripts/php/migrate_from_backup.php
Executable file
File diff suppressed because it is too large
Load Diff
543
api/scripts/php/migrate_from_backup_verbose.php
Executable file
543
api/scripts/php/migrate_from_backup_verbose.php
Executable file
@@ -0,0 +1,543 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script de migration VERBOSE avec détails table par table
|
||||
*
|
||||
* Usage:
|
||||
* php migrate_from_backup_verbose.php \
|
||||
* --source-db=geosector_20251008 \
|
||||
* --target-db=pra_geo \
|
||||
* --entity-id=1178 \
|
||||
* --limit-operations=3
|
||||
*/
|
||||
|
||||
// Inclusion des dépendances de l'API
|
||||
require_once dirname(dirname(__DIR__)) . '/bootstrap.php';
|
||||
|
||||
use GeoSector\Services\ApiService;
|
||||
|
||||
// Configuration
|
||||
const DB_HOST = '13.23.33.4';
|
||||
const DB_PORT = 3306;
|
||||
const DB_USER = 'pra_geo_user';
|
||||
const DB_PASS = 'd2jAAGGWi8fxFrWgXjOA';
|
||||
const DB_USER_ROOT = 'root';
|
||||
const DB_PASS_ROOT = 'MyAlpLocal,90b';
|
||||
|
||||
// Couleurs pour terminal
|
||||
const C_RESET = "\033[0m";
|
||||
const C_RED = "\033[0;31m";
|
||||
const C_GREEN = "\033[0;32m";
|
||||
const C_YELLOW = "\033[1;33m";
|
||||
const C_BLUE = "\033[0;34m";
|
||||
const C_CYAN = "\033[0;36m";
|
||||
const C_BOLD = "\033[1m";
|
||||
|
||||
// Variables globales
|
||||
$sourceDb = null;
|
||||
$targetDb = null;
|
||||
$sourceDbName = null;
|
||||
$targetDbName = null;
|
||||
$entityId = null;
|
||||
$limitOperations = 3;
|
||||
$stats = [
|
||||
'entites' => ['source' => 0, 'migrated' => 0],
|
||||
'users' => ['source' => 0, 'migrated' => 0],
|
||||
'operations' => ['source' => 0, 'migrated' => 0],
|
||||
'ope_sectors' => ['source' => 0, 'migrated' => 0],
|
||||
'sectors_adresses' => ['source' => 0, 'migrated' => 0],
|
||||
'ope_users' => ['source' => 0, 'migrated' => 0],
|
||||
'ope_users_sectors' => ['source' => 0, 'migrated' => 0],
|
||||
'ope_pass' => ['source' => 0, 'migrated' => 0],
|
||||
'ope_pass_histo' => ['source' => 0, 'migrated' => 0],
|
||||
'medias' => ['source' => 0, 'migrated' => 0],
|
||||
];
|
||||
|
||||
// Fonctions utilitaires
|
||||
function println($message, $color = C_RESET) {
|
||||
echo $color . $message . C_RESET . "\n";
|
||||
}
|
||||
|
||||
function printBox($title, $color = C_BLUE) {
|
||||
$width = 70;
|
||||
$titleLen = strlen($title);
|
||||
$padding = ($width - $titleLen - 2) / 2;
|
||||
|
||||
println(str_repeat("═", $width), $color);
|
||||
println(str_repeat(" ", floor($padding)) . $title . str_repeat(" ", ceil($padding)), $color);
|
||||
println(str_repeat("═", $width), $color);
|
||||
}
|
||||
|
||||
function printStep($step, $substep = null) {
|
||||
if ($substep) {
|
||||
println(" ├─ " . $substep, C_CYAN);
|
||||
} else {
|
||||
println("\n" . C_BOLD . "▶ " . $step . C_RESET);
|
||||
}
|
||||
}
|
||||
|
||||
function printStat($label, $source, $migrated, $indent = " ") {
|
||||
$status = ($source === $migrated) ? C_GREEN . "✓" : C_YELLOW . "⚠";
|
||||
println($indent . "📊 {$label}: {$source} source → {$migrated} migré(s) {$status}" . C_RESET);
|
||||
}
|
||||
|
||||
function connectDatabases($sourceDbName, $targetDbName) {
|
||||
global $sourceDb, $targetDb;
|
||||
|
||||
printStep("Connexion aux bases de données");
|
||||
|
||||
try {
|
||||
// Base source
|
||||
$dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
|
||||
DB_HOST, DB_PORT, $sourceDbName);
|
||||
$sourceDb = new PDO($dsn, DB_USER_ROOT, DB_PASS_ROOT, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
printStep("Source connectée: {$sourceDbName}", true);
|
||||
|
||||
// Base cible
|
||||
$dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
|
||||
DB_HOST, DB_PORT, $targetDbName);
|
||||
$targetDb = new PDO($dsn, DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
printStep("Cible connectée: {$targetDbName}", true);
|
||||
|
||||
return true;
|
||||
} catch (PDOException $e) {
|
||||
println("✗ Erreur connexion: " . $e->getMessage(), C_RED);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getEntityInfo($entityId) {
|
||||
global $sourceDb;
|
||||
|
||||
$stmt = $sourceDb->prepare("
|
||||
SELECT rowid, libelle, cp, ville
|
||||
FROM users_entites
|
||||
WHERE rowid = ?
|
||||
");
|
||||
$stmt->execute([$entityId]);
|
||||
return $stmt->fetch();
|
||||
}
|
||||
|
||||
function migrateReferenceTable($tableName) {
|
||||
global $sourceDb, $targetDb;
|
||||
|
||||
printStep("Migration table: {$tableName}");
|
||||
|
||||
// Compter source
|
||||
$count = $sourceDb->query("SELECT COUNT(*) FROM {$tableName}")->fetchColumn();
|
||||
printStep("Source: {$count} enregistrements", true);
|
||||
|
||||
if ($count === 0) {
|
||||
printStep("Aucune donnée à migrer", true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Récupérer les données
|
||||
$rows = $sourceDb->query("SELECT * FROM {$tableName}")->fetchAll();
|
||||
|
||||
// Préparer l'insertion
|
||||
$columns = array_keys($rows[0]);
|
||||
$placeholders = array_map(fn($col) => ":{$col}", $columns);
|
||||
|
||||
$sql = sprintf(
|
||||
"INSERT INTO %s (%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s",
|
||||
$tableName,
|
||||
implode(', ', $columns),
|
||||
implode(', ', $placeholders),
|
||||
implode(', ', array_map(fn($col) => "{$col} = VALUES({$col})", $columns))
|
||||
);
|
||||
|
||||
$stmt = $targetDb->prepare($sql);
|
||||
|
||||
$success = 0;
|
||||
foreach ($rows as $row) {
|
||||
try {
|
||||
$stmt->execute($row);
|
||||
$success++;
|
||||
} catch (PDOException $e) {
|
||||
// Ignorer erreurs
|
||||
}
|
||||
}
|
||||
|
||||
printStep("Migré: {$success}/{$count}", true);
|
||||
return $success;
|
||||
}
|
||||
|
||||
function migrateEntite($entityId) {
|
||||
global $sourceDb, $targetDb, $stats;
|
||||
|
||||
printStep("ÉTAPE 1: Migration de l'entité #{$entityId}");
|
||||
|
||||
// Récupérer l'entité source
|
||||
$stmt = $sourceDb->prepare("
|
||||
SELECT * FROM users_entites WHERE rowid = ?
|
||||
");
|
||||
$stmt->execute([$entityId]);
|
||||
$entity = $stmt->fetch();
|
||||
|
||||
if (!$entity) {
|
||||
println(" ✗ Entité introuvable", C_RED);
|
||||
return false;
|
||||
}
|
||||
|
||||
$stats['entites']['source'] = 1;
|
||||
|
||||
println(" 📋 Entité: " . $entity['libelle']);
|
||||
println(" 📍 Code postal: " . ($entity['cp'] ?? 'N/A'));
|
||||
println(" 🏙️ Ville: " . ($entity['ville'] ?? 'N/A'));
|
||||
|
||||
// Chiffrer les données
|
||||
$encryptedName = ApiService::encryptSearchableData($entity['libelle']);
|
||||
$encryptedEmail = !empty($entity['email']) ? ApiService::encryptSearchableData($entity['email']) : '';
|
||||
$encryptedPhone = !empty($entity['phone']) ? ApiService::encryptData($entity['phone']) : '';
|
||||
$encryptedMobile = !empty($entity['mobile']) ? ApiService::encryptData($entity['mobile']) : '';
|
||||
|
||||
// Insérer dans la cible
|
||||
$sql = "INSERT INTO entites (
|
||||
id, encrypted_name, code_postal, ville, encrypted_email, encrypted_phone, encrypted_mobile,
|
||||
fk_region, fk_type, chk_active, created_at, updated_at
|
||||
) VALUES (
|
||||
:id, :name, :cp, :ville, :email, :phone, :mobile,
|
||||
:region, :type, :active, :created, :updated
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
encrypted_name = VALUES(encrypted_name),
|
||||
code_postal = VALUES(code_postal),
|
||||
ville = VALUES(ville)";
|
||||
|
||||
$stmt = $targetDb->prepare($sql);
|
||||
$stmt->execute([
|
||||
'id' => $entity['rowid'],
|
||||
'name' => $encryptedName,
|
||||
'cp' => $entity['cp'] ?? '',
|
||||
'ville' => $entity['ville'] ?? '',
|
||||
'email' => $encryptedEmail,
|
||||
'phone' => $encryptedPhone,
|
||||
'mobile' => $encryptedMobile,
|
||||
'region' => $entity['fk_region'] ?? 1,
|
||||
'type' => $entity['fk_type'] ?? 1,
|
||||
'active' => $entity['active'] ?? 1,
|
||||
'created' => $entity['date_creat'],
|
||||
'updated' => $entity['date_modif']
|
||||
]);
|
||||
|
||||
$stats['entites']['migrated'] = 1;
|
||||
|
||||
printStat("Entité", 1, 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function migrateUsers($entityId) {
|
||||
global $sourceDb, $targetDb, $stats;
|
||||
|
||||
printStep("ÉTAPE 2: Migration des utilisateurs");
|
||||
|
||||
// Compter source
|
||||
$count = $sourceDb->prepare("SELECT COUNT(*) FROM users WHERE fk_entite = ? AND active = 1");
|
||||
$count->execute([$entityId]);
|
||||
$sourceCount = $count->fetchColumn();
|
||||
|
||||
$stats['users']['source'] = $sourceCount;
|
||||
println(" 📊 Source: {$sourceCount} utilisateurs actifs");
|
||||
|
||||
if ($sourceCount === 0) {
|
||||
println(" ⚠️ Aucun utilisateur à migrer", C_YELLOW);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Récupérer les users
|
||||
$stmt = $sourceDb->prepare("
|
||||
SELECT * FROM users WHERE fk_entite = ? AND active = 1
|
||||
");
|
||||
$stmt->execute([$entityId]);
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
$success = 0;
|
||||
foreach ($users as $user) {
|
||||
try {
|
||||
$encryptedName = ApiService::encryptSearchableData($user['nom']);
|
||||
$encryptedUsername = !empty($user['username']) ? ApiService::encryptSearchableData($user['username']) : '';
|
||||
$encryptedEmail = !empty($user['email']) ? ApiService::encryptSearchableData($user['email']) : '';
|
||||
$encryptedPhone = !empty($user['telephone']) ? ApiService::encryptData($user['telephone']) : '';
|
||||
$encryptedMobile = !empty($user['mobile']) ? ApiService::encryptData($user['mobile']) : '';
|
||||
|
||||
$sql = "INSERT INTO users (
|
||||
id, fk_entite, fk_role, encrypted_name, first_name,
|
||||
encrypted_user_name, user_pass_hash, encrypted_email,
|
||||
encrypted_phone, encrypted_mobile, chk_active, created_at, updated_at
|
||||
) VALUES (
|
||||
:id, :entity, :role, :name, :firstname,
|
||||
:username, :pass, :email,
|
||||
:phone, :mobile, :active, :created, :updated
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
encrypted_name = VALUES(encrypted_name),
|
||||
encrypted_email = VALUES(encrypted_email)";
|
||||
|
||||
$stmt = $targetDb->prepare($sql);
|
||||
$stmt->execute([
|
||||
'id' => $user['rowid'],
|
||||
'entity' => $entityId,
|
||||
'role' => $user['fk_role'] ?? 1,
|
||||
'name' => $encryptedName,
|
||||
'firstname' => $user['prenom'] ?? '',
|
||||
'username' => $encryptedUsername,
|
||||
'pass' => $user['password'] ?? '',
|
||||
'email' => $encryptedEmail,
|
||||
'phone' => $encryptedPhone,
|
||||
'mobile' => $encryptedMobile,
|
||||
'active' => 1,
|
||||
'created' => $user['date_creat'],
|
||||
'updated' => $user['date_modif']
|
||||
]);
|
||||
|
||||
$success++;
|
||||
} catch (PDOException $e) {
|
||||
// Ignorer
|
||||
}
|
||||
}
|
||||
|
||||
$stats['users']['migrated'] = $success;
|
||||
printStat("Utilisateurs", $sourceCount, $success);
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
function migrateOperations($entityId, $limit = 3) {
|
||||
global $sourceDb, $targetDb, $stats;
|
||||
|
||||
printStep("ÉTAPE 3: Migration des opérations (limite: {$limit})");
|
||||
|
||||
// Compter toutes les opérations
|
||||
$count = $sourceDb->prepare("SELECT COUNT(*) FROM operations WHERE fk_entite = ? AND active = 1");
|
||||
$count->execute([$entityId]);
|
||||
$totalCount = $count->fetchColumn();
|
||||
|
||||
println(" 📊 Total disponible: {$totalCount} opérations");
|
||||
println(" 🎯 Limitation: {$limit} dernières opérations");
|
||||
|
||||
$stats['operations']['source'] = min($limit, $totalCount);
|
||||
|
||||
// Récupérer les N dernières opérations
|
||||
$stmt = $sourceDb->prepare("
|
||||
SELECT * FROM operations
|
||||
WHERE fk_entite = ? AND active = 1
|
||||
ORDER BY date_creat DESC
|
||||
LIMIT ?
|
||||
");
|
||||
$stmt->execute([$entityId, $limit]);
|
||||
$operations = $stmt->fetchAll();
|
||||
|
||||
if (empty($operations)) {
|
||||
println(" ⚠️ Aucune opération à migrer", C_YELLOW);
|
||||
return [];
|
||||
}
|
||||
|
||||
$migratedOps = [];
|
||||
foreach ($operations as $op) {
|
||||
try {
|
||||
$sql = "INSERT INTO operations (
|
||||
id, fk_entite, libelle, date_deb, date_fin,
|
||||
chk_distinct_sectors, chk_active, created_at, updated_at
|
||||
) VALUES (
|
||||
:id, :entity, :libelle, :datedeb, :datefin,
|
||||
:distinct, :active, :created, :updated
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
libelle = VALUES(libelle)";
|
||||
|
||||
$stmt = $targetDb->prepare($sql);
|
||||
$stmt->execute([
|
||||
'id' => $op['rowid'],
|
||||
'entity' => $entityId,
|
||||
'libelle' => $op['libelle'],
|
||||
'datedeb' => $op['date_deb'],
|
||||
'datefin' => $op['date_fin'],
|
||||
'distinct' => $op['chk_distinct_sectors'] ?? 0,
|
||||
'active' => 1,
|
||||
'created' => $op['date_creat'],
|
||||
'updated' => $op['date_modif']
|
||||
]);
|
||||
|
||||
$migratedOps[] = $op['rowid'];
|
||||
$stats['operations']['migrated']++;
|
||||
|
||||
println(" ├─ Opération #{$op['rowid']}: " . $op['libelle'], C_GREEN);
|
||||
} catch (PDOException $e) {
|
||||
println(" ├─ ✗ Erreur opération #{$op['rowid']}: " . $e->getMessage(), C_RED);
|
||||
}
|
||||
}
|
||||
|
||||
printStat("Opérations", count($operations), count($migratedOps));
|
||||
|
||||
return $migratedOps;
|
||||
}
|
||||
|
||||
function migrateOperationDetails($operationId, $entityId) {
|
||||
global $sourceDb, $targetDb, $stats;
|
||||
|
||||
println("\n " . C_BOLD . "┌─ Détails opération #{$operationId}" . C_RESET);
|
||||
|
||||
// 1. Compter les passages
|
||||
$passCount = $sourceDb->prepare("SELECT COUNT(*) FROM ope_pass WHERE fk_operation = ?");
|
||||
$passCount->execute([$operationId]);
|
||||
$nbPassages = $passCount->fetchColumn();
|
||||
|
||||
println(" │ 📊 Passages disponibles: {$nbPassages}");
|
||||
|
||||
// 2. Compter les ope_users
|
||||
$opeUsersCount = $sourceDb->prepare("SELECT COUNT(*) FROM ope_users WHERE fk_operation = ?");
|
||||
$opeUsersCount->execute([$operationId]);
|
||||
$nbOpeUsers = $opeUsersCount->fetchColumn();
|
||||
|
||||
$stats['ope_users']['source'] += $nbOpeUsers;
|
||||
println(" │ 👥 Associations users: {$nbOpeUsers}");
|
||||
|
||||
// 3. Compter les secteurs (via ope_users_sectors)
|
||||
$sectorsCount = $sourceDb->prepare("
|
||||
SELECT COUNT(DISTINCT ous.fk_sector)
|
||||
FROM ope_users_sectors ous
|
||||
WHERE ous.fk_operation = ?
|
||||
");
|
||||
$sectorsCount->execute([$operationId]);
|
||||
$nbSectors = $sectorsCount->fetchColumn();
|
||||
|
||||
println(" │ 🗺️ Secteurs distincts: {$nbSectors}");
|
||||
|
||||
println(" └─ " . C_CYAN . "Migration des données associées..." . C_RESET);
|
||||
|
||||
// Migration ope_users (simplifié pour l'exemple)
|
||||
// ... (code de migration réel ici)
|
||||
|
||||
$stats['ope_pass']['source'] += $nbPassages;
|
||||
}
|
||||
|
||||
// === MAIN ===
|
||||
|
||||
function parseArguments($argv) {
|
||||
$args = [
|
||||
'source-db' => null,
|
||||
'target-db' => 'pra_geo',
|
||||
'entity-id' => null,
|
||||
'limit-operations' => 3,
|
||||
'help' => false
|
||||
];
|
||||
|
||||
foreach ($argv as $arg) {
|
||||
if (strpos($arg, '--') === 0) {
|
||||
$parts = explode('=', substr($arg, 2), 2);
|
||||
$key = $parts[0];
|
||||
$value = $parts[1] ?? true;
|
||||
|
||||
if (array_key_exists($key, $args)) {
|
||||
$args[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
// Vérifier CLI
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
die("Ce script doit être exécuté en ligne de commande.\n");
|
||||
}
|
||||
|
||||
$args = parseArguments($argv);
|
||||
|
||||
if ($args['help'] || !$args['source-db'] || !$args['entity-id']) {
|
||||
echo <<<HELP
|
||||
|
||||
Usage: php migrate_from_backup_verbose.php [OPTIONS]
|
||||
|
||||
Options:
|
||||
--source-db=NAME Base source (ex: geosector_20251008) [REQUIS]
|
||||
--target-db=NAME Base cible (défaut: pra_geo)
|
||||
--entity-id=ID ID de l'entité à migrer [REQUIS]
|
||||
--limit-operations=N Nombre d'opérations à migrer (défaut: 3)
|
||||
--help Affiche cette aide
|
||||
|
||||
Exemple:
|
||||
php migrate_from_backup_verbose.php \\
|
||||
--source-db=geosector_20251008 \\
|
||||
--target-db=pra_geo \\
|
||||
--entity-id=1178 \\
|
||||
--limit-operations=3
|
||||
|
||||
HELP;
|
||||
exit($args['help'] ? 0 : 1);
|
||||
}
|
||||
|
||||
$sourceDbName = $args['source-db'];
|
||||
$targetDbName = $args['target-db'];
|
||||
$entityId = (int)$args['entity-id'];
|
||||
$limitOperations = (int)$args['limit-operations'];
|
||||
|
||||
// Bannière
|
||||
printBox("MIGRATION VERBOSE - DÉTAILS TABLE PAR TABLE", C_BLUE);
|
||||
println("📅 Date: " . date('Y-m-d H:i:s'));
|
||||
println("📁 Source: {$sourceDbName}");
|
||||
println("📁 Cible: {$targetDbName}");
|
||||
println("🎯 Entité: #{$entityId}");
|
||||
println("📊 Limite opérations: {$limitOperations}");
|
||||
println("");
|
||||
|
||||
// Connexion
|
||||
if (!connectDatabases($sourceDbName, $targetDbName)) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Récupérer infos entité
|
||||
$entityInfo = getEntityInfo($entityId);
|
||||
if (!$entityInfo) {
|
||||
println("✗ Entité #{$entityId} introuvable", C_RED);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
println("\n📋 Entité trouvée: " . $entityInfo['libelle']);
|
||||
println("📍 CP: " . ($entityInfo['cp'] ?? 'N/A') . " - Ville: " . ($entityInfo['ville'] ?? 'N/A'));
|
||||
println("");
|
||||
|
||||
// Migration des tables de référence (x_*)
|
||||
printBox("TABLES DE RÉFÉRENCE", C_CYAN);
|
||||
$referenceTables = ['x_devises', 'x_entites_types', 'x_types_passages',
|
||||
'x_types_reglements', 'x_users_roles'];
|
||||
foreach ($referenceTables as $table) {
|
||||
migrateReferenceTable($table);
|
||||
}
|
||||
|
||||
// Migration entité
|
||||
printBox("MIGRATION ENTITÉ", C_CYAN);
|
||||
if (!migrateEntite($entityId)) {
|
||||
println("✗ Échec migration entité", C_RED);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Migration users
|
||||
printBox("MIGRATION UTILISATEURS", C_CYAN);
|
||||
migrateUsers($entityId);
|
||||
|
||||
// Migration opérations
|
||||
printBox("MIGRATION OPÉRATIONS", C_CYAN);
|
||||
$migratedOps = migrateOperations($entityId, $limitOperations);
|
||||
|
||||
// Détails par opération
|
||||
foreach ($migratedOps as $opId) {
|
||||
migrateOperationDetails($opId, $entityId);
|
||||
}
|
||||
|
||||
// Résumé final
|
||||
printBox("RÉSUMÉ DE LA MIGRATION", C_GREEN);
|
||||
foreach ($stats as $table => $data) {
|
||||
if ($data['source'] > 0 || $data['migrated'] > 0) {
|
||||
printStat(ucfirst($table), $data['source'], $data['migrated'], " ");
|
||||
}
|
||||
}
|
||||
|
||||
println("\n✅ Migration terminée avec succès!", C_GREEN);
|
||||
exit(0);
|
||||
282
api/scripts/php/verify_migration_structure.php
Normal file
282
api/scripts/php/verify_migration_structure.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
/**
|
||||
* Script de vérification de la cohérence des structures
|
||||
* avant migration entre geosector (source) et geo_app (cible)
|
||||
*
|
||||
* Ce script compare les colonnes de chaque table migrée
|
||||
* et identifie les incohérences potentielles
|
||||
*
|
||||
* Usage: php scripts/php/verify_migration_structure.php
|
||||
*/
|
||||
|
||||
require_once dirname(__DIR__) . '/config.php';
|
||||
|
||||
// Couleurs pour le terminal
|
||||
define('COLOR_GREEN', "\033[0;32m");
|
||||
define('COLOR_RED', "\033[0;31m");
|
||||
define('COLOR_YELLOW', "\033[1;33m");
|
||||
define('COLOR_BLUE', "\033[0;34m");
|
||||
define('COLOR_RESET', "\033[0m");
|
||||
|
||||
// Fonction pour afficher des messages colorés
|
||||
function printColor($message, $color = COLOR_RESET) {
|
||||
echo $color . $message . COLOR_RESET . PHP_EOL;
|
||||
}
|
||||
|
||||
// Fonction pour obtenir les colonnes d'une table
|
||||
function getTableColumns($pdo, $tableName) {
|
||||
try {
|
||||
$stmt = $pdo->query("DESCRIBE `$tableName`");
|
||||
$columns = [];
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$columns[$row['Field']] = [
|
||||
'type' => $row['Type'],
|
||||
'null' => $row['Null'],
|
||||
'key' => $row['Key'],
|
||||
'default' => $row['Default'],
|
||||
'extra' => $row['Extra']
|
||||
];
|
||||
}
|
||||
return $columns;
|
||||
} catch (PDOException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Mappings de colonnes connus
|
||||
$columnMappings = [
|
||||
// Mappings globaux
|
||||
'global' => [
|
||||
'rowid' => 'id',
|
||||
'active' => 'chk_active',
|
||||
'date_creat' => 'created_at',
|
||||
'date_modif' => 'updated_at',
|
||||
],
|
||||
// Mappings spécifiques par table
|
||||
'users_entites' => [
|
||||
'table_target' => 'entites',
|
||||
'mappings' => [
|
||||
'libelle' => 'encrypted_name',
|
||||
'tel1' => 'encrypted_phone',
|
||||
'tel2' => 'encrypted_mobile',
|
||||
'email' => 'encrypted_email',
|
||||
'iban' => 'encrypted_iban',
|
||||
'bic' => 'encrypted_bic',
|
||||
'cp' => 'code_postal',
|
||||
]
|
||||
],
|
||||
'users' => [
|
||||
'mappings' => [
|
||||
'libelle' => 'encrypted_name',
|
||||
'username' => 'encrypted_user_name',
|
||||
'userpswd' => 'user_pass_hash',
|
||||
'userpass' => 'user_pass_hash',
|
||||
'prenom' => 'first_name',
|
||||
'nom_tournee' => 'sect_name',
|
||||
'telephone' => 'encrypted_phone',
|
||||
'mobile' => 'encrypted_mobile',
|
||||
'email' => 'encrypted_email',
|
||||
'alert_email' => 'chk_alert_email',
|
||||
]
|
||||
],
|
||||
'ope_pass' => [
|
||||
'mappings' => [
|
||||
'date_eve' => 'passed_at',
|
||||
'libelle' => 'encrypted_name',
|
||||
'email' => 'encrypted_email',
|
||||
'phone' => 'encrypted_phone',
|
||||
'recu' => 'nom_recu',
|
||||
]
|
||||
],
|
||||
'medias' => [
|
||||
'mappings' => [
|
||||
'support_rowid' => 'support_id',
|
||||
]
|
||||
],
|
||||
'x_villes' => [
|
||||
'mappings' => [
|
||||
'cp' => 'code_postal',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
// Tables à vérifier (source => cible)
|
||||
$tablesToVerify = [
|
||||
'x_devises' => 'x_devises',
|
||||
'x_entites_types' => 'x_entites_types',
|
||||
'x_types_passages' => 'x_types_passages',
|
||||
'x_types_reglements' => 'x_types_reglements',
|
||||
'x_users_roles' => 'x_users_roles',
|
||||
'x_pays' => 'x_pays',
|
||||
'x_regions' => 'x_regions',
|
||||
'x_departements' => 'x_departements',
|
||||
'x_villes' => 'x_villes',
|
||||
'users_entites' => 'entites',
|
||||
'users' => 'users',
|
||||
'operations' => 'operations',
|
||||
'ope_users' => 'ope_users',
|
||||
'ope_users_sectors' => 'ope_users_sectors',
|
||||
'ope_pass' => 'ope_pass',
|
||||
'ope_pass_histo' => 'ope_pass_histo',
|
||||
'medias' => 'medias',
|
||||
'sectors_adresses' => 'sectors_adresses',
|
||||
];
|
||||
|
||||
try {
|
||||
printColor("\n╔══════════════════════════════════════════════════════════════╗", COLOR_BLUE);
|
||||
printColor("║ VÉRIFICATION DES STRUCTURES DE MIGRATION ║", COLOR_BLUE);
|
||||
printColor("╚══════════════════════════════════════════════════════════════╝", COLOR_BLUE);
|
||||
|
||||
// Connexion aux bases de données
|
||||
printColor("\n[INFO] Connexion aux bases de données...", COLOR_BLUE);
|
||||
$sourceDb = getSourceConnection();
|
||||
$targetDb = getTargetConnection();
|
||||
printColor("[OK] Connexions établies", COLOR_GREEN);
|
||||
|
||||
$totalIssues = 0;
|
||||
$totalWarnings = 0;
|
||||
$totalTables = count($tablesToVerify);
|
||||
|
||||
foreach ($tablesToVerify as $sourceTable => $targetTable) {
|
||||
printColor("\n" . str_repeat("─", 70), COLOR_BLUE);
|
||||
printColor("📊 Table: $sourceTable → $targetTable", COLOR_BLUE);
|
||||
printColor(str_repeat("─", 70), COLOR_BLUE);
|
||||
|
||||
// Récupérer les colonnes
|
||||
$sourceCols = getTableColumns($sourceDb, $sourceTable);
|
||||
$targetCols = getTableColumns($targetDb, $targetTable);
|
||||
|
||||
if ($sourceCols === null) {
|
||||
printColor("❌ ERREUR: Table source '$sourceTable' introuvable", COLOR_RED);
|
||||
$totalIssues++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($targetCols === null) {
|
||||
printColor("❌ ERREUR: Table cible '$targetTable' introuvable", COLOR_RED);
|
||||
$totalIssues++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Récupérer les mappings pour cette table
|
||||
$tableMappings = $columnMappings['global'];
|
||||
if (isset($columnMappings[$sourceTable]['mappings'])) {
|
||||
$tableMappings = array_merge($tableMappings, $columnMappings[$sourceTable]['mappings']);
|
||||
}
|
||||
|
||||
// Vérifier chaque colonne source
|
||||
$unmappedSourceCols = [];
|
||||
$mappedCols = 0;
|
||||
|
||||
foreach ($sourceCols as $sourceCol => $sourceInfo) {
|
||||
// Chercher la colonne cible
|
||||
$targetCol = $tableMappings[$sourceCol] ?? $sourceCol;
|
||||
|
||||
if (isset($targetCols[$targetCol])) {
|
||||
$mappedCols++;
|
||||
// Colonne existe et mappée correctement
|
||||
} else {
|
||||
// Vérifier si c'est une colonne qui doit être ignorée
|
||||
$ignoredCols = ['dir0', 'dir1', 'dir2', 'type_fichier', 'position', 'hauteur', 'largeur',
|
||||
'niveaugris', 'lieudit', 'chk_habitat_vide', 'lot_nb_passages', 'departement',
|
||||
'fk_user', 'chk_api_adresse', 'num_adherent', 'libelle_naissance', 'josh',
|
||||
'email_secondaire', 'infos', 'ltt', 'lng', 'sector', 'dept_naissance',
|
||||
'commune_naissance', 'anciennete', 'fk_categorie', 'fk_sous_categorie',
|
||||
'adresse_1', 'adresse_2', 'cp', 'ville', 'matricule', 'fk_grade',
|
||||
'chk_adherent_ud', 'chk_adherent_ur', 'chk_adherent_fns', 'chk_archive',
|
||||
'chk_double_affectation', 'date_creat', 'appname', 'http_host', 'tva_intra',
|
||||
'rcs', 'siret', 'ape', 'couleur', 'prefecture', 'fk_titre_gerant',
|
||||
'gerant_prenom', 'gerant_nom', 'site_url', 'gerant_signature',
|
||||
'tampon_signature', 'banque_libelle', 'banque_adresse', 'banque_cp',
|
||||
'banque_ville', 'genbase', 'groupebase', 'userbase', 'passbase', 'demo',
|
||||
'lib_vert', 'lib_verts', 'lib_orange', 'lib_oranges', 'lib_rouge', 'lib_rouges',
|
||||
'lib_bleu', 'lib_bleus', 'icon_siege', 'icon_siege_color', 'btn_width',
|
||||
'nbmembres', 'nbconnex'];
|
||||
|
||||
if (in_array($sourceCol, $ignoredCols)) {
|
||||
// Colonne volontairement non migrée
|
||||
continue;
|
||||
}
|
||||
|
||||
$unmappedSourceCols[] = $sourceCol;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les nouvelles colonnes dans la cible
|
||||
$newTargetCols = [];
|
||||
foreach ($targetCols as $targetCol => $targetInfo) {
|
||||
// Vérifier si cette colonne existe dans la source
|
||||
$sourceCol = array_search($targetCol, $tableMappings);
|
||||
if ($sourceCol === false) {
|
||||
$sourceCol = $targetCol; // Même nom
|
||||
}
|
||||
|
||||
if (!isset($sourceCols[$sourceCol])) {
|
||||
// Vérifier si c'est une colonne attendue (timestamp auto, etc.)
|
||||
$autoColumns = ['created_at', 'updated_at', 'id'];
|
||||
if (!in_array($targetCol, $autoColumns)) {
|
||||
$newTargetCols[] = $targetCol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Affichage des résultats
|
||||
printColor("✓ Colonnes source mappées: $mappedCols/" . count($sourceCols), COLOR_GREEN);
|
||||
|
||||
if (!empty($unmappedSourceCols)) {
|
||||
printColor("⚠ Colonnes source NON mappées:", COLOR_YELLOW);
|
||||
foreach ($unmappedSourceCols as $col) {
|
||||
printColor(" - $col ({$sourceCols[$col]['type']})", COLOR_YELLOW);
|
||||
}
|
||||
$totalWarnings += count($unmappedSourceCols);
|
||||
}
|
||||
|
||||
if (!empty($newTargetCols)) {
|
||||
printColor("ℹ Nouvelles colonnes dans cible (seront NULL/défaut):", COLOR_YELLOW);
|
||||
foreach ($newTargetCols as $col) {
|
||||
$defaultValue = $targetCols[$col]['default'] ?? 'NULL';
|
||||
$nullable = $targetCols[$col]['null'] === 'YES' ? '(nullable)' : '(NOT NULL)';
|
||||
printColor(" - $col ({$targetCols[$col]['type']}) = $defaultValue $nullable", COLOR_YELLOW);
|
||||
}
|
||||
$totalWarnings += count($newTargetCols);
|
||||
}
|
||||
|
||||
if (empty($unmappedSourceCols) && empty($newTargetCols)) {
|
||||
printColor("✓ Aucun problème détecté", COLOR_GREEN);
|
||||
}
|
||||
}
|
||||
|
||||
// Résumé final
|
||||
printColor("\n" . str_repeat("═", 70), COLOR_BLUE);
|
||||
printColor("📈 RÉSUMÉ DE LA VÉRIFICATION", COLOR_BLUE);
|
||||
printColor(str_repeat("═", 70), COLOR_BLUE);
|
||||
printColor("Tables vérifiées: $totalTables", COLOR_BLUE);
|
||||
|
||||
if ($totalIssues > 0) {
|
||||
printColor("❌ Erreurs critiques: $totalIssues", COLOR_RED);
|
||||
} else {
|
||||
printColor("✓ Aucune erreur critique", COLOR_GREEN);
|
||||
}
|
||||
|
||||
if ($totalWarnings > 0) {
|
||||
printColor("⚠ Avertissements: $totalWarnings", COLOR_YELLOW);
|
||||
printColor(" (colonnes non mappées ou nouvelles colonnes)", COLOR_YELLOW);
|
||||
} else {
|
||||
printColor("✓ Aucun avertissement", COLOR_GREEN);
|
||||
}
|
||||
|
||||
printColor("\n💡 Recommandations:", COLOR_BLUE);
|
||||
printColor(" - Vérifiez que les colonnes non mappées sont intentionnelles", COLOR_RESET);
|
||||
printColor(" - Les nouvelles colonnes cible utiliseront leurs valeurs par défaut", COLOR_RESET);
|
||||
printColor(" - Consultez README-migration.md pour plus de détails", COLOR_RESET);
|
||||
|
||||
// Fermer le tunnel SSH
|
||||
closeSshTunnel();
|
||||
|
||||
printColor("\n✓ Vérification terminée\n", COLOR_GREEN);
|
||||
|
||||
} catch (Exception $e) {
|
||||
printColor("\n❌ ERREUR CRITIQUE: " . $e->getMessage(), COLOR_RED);
|
||||
closeSshTunnel();
|
||||
exit(1);
|
||||
}
|
||||
34
api/scripts/sql/add_unique_constraints_SIMPLE.sql
Normal file
34
api/scripts/sql/add_unique_constraints_SIMPLE.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- ========================================
|
||||
-- Script SIMPLE d'ajout de contraintes UNIQUE
|
||||
-- Pour tables avec peu de données (pas de suppression de doublons)
|
||||
-- Date: 2025-10-10
|
||||
-- ========================================
|
||||
|
||||
USE pra_geo;
|
||||
|
||||
-- Vérifier d'abord s'il y a des doublons
|
||||
SELECT 'Vérification doublons ope_users...' as status;
|
||||
SELECT fk_operation, fk_user, COUNT(*) as count
|
||||
FROM ope_users
|
||||
GROUP BY fk_operation, fk_user
|
||||
HAVING count > 1;
|
||||
|
||||
SELECT 'Vérification doublons ope_users_sectors...' as status;
|
||||
SELECT fk_operation, fk_user, fk_sector, COUNT(*) as count
|
||||
FROM ope_users_sectors
|
||||
GROUP BY fk_operation, fk_user, fk_sector
|
||||
HAVING count > 1;
|
||||
|
||||
-- Ajouter les contraintes UNIQUE directement
|
||||
-- Si des doublons existent, MySQL retournera une erreur explicite
|
||||
ALTER TABLE ope_users
|
||||
ADD UNIQUE KEY `idx_operation_user` (`fk_operation`, `fk_user`);
|
||||
|
||||
ALTER TABLE ope_users_sectors
|
||||
ADD UNIQUE KEY `idx_operation_user_sector` (`fk_operation`, `fk_user`, `fk_sector`);
|
||||
|
||||
-- Vérification
|
||||
SHOW INDEX FROM ope_users WHERE Key_name = 'idx_operation_user';
|
||||
SHOW INDEX FROM ope_users_sectors WHERE Key_name = 'idx_operation_user_sector';
|
||||
|
||||
SELECT 'TERMINÉ ✓' as status;
|
||||
59
api/scripts/sql/add_unique_constraints_ope_tables.sql
Normal file
59
api/scripts/sql/add_unique_constraints_ope_tables.sql
Normal file
@@ -0,0 +1,59 @@
|
||||
-- ========================================
|
||||
-- Script d'ajout de contraintes UNIQUE
|
||||
-- Pour éviter les doublons dans ope_users et ope_users_sectors
|
||||
-- Date: 2025-10-10
|
||||
-- ========================================
|
||||
|
||||
USE pra_geo;
|
||||
|
||||
-- ========================================
|
||||
-- 1. TABLE ope_users
|
||||
-- ========================================
|
||||
|
||||
-- Vérifier et supprimer les doublons existants avant d'ajouter la contrainte
|
||||
-- (Garde la première occurrence, supprime les duplicatas)
|
||||
DELETE ou1 FROM ope_users ou1
|
||||
INNER JOIN ope_users ou2
|
||||
WHERE ou1.id > ou2.id
|
||||
AND ou1.fk_operation = ou2.fk_operation
|
||||
AND ou1.fk_user = ou2.fk_user;
|
||||
|
||||
-- Ajouter la contrainte UNIQUE sur (fk_operation, fk_user)
|
||||
ALTER TABLE ope_users
|
||||
ADD UNIQUE KEY `idx_operation_user` (`fk_operation`, `fk_user`);
|
||||
|
||||
-- ========================================
|
||||
-- 2. TABLE ope_users_sectors
|
||||
-- ========================================
|
||||
|
||||
-- Vérifier et supprimer les doublons existants avant d'ajouter la contrainte
|
||||
-- (Garde la première occurrence, supprime les duplicatas)
|
||||
DELETE ous1 FROM ope_users_sectors ous1
|
||||
INNER JOIN ope_users_sectors ous2
|
||||
WHERE ous1.id > ous2.id
|
||||
AND ous1.fk_operation = ous2.fk_operation
|
||||
AND ous1.fk_user = ous2.fk_user
|
||||
AND ous1.fk_sector = ous2.fk_sector;
|
||||
|
||||
-- Ajouter la contrainte UNIQUE sur (fk_operation, fk_user, fk_sector)
|
||||
ALTER TABLE ope_users_sectors
|
||||
ADD UNIQUE KEY `idx_operation_user_sector` (`fk_operation`, `fk_user`, `fk_sector`);
|
||||
|
||||
-- ========================================
|
||||
-- Vérification
|
||||
-- ========================================
|
||||
|
||||
-- Vérifier les contraintes ajoutées
|
||||
SHOW INDEX FROM ope_users WHERE Key_name = 'idx_operation_user';
|
||||
SHOW INDEX FROM ope_users_sectors WHERE Key_name = 'idx_operation_user_sector';
|
||||
|
||||
-- Compter les doublons restants (devrait retourner 0 lignes)
|
||||
SELECT fk_operation, fk_user, COUNT(*) as count
|
||||
FROM ope_users
|
||||
GROUP BY fk_operation, fk_user
|
||||
HAVING count > 1;
|
||||
|
||||
SELECT fk_operation, fk_user, fk_sector, COUNT(*) as count
|
||||
FROM ope_users_sectors
|
||||
GROUP BY fk_operation, fk_user, fk_sector
|
||||
HAVING count > 1;
|
||||
70
api/scripts/sql/add_unique_constraints_ope_tables_FAST.sql
Normal file
70
api/scripts/sql/add_unique_constraints_ope_tables_FAST.sql
Normal file
@@ -0,0 +1,70 @@
|
||||
-- ========================================
|
||||
-- Script OPTIMISÉ d'ajout de contraintes UNIQUE
|
||||
-- Pour tables avec beaucoup de données
|
||||
-- Date: 2025-10-10
|
||||
-- ========================================
|
||||
|
||||
USE pra_geo;
|
||||
|
||||
-- ========================================
|
||||
-- OPTION 1 : Compter les doublons d'abord
|
||||
-- ========================================
|
||||
|
||||
SELECT 'Analyse des doublons dans ope_users...' as status;
|
||||
SELECT COUNT(*) as total_rows,
|
||||
COUNT(DISTINCT fk_operation, fk_user) as unique_combinations,
|
||||
COUNT(*) - COUNT(DISTINCT fk_operation, fk_user) as duplicates
|
||||
FROM ope_users;
|
||||
|
||||
SELECT 'Analyse des doublons dans ope_users_sectors...' as status;
|
||||
SELECT COUNT(*) as total_rows,
|
||||
COUNT(DISTINCT fk_operation, fk_user, fk_sector) as unique_combinations,
|
||||
COUNT(*) - COUNT(DISTINCT fk_operation, fk_user, fk_sector) as duplicates
|
||||
FROM ope_users_sectors;
|
||||
|
||||
-- ========================================
|
||||
-- OPTION 2 : Supprimer RAPIDEMENT les doublons
|
||||
-- Créer une table temporaire avec les IDs à garder
|
||||
-- ========================================
|
||||
|
||||
-- Pour ope_users
|
||||
CREATE TEMPORARY TABLE ope_users_to_keep AS
|
||||
SELECT MIN(id) as id_to_keep, fk_operation, fk_user
|
||||
FROM ope_users
|
||||
GROUP BY fk_operation, fk_user;
|
||||
|
||||
-- Supprimer tous les doublons (plus rapide avec NOT IN + subquery)
|
||||
DELETE FROM ope_users
|
||||
WHERE id NOT IN (SELECT id_to_keep FROM ope_users_to_keep);
|
||||
|
||||
DROP TEMPORARY TABLE ope_users_to_keep;
|
||||
|
||||
-- Pour ope_users_sectors
|
||||
CREATE TEMPORARY TABLE ope_users_sectors_to_keep AS
|
||||
SELECT MIN(id) as id_to_keep, fk_operation, fk_user, fk_sector
|
||||
FROM ope_users_sectors
|
||||
GROUP BY fk_operation, fk_user, fk_sector;
|
||||
|
||||
DELETE FROM ope_users_sectors
|
||||
WHERE id NOT IN (SELECT id_to_keep FROM ope_users_sectors_to_keep);
|
||||
|
||||
DROP TEMPORARY TABLE ope_users_sectors_to_keep;
|
||||
|
||||
-- ========================================
|
||||
-- OPTION 3 : Ajouter les contraintes UNIQUE
|
||||
-- ========================================
|
||||
|
||||
ALTER TABLE ope_users
|
||||
ADD UNIQUE KEY `idx_operation_user` (`fk_operation`, `fk_user`);
|
||||
|
||||
ALTER TABLE ope_users_sectors
|
||||
ADD UNIQUE KEY `idx_operation_user_sector` (`fk_operation`, `fk_user`, `fk_sector`);
|
||||
|
||||
-- ========================================
|
||||
-- Vérification finale
|
||||
-- ========================================
|
||||
|
||||
SHOW INDEX FROM ope_users WHERE Key_name = 'idx_operation_user';
|
||||
SHOW INDEX FROM ope_users_sectors WHERE Key_name = 'idx_operation_user_sector';
|
||||
|
||||
SELECT 'TERMINÉ - Contraintes UNIQUE ajoutées avec succès' as status;
|
||||
181
api/scripts/sql/truncate_data_tables.sql
Normal file
181
api/scripts/sql/truncate_data_tables.sql
Normal file
@@ -0,0 +1,181 @@
|
||||
-- =========================================================
|
||||
-- Script de vidage des tables de données (PRODUCTION)
|
||||
-- Option B : Vider TOUTES les tables SAUF x_* et entité 1
|
||||
-- Conserve les tables de référence x_*
|
||||
-- Conserve l'entité id=1 (super admins) et ses users/opérations
|
||||
-- Date: 2025-10-09
|
||||
-- =========================================================
|
||||
|
||||
-- Désactiver les contraintes de clés étrangères temporairement
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- =========================================================
|
||||
-- TABLES CHAT
|
||||
-- =========================================================
|
||||
TRUNCATE TABLE chat_read_receipts;
|
||||
TRUNCATE TABLE chat_messages;
|
||||
TRUNCATE TABLE chat_participants;
|
||||
TRUNCATE TABLE chat_rooms;
|
||||
|
||||
-- =========================================================
|
||||
-- TABLES EMAIL
|
||||
-- =========================================================
|
||||
TRUNCATE TABLE email_queue;
|
||||
TRUNCATE TABLE email_counter;
|
||||
|
||||
-- =========================================================
|
||||
-- TABLES SÉCURITÉ
|
||||
-- =========================================================
|
||||
TRUNCATE TABLE sec_failed_login_attempts;
|
||||
TRUNCATE TABLE sec_blocked_ips;
|
||||
TRUNCATE TABLE sec_alerts;
|
||||
TRUNCATE TABLE sec_performance_metrics;
|
||||
|
||||
-- =========================================================
|
||||
-- TABLES STRIPE
|
||||
-- =========================================================
|
||||
TRUNCATE TABLE stripe_webhooks;
|
||||
TRUNCATE TABLE stripe_payment_history;
|
||||
TRUNCATE TABLE stripe_refunds;
|
||||
TRUNCATE TABLE stripe_terminal_readers;
|
||||
TRUNCATE TABLE stripe_android_certified_devices;
|
||||
-- NOTE: stripe_accounts conservé car lié à entites via FK
|
||||
|
||||
-- =========================================================
|
||||
-- TABLES DONNÉES MÉTIER (conserver entité 1)
|
||||
-- =========================================================
|
||||
|
||||
-- 1. Supprimer les devices des users (sauf entité 1)
|
||||
DELETE FROM user_devices
|
||||
WHERE fk_user IN (SELECT id FROM users WHERE fk_entite != 1);
|
||||
|
||||
-- 2. Supprimer les sessions (sauf users entité 1)
|
||||
DELETE FROM z_sessions
|
||||
WHERE fk_user IN (SELECT id FROM users WHERE fk_entite != 1);
|
||||
|
||||
-- 3. Supprimer les médias (sauf entité 1)
|
||||
DELETE FROM medias WHERE fk_entite != 1;
|
||||
|
||||
-- 4. Supprimer les comptes Stripe (sauf entité 1)
|
||||
DELETE FROM stripe_accounts WHERE fk_entite != 1;
|
||||
|
||||
-- 5. Supprimer l'historique des passages (sauf entité 1)
|
||||
DELETE FROM ope_pass_histo
|
||||
WHERE fk_pass IN (
|
||||
SELECT id FROM ope_pass
|
||||
WHERE fk_operation IN (
|
||||
SELECT id FROM operations WHERE fk_entite != 1
|
||||
)
|
||||
);
|
||||
|
||||
-- 6. Supprimer les passages (sauf ceux des opérations de l'entité 1)
|
||||
DELETE FROM ope_pass
|
||||
WHERE fk_operation IN (
|
||||
SELECT id FROM operations WHERE fk_entite != 1
|
||||
);
|
||||
|
||||
-- 7. Supprimer les associations users-sectors (sauf entité 1)
|
||||
DELETE FROM ope_users_sectors
|
||||
WHERE fk_operation IN (
|
||||
SELECT id FROM operations WHERE fk_entite != 1
|
||||
);
|
||||
|
||||
-- 8. Supprimer les associations users-operations (sauf entité 1)
|
||||
DELETE FROM ope_users
|
||||
WHERE fk_operation IN (
|
||||
SELECT id FROM operations WHERE fk_entite != 1
|
||||
);
|
||||
|
||||
-- 9. Supprimer les adresses de secteurs (sauf entité 1)
|
||||
DELETE FROM sectors_adresses
|
||||
WHERE fk_sector IN (
|
||||
SELECT id FROM ope_sectors WHERE fk_operation IN (
|
||||
SELECT id FROM operations WHERE fk_entite != 1
|
||||
)
|
||||
);
|
||||
|
||||
-- 10. Supprimer les secteurs (sauf ceux de l'entité 1)
|
||||
DELETE FROM ope_sectors
|
||||
WHERE fk_operation IN (
|
||||
SELECT id FROM operations WHERE fk_entite != 1
|
||||
);
|
||||
|
||||
-- 11. Supprimer les opérations (sauf celles de l'entité 1)
|
||||
DELETE FROM operations WHERE fk_entite != 1;
|
||||
|
||||
-- 12. Supprimer les utilisateurs (sauf ceux de l'entité 1)
|
||||
DELETE FROM users WHERE fk_entite != 1;
|
||||
|
||||
-- 13. Supprimer les entités (sauf l'entité 1)
|
||||
DELETE FROM entites WHERE id != 1;
|
||||
|
||||
-- 14. Vider la table params (paramètres globaux)
|
||||
TRUNCATE TABLE params;
|
||||
|
||||
-- Réactiver les contraintes de clés étrangères
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- =========================================================
|
||||
-- VÉRIFICATIONS POST-VIDAGE
|
||||
-- =========================================================
|
||||
|
||||
SELECT '========================================' as '';
|
||||
SELECT '=== TABLES DE DONNÉES (après vidage) ===' as '';
|
||||
SELECT '========================================' as '';
|
||||
|
||||
SELECT 'entites' as table_name, COUNT(*) as count FROM entites
|
||||
UNION ALL SELECT 'users', COUNT(*) FROM users
|
||||
UNION ALL SELECT 'operations', COUNT(*) FROM operations
|
||||
UNION ALL SELECT 'ope_sectors', COUNT(*) FROM ope_sectors
|
||||
UNION ALL SELECT 'ope_pass', COUNT(*) FROM ope_pass
|
||||
UNION ALL SELECT 'medias', COUNT(*) FROM medias
|
||||
UNION ALL SELECT 'user_devices', COUNT(*) FROM user_devices
|
||||
UNION ALL SELECT 'z_sessions', COUNT(*) FROM z_sessions;
|
||||
|
||||
SELECT '' as '';
|
||||
SELECT '========================================' as '';
|
||||
SELECT '=== TABLES CHAT (doivent être vides) ===' as '';
|
||||
SELECT '========================================' as '';
|
||||
|
||||
SELECT 'chat_rooms' as table_name, COUNT(*) as count FROM chat_rooms
|
||||
UNION ALL SELECT 'chat_messages', COUNT(*) FROM chat_messages
|
||||
UNION ALL SELECT 'chat_participants', COUNT(*) FROM chat_participants
|
||||
UNION ALL SELECT 'chat_read_receipts', COUNT(*) FROM chat_read_receipts;
|
||||
|
||||
SELECT '' as '';
|
||||
SELECT '========================================' as '';
|
||||
SELECT '=== TABLES STRIPE (vides sauf accounts) ===' as '';
|
||||
SELECT '========================================' as '';
|
||||
|
||||
SELECT 'stripe_accounts' as table_name, COUNT(*) as count FROM stripe_accounts
|
||||
UNION ALL SELECT 'stripe_webhooks', COUNT(*) FROM stripe_webhooks
|
||||
UNION ALL SELECT 'stripe_terminal_readers', COUNT(*) FROM stripe_terminal_readers
|
||||
UNION ALL SELECT 'stripe_android_certified_devices', COUNT(*) FROM stripe_android_certified_devices;
|
||||
|
||||
SELECT '' as '';
|
||||
SELECT '========================================' as '';
|
||||
SELECT '=== ENTITÉ 1 (doit être conservée) ===' as '';
|
||||
SELECT '========================================' as '';
|
||||
|
||||
SELECT id, encrypted_name, encrypted_email, chk_active FROM entites WHERE id = 1;
|
||||
|
||||
SELECT '' as '';
|
||||
SELECT 'Nombre de users entité 1:' as info, COUNT(*) as count FROM users WHERE fk_entite = 1;
|
||||
SELECT 'Nombre d\'opérations entité 1:' as info, COUNT(*) as count FROM operations WHERE fk_entite = 1;
|
||||
|
||||
SELECT '' as '';
|
||||
SELECT '========================================' as '';
|
||||
SELECT '=== TABLES x_* (doivent être remplies) ===' as '';
|
||||
SELECT '========================================' as '';
|
||||
|
||||
SELECT 'x_devises' as table_name, COUNT(*) as count FROM x_devises
|
||||
UNION ALL SELECT 'x_pays', COUNT(*) FROM x_pays
|
||||
UNION ALL SELECT 'x_regions', COUNT(*) FROM x_regions
|
||||
UNION ALL SELECT 'x_departements', COUNT(*) FROM x_departements
|
||||
UNION ALL SELECT 'x_villes', COUNT(*) FROM x_villes
|
||||
UNION ALL SELECT 'x_departements_contours', COUNT(*) FROM x_departements_contours
|
||||
UNION ALL SELECT 'x_entites_types', COUNT(*) FROM x_entites_types
|
||||
UNION ALL SELECT 'x_types_passages', COUNT(*) FROM x_types_passages
|
||||
UNION ALL SELECT 'x_types_reglements', COUNT(*) FROM x_types_reglements
|
||||
UNION ALL SELECT 'x_users_roles', COUNT(*) FROM x_users_roles
|
||||
UNION ALL SELECT 'x_users_titres', COUNT(*) FROM x_users_titres;
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script de test pour générer manuellement un reçu
|
||||
* Usage: php generate_receipt_manual.php <passage_id>
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Simuler l'environnement web pour AppConfig en CLI
|
||||
if (php_sapi_name() === 'cli') {
|
||||
$_SERVER['SERVER_NAME'] = 'dapp.geosector.fr'; // DEV
|
||||
$_SERVER['HTTP_HOST'] = $_SERVER['SERVER_NAME'];
|
||||
$_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';
|
||||
require_once __DIR__ . '/../../src/Services/ReceiptService.php';
|
||||
|
||||
// Vérifier qu'un ID de passage est fourni
|
||||
if ($argc < 2) {
|
||||
echo "Usage: php generate_receipt_manual.php <passage_id>\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$passageId = (int)$argv[1];
|
||||
|
||||
try {
|
||||
echo "=== Test de génération de reçu ===\n";
|
||||
echo "Passage ID: $passageId\n\n";
|
||||
|
||||
// Initialisation de la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$dbConfig = $appConfig->getDatabaseConfig();
|
||||
|
||||
// Initialiser la base de données
|
||||
Database::init($dbConfig);
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "✓ Connexion à la base de données OK\n";
|
||||
|
||||
// Vérifier le passage
|
||||
$stmt = $db->prepare('SELECT id, fk_type, encrypted_email, nom_recu FROM ope_pass WHERE id = ?');
|
||||
$stmt->execute([$passageId]);
|
||||
$passage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$passage) {
|
||||
echo "✗ Passage $passageId non trouvé\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "✓ Passage trouvé\n";
|
||||
echo " - fk_type: " . $passage['fk_type'] . "\n";
|
||||
echo " - encrypted_email: " . (!empty($passage['encrypted_email']) ? 'OUI' : 'NON') . "\n";
|
||||
echo " - nom_recu: " . ($passage['nom_recu'] ?: 'vide') . "\n\n";
|
||||
|
||||
// Déchiffrer l'email
|
||||
if (!empty($passage['encrypted_email'])) {
|
||||
$email = \ApiService::decryptSearchableData($passage['encrypted_email']);
|
||||
echo " - Email déchiffré: $email\n";
|
||||
echo " - Email valide: " . (filter_var($email, FILTER_VALIDATE_EMAIL) ? 'OUI' : 'NON') . "\n\n";
|
||||
} else {
|
||||
echo "✗ Aucun email chiffré trouvé\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Générer le reçu
|
||||
echo "Génération du reçu...\n";
|
||||
$receiptService = new \App\Services\ReceiptService();
|
||||
$result = $receiptService->generateReceiptForPassage($passageId);
|
||||
|
||||
if ($result) {
|
||||
echo "✓ Reçu généré avec succès !\n\n";
|
||||
|
||||
// Vérifier l'email dans la queue
|
||||
$stmt = $db->prepare('SELECT id, to_email, status, created_at FROM email_queue WHERE fk_pass = ? ORDER BY created_at DESC LIMIT 1');
|
||||
$stmt->execute([$passageId]);
|
||||
$queueEmail = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($queueEmail) {
|
||||
echo "✓ Email ajouté à la queue\n";
|
||||
echo " - Queue ID: " . $queueEmail['id'] . "\n";
|
||||
echo " - Destinataire: " . $queueEmail['to_email'] . "\n";
|
||||
echo " - Status: " . $queueEmail['status'] . "\n";
|
||||
echo " - Créé: " . $queueEmail['created_at'] . "\n";
|
||||
} else {
|
||||
echo "✗ Aucun email trouvé dans la queue\n";
|
||||
}
|
||||
} else {
|
||||
echo "✗ Échec de la génération du reçu\n";
|
||||
echo "Consultez /var/www/geosector/api/logs/api.log pour plus de détails\n";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "✗ ERREUR: " . $e->getMessage() . "\n";
|
||||
echo $e->getTraceAsString() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "\n=== Fin du test ===\n";
|
||||
exit(0);
|
||||
@@ -6,9 +6,9 @@ declare(strict_types=1);
|
||||
* Configuration de l'application Geosector
|
||||
*
|
||||
* Ce fichier contient la configuration de l'application Geosector pour les trois environnements :
|
||||
* - Production (app.geosector.fr)
|
||||
* - Production (app3.geosector.fr)
|
||||
* - Recette (rapp.geosector.fr)
|
||||
* - Développement (app.geo.dev)
|
||||
* - Développement (dapp.geosector.fr)
|
||||
*
|
||||
* Il inclut les paramètres de base de données, les informations SMTP,
|
||||
* les clés de chiffrement et les configurations des services externes (Mapbox, Stripe, SMS OVH).
|
||||
@@ -24,6 +24,25 @@ class AppConfig {
|
||||
// Récupération du host directement depuis SERVER_NAME ou HTTP_HOST
|
||||
$this->currentHost = $_SERVER['SERVER_NAME'] ?? $_SERVER['HTTP_HOST'] ?? '';
|
||||
|
||||
// Si on est en CLI (CRON, scripts), tenter de détecter via le marqueur d'environnement
|
||||
if (empty($this->currentHost) && php_sapi_name() === 'cli') {
|
||||
$markerFile = __DIR__ . '/../../.env_marker';
|
||||
if (file_exists($markerFile)) {
|
||||
$envMarker = trim(file_get_contents($markerFile));
|
||||
switch ($envMarker) {
|
||||
case 'production':
|
||||
$this->currentHost = 'app3.geosector.fr';
|
||||
break;
|
||||
case 'recette':
|
||||
$this->currentHost = 'rapp.geosector.fr';
|
||||
break;
|
||||
case 'development':
|
||||
$this->currentHost = 'dapp.geosector.fr';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les autres en-têtes pour une utilisation ultérieure si nécessaire
|
||||
// getallheaders() n'existe pas en CLI, donc on vérifie
|
||||
$this->headers = function_exists('getallheaders') ? getallheaders() : [];
|
||||
@@ -81,10 +100,10 @@ class AppConfig {
|
||||
];
|
||||
|
||||
// Configuration PRODUCTION
|
||||
$this->config['app.geosector.fr'] = array_merge($baseConfig, [
|
||||
$this->config['app3.geosector.fr'] = array_merge($baseConfig, [
|
||||
'env' => 'production',
|
||||
'database' => [
|
||||
'host' => '13.23.33.4', // Container maria4 sur IN4
|
||||
'host' => '13.23.33.4', // Container maria4 sur IN4 (51.159.7.190)
|
||||
'name' => 'pra_geo',
|
||||
'username' => 'pra_geo_user',
|
||||
'password' => 'd2jAAGGWi8fxFrWgXjOA',
|
||||
@@ -93,17 +112,23 @@ class AppConfig {
|
||||
'host' => '13.23.33.4', // Container maria4 sur IN4
|
||||
'name' => 'adresses',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeo.User',
|
||||
'password' => 'd66,AdrGeoPrd.User',
|
||||
],
|
||||
'buildings_database' => [
|
||||
'host' => '13.23.33.4', // Container maria4 sur IN4
|
||||
'name' => 'batiments',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoPrd.User',
|
||||
],
|
||||
// Configuration Stripe PRODUCTION - Clés LIVE du CLIENT
|
||||
'stripe' => [
|
||||
'public_key_test' => 'pk_test_XXXXXX', // Non utilisé en PROD
|
||||
'secret_key_test' => 'sk_test_XXXXXX', // Non utilisé en PROD
|
||||
'public_key_live' => 'CLIENT_PK_LIVE_A_REMPLACER', // ← À REMPLACER avec pk_live_...
|
||||
'secret_key_live' => 'CLIENT_SK_LIVE_A_REMPLACER', // ← À REMPLACER avec sk_live_...
|
||||
'public_key_live' => 'pk_live_51S5oMd1tQE0jBEomdRW82RvqAFjmqN45szbU08t8nDk4yc5QnhAJtPrP1IZJB48fF1pePUqrGsM5vyAhhoaWCT8d00nh51QIsU',
|
||||
'secret_key_live' => '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',
|
||||
'webhook_secret_test' => 'whsec_test_XXXXXXXXXXXX',
|
||||
'webhook_secret_live' => 'whsec_live_XXXXXXXXXXXX',
|
||||
'api_version' => '2024-06-20',
|
||||
'webhook_secret_live' => 'whsec_gFnA6pR92RLdbAS2T6CSC18xsSdNBZHR',
|
||||
'api_version' => '2025-08-27.basil',
|
||||
'application_fee_percent' => 0,
|
||||
'application_fee_minimum' => 0,
|
||||
'mode' => 'live', // ← MODE LIVE pour la production
|
||||
@@ -114,17 +139,17 @@ class AppConfig {
|
||||
$this->config['rapp.geosector.fr'] = array_merge($baseConfig, [
|
||||
'env' => 'recette',
|
||||
'database' => [
|
||||
// Configuration future avec maria3 (à activer après migration)
|
||||
// 'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
// 'name' => 'rca_geo',
|
||||
// 'username' => 'rca_geo_user',
|
||||
// 'password' => 'UPf3C0cQ805LypyM71iW',
|
||||
// Configuration maria3 activée (migration effectuée le 16/10/2025)
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'rca_geo',
|
||||
'username' => 'rca_geo_user',
|
||||
'password' => 'UPf3C0cQ805LypyM71iW',
|
||||
|
||||
// Configuration actuelle - base locale dans rca-geo
|
||||
'host' => 'localhost',
|
||||
'name' => 'geo_app',
|
||||
'username' => 'geo_app_user_rec',
|
||||
'password' => 'UPf3C0cQ805LypyM71iW', // À ajuster si nécessaire
|
||||
// Configuration AVANT migration (base locale dans rca-geo) - DÉSACTIVÉE
|
||||
// 'host' => 'localhost',
|
||||
// 'name' => 'geo_app',
|
||||
// 'username' => 'geo_app_user_rec',
|
||||
// 'password' => 'UPf3C0cQ805LypyM71iW',
|
||||
],
|
||||
'addresses_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
@@ -132,15 +157,21 @@ class AppConfig {
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoRec.User',
|
||||
],
|
||||
'buildings_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'batiments',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoRec.User',
|
||||
],
|
||||
// Configuration Stripe RECETTE - Clés TEST du CLIENT
|
||||
'stripe' => [
|
||||
'public_key_test' => 'pk_test_51S5oMd1tQE0jBEomd1u28D1bUujOcl87ASuGf9xulcz4rY27QfHrLBtQj20MVlWta4AGXsX0YMfeOJFE66AlGlkz00vG30U8Rr',
|
||||
'secret_key_test' => 'sk_test_51S5oMd1tQE0jBEomAhzPBvUcCf0HX9ydK0xq7DagKnidp3JsovbQoVaTj24TKSUPvujQA3PP7IpIS8iWzAd15Rte00TETmbimh',
|
||||
'public_key_test' => 'pk_test_51S5oN00EZ9a0jvy2VSPjAYyCiJWci8lwfuakc0wpStt5YWq8RlQWyliICYIWHwTaejeW8uMSKA6KTfsfUAOvjRi500XPXWRFhJ',
|
||||
'secret_key_test' => 'sk_test_51S5oN00EZ9a0jvy2paTcHY91Alh5QIMJLJZJGJ188jXqte5AkxwymbLoLDiLcCn0uQH41WC75UM03HPDDp04gl7h00wfno08gE',
|
||||
'public_key_live' => 'pk_live_XXXXXX', // Non utilisé en REC
|
||||
'secret_key_live' => 'sk_live_XXXXXX', // Non utilisé en REC
|
||||
'webhook_secret_test' => 'whsec_test_XXXXXXXXXXXX',
|
||||
'webhook_secret_test' => 'whsec_avExshr0MeWTI7wXP8478XVUkrbYG8hs',
|
||||
'webhook_secret_live' => 'whsec_live_XXXXXXXXXXXX',
|
||||
'api_version' => '2024-06-20',
|
||||
'api_version' => '2025-08-27.basil',
|
||||
'application_fee_percent' => 0,
|
||||
'application_fee_minimum' => 0,
|
||||
'mode' => 'test', // ← MODE TEST pour la recette
|
||||
@@ -151,17 +182,17 @@ class AppConfig {
|
||||
$this->config['dapp.geosector.fr'] = array_merge($baseConfig, [
|
||||
'env' => 'development',
|
||||
'database' => [
|
||||
// Configuration future avec maria3 (à activer après migration)
|
||||
// 'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
// 'name' => 'dva_geo',
|
||||
// 'username' => 'dva_geo_user',
|
||||
// 'password' => 'CBq9tKHj6PGPZuTmAHV7',
|
||||
// Configuration maria3 (migration effectuée le 07/10/2025)
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'dva_geo',
|
||||
'username' => 'dva_geo_user',
|
||||
'password' => 'CBq9tKHj6PGPZuTmAHV7',
|
||||
|
||||
// Configuration actuelle - base locale dans dva-geo
|
||||
'host' => 'localhost',
|
||||
'name' => 'geo_app',
|
||||
'username' => 'geo_app_user_dev',
|
||||
'password' => 'CBq9tKHj6PGPZuTmAHV7', // À ajuster si nécessaire
|
||||
// Configuration locale AVANT migration (sauvegarde)
|
||||
// 'host' => 'localhost',
|
||||
// 'name' => 'geo_app',
|
||||
// 'username' => 'geo_app_user_dev',
|
||||
// 'password' => 'CBq9tKHj6PGPZuTmAHV7',
|
||||
],
|
||||
'addresses_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
@@ -169,6 +200,12 @@ class AppConfig {
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoDev.User',
|
||||
],
|
||||
'buildings_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'batiments',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoDev.User',
|
||||
],
|
||||
// Configuration Stripe DÉVELOPPEMENT - Clés TEST de Pierre (plateforme de test existante)
|
||||
'stripe' => [
|
||||
'public_key_test' => 'pk_test_51QwoVN00pblGEgsXkf8qlXmLGEpxDQcG0KLRpjrGLjJHd7AVZ4Iwd6ChgdjO0w0n3vRqwNCEW8KnHUe5eh3uIlkV00k07kCBmd',
|
||||
@@ -177,7 +214,7 @@ class AppConfig {
|
||||
'secret_key_live' => 'sk_live_XXXXXX', // Non utilisé en DEV
|
||||
'webhook_secret_test' => 'whsec_test_XXXXXXXXXXXX',
|
||||
'webhook_secret_live' => 'whsec_live_XXXXXXXXXXXX',
|
||||
'api_version' => '2024-06-20',
|
||||
'api_version' => '2025-08-27.basil',
|
||||
'application_fee_percent' => 0,
|
||||
'application_fee_minimum' => 0,
|
||||
'mode' => 'test', // ← MODE TEST pour le développement
|
||||
@@ -197,13 +234,20 @@ class AppConfig {
|
||||
|
||||
// Si l'hôte n'existe pas dans la configuration, tenter une correction
|
||||
if (!isset($this->config[$this->currentHost])) {
|
||||
// Gestion des cas spéciaux (anciennes URLs)
|
||||
if ($this->currentHost === 'app.geosector.fr') {
|
||||
$this->currentHost = 'app3.geosector.fr';
|
||||
}
|
||||
|
||||
// Essayer de faire correspondre avec l'un des hôtes connus
|
||||
$knownHosts = array_keys($this->config);
|
||||
foreach ($knownHosts as $host) {
|
||||
if (strpos($this->currentHost, str_replace(['app.', 'rapp.', 'dapp.'], '', $host)) !== false) {
|
||||
// Correspondance trouvée, utiliser cette configuration
|
||||
$this->currentHost = $host;
|
||||
break;
|
||||
if (!isset($this->config[$this->currentHost])) {
|
||||
$knownHosts = array_keys($this->config);
|
||||
foreach ($knownHosts as $host) {
|
||||
if (strpos($this->currentHost, str_replace(['app3.', 'rapp.', 'dapp.'], '', $host)) !== false) {
|
||||
// Correspondance trouvée, utiliser cette configuration
|
||||
$this->currentHost = $host;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +275,7 @@ class AppConfig {
|
||||
/**
|
||||
* Retourne l'identifiant de l'application basé sur l'hôte
|
||||
*
|
||||
* @return string L'identifiant de l'application (app.geosector.fr, rapp.geosector.fr, dapp.geosector.fr)
|
||||
* @return string L'identifiant de l'application (app3.geosector.fr, rapp.geosector.fr, dapp.geosector.fr)
|
||||
*/
|
||||
public function getAppIdentifier(): string {
|
||||
return $this->currentHost;
|
||||
@@ -293,7 +337,7 @@ class AppConfig {
|
||||
|
||||
/**
|
||||
* Retourne la configuration de la base de données
|
||||
*
|
||||
*
|
||||
* @return array Configuration de la base de données
|
||||
*/
|
||||
public function getDatabaseConfig(): array {
|
||||
@@ -302,13 +346,22 @@ class AppConfig {
|
||||
|
||||
/**
|
||||
* Retourne la configuration de la base de données des adresses
|
||||
*
|
||||
*
|
||||
* @return array Configuration de la base de données des adresses
|
||||
*/
|
||||
public function getAddressesDatabaseConfig(): array {
|
||||
return $this->getCurrentConfig()['addresses_database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la configuration de la base de données des bâtiments
|
||||
*
|
||||
* @return array Configuration de la base de données des bâtiments
|
||||
*/
|
||||
public function getBuildingsDatabaseConfig(): array {
|
||||
return $this->getCurrentConfig()['buildings_database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la clé de chiffrement
|
||||
*
|
||||
@@ -410,13 +463,23 @@ class AppConfig {
|
||||
|
||||
/**
|
||||
* Retourne l'adresse IP du client
|
||||
*
|
||||
*
|
||||
* @return string L'adresse IP du client
|
||||
*/
|
||||
public function getClientIp(): string {
|
||||
return $this->clientIp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la redirection vers RECETTE est activée
|
||||
*
|
||||
* @return bool True si la redirection est activée
|
||||
*/
|
||||
public function shouldRedirectToRecette(): bool {
|
||||
$value = getenv('REDIRECT_TO_REC');
|
||||
return $value === 'true' || $value === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la configuration des backups
|
||||
*
|
||||
|
||||
@@ -7,13 +7,19 @@ namespace App\Controllers;
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
|
||||
// Les classes sont déjà incluses via require_once, pas besoin de 'use' statements
|
||||
use PDO;
|
||||
use Database;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
|
||||
class ChatController {
|
||||
private \PDO $db;
|
||||
private PDO $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = \Database::getInstance();
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,8 +30,8 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$userId = \Session::getUserId();
|
||||
$entityId = \Session::getEntityId();
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
|
||||
// Vérifier si c'est une synchronisation incrémentale
|
||||
$updatedAfter = $_GET['updated_after'] ?? null;
|
||||
@@ -186,7 +192,7 @@ class ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'sync_timestamp' => $syncTimestamp,
|
||||
'has_changes' => !empty($rooms),
|
||||
@@ -194,11 +200,11 @@ class ChatController {
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la récupération des conversations', [
|
||||
LogService::log('Erreur lors de la récupération des conversations', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -213,9 +219,9 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$entityId = \Session::getEntityId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
$userRole = $this->getUserRole($userId);
|
||||
|
||||
// Récupérer le temp_id s'il est fourni (pour la synchronisation offline)
|
||||
@@ -223,7 +229,7 @@ class ChatController {
|
||||
|
||||
// Validation des données
|
||||
if (!isset($data['type']) || !in_array($data['type'], ['private', 'group', 'broadcast'])) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Type de conversation invalide'
|
||||
], 400);
|
||||
@@ -233,7 +239,7 @@ class ChatController {
|
||||
// Vérification des permissions pour broadcast
|
||||
// Seuls les super admins (role = 9) peuvent créer des broadcasts
|
||||
if ($data['type'] === 'broadcast' && $userRole != 9) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seuls les super administrateurs peuvent créer des annonces'
|
||||
], 403);
|
||||
@@ -242,7 +248,7 @@ class ChatController {
|
||||
|
||||
// Validation des participants
|
||||
if (!isset($data['participants']) || !is_array($data['participants']) || empty($data['participants'])) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Au moins un participant requis'
|
||||
], 400);
|
||||
@@ -251,7 +257,7 @@ class ChatController {
|
||||
|
||||
// Pour une conversation privée, limiter à 2 participants (incluant le créateur)
|
||||
if ($data['type'] === 'private' && count($data['participants']) > 1) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Une conversation privée ne peut avoir que 2 participants'
|
||||
], 400);
|
||||
@@ -272,7 +278,7 @@ class ChatController {
|
||||
if ($tempId !== null) {
|
||||
$existingRoom['temp_id'] = $tempId;
|
||||
}
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'room' => $existingRoom,
|
||||
'existing' => true
|
||||
@@ -351,7 +357,7 @@ class ChatController {
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
\LogService::log('Conversation créée', [
|
||||
LogService::log('Conversation créée', [
|
||||
'level' => 'info',
|
||||
'room_id' => $roomId,
|
||||
'type' => $data['type'],
|
||||
@@ -367,7 +373,7 @@ class ChatController {
|
||||
$room['temp_id'] = $tempId;
|
||||
}
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'room' => $room
|
||||
], 201);
|
||||
@@ -378,20 +384,20 @@ class ChatController {
|
||||
}
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la création de la conversation', [
|
||||
LogService::log('Erreur lors de la création de la conversation', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
} catch (\Exception $e) {
|
||||
\LogService::log('Erreur lors de la création de la conversation', [
|
||||
LogService::log('Erreur lors de la création de la conversation', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 400);
|
||||
@@ -406,8 +412,8 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Récupérer le temp_id s'il est fourni (pour la synchronisation offline)
|
||||
$tempId = $data['temp_id'] ?? null;
|
||||
@@ -423,7 +429,7 @@ class ChatController {
|
||||
$room = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$room) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Conversation non trouvée ou accès non autorisé'
|
||||
], 404);
|
||||
@@ -432,7 +438,7 @@ class ChatController {
|
||||
|
||||
// Vérifier les permissions
|
||||
if ($room['created_by'] != $userId && !$room['is_admin']) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seul le créateur ou un admin peut modifier la conversation'
|
||||
], 403);
|
||||
@@ -460,24 +466,24 @@ class ChatController {
|
||||
$updatedRoom['temp_id'] = $tempId;
|
||||
}
|
||||
|
||||
\LogService::log('Conversation mise à jour', [
|
||||
LogService::log('Conversation mise à jour', [
|
||||
'level' => 'info',
|
||||
'room_id' => $roomId,
|
||||
'updated_by' => $userId
|
||||
]);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'room' => $updatedRoom
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la mise à jour de la conversation', [
|
||||
LogService::log('Erreur lors de la mise à jour de la conversation', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -493,7 +499,7 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$userId = \Session::getUserId();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Vérifier que la room existe et récupérer le créateur
|
||||
$stmt = $this->db->prepare('
|
||||
@@ -506,7 +512,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que la room existe
|
||||
if (!$room) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Conversation non trouvée'
|
||||
], 404);
|
||||
@@ -515,7 +521,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que la room n'est pas déjà supprimée
|
||||
if ($room['is_active'] == 0) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Cette conversation est déjà supprimée'
|
||||
], 400);
|
||||
@@ -524,7 +530,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que l'utilisateur est le créateur
|
||||
if ($room['created_by'] != $userId) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seul le créateur de la conversation peut la supprimer'
|
||||
], 403);
|
||||
@@ -554,13 +560,13 @@ class ChatController {
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
\LogService::log('Conversation supprimée', [
|
||||
LogService::log('Conversation supprimée', [
|
||||
'level' => 'info',
|
||||
'room_id' => $roomId,
|
||||
'deleted_by' => $userId
|
||||
]);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => 'Conversation supprimée avec succès'
|
||||
]);
|
||||
@@ -573,12 +579,12 @@ class ChatController {
|
||||
}
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la suppression de la conversation', [
|
||||
LogService::log('Erreur lors de la suppression de la conversation', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -593,11 +599,11 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$userId = \Session::getUserId();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
if (!$this->isUserInRoom($userId, $roomId)) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé à cette conversation'
|
||||
], 403);
|
||||
@@ -658,7 +664,7 @@ class ChatController {
|
||||
|
||||
// Déchiffrer les noms
|
||||
foreach ($messages as &$message) {
|
||||
$message['sender_name'] = \ApiService::decryptData($message['sender_name']);
|
||||
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
|
||||
$message['is_read'] = (bool)$message['is_read'];
|
||||
$message['is_mine'] = ($message['sender_id'] == $userId);
|
||||
}
|
||||
@@ -675,7 +681,7 @@ class ChatController {
|
||||
// Compter les messages non lus restants (devrait être 0)
|
||||
$unreadCount = $this->getUnreadCount($roomId, $userId);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'messages' => $messages,
|
||||
'has_more' => count($messages) === $limit,
|
||||
@@ -684,12 +690,12 @@ class ChatController {
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la récupération des messages', [
|
||||
LogService::log('Erreur lors de la récupération des messages', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -704,15 +710,15 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Récupérer le temp_id s'il est fourni (pour la synchronisation offline)
|
||||
$tempId = $data['temp_id'] ?? null;
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
if (!$this->isUserInRoom($userId, $roomId)) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé à cette conversation'
|
||||
], 403);
|
||||
@@ -724,7 +730,7 @@ class ChatController {
|
||||
if ($roomInfo && $roomInfo['type'] === 'broadcast') {
|
||||
// Pour les broadcasts, seul le créateur peut écrire
|
||||
if ($roomInfo['created_by'] != $userId) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Seul l\'administrateur peut poster dans une annonce'
|
||||
], 403);
|
||||
@@ -733,7 +739,7 @@ class ChatController {
|
||||
} else {
|
||||
// Pour les autres types, vérifier can_write
|
||||
if (!$this->canUserWrite($userId, $roomId)) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Vous n\'avez pas la permission d\'écrire dans cette conversation'
|
||||
], 403);
|
||||
@@ -743,7 +749,7 @@ class ChatController {
|
||||
|
||||
// Validation du contenu
|
||||
if (!isset($data['content']) || empty(trim($data['content']))) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le message ne peut pas être vide'
|
||||
], 400);
|
||||
@@ -754,7 +760,7 @@ class ChatController {
|
||||
|
||||
// Limiter la longueur du message
|
||||
if (mb_strlen($content, 'UTF-8') > 5000) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Message trop long (max 5000 caractères)'
|
||||
], 400);
|
||||
@@ -799,7 +805,7 @@ class ChatController {
|
||||
$msgStmt->execute(['id' => $messageId]);
|
||||
$message = $msgStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
$message['sender_name'] = \ApiService::decryptData($message['sender_name']);
|
||||
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
|
||||
$message['is_mine'] = true;
|
||||
$message['is_read'] = false;
|
||||
$message['read_count'] = 0;
|
||||
@@ -809,25 +815,25 @@ class ChatController {
|
||||
$message['temp_id'] = $tempId;
|
||||
}
|
||||
|
||||
\LogService::log('Message envoyé', [
|
||||
LogService::log('Message envoyé', [
|
||||
'level' => 'debug',
|
||||
'room_id' => $roomId,
|
||||
'message_id' => $messageId,
|
||||
'sender_id' => $userId
|
||||
]);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => $message
|
||||
], 201);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de l\'envoi du message', [
|
||||
LogService::log('Erreur lors de l\'envoi du message', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -842,8 +848,8 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Récupérer le temp_id s'il est fourni (pour la synchronisation offline)
|
||||
$tempId = $data['temp_id'] ?? null;
|
||||
@@ -858,7 +864,7 @@ class ChatController {
|
||||
$message = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$message) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Message non trouvé'
|
||||
], 404);
|
||||
@@ -867,7 +873,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que l'utilisateur est le sender du message
|
||||
if ($message['sender_id'] != $userId) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Vous ne pouvez modifier que vos propres messages'
|
||||
], 403);
|
||||
@@ -876,7 +882,7 @@ class ChatController {
|
||||
|
||||
// Vérifier que le message n'est pas supprimé
|
||||
if ($message['is_deleted']) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Ce message a été supprimé'
|
||||
], 400);
|
||||
@@ -885,7 +891,7 @@ class ChatController {
|
||||
|
||||
// Validation du contenu
|
||||
if (!isset($data['content']) || empty(trim($data['content']))) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le message ne peut pas être vide'
|
||||
], 400);
|
||||
@@ -896,7 +902,7 @@ class ChatController {
|
||||
|
||||
// Limiter la longueur du message
|
||||
if (mb_strlen($content, 'UTF-8') > 5000) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Message trop long (max 5000 caractères)'
|
||||
], 400);
|
||||
@@ -931,7 +937,7 @@ class ChatController {
|
||||
$msgStmt->execute(['id' => $messageId]);
|
||||
$updatedMessage = $msgStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
$updatedMessage['sender_name'] = \ApiService::decryptData($updatedMessage['sender_name']);
|
||||
$updatedMessage['sender_name'] = ApiService::decryptData($updatedMessage['sender_name']);
|
||||
$updatedMessage['is_mine'] = true;
|
||||
|
||||
// Ajouter le temp_id à la réponse si fourni
|
||||
@@ -939,24 +945,24 @@ class ChatController {
|
||||
$updatedMessage['temp_id'] = $tempId;
|
||||
}
|
||||
|
||||
\LogService::log('Message modifié', [
|
||||
LogService::log('Message modifié', [
|
||||
'level' => 'debug',
|
||||
'message_id' => $messageId,
|
||||
'sender_id' => $userId
|
||||
]);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => $updatedMessage
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la modification du message', [
|
||||
LogService::log('Erreur lors de la modification du message', [
|
||||
'level' => 'error',
|
||||
'message_id' => $messageId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -971,12 +977,12 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$data = \Request::getJson();
|
||||
$userId = \Session::getUserId();
|
||||
$data = Request::getJson();
|
||||
$userId = Session::getUserId();
|
||||
|
||||
// Vérifier que l'utilisateur est participant
|
||||
if (!$this->isUserInRoom($userId, $roomId)) {
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé à cette conversation'
|
||||
], 403);
|
||||
@@ -1041,18 +1047,18 @@ class ChatController {
|
||||
]);
|
||||
$result = $countStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'unread_count' => (int)$result['unread_count']
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors du marquage comme lu', [
|
||||
LogService::log('Erreur lors du marquage comme lu', [
|
||||
'level' => 'error',
|
||||
'room_id' => $roomId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -1067,8 +1073,8 @@ class ChatController {
|
||||
// L'authentification est déjà vérifiée par le Router pour les routes privées
|
||||
|
||||
try {
|
||||
$userId = \Session::getUserId();
|
||||
$entityId = \Session::getEntityId();
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
$userRole = $this->getUserRole($userId);
|
||||
|
||||
$sql = '
|
||||
@@ -1122,11 +1128,11 @@ class ChatController {
|
||||
|
||||
foreach ($recipients as &$recipient) {
|
||||
// Déchiffrer le nom
|
||||
$recipient['name'] = \ApiService::decryptData($recipient['name']);
|
||||
$recipient['name'] = ApiService::decryptData($recipient['name']);
|
||||
|
||||
// Déchiffrer le nom de l'entité
|
||||
$entiteName = $recipient['entite_name'] ?
|
||||
\ApiService::decryptData($recipient['entite_name']) :
|
||||
ApiService::decryptData($recipient['entite_name']) :
|
||||
'Sans entité';
|
||||
|
||||
// Créer une copie pour recipients_by_entity
|
||||
@@ -1146,18 +1152,18 @@ class ChatController {
|
||||
$recipientsDecrypted[] = $recipient;
|
||||
}
|
||||
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'recipients' => $recipientsDecrypted,
|
||||
'recipients_by_entity' => $recipientsByEntity
|
||||
]);
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
\LogService::log('Erreur lors de la récupération des destinataires', [
|
||||
LogService::log('Erreur lors de la récupération des destinataires', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
\Response::json([
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
@@ -1225,7 +1231,7 @@ class ChatController {
|
||||
$participants = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($participants as &$participant) {
|
||||
$participant['name'] = \ApiService::decryptData($participant['name']);
|
||||
$participant['name'] = ApiService::decryptData($participant['name']);
|
||||
}
|
||||
|
||||
return $participants;
|
||||
@@ -1349,7 +1355,7 @@ class ChatController {
|
||||
|
||||
// Déchiffrer les noms et convertir les booléens
|
||||
foreach ($messages as &$message) {
|
||||
$message['sender_name'] = \ApiService::decryptData($message['sender_name']);
|
||||
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
|
||||
$message['is_read'] = (bool)$message['is_read'];
|
||||
$message['is_mine'] = (bool)$message['is_mine'];
|
||||
}
|
||||
@@ -1398,7 +1404,7 @@ class ChatController {
|
||||
|
||||
// Déchiffrer les noms et convertir les booléens
|
||||
foreach ($messages as &$message) {
|
||||
$message['sender_name'] = \ApiService::decryptData($message['sender_name']);
|
||||
$message['sender_name'] = ApiService::decryptData($message['sender_name']);
|
||||
$message['is_read'] = (bool)$message['is_read'];
|
||||
$message['is_mine'] = (bool)$message['is_mine'];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
|
||||
use PDO;
|
||||
@@ -14,8 +15,10 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\FileService;
|
||||
use Exception;
|
||||
|
||||
class EntiteController {
|
||||
@@ -74,13 +77,12 @@ class EntiteController {
|
||||
throw new Exception('Erreur lors de la création de l\'entité');
|
||||
}
|
||||
|
||||
LogService::log('Création d\'une nouvelle entité GeoSector', [
|
||||
'level' => 'info',
|
||||
'entiteId' => $entiteId,
|
||||
'name' => $name,
|
||||
'postalCode' => $postalCode,
|
||||
'cityName' => $cityName
|
||||
]);
|
||||
// Log de création de l'entité
|
||||
EventLogService::logEntityCreated(
|
||||
(int)$entiteId,
|
||||
1, // fk_type toujours à 1 dans cette méthode
|
||||
$postalCode
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $entiteId,
|
||||
@@ -220,12 +222,12 @@ class EntiteController {
|
||||
throw new Exception('Erreur lors de la création de l\'entité');
|
||||
}
|
||||
|
||||
LogService::log('Création d\'une nouvelle entité GeoSector via getOrCreateEntiteByPostalCode', [
|
||||
'level' => 'info',
|
||||
'entiteId' => $entiteId,
|
||||
'name' => $name,
|
||||
'postalCode' => $postalCode
|
||||
]);
|
||||
// Log de création de l'entité
|
||||
EventLogService::logEntityCreated(
|
||||
$entiteId,
|
||||
1, // fk_type toujours à 1 dans cette méthode
|
||||
$postalCode
|
||||
);
|
||||
|
||||
return $entiteId;
|
||||
} catch (Exception $e) {
|
||||
@@ -559,10 +561,8 @@ class EntiteController {
|
||||
$params[] = $data['gps_lng'];
|
||||
}
|
||||
|
||||
if (isset($data['stripe_id'])) {
|
||||
$updateFields[] = 'encrypted_stripe_id = ?';
|
||||
$params[] = ApiService::encryptData($data['stripe_id']);
|
||||
}
|
||||
// Note: stripe_id ne peut plus être modifié ici
|
||||
// Les données Stripe sont gérées via la table stripe_accounts
|
||||
|
||||
if (isset($data['chk_demo'])) {
|
||||
$updateFields[] = 'chk_demo = ?';
|
||||
@@ -629,12 +629,23 @@ class EntiteController {
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Mise à jour d\'une entité GeoSector', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'entiteId' => $entiteId,
|
||||
'isAdmin' => $isAdmin
|
||||
]);
|
||||
// Log de mise à jour de l'entité
|
||||
$changes = [];
|
||||
$encryptedFields = ['name', 'email', 'phone', 'mobile'];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array($key, $encryptedFields)) {
|
||||
// Champs sensibles : booléen uniquement
|
||||
$changes['encrypted_' . $key] = true;
|
||||
} else {
|
||||
// Champs non sensibles : valeur
|
||||
$changes[$key] = ['new' => $value];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logEntityUpdated((int)$entiteId, $changes);
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
@@ -738,7 +749,7 @@ class EntiteController {
|
||||
|
||||
// Créer le dossier de destination
|
||||
require_once __DIR__ . '/../Services/FileService.php';
|
||||
$fileService = new \FileService();
|
||||
$fileService = new FileService();
|
||||
$uploadPath = "/{$entiteId}/logo";
|
||||
$fullPath = $fileService->createDirectory($entiteId, $uploadPath);
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
use Exception;
|
||||
|
||||
class FileController {
|
||||
|
||||
115
api/src/Controllers/HealthController.php
Normal file
115
api/src/Controllers/HealthController.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Database;
|
||||
use Response;
|
||||
use PDO;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* HealthController
|
||||
*
|
||||
* Endpoint de vérification de santé de l'API
|
||||
* Route publique pour permettre le monitoring automatique
|
||||
*/
|
||||
class HealthController
|
||||
{
|
||||
private PDO $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
/**
|
||||
* Vérifie la santé de l'API
|
||||
* GET /api/health
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function check(): void
|
||||
{
|
||||
$checks = [
|
||||
'api' => 'ok',
|
||||
'database' => $this->checkDatabase(),
|
||||
'directories' => $this->checkDirectories()
|
||||
];
|
||||
|
||||
// Déterminer le statut global
|
||||
$status = in_array('error', $checks, true) ? 'error' : 'ok';
|
||||
$httpCode = $status === 'ok' ? 200 : 503;
|
||||
|
||||
Response::json([
|
||||
'status' => $status,
|
||||
'checks' => $checks,
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'environment' => $this->getEnvironment()
|
||||
], $httpCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie la connexion à la base de données
|
||||
*
|
||||
* @return string 'ok' ou 'error'
|
||||
*/
|
||||
private function checkDatabase(): string
|
||||
{
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->query("SELECT 1");
|
||||
return $stmt ? 'ok' : 'error';
|
||||
} catch (Exception $e) {
|
||||
error_log("Health check database error: " . $e->getMessage());
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'accessibilité des dossiers critiques
|
||||
*
|
||||
* @return string 'ok' ou 'error'
|
||||
*/
|
||||
private function checkDirectories(): string
|
||||
{
|
||||
$basePath = __DIR__ . '/../../';
|
||||
$requiredDirs = ['logs', 'uploads'];
|
||||
|
||||
foreach ($requiredDirs as $dir) {
|
||||
$fullPath = $basePath . $dir;
|
||||
|
||||
if (!is_dir($fullPath)) {
|
||||
error_log("Health check: Directory not found: $fullPath");
|
||||
return 'error';
|
||||
}
|
||||
|
||||
if (!is_writable($fullPath)) {
|
||||
error_log("Health check: Directory not writable: $fullPath");
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
/**
|
||||
* Détecte l'environnement actuel
|
||||
*
|
||||
* @return string 'dev', 'recette' ou 'production'
|
||||
*/
|
||||
private function getEnvironment(): string
|
||||
{
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'unknown';
|
||||
|
||||
if (str_contains($host, 'dapp.geosector.fr')) {
|
||||
return 'dev';
|
||||
} elseif (str_contains($host, 'rapp.geosector.fr')) {
|
||||
return 'recette';
|
||||
} elseif (str_contains($host, 'app3.geosector.fr') || str_contains($host, 'app.geosector.fr')) {
|
||||
return 'production';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,12 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\EventLogService;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/EntiteController.php';
|
||||
require_once __DIR__ . '/../Services/Security/SecurityMonitor.php';
|
||||
@@ -55,14 +57,6 @@ class LoginController {
|
||||
// admin accessible uniquement aux fk_role>1 (admins amicale + super-admins)
|
||||
$roleCondition = ($interface === 'user') ? 'AND fk_role IN (1, 2)' : 'AND fk_role>1';
|
||||
|
||||
// Log pour le debug
|
||||
LogService::log('Tentative de connexion GeoSector', [
|
||||
'level' => 'info',
|
||||
'username' => $username,
|
||||
'type' => $interface,
|
||||
'role_condition' => $roleCondition
|
||||
]);
|
||||
|
||||
// Requête optimisée: on récupère l'utilisateur et son entité en une seule fois avec LEFT JOIN
|
||||
$stmt = $this->db->prepare(
|
||||
'SELECT
|
||||
@@ -83,11 +77,8 @@ class LoginController {
|
||||
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||
SecurityMonitor::recordFailedLogin($clientIp, $username, 'user_not_found', $userAgent);
|
||||
|
||||
LogService::log('Tentative de connexion GeoSector échouée : utilisateur non trouvé', [
|
||||
'level' => 'warning',
|
||||
'username' => $username
|
||||
]);
|
||||
|
||||
EventLogService::logLoginFailed($username, 'user_not_found', 1);
|
||||
Response::json(['error' => 'Identifiants invalides'], 401);
|
||||
return;
|
||||
}
|
||||
@@ -100,22 +91,15 @@ class LoginController {
|
||||
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||
SecurityMonitor::recordFailedLogin($clientIp, $username, 'invalid_password', $userAgent);
|
||||
|
||||
LogService::log('Tentative de connexion GeoSector échouée : mot de passe incorrect', [
|
||||
'level' => 'warning',
|
||||
'username' => $username
|
||||
]);
|
||||
|
||||
EventLogService::logLoginFailed($username, 'invalid_password', 1);
|
||||
Response::json(['error' => 'Identifiants invalides'], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier si l'utilisateur a une entité et si elle est active
|
||||
if (!empty($user['fk_entite']) && (!isset($user['entite_chk_active']) || $user['entite_chk_active'] != 1)) {
|
||||
LogService::log('Tentative de connexion GeoSector échouée : entité non active', [
|
||||
'level' => 'warning',
|
||||
'username' => $username,
|
||||
'entite_id' => $user['fk_entite']
|
||||
]);
|
||||
EventLogService::logLoginFailed($username, 'account_inactive', 1);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Votre amicale n\'est pas activée. Veuillez contacter votre administrateur.'
|
||||
@@ -307,16 +291,33 @@ class LoginController {
|
||||
// Récupérer l'ID de l'opération active (première opération retournée)
|
||||
$activeOperationId = $operations[0]['id'];
|
||||
|
||||
// Récupérer ope_user_id pour l'utilisateur connecté et l'opération active
|
||||
$opeUserStmt = $this->db->prepare(
|
||||
'SELECT id FROM ope_users WHERE fk_user = ? AND fk_operation = ?'
|
||||
);
|
||||
$opeUserStmt->execute([$user['id'], $activeOperationId]);
|
||||
$opeUser = $opeUserStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($opeUser) {
|
||||
$userData['ope_user_id'] = $opeUser['id'];
|
||||
} else {
|
||||
$userData['ope_user_id'] = null;
|
||||
}
|
||||
|
||||
// 2. Récupérer les secteurs selon l'interface et le rôle
|
||||
if ($interface === 'user') {
|
||||
// Interface utilisateur : seulement les secteurs affectés à l'utilisateur
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $user['id']]);
|
||||
// Utiliser ope_user_id au lieu de users.id
|
||||
$opeUserId = $userData['ope_user_id'];
|
||||
if ($opeUserId) {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $opeUserId]);
|
||||
}
|
||||
} elseif ($interface === 'admin' && $user['fk_role'] == 2) {
|
||||
// Interface admin avec rôle 2 : tous les secteurs distincts de l'opération
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
@@ -344,11 +345,12 @@ class LoginController {
|
||||
// 3. Récupérer les passages selon l'interface et le rôle
|
||||
if ($interface === 'user' && !empty($sectors)) {
|
||||
// Interface utilisateur : passages de l'utilisateur + passages à finaliser sur ses secteurs
|
||||
$userId = $user['id'];
|
||||
// Utiliser ope_user_id au lieu de users.id
|
||||
$opeUserId = $userData['ope_user_id'];
|
||||
$sectorIds = array_column($sectors, 'id');
|
||||
$sectorIdsString = implode(',', $sectorIds);
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
if (!empty($sectorIdsString) && $opeUserId) {
|
||||
$passagesStmt = $this->db->prepare(
|
||||
"SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
@@ -362,7 +364,7 @@ class LoginController {
|
||||
)
|
||||
ORDER BY passed_at DESC"
|
||||
);
|
||||
$passagesStmt->execute([$activeOperationId, $userId, $userId]);
|
||||
$passagesStmt->execute([$activeOperationId, $opeUserId, $opeUserId]);
|
||||
}
|
||||
} elseif ($interface === 'admin' && $user['fk_role'] == 2) {
|
||||
// Interface admin avec rôle 2 : tous les passages de l'opération
|
||||
@@ -423,13 +425,14 @@ class LoginController {
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
$usersSectorsStmt = $this->db->prepare(
|
||||
"SELECT DISTINCT u.id, u.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
FROM users u
|
||||
JOIN ope_users_sectors us ON u.id = us.fk_user
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
AND u.chk_active = 1
|
||||
"SELECT DISTINCT u.id as user_id, ou.id as ope_user_id, ou.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
FROM users u
|
||||
JOIN ope_users ou ON u.id = ou.fk_user
|
||||
JOIN ope_users_sectors us ON ou.id = us.fk_user AND ou.fk_operation = us.fk_operation
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
AND u.chk_active = 1
|
||||
AND u.id != ?" // Exclure l'utilisateur connecté
|
||||
);
|
||||
$usersSectorsStmt->execute([$activeOperationId, $user['id']]);
|
||||
@@ -458,14 +461,27 @@ class LoginController {
|
||||
|
||||
// 6. Récupérer les membres (users de l'entité du user) si nécessaire
|
||||
if ($interface === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
// Si on a une opération active, on récupère aussi ope_user_id
|
||||
if (isset($activeOperationId)) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT u.id, u.fk_role, u.fk_entite, u.fk_titre, u.encrypted_name, u.first_name, u.sect_name,
|
||||
u.encrypted_user_name, u.encrypted_phone, u.encrypted_mobile, u.encrypted_email,
|
||||
u.date_naissance, u.date_embauche, u.chk_active, ou.id as ope_user_id
|
||||
FROM users u
|
||||
LEFT JOIN ope_users ou ON u.id = ou.fk_user AND ou.fk_operation = ?
|
||||
WHERE u.fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$activeOperationId, $user['fk_entite']]);
|
||||
} else {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
}
|
||||
$membres = $membresStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($membres)) {
|
||||
@@ -474,6 +490,7 @@ class LoginController {
|
||||
foreach ($membres as $membre) {
|
||||
$membreItem = [
|
||||
'id' => $membre['id'],
|
||||
'ope_user_id' => $membre['ope_user_id'] ?? null,
|
||||
'fk_role' => $membre['fk_role'],
|
||||
'fk_entite' => $membre['fk_entite'],
|
||||
'fk_titre' => $membre['fk_titre'],
|
||||
@@ -537,13 +554,15 @@ class LoginController {
|
||||
if ($user['fk_role'] <= 2) {
|
||||
// User normal ou admin avec fk_role=2: uniquement son amicale
|
||||
$amicaleStmt = $this->db->prepare(
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
sa.stripe_account_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass,
|
||||
sa.stripe_location_id
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
WHERE e.id = ? AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute([$user['fk_entite']]);
|
||||
@@ -551,13 +570,15 @@ class LoginController {
|
||||
} else {
|
||||
// Admin avec fk_role>2: toutes les amicales sauf id=1
|
||||
$amicaleStmt = $this->db->prepare(
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
sa.stripe_account_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass,
|
||||
sa.stripe_location_id
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
WHERE e.id != 1 AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute();
|
||||
@@ -872,6 +893,9 @@ class LoginController {
|
||||
// Ajouter les données du chat à la réponse
|
||||
$response['chat'] = $chatData;
|
||||
|
||||
// Log de connexion réussie
|
||||
EventLogService::logLoginSuccess($user['id'], $user['fk_entite'] ?? null, $username);
|
||||
|
||||
// Envoi de la réponse
|
||||
Response::json($response);
|
||||
} catch (PDOException $e) {
|
||||
@@ -918,14 +942,6 @@ class LoginController {
|
||||
// Déterminer le roleCondition selon le mode (même logique que login)
|
||||
$roleCondition = ($mode === 'user') ? 'AND fk_role IN (1, 2)' : 'AND fk_role>1';
|
||||
|
||||
// Log pour le debug
|
||||
LogService::log('Rafraîchissement session GeoSector', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'mode' => $mode,
|
||||
'role_condition' => $roleCondition
|
||||
]);
|
||||
|
||||
// 4. Requête pour récupérer l'utilisateur et son entité (même requête que login)
|
||||
$stmt = $this->db->prepare(
|
||||
'SELECT
|
||||
@@ -1074,15 +1090,32 @@ class LoginController {
|
||||
|
||||
$activeOperationId = $operations[0]['id'];
|
||||
|
||||
// Récupérer ope_user_id pour l'utilisateur connecté et l'opération active
|
||||
$opeUserStmt = $this->db->prepare(
|
||||
'SELECT id FROM ope_users WHERE fk_user = ? AND fk_operation = ?'
|
||||
);
|
||||
$opeUserStmt->execute([$user['id'], $activeOperationId]);
|
||||
$opeUser = $opeUserStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($opeUser) {
|
||||
$userData['ope_user_id'] = $opeUser['id'];
|
||||
} else {
|
||||
$userData['ope_user_id'] = null;
|
||||
}
|
||||
|
||||
// Récupérer les secteurs selon le mode et le rôle
|
||||
if ($mode === 'user') {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $user['id']]);
|
||||
// Utiliser ope_user_id au lieu de users.id
|
||||
$opeUserId = $userData['ope_user_id'];
|
||||
if ($opeUserId) {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT s.id, s.libelle, s.color, s.sector
|
||||
FROM ope_sectors s
|
||||
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||
);
|
||||
$sectorsStmt->execute([$activeOperationId, $opeUserId]);
|
||||
}
|
||||
} elseif ($mode === 'admin' && $user['fk_role'] == 2) {
|
||||
$sectorsStmt = $this->db->prepare(
|
||||
'SELECT DISTINCT s.id, s.libelle, s.color, s.sector
|
||||
@@ -1106,10 +1139,12 @@ class LoginController {
|
||||
|
||||
// Récupérer les passages selon le mode et le rôle
|
||||
if ($mode === 'user' && !empty($sectors)) {
|
||||
// Utiliser ope_user_id au lieu de users.id
|
||||
$opeUserId = $userData['ope_user_id'];
|
||||
$sectorIds = array_column($sectors, 'id');
|
||||
$sectorIdsString = implode(',', $sectorIds);
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
if (!empty($sectorIdsString) && $opeUserId) {
|
||||
$passagesStmt = $this->db->prepare(
|
||||
"SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
@@ -1123,7 +1158,7 @@ class LoginController {
|
||||
)
|
||||
ORDER BY passed_at DESC"
|
||||
);
|
||||
$passagesStmt->execute([$activeOperationId, $user['id'], $user['id']]);
|
||||
$passagesStmt->execute([$activeOperationId, $opeUserId, $opeUserId]);
|
||||
}
|
||||
} elseif ($mode === 'admin' && $user['fk_role'] == 2) {
|
||||
$passagesStmt = $this->db->prepare(
|
||||
@@ -1177,9 +1212,10 @@ class LoginController {
|
||||
|
||||
if (!empty($sectorIdsString)) {
|
||||
$usersSectorsStmt = $this->db->prepare(
|
||||
"SELECT DISTINCT u.id, u.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
"SELECT DISTINCT u.id as user_id, ou.id as ope_user_id, ou.first_name, u.encrypted_name, u.sect_name, us.fk_sector
|
||||
FROM users u
|
||||
JOIN ope_users_sectors us ON u.id = us.fk_user
|
||||
JOIN ope_users ou ON u.id = ou.fk_user
|
||||
JOIN ope_users_sectors us ON ou.id = us.fk_user AND ou.fk_operation = us.fk_operation
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
@@ -1209,20 +1245,34 @@ class LoginController {
|
||||
// Récupérer les membres si nécessaire
|
||||
$membresData = [];
|
||||
if ($mode === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
// Si on a une opération active, on récupère aussi ope_user_id
|
||||
if (isset($activeOperationId)) {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT u.id, u.fk_role, u.fk_entite, u.fk_titre, u.encrypted_name, u.first_name, u.sect_name,
|
||||
u.encrypted_user_name, u.encrypted_phone, u.encrypted_mobile, u.encrypted_email,
|
||||
u.date_naissance, u.date_embauche, u.chk_active, ou.id as ope_user_id
|
||||
FROM users u
|
||||
LEFT JOIN ope_users ou ON u.id = ou.fk_user AND ou.fk_operation = ?
|
||||
WHERE u.fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$activeOperationId, $user['fk_entite']]);
|
||||
} else {
|
||||
$membresStmt = $this->db->prepare(
|
||||
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
WHERE fk_entite = ?'
|
||||
);
|
||||
$membresStmt->execute([$user['fk_entite']]);
|
||||
}
|
||||
$membres = $membresStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($membres)) {
|
||||
foreach ($membres as $membre) {
|
||||
$membreItem = [
|
||||
'id' => $membre['id'],
|
||||
'ope_user_id' => $membre['ope_user_id'] ?? null,
|
||||
'fk_role' => $membre['fk_role'],
|
||||
'fk_entite' => $membre['fk_entite'],
|
||||
'fk_titre' => $membre['fk_titre'],
|
||||
@@ -1279,10 +1329,12 @@ class LoginController {
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
sa.stripe_account_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass,
|
||||
sa.stripe_location_id
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
WHERE e.id = ? AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute([$user['fk_entite']]);
|
||||
@@ -1292,10 +1344,12 @@ class LoginController {
|
||||
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
|
||||
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
|
||||
e.encrypted_email as email, e.gps_lat, e.gps_lng,
|
||||
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass
|
||||
sa.stripe_account_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
|
||||
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe, e.chk_user_delete_pass,
|
||||
sa.stripe_location_id
|
||||
FROM entites e
|
||||
LEFT JOIN x_regions r ON e.fk_region = r.id
|
||||
LEFT JOIN stripe_accounts sa ON e.id = sa.fk_entite
|
||||
WHERE e.id != 1 AND e.chk_active = 1'
|
||||
);
|
||||
$amicaleStmt->execute();
|
||||
@@ -1830,13 +1884,13 @@ class LoginController {
|
||||
}
|
||||
*/
|
||||
|
||||
// 5. Vérification de l'existence du code postal dans la table entites
|
||||
$checkPostalStmt = $this->db->prepare('SELECT id FROM entites WHERE code_postal = ?');
|
||||
$checkPostalStmt->execute([$postalCode]);
|
||||
// 5. Vérification de l'existence du code postal + ville dans la table entites
|
||||
$checkPostalStmt = $this->db->prepare('SELECT id FROM entites WHERE code_postal = ? AND ville = ?');
|
||||
$checkPostalStmt->execute([$postalCode, $cityName]);
|
||||
if ($checkPostalStmt->fetch()) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Une amicale est déjà inscrite à ce code postal'
|
||||
'message' => 'Une amicale est déjà inscrite pour ce code postal et cette ville'
|
||||
], 409);
|
||||
return;
|
||||
}
|
||||
@@ -2073,16 +2127,15 @@ class LoginController {
|
||||
// Méthodes auxiliaires
|
||||
|
||||
public function logout(): void {
|
||||
$userId = Session::getUserId() ?? null;
|
||||
$userEmail = Session::getUserEmail() ?? 'anonyme';
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
|
||||
Session::logout();
|
||||
|
||||
LogService::log('Déconnexion GeoSector réussie', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'email' => $userEmail
|
||||
]);
|
||||
// Log de déconnexion
|
||||
if ($userId) {
|
||||
EventLogService::logLogout($userId, $entityId, 0);
|
||||
}
|
||||
|
||||
// Retourner une réponse standardisée
|
||||
Response::json([
|
||||
@@ -2106,12 +2159,20 @@ class LoginController {
|
||||
// Formater la ville et le code postal pour la recherche
|
||||
$citySearch = urlencode($cityName . ' ' . $postalCode);
|
||||
|
||||
// Créer un contexte avec timeout de 2 secondes
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 2,
|
||||
'ignore_errors' => true
|
||||
]
|
||||
]);
|
||||
|
||||
foreach ($keywords as $keyword) {
|
||||
// Construire l'URL de recherche pour l'API adresse.gouv.fr
|
||||
$searchUrl = "https://api-adresse.data.gouv.fr/search/?q=" . urlencode($keyword) . "+$citySearch&limit=5";
|
||||
|
||||
// Effectuer la requête HTTP
|
||||
$response = @file_get_contents($searchUrl);
|
||||
// Effectuer la requête HTTP avec timeout
|
||||
$response = @file_get_contents($searchUrl, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
LogService::log('Erreur lors de la requête à l\'API adresse.gouv.fr', [
|
||||
@@ -2159,9 +2220,19 @@ class LoginController {
|
||||
}
|
||||
}
|
||||
|
||||
// Si aucune caserne n'a été trouvée avec les mots-clés, utiliser les coordonnées du centre de la ville
|
||||
// Si aucune caserne trouvée, chercher simplement ville + code postal avec timeout
|
||||
$citySearch = urlencode($cityName . ' ' . $postalCode);
|
||||
$cityUrl = "https://api-adresse.data.gouv.fr/search/?q=$citySearch&limit=1";
|
||||
$cityResponse = @file_get_contents($cityUrl);
|
||||
|
||||
// Créer un contexte avec timeout de 2 secondes
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 2,
|
||||
'ignore_errors' => true
|
||||
]
|
||||
]);
|
||||
|
||||
$cityResponse = @file_get_contents($cityUrl, false, $context);
|
||||
|
||||
if ($cityResponse !== false) {
|
||||
$cityData = json_decode($cityResponse, true);
|
||||
@@ -2169,7 +2240,7 @@ class LoginController {
|
||||
if ($cityData && isset($cityData['features'][0]['geometry']['coordinates'])) {
|
||||
$coordinates = $cityData['features'][0]['geometry']['coordinates'];
|
||||
|
||||
LogService::log('Utilisation des coordonnées du centre de la ville', [
|
||||
LogService::log('Coordonnées GPS récupérées pour l\'adresse', [
|
||||
'level' => 'info',
|
||||
'city' => $cityName,
|
||||
'postalCode' => $postalCode
|
||||
@@ -2183,6 +2254,12 @@ class LoginController {
|
||||
}
|
||||
|
||||
// Aucune coordonnée trouvée
|
||||
LogService::log('Aucune coordonnée GPS trouvée (timeout ou adresse invalide)', [
|
||||
'level' => 'warning',
|
||||
'city' => $cityName,
|
||||
'postalCode' => $postalCode
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
544
api/src/Controllers/MigrationController.php
Normal file
544
api/src/Controllers/MigrationController.php
Normal file
@@ -0,0 +1,544 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/MigrationService.php';
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Database;
|
||||
use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use App\Services\LogService;
|
||||
use App\Services\MigrationService;
|
||||
use Exception;
|
||||
|
||||
class MigrationController {
|
||||
private PDO $db;
|
||||
private AppConfig $appConfig;
|
||||
private MigrationService $migrationService;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance();
|
||||
$this->appConfig = AppConfig::getInstance();
|
||||
$this->migrationService = new MigrationService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Teste les connexions aux bases de données source et cible
|
||||
*
|
||||
* GET /api/migrations/test-connections
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testConnections(): void {
|
||||
try {
|
||||
$result = $this->migrationService->testConnections();
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'connections' => $result
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors du test des connexions', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les entités disponibles à migrer depuis la base source
|
||||
*
|
||||
* GET /api/migrations/entities/available
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getAvailableEntities(): void {
|
||||
try {
|
||||
$entities = $this->migrationService->getAvailableEntities();
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'count' => count($entities),
|
||||
'entities' => $entities
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération des entités disponibles', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les détails d'une entité source
|
||||
*
|
||||
* GET /api/migrations/entities/:id
|
||||
*
|
||||
* @param int $id ID de l'entité dans la base source
|
||||
* @return void
|
||||
*/
|
||||
public function getEntityDetails(int $id): void {
|
||||
try {
|
||||
$entity = $this->migrationService->getEntityDetails($id);
|
||||
|
||||
if (!$entity) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Entité non trouvée'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity' => $entity
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération des détails de l\'entité', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre une entité complète ou par étapes
|
||||
*
|
||||
* POST /api/migrations/entity
|
||||
* Body: {
|
||||
* "entity_id": 45,
|
||||
* "steps": ["users", "operations"], // Optionnel
|
||||
* "dry_run": false, // Optionnel
|
||||
* "truncate": false // Optionnel
|
||||
* }
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function migrateEntity(): void {
|
||||
try {
|
||||
$data = Request::getJsonBody();
|
||||
|
||||
// Validation
|
||||
if (!isset($data['entity_id'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le champ entity_id est requis'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$entityId = (int) $data['entity_id'];
|
||||
$steps = $data['steps'] ?? null;
|
||||
$dryRun = $data['dry_run'] ?? false;
|
||||
$truncate = $data['truncate'] ?? false;
|
||||
|
||||
// Vérifier les permissions (admin uniquement)
|
||||
$userRole = Session::get('fk_role');
|
||||
if ($userRole != 3) { // 3 = admin
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès refusé. Cette action nécessite les droits administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Début de migration d\'entité', [
|
||||
'level' => 'info',
|
||||
'entity_id' => $entityId,
|
||||
'steps' => $steps,
|
||||
'dry_run' => $dryRun,
|
||||
'truncate' => $truncate,
|
||||
'user_id' => Session::get('user_id')
|
||||
]);
|
||||
|
||||
// Exécuter la migration
|
||||
$result = $this->migrationService->migrateEntity(
|
||||
$entityId,
|
||||
$steps,
|
||||
$dryRun,
|
||||
$truncate
|
||||
);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $entityId,
|
||||
'entity_name' => $result['entity_name'],
|
||||
'migration_id' => $result['migration_id'],
|
||||
'steps_completed' => $result['steps_completed'],
|
||||
'total_duration_ms' => $result['total_duration_ms'],
|
||||
'summary' => $result['summary']
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la migration d\'entité', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $data['entity_id'] ?? null
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre une étape spécifique pour une entité
|
||||
*
|
||||
* POST /api/migrations/entity/step
|
||||
* Body: {
|
||||
* "entity_id": 45,
|
||||
* "step": "users",
|
||||
* "dry_run": false,
|
||||
* "options": {}
|
||||
* }
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function migrateEntityStep(): void {
|
||||
try {
|
||||
$data = Request::getJsonBody();
|
||||
|
||||
// Validation
|
||||
if (!isset($data['entity_id']) || !isset($data['step'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Les champs entity_id et step sont requis'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$entityId = (int) $data['entity_id'];
|
||||
$step = $data['step'];
|
||||
$dryRun = $data['dry_run'] ?? false;
|
||||
$options = $data['options'] ?? [];
|
||||
|
||||
// Vérifier les permissions (admin uniquement)
|
||||
$userRole = Session::get('fk_role');
|
||||
if ($userRole != 3) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès refusé. Cette action nécessite les droits administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Début de migration d\'étape', [
|
||||
'level' => 'info',
|
||||
'entity_id' => $entityId,
|
||||
'step' => $step,
|
||||
'dry_run' => $dryRun,
|
||||
'options' => $options,
|
||||
'user_id' => Session::get('user_id')
|
||||
]);
|
||||
|
||||
// Exécuter l'étape de migration
|
||||
$result = $this->migrationService->migrateStep(
|
||||
$entityId,
|
||||
$step,
|
||||
$dryRun,
|
||||
$options
|
||||
);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $entityId,
|
||||
'step' => $step,
|
||||
'records_migrated' => $result['records_migrated'],
|
||||
'duration_ms' => $result['duration_ms'],
|
||||
'warnings' => $result['warnings'] ?? [],
|
||||
'details' => $result['details'] ?? []
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la migration d\'étape', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $data['entity_id'] ?? null,
|
||||
'step' => $data['step'] ?? null
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le statut de migration d'une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/status
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function getMigrationStatus(int $id): void {
|
||||
try {
|
||||
$status = $this->migrationService->getMigrationStatus($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'migration_status' => $status
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération du statut de migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les logs de migration d'une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/logs
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function getMigrationLogs(int $id): void {
|
||||
try {
|
||||
$logs = $this->migrationService->getMigrationLogs($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'logs' => $logs
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération des logs de migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un rapport de migration pour une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/report
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function getMigrationReport(int $id): void {
|
||||
try {
|
||||
$report = $this->migrationService->generateMigrationReport($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'report' => $report
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la génération du rapport de migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare les données source vs cible pour une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/compare
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function compareEntityData(int $id): void {
|
||||
try {
|
||||
$comparison = $this->migrationService->compareEntityData($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'comparison' => $comparison
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la comparaison des données', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'intégrité des données migrées pour une entité
|
||||
*
|
||||
* GET /api/migrations/entity/:id/verify
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function verifyMigration(int $id): void {
|
||||
try {
|
||||
$verification = $this->migrationService->verifyMigration($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'verification' => $verification
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la vérification de la migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Annule la migration d'une entité (rollback)
|
||||
*
|
||||
* DELETE /api/migrations/entity/:id
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return void
|
||||
*/
|
||||
public function rollbackEntity(int $id): void {
|
||||
try {
|
||||
// Vérifier les permissions (admin uniquement)
|
||||
$userRole = Session::get('fk_role');
|
||||
if ($userRole != 3) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès refusé. Cette action nécessite les droits administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Début de rollback d\'entité', [
|
||||
'level' => 'warning',
|
||||
'entity_id' => $id,
|
||||
'user_id' => Session::get('user_id')
|
||||
]);
|
||||
|
||||
$result = $this->migrationService->rollbackEntity($id);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'message' => 'Migration annulée avec succès',
|
||||
'deleted_records' => $result['deleted_records']
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors du rollback de la migration', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une étape spécifique de la migration
|
||||
*
|
||||
* DELETE /api/migrations/entity/:id/step/:step
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @param string $step Nom de l'étape
|
||||
* @return void
|
||||
*/
|
||||
public function rollbackStep(int $id, string $step): void {
|
||||
try {
|
||||
// Vérifier les permissions (admin uniquement)
|
||||
$userRole = Session::get('fk_role');
|
||||
if ($userRole != 3) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès refusé. Cette action nécessite les droits administrateur.'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Début de rollback d\'étape', [
|
||||
'level' => 'warning',
|
||||
'entity_id' => $id,
|
||||
'step' => $step,
|
||||
'user_id' => Session::get('user_id')
|
||||
]);
|
||||
|
||||
$result = $this->migrationService->rollbackStep($id, $step);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entity_id' => $id,
|
||||
'step' => $step,
|
||||
'message' => 'Étape annulée avec succès',
|
||||
'deleted_records' => $result['deleted_records']
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors du rollback de l\'étape', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'entity_id' => $id,
|
||||
'step' => $step
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ExportService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/OperationDataService.php';
|
||||
@@ -16,10 +17,11 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ExportService;
|
||||
use ApiService;
|
||||
use OperationDataService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ExportService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\OperationDataService;
|
||||
use Exception;
|
||||
use DateTime;
|
||||
|
||||
@@ -378,34 +380,37 @@ class OperationController {
|
||||
$newSectId = (int)$this->db->lastInsertId();
|
||||
$duplicatedSectors++;
|
||||
|
||||
// Étape 4.3 : Dupliquer les users_sectors en vérifiant que fk_user existe dans ope_users
|
||||
// Étape 4.3 : Dupliquer les users_sectors en convertissant ancien ope_users.id → nouvel ope_users.id
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, fk_user_creat)
|
||||
SELECT ?, ous.fk_user, ?, ?
|
||||
SELECT ?, new_ou.id, ?, ?
|
||||
FROM ope_users_sectors ous
|
||||
INNER JOIN ope_users ou ON ou.fk_user = ous.fk_user AND ou.fk_operation = ?
|
||||
INNER JOIN ope_users old_ou ON old_ou.id = ous.fk_user AND old_ou.fk_operation = ?
|
||||
INNER JOIN ope_users new_ou ON new_ou.fk_user = old_ou.fk_user AND new_ou.fk_operation = ?
|
||||
WHERE ous.fk_operation = ? AND ous.fk_sector = ? AND ous.chk_active = 1
|
||||
');
|
||||
$stmt->execute([$newOpeId, $newSectId, $userId, $newOpeId, $oldOpeId, $oldSectId]);
|
||||
$stmt->execute([$newOpeId, $newSectId, $userId, $oldOpeId, $newOpeId, $oldOpeId, $oldSectId]);
|
||||
$duplicatedUsersSectors += $stmt->rowCount();
|
||||
|
||||
// Étape 4.4 : Dupliquer les passages avec les valeurs par défaut spécifiées
|
||||
// Étape 4.4 : Dupliquer les passages en convertissant ancien ope_users.id → nouvel ope_users.id
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO ope_pass (
|
||||
fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis, ville,
|
||||
fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis, ville,
|
||||
fk_habitat, appt, niveau, residence, gps_lat, gps_lng, encrypted_name,
|
||||
fk_type, passed_at, montant, fk_type_reglement, chk_email_sent, chk_striped,
|
||||
docremis, nb_passages, chk_map_create, chk_mobile, chk_synchro, anomalie,
|
||||
fk_type, passed_at, montant, fk_type_reglement, chk_email_sent, chk_striped,
|
||||
docremis, nb_passages, chk_map_create, chk_mobile, chk_synchro, anomalie,
|
||||
fk_user_creat, chk_active
|
||||
)
|
||||
SELECT
|
||||
?, ?, fk_user, fk_adresse, numero, rue, rue_bis, ville,
|
||||
fk_habitat, appt, niveau, residence, gps_lat, gps_lng, encrypted_name,
|
||||
2, NULL, 0, 4, 0, 0, 0, 1, 0, 0, 1, 0, ?, 1
|
||||
FROM ope_pass
|
||||
WHERE fk_operation = ? AND fk_sector = ? AND chk_active = 1
|
||||
SELECT
|
||||
?, ?, new_ou.id, op.fk_adresse, op.numero, op.rue, op.rue_bis, op.ville,
|
||||
op.fk_habitat, op.appt, op.niveau, op.residence, op.gps_lat, op.gps_lng, op.encrypted_name,
|
||||
2, NULL, 0, 4, 0, 0, 0, 0, 0, 0, 1, 0, ?, 1
|
||||
FROM ope_pass op
|
||||
INNER JOIN ope_users old_ou ON old_ou.id = op.fk_user AND old_ou.fk_operation = ?
|
||||
INNER JOIN ope_users new_ou ON new_ou.fk_user = old_ou.fk_user AND new_ou.fk_operation = ?
|
||||
WHERE op.fk_operation = ? AND op.fk_sector = ? AND op.chk_active = 1
|
||||
');
|
||||
$stmt->execute([$newOpeId, $newSectId, $userId, $oldOpeId, $oldSectId]);
|
||||
$stmt->execute([$newOpeId, $newSectId, $userId, $oldOpeId, $newOpeId, $oldOpeId, $oldSectId]);
|
||||
$duplicatedPassages += $stmt->rowCount();
|
||||
}
|
||||
|
||||
@@ -455,19 +460,12 @@ class OperationController {
|
||||
// Étape 7 : Préparer la réponse avec les groupes JSON
|
||||
$response = OperationDataService::prepareOperationResponse($this->db, $newOpeId, $entiteId);
|
||||
|
||||
LogService::log('Création opération terminée avec succès', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'entiteId' => $entiteId,
|
||||
'newOpeId' => $newOpeId,
|
||||
'oldOpeId' => $oldOpeId,
|
||||
'stats' => [
|
||||
'insertedUsers' => $insertedUsers,
|
||||
'duplicatedSectors' => $duplicatedSectors,
|
||||
'duplicatedUsersSectors' => $duplicatedUsersSectors,
|
||||
'duplicatedPassages' => $duplicatedPassages
|
||||
]
|
||||
]);
|
||||
// Log de création de l'opération
|
||||
EventLogService::logOperationCreated(
|
||||
$newOpeId,
|
||||
$data['date_deb'],
|
||||
$data['date_fin']
|
||||
);
|
||||
|
||||
Response::json($response, 201);
|
||||
} catch (Exception $e) {
|
||||
@@ -621,12 +619,24 @@ class OperationController {
|
||||
$operationId
|
||||
]);
|
||||
|
||||
LogService::log('Mise à jour d\'une opération', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'entiteId' => $entiteId,
|
||||
'operationId' => $operationId
|
||||
]);
|
||||
// Log de mise à jour de l'opération
|
||||
$changes = [];
|
||||
if (isset($data['libelle']) || isset($data['name'])) {
|
||||
$changes['libelle'] = ['new' => $libelle];
|
||||
}
|
||||
if (isset($data['date_deb'])) {
|
||||
$changes['date_deb'] = ['new' => $data['date_deb']];
|
||||
}
|
||||
if (isset($data['date_fin'])) {
|
||||
$changes['date_fin'] = ['new' => $data['date_fin']];
|
||||
}
|
||||
if (isset($data['chk_distinct_sectors'])) {
|
||||
$changes['chk_distinct_sectors'] = ['new' => (int)$data['chk_distinct_sectors']];
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logOperationUpdated($operationId, $changes);
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
@@ -820,25 +830,8 @@ class OperationController {
|
||||
// Valider la transaction
|
||||
$this->db->commit();
|
||||
|
||||
LogService::log('Suppression complète d\'une opération et de toutes ses données', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'userRole' => $userRole,
|
||||
'userEntiteId' => $userEntiteId,
|
||||
'operationEntiteId' => $operationEntiteId,
|
||||
'operationId' => $operationId,
|
||||
'operationActive' => $operationActive,
|
||||
'deletedCounts' => [
|
||||
'medias' => $deletedMedias,
|
||||
'ope_pass_histo' => $deletedPassHisto,
|
||||
'ope_pass' => $deletedPass,
|
||||
'ope_users_sectors' => $deletedUsersSectors,
|
||||
'sectors_adresses' => $deletedSectorsAdresses,
|
||||
'ope_sectors' => $deletedSectors,
|
||||
'ope_users' => $deletedUsers,
|
||||
'operations' => 1
|
||||
]
|
||||
]);
|
||||
// Log de suppression de l'opération (suppression physique)
|
||||
EventLogService::logOperationDeleted($operationId, false);
|
||||
|
||||
// Préparer la réponse selon le statut de l'opération supprimée
|
||||
$response = [
|
||||
@@ -948,13 +941,14 @@ class OperationController {
|
||||
|
||||
// Récupérer les relations utilisateurs-secteurs
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT
|
||||
SELECT
|
||||
ous.id, ous.fk_operation, ous.fk_user, ous.fk_sector,
|
||||
ous.created_at, ous.updated_at, ous.chk_active,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name,
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name,
|
||||
s.libelle as sector_name
|
||||
FROM ope_users_sectors ous
|
||||
INNER JOIN users u ON u.id = ous.fk_user
|
||||
INNER JOIN ope_users ou ON ou.id = ous.fk_user
|
||||
INNER JOIN users u ON u.id = ou.fk_user
|
||||
INNER JOIN ope_sectors s ON s.id = ous.fk_sector
|
||||
WHERE ous.fk_operation = ? AND ous.chk_active = 1
|
||||
ORDER BY s.libelle, u.encrypted_name
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/ReceiptService.php';
|
||||
|
||||
@@ -15,8 +16,9 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ApiService;
|
||||
use Exception;
|
||||
use DateTime;
|
||||
|
||||
@@ -233,13 +235,14 @@ class PassageController {
|
||||
p.fk_habitat, p.appt, p.niveau, p.residence, p.gps_lat, p.gps_lng,
|
||||
p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
|
||||
p.encrypted_email, p.encrypted_phone, p.nom_recu, p.date_recu,
|
||||
p.chk_email_sent, p.stripe_payment_id, p.docremis, p.date_repasser, p.nb_passages,
|
||||
p.chk_email_sent, p.stripe_payment_id, p.stripe_payment_link_id, p.docremis, p.date_repasser, p.nb_passages,
|
||||
p.chk_mobile, p.anomalie, p.created_at, p.updated_at, p.chk_active,
|
||||
o.libelle as operation_libelle,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name
|
||||
FROM ope_pass p
|
||||
INNER JOIN operations o ON p.fk_operation = o.id
|
||||
INNER JOIN users u ON p.fk_user = u.id
|
||||
INNER JOIN ope_users ou ON p.fk_user = ou.id
|
||||
INNER JOIN users u ON ou.fk_user = u.id
|
||||
WHERE $whereClause AND p.chk_active = 1
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
@@ -324,13 +327,14 @@ class PassageController {
|
||||
$passageId = (int)$id;
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT
|
||||
p.*,
|
||||
SELECT
|
||||
p.*,
|
||||
o.libelle as operation_libelle,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name
|
||||
FROM ope_pass p
|
||||
INNER JOIN operations o ON p.fk_operation = o.id
|
||||
INNER JOIN users u ON p.fk_user = u.id
|
||||
INNER JOIN ope_users ou ON p.fk_user = ou.id
|
||||
INNER JOIN users u ON ou.fk_user = u.id
|
||||
WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
|
||||
');
|
||||
|
||||
@@ -410,12 +414,13 @@ class PassageController {
|
||||
p.id, p.fk_operation, p.fk_sector, p.fk_user, p.passed_at,
|
||||
p.numero, p.rue, p.rue_bis, p.ville, p.gps_lat, p.gps_lng,
|
||||
p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
|
||||
p.encrypted_email, p.encrypted_phone, p.stripe_payment_id, p.chk_email_sent,
|
||||
p.encrypted_email, p.encrypted_phone, p.stripe_payment_id, p.stripe_payment_link_id, p.chk_email_sent,
|
||||
p.docremis, p.date_repasser, p.nb_passages, p.chk_mobile,
|
||||
p.anomalie, p.created_at, p.updated_at,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name
|
||||
FROM ope_pass p
|
||||
INNER JOIN users u ON p.fk_user = u.id
|
||||
INNER JOIN ope_users ou ON p.fk_user = ou.id
|
||||
INNER JOIN users u ON ou.fk_user = u.id
|
||||
WHERE p.fk_operation = ? AND p.chk_active = 1
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
@@ -510,6 +515,24 @@ class PassageController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer ope_users.id pour l'utilisateur du passage
|
||||
// $data['fk_user'] contient users.id, on doit le convertir en ope_users.id
|
||||
$passageUserId = (int)$data['fk_user'];
|
||||
$stmtOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtOpeUser->execute([$passageUserId, $operationId]);
|
||||
$opeUserId = $stmtOpeUser->fetchColumn();
|
||||
|
||||
if (!$opeUserId) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Utilisateur non trouvé dans cette opération'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chiffrement des données sensibles
|
||||
$encryptedName = '';
|
||||
if (isset($data['name']) && !empty(trim($data['name']))) {
|
||||
@@ -527,7 +550,7 @@ class PassageController {
|
||||
$insertData = [
|
||||
'fk_operation' => $operationId,
|
||||
'fk_sector' => isset($data['fk_sector']) ? (int)$data['fk_sector'] : 0,
|
||||
'fk_user' => (int)$data['fk_user'],
|
||||
'fk_user' => $opeUserId,
|
||||
'fk_adresse' => $data['fk_adresse'] ?? '',
|
||||
'passed_at' => isset($data['passed_at']) ? $data['passed_at'] : null,
|
||||
'fk_type' => isset($data['fk_type']) ? (int)$data['fk_type'] : 0,
|
||||
@@ -569,12 +592,14 @@ class PassageController {
|
||||
|
||||
$passageId = $this->db->lastInsertId();
|
||||
|
||||
LogService::log('Création d\'un nouveau passage', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'passageId' => $passageId,
|
||||
'operationId' => $operationId
|
||||
]);
|
||||
// Log de création du passage
|
||||
EventLogService::logPassageCreated(
|
||||
(int)$passageId,
|
||||
$insertData['fk_operation'],
|
||||
$insertData['fk_sector'],
|
||||
$insertData['montant'],
|
||||
(string)$insertData['fk_type_reglement']
|
||||
);
|
||||
|
||||
// Enregistrer la génération du reçu dans shutdown_function pour garantir son exécution
|
||||
// Même si le worker FPM est tué après fastcgi_finish_request()
|
||||
@@ -702,16 +727,33 @@ class PassageController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer ope_users.id pour l'utilisateur connecté
|
||||
$operationId = $passage['fk_operation'];
|
||||
$stmtCurrentOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtCurrentOpeUser->execute([$userId, $operationId]);
|
||||
$currentOpeUserId = $stmtCurrentOpeUser->fetchColumn();
|
||||
|
||||
if (!$currentOpeUserId) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Utilisateur connecté non trouvé dans cette opération'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Si le passage était de type 2 et que l'utilisateur actuel est différent du créateur
|
||||
// On force l'attribution du passage à l'utilisateur actuel
|
||||
if ((int)$passage['fk_type'] === 2 && (int)$passage['fk_user'] !== $userId) {
|
||||
$data['fk_user'] = $userId;
|
||||
if ((int)$passage['fk_type'] === 2 && (int)$passage['fk_user'] !== $currentOpeUserId) {
|
||||
$data['fk_user'] = $currentOpeUserId;
|
||||
|
||||
LogService::log('Attribution automatique d\'un passage type 2 à l\'utilisateur', [
|
||||
'level' => 'info',
|
||||
'passageId' => $passageId,
|
||||
'ancien_user' => $passage['fk_user'],
|
||||
'nouveau_user' => $userId
|
||||
'nouveau_user' => $currentOpeUserId
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -722,7 +764,7 @@ class PassageController {
|
||||
// Champs pouvant être mis à jour
|
||||
$updatableFields = [
|
||||
'fk_sector',
|
||||
'fk_user',
|
||||
// Note: fk_user est traité séparément pour conversion users.id -> ope_users.id
|
||||
'fk_adresse',
|
||||
'passed_at',
|
||||
'fk_type',
|
||||
@@ -740,6 +782,7 @@ class PassageController {
|
||||
'fk_type_reglement',
|
||||
'remarque',
|
||||
'stripe_payment_id',
|
||||
'stripe_payment_link_id',
|
||||
'nom_recu',
|
||||
'date_recu',
|
||||
'docremis',
|
||||
@@ -756,6 +799,48 @@ class PassageController {
|
||||
}
|
||||
}
|
||||
|
||||
// Traitement spécial pour fk_user : conversion users.id -> ope_users.id
|
||||
if (isset($data['fk_user'])) {
|
||||
// Si $data['fk_user'] vient de l'attribution automatique, c'est déjà ope_users.id
|
||||
// Sinon, on doit convertir users.id en ope_users.id
|
||||
$providedUserId = (int)$data['fk_user'];
|
||||
|
||||
// Vérifier si c'est déjà un ope_users.id valide
|
||||
$stmtCheckOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE id = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtCheckOpeUser->execute([$providedUserId, $operationId]);
|
||||
$isOpeUserId = $stmtCheckOpeUser->fetchColumn();
|
||||
|
||||
if ($isOpeUserId) {
|
||||
// C'est déjà un ope_users.id valide
|
||||
$updateFields[] = "fk_user = ?";
|
||||
$params[] = $providedUserId;
|
||||
} else {
|
||||
// C'est probablement un users.id, on le convertit
|
||||
$stmtGetOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtGetOpeUser->execute([$providedUserId, $operationId]);
|
||||
$convertedOpeUserId = $stmtGetOpeUser->fetchColumn();
|
||||
|
||||
if ($convertedOpeUserId) {
|
||||
$updateFields[] = "fk_user = ?";
|
||||
$params[] = $convertedOpeUserId;
|
||||
} else {
|
||||
// Utilisateur non trouvé, on ignore cette mise à jour
|
||||
LogService::log('Tentative de mise à jour avec un utilisateur invalide', [
|
||||
'level' => 'warning',
|
||||
'passageId' => $passageId,
|
||||
'provided_user_id' => $providedUserId,
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion des champs chiffrés
|
||||
if (array_key_exists('name', $data)) {
|
||||
$updateFields[] = "encrypted_name = ?";
|
||||
@@ -791,11 +876,21 @@ class PassageController {
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
LogService::log('Mise à jour d\'un passage', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
// Log de mise à jour du passage (changements simplifiés)
|
||||
$changes = [];
|
||||
foreach ($data as $key => $value) {
|
||||
// Ne logger que les champs non sensibles
|
||||
if (!in_array($key, ['name', 'email', 'phone', 'encrypted_name', 'encrypted_email', 'encrypted_phone'])) {
|
||||
$changes[$key] = ['new' => $value];
|
||||
} else {
|
||||
// Indiquer qu'un champ chiffré a été modifié
|
||||
$changes[$key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logPassageUpdated((int)$passageId, $changes);
|
||||
}
|
||||
|
||||
// Enregistrer la génération du reçu dans shutdown_function pour garantir son exécution
|
||||
// Même si le worker FPM est tué après fastcgi_finish_request()
|
||||
@@ -944,7 +1039,7 @@ class PassageController {
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.id
|
||||
SELECT p.id, p.fk_operation
|
||||
FROM ope_pass p
|
||||
INNER JOIN operations o ON p.fk_operation = o.id
|
||||
WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
|
||||
@@ -962,18 +1057,19 @@ class PassageController {
|
||||
|
||||
// Désactiver le passage (soft delete)
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE ope_pass
|
||||
UPDATE ope_pass
|
||||
SET chk_active = 0, updated_at = NOW(), fk_user_modif = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
|
||||
$stmt->execute([$userId, $passageId]);
|
||||
|
||||
LogService::log('Suppression d\'un passage', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
// Log de suppression du passage
|
||||
EventLogService::logPassageDeleted(
|
||||
$passageId,
|
||||
(int)$passage['fk_operation'],
|
||||
true // soft delete
|
||||
);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
|
||||
@@ -9,7 +9,7 @@ require_once __DIR__ . '/../Services/LogService.php';
|
||||
|
||||
use Request;
|
||||
use Response;
|
||||
use LogService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\PasswordSecurityService;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,14 +3,14 @@ namespace App\Controllers;
|
||||
|
||||
use Database;
|
||||
use Response;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use AddressService;
|
||||
use DepartmentBoundaryService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\AddressService;
|
||||
use App\Services\DepartmentBoundaryService;
|
||||
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/AddressService.php';
|
||||
require_once __DIR__ . '/../Services/DepartmentBoundaryService.php';
|
||||
|
||||
class SectorController
|
||||
{
|
||||
@@ -193,14 +193,31 @@ class SectorController
|
||||
|
||||
// Affectation des users si fournis
|
||||
if (!empty($users)) {
|
||||
$queryMember = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, fk_user_creat, chk_active)
|
||||
VALUES (:operation_id, :user_id, :sector_id, NOW(), :user_creat, 1)";
|
||||
$queryMember = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, updated_at, fk_user_creat, chk_active)
|
||||
VALUES (:operation_id, :user_id, :sector_id, NOW(), NOW(), :user_creat, 1)";
|
||||
$stmtMember = $this->db->prepare($queryMember);
|
||||
|
||||
|
||||
foreach ($users as $memberId) {
|
||||
// $memberId est DÉJÀ ope_users.id (envoyé par Flutter)
|
||||
// Vérifier que cet ope_users.id existe et appartient bien à l'opération
|
||||
$stmtOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE id = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtOpeUser->execute([$memberId, $operationId]);
|
||||
$opeUserId = $stmtOpeUser->fetchColumn();
|
||||
|
||||
if (!$opeUserId) {
|
||||
$this->logService->warning('ope_users.id non trouvé pour cette opération', [
|
||||
'ope_users_id' => $memberId,
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmtMember->execute([
|
||||
'operation_id' => $operationId,
|
||||
'user_id' => $memberId,
|
||||
'user_id' => $opeUserId,
|
||||
'sector_id' => $sectorId,
|
||||
'user_creat' => $userId
|
||||
]);
|
||||
@@ -268,16 +285,24 @@ class SectorController
|
||||
$passagesCreated = 0; // Initialiser le compteur de passages
|
||||
try {
|
||||
$addresses = $this->addressService->getAddressesInPolygon($coordinates, $entityId);
|
||||
|
||||
|
||||
// Enrichir les adresses avec les données bâtiments
|
||||
$addresses = $this->addressService->enrichAddressesWithBuildings($addresses, $entityId);
|
||||
|
||||
if (!empty($addresses)) {
|
||||
$queryAddress = "INSERT INTO sectors_adresses (fk_sector, fk_adresse, numero, rue, rue_bis, cp, ville, gps_lat, gps_lng)
|
||||
VALUES (:sector_id, :address_id, :numero, :rue, :rue_bis, :cp, :ville, :gps_lat, :gps_lng)";
|
||||
$queryAddress = "INSERT INTO sectors_adresses (
|
||||
fk_sector, fk_adresse, numero, rue, rue_bis, cp, ville, gps_lat, gps_lng,
|
||||
fk_batiment, fk_habitat, nb_niveau, nb_log, residence, alt_sol
|
||||
) VALUES (
|
||||
:sector_id, :address_id, :numero, :rue, :rue_bis, :cp, :ville, :gps_lat, :gps_lng,
|
||||
:fk_batiment, :fk_habitat, :nb_niveau, :nb_log, :residence, :alt_sol
|
||||
)";
|
||||
$stmtAddress = $this->db->prepare($queryAddress);
|
||||
|
||||
|
||||
foreach ($addresses as $address) {
|
||||
// Extraire le rue_bis si présent (généralement vide)
|
||||
$rueBis = '';
|
||||
|
||||
|
||||
$stmtAddress->execute([
|
||||
'sector_id' => $sectorId,
|
||||
'address_id' => $address['id'],
|
||||
@@ -287,60 +312,111 @@ class SectorController
|
||||
'cp' => $address['code_postal'],
|
||||
'ville' => $address['commune'],
|
||||
'gps_lat' => $address['latitude'],
|
||||
'gps_lng' => $address['longitude']
|
||||
'gps_lng' => $address['longitude'],
|
||||
'fk_batiment' => $address['fk_batiment'] ?? null,
|
||||
'fk_habitat' => $address['fk_habitat'] ?? 1,
|
||||
'nb_niveau' => $address['nb_niveau'] ?? null,
|
||||
'nb_log' => $address['nb_log'] ?? null,
|
||||
'residence' => $address['residence'] ?? '',
|
||||
'alt_sol' => $address['alt_sol'] ?? null
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// Créer les passages pour chaque adresse
|
||||
if (!empty($users)) {
|
||||
$firstUserId = $users[0]; // Premier user pour l'affectation des passages
|
||||
$passageQuery = "INSERT INTO ope_pass (
|
||||
fk_operation, fk_sector, fk_user, fk_adresse,
|
||||
numero, rue, rue_bis, ville,
|
||||
gps_lat, gps_lng, fk_type, encrypted_name,
|
||||
created_at, fk_user_creat, chk_active
|
||||
) VALUES (
|
||||
:operation_id, :sector_id, :user_id, :fk_adresse,
|
||||
:numero, :rue, :rue_bis, :ville,
|
||||
:gps_lat, :gps_lng, 2, '',
|
||||
NOW(), :user_creat, 1
|
||||
)";
|
||||
$passageStmt = $this->db->prepare($passageQuery);
|
||||
|
||||
$passagesCreated = 0;
|
||||
foreach ($addresses as $address) {
|
||||
// Vérifier si cette adresse n'est pas déjà utilisée par un passage orphelin
|
||||
if (in_array($address['id'], $addressesToExclude)) {
|
||||
continue; // Passer à l'adresse suivante
|
||||
}
|
||||
|
||||
try {
|
||||
// Extraire le rue_bis si présent (généralement vide)
|
||||
$rueBis = '';
|
||||
|
||||
$passageStmt->execute([
|
||||
'operation_id' => $operationId,
|
||||
'sector_id' => $sectorId,
|
||||
'user_id' => $firstUserId,
|
||||
'fk_adresse' => $address['id'],
|
||||
'numero' => $address['numero'],
|
||||
'rue' => $address['voie'],
|
||||
'rue_bis' => $rueBis,
|
||||
'ville' => $address['commune'],
|
||||
'gps_lat' => $address['latitude'],
|
||||
'gps_lng' => $address['longitude'],
|
||||
'user_creat' => $userId
|
||||
]);
|
||||
$passagesCreated++;
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->warning('Erreur lors de la création d\'un passage', [
|
||||
'address_id' => $address['id'],
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
// Récupérer ope_users.id pour le premier utilisateur
|
||||
// $users[0] est DÉJÀ ope_users.id (envoyé par Flutter)
|
||||
$stmtFirstOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE id = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtFirstOpeUser->execute([$users[0], $operationId]);
|
||||
$firstOpeUserId = $stmtFirstOpeUser->fetchColumn();
|
||||
|
||||
if (!$firstOpeUserId) {
|
||||
$this->logService->warning('Premier ope_users.id non trouvé pour cette opération', [
|
||||
'ope_users_id' => $users[0],
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
// Pas de création de passages sans utilisateur valide dans ope_users
|
||||
} else {
|
||||
$passageQuery = "INSERT INTO ope_pass (
|
||||
fk_operation, fk_sector, fk_user, fk_adresse,
|
||||
numero, rue, rue_bis, ville, residence, appt, fk_habitat,
|
||||
gps_lat, gps_lng, fk_type, nb_passages, encrypted_name,
|
||||
created_at, fk_user_creat, chk_active
|
||||
) VALUES (
|
||||
:operation_id, :sector_id, :user_id, :fk_adresse,
|
||||
:numero, :rue, :rue_bis, :ville, :residence, :appt, :fk_habitat,
|
||||
:gps_lat, :gps_lng, 2, 0, '',
|
||||
NOW(), :user_creat, 1
|
||||
)";
|
||||
$passageStmt = $this->db->prepare($passageQuery);
|
||||
|
||||
$passagesCreated = 0;
|
||||
foreach ($addresses as $address) {
|
||||
// Vérifier si cette adresse n'est pas déjà utilisée par un passage orphelin
|
||||
if (in_array($address['id'], $addressesToExclude)) {
|
||||
continue; // Passer à l'adresse suivante
|
||||
}
|
||||
|
||||
try {
|
||||
// Extraire le rue_bis si présent (généralement vide)
|
||||
$rueBis = '';
|
||||
|
||||
// Déterminer le nombre de passages à créer
|
||||
$fkHabitat = $address['fk_habitat'] ?? 1;
|
||||
$nbLog = ($fkHabitat == 2 && isset($address['nb_log'])) ? (int)$address['nb_log'] : 1;
|
||||
$residence = $address['residence'] ?? '';
|
||||
|
||||
// IMPORTANT : Uniformisation GPS pour les immeubles
|
||||
// Tous les passages d'une même adresse partagent les mêmes coordonnées GPS
|
||||
// Issues de la table adresses enrichie (gps_lat, gps_lng)
|
||||
$gpsLat = $address['latitude'];
|
||||
$gpsLng = $address['longitude'];
|
||||
|
||||
// Créer 1 passage pour maison individuelle, nb_log passages pour immeuble
|
||||
for ($i = 1; $i <= $nbLog; $i++) {
|
||||
$appt = ($fkHabitat == 2) ? (string)$i : ''; // Numéro d'appartement pour immeubles
|
||||
|
||||
$passageStmt->execute([
|
||||
'operation_id' => $operationId,
|
||||
'sector_id' => $sectorId,
|
||||
'user_id' => $firstOpeUserId,
|
||||
'fk_adresse' => $address['id'],
|
||||
'numero' => $address['numero'],
|
||||
'rue' => $address['voie'],
|
||||
'rue_bis' => $rueBis,
|
||||
'ville' => $address['commune'],
|
||||
'residence' => $residence,
|
||||
'appt' => $appt,
|
||||
'fk_habitat' => $fkHabitat,
|
||||
'gps_lat' => $gpsLat,
|
||||
'gps_lng' => $gpsLng,
|
||||
'user_creat' => $userId
|
||||
]);
|
||||
$passagesCreated++;
|
||||
}
|
||||
|
||||
// Log pour vérifier l'uniformisation GPS (surtout pour immeubles)
|
||||
if ($fkHabitat == 2 && $nbLog > 1) {
|
||||
$this->logService->info('[SectorController] Création passages immeuble avec GPS uniformisés', [
|
||||
'address_id' => $address['id'],
|
||||
'nb_passages' => $nbLog,
|
||||
'gps_lat' => $gpsLat,
|
||||
'gps_lng' => $gpsLng,
|
||||
'residence' => $residence
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->warning('Erreur lors de la création d\'un passage', [
|
||||
'address_id' => $address['id'],
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
@@ -351,9 +427,16 @@ class SectorController
|
||||
'entity_id' => $entityId
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
|
||||
// Log de création du secteur
|
||||
EventLogService::logSectorCreated(
|
||||
(int)$sectorId,
|
||||
(int)$operationId,
|
||||
$sectorData['libelle']
|
||||
);
|
||||
|
||||
// Préparer les données de réponse
|
||||
$responseData = [
|
||||
'sector_id' => $sectorId
|
||||
@@ -413,9 +496,10 @@ class SectorController
|
||||
}
|
||||
|
||||
// Récupérer les users affectés
|
||||
$usersQuery = "SELECT u.id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector
|
||||
$usersQuery = "SELECT u.id, ou.id as ope_user_id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector
|
||||
FROM ope_users_sectors ous
|
||||
JOIN users u ON ous.fk_user = u.id
|
||||
JOIN ope_users ou ON ous.fk_user = ou.id
|
||||
JOIN users u ON ou.fk_user = u.id
|
||||
WHERE ous.fk_sector = :sector_id";
|
||||
$usersStmt = $this->db->prepare($usersQuery);
|
||||
$usersStmt->execute(['sector_id' => $sectorId]);
|
||||
@@ -425,7 +509,8 @@ class SectorController
|
||||
$responseData['users_sectors'] = [];
|
||||
foreach ($usersSectors as $userSector) {
|
||||
$userData = [
|
||||
'id' => $userSector['id'],
|
||||
'user_id' => $userSector['id'],
|
||||
'ope_user_id' => $userSector['ope_user_id'],
|
||||
'first_name' => $userSector['first_name'] ?? '',
|
||||
'sect_name' => $userSector['sect_name'] ?? '',
|
||||
'fk_sector' => $userSector['fk_sector'],
|
||||
@@ -498,24 +583,27 @@ class SectorController
|
||||
try {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$entityId = $_SESSION['entity_id'] ?? null;
|
||||
|
||||
|
||||
if (!$entityId) {
|
||||
Response::json(['status' => 'error', 'message' => 'Entité non définie'], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Vérifier que le secteur appartient à l'entité
|
||||
$checkQuery = "SELECT s.id
|
||||
$checkQuery = "SELECT s.id, s.fk_operation, s.libelle
|
||||
FROM ope_sectors s
|
||||
JOIN operations o ON s.fk_operation = o.id
|
||||
WHERE s.id = :id AND o.fk_entite = :entity_id";
|
||||
$checkStmt = $this->db->prepare($checkQuery);
|
||||
$checkStmt->execute(['id' => $id, 'entity_id' => $entityId]);
|
||||
|
||||
if (!$checkStmt->fetch()) {
|
||||
|
||||
$existingSector = $checkStmt->fetch();
|
||||
if (!$existingSector) {
|
||||
Response::json(['status' => 'error', 'message' => 'Secteur non trouvé'], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$operationId = $existingSector['fk_operation'];
|
||||
|
||||
$this->db->beginTransaction();
|
||||
|
||||
@@ -580,8 +668,8 @@ class SectorController
|
||||
|
||||
// Ajouter les nouvelles affectations
|
||||
if (!empty($data['users'])) {
|
||||
$insertQuery = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, fk_user_creat, chk_active)
|
||||
VALUES (:operation_id, :user_id, :sector_id, NOW(), :user_creat, 1)";
|
||||
$insertQuery = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, updated_at, fk_user_creat, chk_active)
|
||||
VALUES (:operation_id, :user_id, :sector_id, NOW(), NOW(), :user_creat, 1)";
|
||||
$this->logService->info('[UPDATE USERS] SQL - Requête INSERT préparée', [
|
||||
'query' => $insertQuery
|
||||
]);
|
||||
@@ -591,9 +679,27 @@ class SectorController
|
||||
$failedUsers = [];
|
||||
foreach ($data['users'] as $memberId) {
|
||||
try {
|
||||
// $memberId est DÉJÀ ope_users.id (envoyé par Flutter)
|
||||
// Vérifier que cet ope_users.id existe et appartient bien à l'opération
|
||||
$stmtOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE id = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtOpeUser->execute([$memberId, $operationId]);
|
||||
$opeUserId = $stmtOpeUser->fetchColumn();
|
||||
|
||||
if (!$opeUserId) {
|
||||
$this->logService->warning('[UPDATE USERS] ope_users.id non trouvé pour cette opération', [
|
||||
'ope_users_id' => $memberId,
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
$failedUsers[] = $memberId;
|
||||
continue;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'operation_id' => $operationId,
|
||||
'user_id' => $memberId,
|
||||
'user_id' => $opeUserId,
|
||||
'sector_id' => $id,
|
||||
'user_creat' => $_SESSION['user_id'] ?? null
|
||||
];
|
||||
@@ -626,14 +732,25 @@ class SectorController
|
||||
}
|
||||
}
|
||||
|
||||
// Gérer les passages si le secteur a changé
|
||||
// Gérer les passages si le secteur a changé ET si chk_adresses_change = 1
|
||||
$passageCounters = [
|
||||
'passages_orphaned' => 0,
|
||||
'passages_updated' => 0,
|
||||
'passages_created' => 0,
|
||||
'passages_kept' => 0
|
||||
];
|
||||
if (isset($data['sector'])) {
|
||||
|
||||
// chk_adresses_change : 0=ne pas toucher aux adresses/passages, 1=recalculer (défaut)
|
||||
$chkAdressesChange = $data['chk_adresses_change'] ?? 1;
|
||||
|
||||
if (isset($data['sector']) && $chkAdressesChange == 0) {
|
||||
$this->logService->info('[UPDATE] Modification secteur sans recalcul adresses/passages', [
|
||||
'sector_id' => $id,
|
||||
'chk_adresses_change' => $chkAdressesChange
|
||||
]);
|
||||
}
|
||||
|
||||
if (isset($data['sector']) && $chkAdressesChange == 1) {
|
||||
// Mettre à jour les adresses du secteur AVANT de traiter les passages
|
||||
try {
|
||||
// Supprimer les anciennes adresses
|
||||
@@ -660,17 +777,25 @@ class SectorController
|
||||
]);
|
||||
|
||||
$addresses = $this->addressService->getAddressesInPolygon($coordinates, $entityId);
|
||||
|
||||
|
||||
// Enrichir les adresses avec les données bâtiments
|
||||
$addresses = $this->addressService->enrichAddressesWithBuildings($addresses, $entityId);
|
||||
|
||||
$this->logService->info('[UPDATE] Adresses récupérées', [
|
||||
'sector_id' => $id,
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
|
||||
|
||||
if (!empty($addresses)) {
|
||||
$queryAddress = "INSERT INTO sectors_adresses (fk_sector, fk_adresse, numero, rue, cp, ville, gps_lat, gps_lng)
|
||||
VALUES (:sector_id, :address_id, :numero, :rue, :cp, :ville, :gps_lat, :gps_lng)";
|
||||
$queryAddress = "INSERT INTO sectors_adresses (
|
||||
fk_sector, fk_adresse, numero, rue, cp, ville, gps_lat, gps_lng,
|
||||
fk_batiment, fk_habitat, nb_niveau, nb_log, residence, alt_sol
|
||||
) VALUES (
|
||||
:sector_id, :address_id, :numero, :rue, :cp, :ville, :gps_lat, :gps_lng,
|
||||
:fk_batiment, :fk_habitat, :nb_niveau, :nb_log, :residence, :alt_sol
|
||||
)";
|
||||
$stmtAddress = $this->db->prepare($queryAddress);
|
||||
|
||||
|
||||
foreach ($addresses as $address) {
|
||||
$stmtAddress->execute([
|
||||
'sector_id' => $id,
|
||||
@@ -680,7 +805,13 @@ class SectorController
|
||||
'cp' => $address['code_postal'],
|
||||
'ville' => $address['commune'],
|
||||
'gps_lat' => $address['latitude'],
|
||||
'gps_lng' => $address['longitude']
|
||||
'gps_lng' => $address['longitude'],
|
||||
'fk_batiment' => $address['fk_batiment'] ?? null,
|
||||
'fk_habitat' => $address['fk_habitat'] ?? 1,
|
||||
'nb_niveau' => $address['nb_niveau'] ?? null,
|
||||
'nb_log' => $address['nb_log'] ?? null,
|
||||
'residence' => $address['residence'] ?? '',
|
||||
'alt_sol' => $address['alt_sol'] ?? null
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -715,10 +846,29 @@ class SectorController
|
||||
|
||||
// Commit des modifications (users et/ou secteur)
|
||||
$this->db->commit();
|
||||
|
||||
|
||||
// Log de mise à jour du secteur
|
||||
$changes = [];
|
||||
if (isset($data['libelle'])) {
|
||||
$changes['libelle'] = ['new' => $data['libelle']];
|
||||
}
|
||||
if (isset($data['color'])) {
|
||||
$changes['color'] = ['new' => $data['color']];
|
||||
}
|
||||
if (isset($data['sector'])) {
|
||||
$changes['sector'] = true; // Polygon modifié
|
||||
}
|
||||
if (isset($data['users'])) {
|
||||
$changes['users'] = true; // Affectation modifiée
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logSectorUpdated((int)$id, (int)$operationId, $changes);
|
||||
}
|
||||
|
||||
// Récupérer le secteur mis à jour
|
||||
$query = "
|
||||
SELECT
|
||||
SELECT
|
||||
s.id,
|
||||
s.libelle,
|
||||
s.color,
|
||||
@@ -726,57 +876,61 @@ class SectorController
|
||||
FROM ope_sectors s
|
||||
WHERE s.id = :id
|
||||
";
|
||||
|
||||
|
||||
$stmt = $this->db->prepare($query);
|
||||
$stmt->execute(['id' => $id]);
|
||||
$sector = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
// Récupérer tous les passages du secteur
|
||||
$passagesQuery = "SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at,
|
||||
numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email,
|
||||
encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
FROM ope_pass
|
||||
WHERE fk_sector = :sector_id
|
||||
ORDER BY id";
|
||||
$passagesStmt = $this->db->prepare($passagesQuery);
|
||||
$passagesStmt->execute(['sector_id' => $id]);
|
||||
$passages = $passagesStmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
// Déchiffrer les données sensibles des passages
|
||||
|
||||
// Récupérer les passages UNIQUEMENT si chk_adresses_change = 1
|
||||
$passagesDecrypted = [];
|
||||
foreach ($passages as $passage) {
|
||||
// Déchiffrement du nom
|
||||
$passage['name'] = '';
|
||||
if (!empty($passage['encrypted_name'])) {
|
||||
$passage['name'] = ApiService::decryptData($passage['encrypted_name']);
|
||||
}
|
||||
unset($passage['encrypted_name']);
|
||||
|
||||
// Déchiffrement de l'email
|
||||
$passage['email'] = '';
|
||||
if (!empty($passage['encrypted_email'])) {
|
||||
$decryptedEmail = ApiService::decryptSearchableData($passage['encrypted_email']);
|
||||
if ($decryptedEmail) {
|
||||
$passage['email'] = $decryptedEmail;
|
||||
if ($chkAdressesChange == 1) {
|
||||
// Récupérer tous les passages du secteur
|
||||
$passagesQuery = "SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at,
|
||||
numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email,
|
||||
encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages
|
||||
FROM ope_pass
|
||||
WHERE fk_sector = :sector_id
|
||||
ORDER BY id";
|
||||
$passagesStmt = $this->db->prepare($passagesQuery);
|
||||
$passagesStmt->execute(['sector_id' => $id]);
|
||||
$passages = $passagesStmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
// Déchiffrer les données sensibles des passages
|
||||
foreach ($passages as $passage) {
|
||||
// Déchiffrement du nom
|
||||
$passage['name'] = '';
|
||||
if (!empty($passage['encrypted_name'])) {
|
||||
$passage['name'] = ApiService::decryptData($passage['encrypted_name']);
|
||||
}
|
||||
unset($passage['encrypted_name']);
|
||||
|
||||
// Déchiffrement de l'email
|
||||
$passage['email'] = '';
|
||||
if (!empty($passage['encrypted_email'])) {
|
||||
$decryptedEmail = ApiService::decryptSearchableData($passage['encrypted_email']);
|
||||
if ($decryptedEmail) {
|
||||
$passage['email'] = $decryptedEmail;
|
||||
}
|
||||
}
|
||||
unset($passage['encrypted_email']);
|
||||
|
||||
// Déchiffrement du téléphone
|
||||
$passage['phone'] = '';
|
||||
if (!empty($passage['encrypted_phone'])) {
|
||||
$passage['phone'] = ApiService::decryptData($passage['encrypted_phone']);
|
||||
}
|
||||
unset($passage['encrypted_phone']);
|
||||
|
||||
$passagesDecrypted[] = $passage;
|
||||
}
|
||||
unset($passage['encrypted_email']);
|
||||
|
||||
// Déchiffrement du téléphone
|
||||
$passage['phone'] = '';
|
||||
if (!empty($passage['encrypted_phone'])) {
|
||||
$passage['phone'] = ApiService::decryptData($passage['encrypted_phone']);
|
||||
}
|
||||
unset($passage['encrypted_phone']);
|
||||
|
||||
$passagesDecrypted[] = $passage;
|
||||
}
|
||||
|
||||
// Récupérer les users affectés (avec READ UNCOMMITTED pour forcer la lecture des données fraîches)
|
||||
$usersQuery = "SELECT u.id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector
|
||||
$usersQuery = "SELECT u.id, ou.id as ope_user_id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector
|
||||
FROM ope_users_sectors ous
|
||||
JOIN users u ON ous.fk_user = u.id
|
||||
JOIN ope_users ou ON ous.fk_user = ou.id
|
||||
JOIN users u ON ou.fk_user = u.id
|
||||
WHERE ous.fk_sector = :sector_id
|
||||
ORDER BY u.id";
|
||||
|
||||
@@ -801,7 +955,8 @@ class SectorController
|
||||
$usersDecrypted = [];
|
||||
foreach ($usersSectors as $userSector) {
|
||||
$userData = [
|
||||
'id' => $userSector['id'],
|
||||
'user_id' => $userSector['id'],
|
||||
'ope_user_id' => $userSector['ope_user_id'],
|
||||
'first_name' => $userSector['first_name'] ?? '',
|
||||
'sect_name' => $userSector['sect_name'] ?? '',
|
||||
'fk_sector' => $userSector['fk_sector'],
|
||||
@@ -934,18 +1089,20 @@ class SectorController
|
||||
}
|
||||
|
||||
// Vérifier que le secteur existe et récupérer ses informations
|
||||
$checkQuery = "SELECT s.id, s.libelle, o.fk_entite
|
||||
$checkQuery = "SELECT s.id, s.libelle, s.fk_operation, o.fk_entite
|
||||
FROM ope_sectors s
|
||||
JOIN operations o ON s.fk_operation = o.id
|
||||
WHERE s.id = :id";
|
||||
$checkStmt = $this->db->prepare($checkQuery);
|
||||
$checkStmt->execute(['id' => $id]);
|
||||
$sector = $checkStmt->fetch();
|
||||
|
||||
|
||||
if (!$sector || $sector['fk_entite'] != $entityId) {
|
||||
Response::json(['status' => 'error', 'message' => 'Secteur non trouvé ou non autorisé'], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$operationId = $sector['fk_operation'];
|
||||
|
||||
$this->db->beginTransaction();
|
||||
|
||||
@@ -1001,9 +1158,16 @@ class SectorController
|
||||
$deleteQuery = "DELETE FROM ope_sectors WHERE id = :id";
|
||||
$deleteStmt = $this->db->prepare($deleteQuery);
|
||||
$deleteStmt->execute(['id' => $id]);
|
||||
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
|
||||
// Log de suppression du secteur (suppression physique = false)
|
||||
EventLogService::logSectorDeleted(
|
||||
(int)$id,
|
||||
(int)$operationId,
|
||||
false // suppression physique (DELETE)
|
||||
);
|
||||
|
||||
// Déchiffrer les données sensibles des passages
|
||||
$passagesDecrypted = [];
|
||||
foreach ($passagesToUpdate as $passage) {
|
||||
@@ -1249,8 +1413,11 @@ class SectorController
|
||||
}
|
||||
|
||||
// 2. CRÉATION/MISE À JOUR DES PASSAGES POUR LES NOUVELLES ADRESSES (OPTIMISÉE)
|
||||
// Récupérer toutes les adresses du secteur depuis sectors_adresses
|
||||
$addressesQuery = "SELECT * FROM sectors_adresses WHERE fk_sector = :sector_id";
|
||||
// Récupérer toutes les adresses du secteur depuis sectors_adresses (avec colonnes bâtiments)
|
||||
$addressesQuery = "SELECT
|
||||
fk_sector, fk_adresse, numero, rue, rue_bis, cp, ville, gps_lat, gps_lng,
|
||||
fk_batiment, fk_habitat, nb_niveau, nb_log, residence, alt_sol
|
||||
FROM sectors_adresses WHERE fk_sector = :sector_id";
|
||||
$addressesStmt = $this->db->prepare($addressesQuery);
|
||||
$addressesStmt->execute(['sector_id' => $sectorId]);
|
||||
$addresses = $addressesStmt->fetchAll();
|
||||
@@ -1268,93 +1435,121 @@ class SectorController
|
||||
$firstUserId = $firstUser ? $firstUser['fk_user'] : null;
|
||||
|
||||
if ($firstUserId && !empty($addresses)) {
|
||||
$this->logService->info('[updatePassagesForSector] Optimisation passages', [
|
||||
$this->logService->info('[updatePassagesForSector] Traitement des passages', [
|
||||
'user_id' => $firstUserId,
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
|
||||
// OPTIMISATION : Récupérer TOUS les passages existants en UNE requête
|
||||
$addressIds = array_filter(array_column($addresses, 'fk_adresse'));
|
||||
|
||||
// Construire la requête pour récupérer tous les passages existants
|
||||
// Récupérer TOUS les passages existants pour cette opération en UNE requête
|
||||
$existingQuery = "
|
||||
SELECT id, fk_adresse, numero, rue, rue_bis, ville
|
||||
SELECT id, fk_adresse, numero, rue, rue_bis, ville, residence, appt, fk_habitat,
|
||||
fk_type, encrypted_name, created_at
|
||||
FROM ope_pass
|
||||
WHERE fk_operation = :operation_id
|
||||
AND (";
|
||||
|
||||
$params = ['operation_id' => $operationId];
|
||||
$conditions = [];
|
||||
|
||||
// Condition pour les fk_adresse
|
||||
if (!empty($addressIds)) {
|
||||
$placeholders = [];
|
||||
foreach ($addressIds as $idx => $addrId) {
|
||||
$key = 'addr_' . $idx;
|
||||
$placeholders[] = ':' . $key;
|
||||
$params[$key] = $addrId;
|
||||
}
|
||||
$conditions[] = "fk_adresse IN (" . implode(',', $placeholders) . ")";
|
||||
}
|
||||
|
||||
// Condition pour les données d'adresse (numero, rue, ville)
|
||||
$addressConditions = [];
|
||||
foreach ($addresses as $idx => $addr) {
|
||||
$numKey = 'num_' . $idx;
|
||||
$rueKey = 'rue_' . $idx;
|
||||
$bisKey = 'bis_' . $idx;
|
||||
$villeKey = 'ville_' . $idx;
|
||||
|
||||
$addressConditions[] = "(numero = :$numKey AND rue = :$rueKey AND rue_bis = :$bisKey AND ville = :$villeKey)";
|
||||
$params[$numKey] = $addr['numero'];
|
||||
$params[$rueKey] = $addr['rue'];
|
||||
$params[$bisKey] = $addr['rue_bis'];
|
||||
$params[$villeKey] = $addr['ville'];
|
||||
}
|
||||
|
||||
if (!empty($addressConditions)) {
|
||||
$conditions[] = "(" . implode(' OR ', $addressConditions) . ")";
|
||||
}
|
||||
|
||||
$existingQuery .= implode(' OR ', $conditions) . ")";
|
||||
WHERE fk_operation = :operation_id";
|
||||
|
||||
$existingStmt = $this->db->prepare($existingQuery);
|
||||
$existingStmt->execute($params);
|
||||
$existingStmt->execute(['operation_id' => $operationId]);
|
||||
$existingPassages = $existingStmt->fetchAll();
|
||||
|
||||
// Indexer les passages existants pour recherche rapide
|
||||
// Indexer les passages existants par clé : numero|rue|rue_bis|ville
|
||||
$passagesByAddress = [];
|
||||
$passagesByData = [];
|
||||
foreach ($existingPassages as $p) {
|
||||
if (!empty($p['fk_adresse'])) {
|
||||
$passagesByAddress[$p['fk_adresse']] = $p;
|
||||
$addressKey = $p['numero'] . '|' . $p['rue'] . '|' . ($p['rue_bis'] ?? '') . '|' . $p['ville'];
|
||||
if (!isset($passagesByAddress[$addressKey])) {
|
||||
$passagesByAddress[$addressKey] = [];
|
||||
}
|
||||
$dataKey = $p['numero'] . '|' . $p['rue'] . '|' . $p['rue_bis'] . '|' . $p['ville'];
|
||||
$passagesByData[$dataKey] = $p;
|
||||
$passagesByAddress[$addressKey][] = $p;
|
||||
}
|
||||
|
||||
// Préparer les listes pour batch insert/update
|
||||
// Traiter chaque adresse du secteur
|
||||
$toInsert = [];
|
||||
$toUpdate = [];
|
||||
$toDelete = [];
|
||||
|
||||
foreach ($addresses as $address) {
|
||||
// Vérification en mémoire PHP (0 requête)
|
||||
if (!empty($address['fk_adresse']) && isset($passagesByAddress[$address['fk_adresse']])) {
|
||||
continue; // Déjà existant avec bon fk_adresse
|
||||
}
|
||||
$addressKey = $address['numero'] . '|' . $address['rue'] . '|' . ($address['rue_bis'] ?? '') . '|' . $address['ville'];
|
||||
$existingAtAddress = $passagesByAddress[$addressKey] ?? [];
|
||||
$nbExisting = count($existingAtAddress);
|
||||
|
||||
$dataKey = $address['numero'] . '|' . $address['rue'] . '|' . $address['rue_bis'] . '|' . $address['ville'];
|
||||
if (isset($passagesByData[$dataKey])) {
|
||||
// Passage existant mais sans fk_adresse ou avec fk_adresse différent
|
||||
if (!empty($address['fk_adresse']) && $passagesByData[$dataKey]['fk_adresse'] != $address['fk_adresse']) {
|
||||
$toUpdate[] = [
|
||||
'id' => $passagesByData[$dataKey]['id'],
|
||||
'fk_adresse' => $address['fk_adresse']
|
||||
$fkHabitat = $address['fk_habitat'] ?? 1;
|
||||
$nbLog = ($fkHabitat == 2 && isset($address['nb_log'])) ? (int)$address['nb_log'] : 1;
|
||||
$residence = $address['residence'] ?? '';
|
||||
|
||||
// IMPORTANT : Uniformisation GPS pour les immeubles
|
||||
// Tous les passages d'une même adresse doivent partager les mêmes coordonnées GPS
|
||||
// Issues de sectors_adresses (gps_lat, gps_lng)
|
||||
$gpsLat = $address['gps_lat'];
|
||||
$gpsLng = $address['gps_lng'];
|
||||
|
||||
// CAS 1 : Maison individuelle (fk_habitat=1)
|
||||
if ($fkHabitat == 1) {
|
||||
if ($nbExisting == 0) {
|
||||
// INSERT 1 passage
|
||||
$toInsert[] = [
|
||||
'address' => $address,
|
||||
'residence' => '',
|
||||
'appt' => '',
|
||||
'fk_habitat' => 1
|
||||
];
|
||||
} else {
|
||||
// UPDATE le premier passage avec fk_habitat=1
|
||||
$toUpdate[] = [
|
||||
'id' => $existingAtAddress[0]['id'],
|
||||
'fk_habitat' => 1,
|
||||
'residence' => '',
|
||||
'gps_lat' => $gpsLat,
|
||||
'gps_lng' => $gpsLng
|
||||
];
|
||||
// Les autres passages (si >1) ne sont PAS touchés
|
||||
}
|
||||
}
|
||||
// CAS 2 : Immeuble (fk_habitat=2)
|
||||
else if ($fkHabitat == 2) {
|
||||
// UPDATE TOUS les passages existants avec fk_habitat=2, residence et GPS
|
||||
foreach ($existingAtAddress as $existing) {
|
||||
$updates = [
|
||||
'id' => $existing['id'],
|
||||
'fk_habitat' => 2,
|
||||
'gps_lat' => $gpsLat,
|
||||
'gps_lng' => $gpsLng
|
||||
];
|
||||
// Update residence seulement si non vide
|
||||
if (!empty($residence)) {
|
||||
$updates['residence'] = $residence;
|
||||
}
|
||||
$toUpdate[] = $updates;
|
||||
}
|
||||
|
||||
// Si moins de nb_log passages : INSERT les manquants
|
||||
if ($nbExisting < $nbLog) {
|
||||
$nbToInsert = $nbLog - $nbExisting;
|
||||
for ($i = 0; $i < $nbToInsert; $i++) {
|
||||
$toInsert[] = [
|
||||
'address' => $address,
|
||||
'residence' => $residence,
|
||||
'appt' => '', // Pas de numéro d'appt prédéfini
|
||||
'fk_habitat' => 2
|
||||
];
|
||||
}
|
||||
}
|
||||
// Si plus de nb_log passages : DELETE les non visités en trop
|
||||
else if ($nbExisting > $nbLog) {
|
||||
$nbToDelete = $nbExisting - $nbLog;
|
||||
// Trier les passages par created_at ASC (les plus anciens d'abord)
|
||||
usort($existingAtAddress, function($a, $b) {
|
||||
return strtotime($a['created_at']) - strtotime($b['created_at']);
|
||||
});
|
||||
|
||||
$deleted = 0;
|
||||
foreach ($existingAtAddress as $existing) {
|
||||
if ($deleted >= $nbToDelete) break;
|
||||
// Supprimer seulement si fk_type=2 ET encrypted_name vide
|
||||
if ($existing['fk_type'] == 2 && ($existing['encrypted_name'] === '' || $existing['encrypted_name'] === null)) {
|
||||
$toDelete[] = $existing['id'];
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Nouveau passage à créer
|
||||
$toInsert[] = $address;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1364,19 +1559,24 @@ class SectorController
|
||||
$insertParams = [];
|
||||
$paramIndex = 0;
|
||||
|
||||
foreach ($toInsert as $addr) {
|
||||
foreach ($toInsert as $item) {
|
||||
$addr = $item['address'];
|
||||
$values[] = "(:op$paramIndex, :sect$paramIndex, :usr$paramIndex, :addr$paramIndex,
|
||||
:num$paramIndex, :rue$paramIndex, :bis$paramIndex, :ville$paramIndex,
|
||||
:lat$paramIndex, :lng$paramIndex, 2, '', NOW(), :creat$paramIndex, 1)";
|
||||
:res$paramIndex, :appt$paramIndex, :habitat$paramIndex,
|
||||
:lat$paramIndex, :lng$paramIndex, 2, 0, '', NOW(), :creat$paramIndex, 1)";
|
||||
|
||||
$insertParams["op$paramIndex"] = $operationId;
|
||||
$insertParams["sect$paramIndex"] = $sectorId;
|
||||
$insertParams["usr$paramIndex"] = $firstUserId;
|
||||
$insertParams["addr$paramIndex"] = $addr['fk_adresse'];
|
||||
$insertParams["addr$paramIndex"] = $addr['fk_adresse'] ?? null;
|
||||
$insertParams["num$paramIndex"] = $addr['numero'];
|
||||
$insertParams["rue$paramIndex"] = $addr['rue'];
|
||||
$insertParams["bis$paramIndex"] = $addr['rue_bis'];
|
||||
$insertParams["bis$paramIndex"] = $addr['rue_bis'] ?? '';
|
||||
$insertParams["ville$paramIndex"] = $addr['ville'];
|
||||
$insertParams["res$paramIndex"] = $item['residence'];
|
||||
$insertParams["appt$paramIndex"] = $item['appt'];
|
||||
$insertParams["habitat$paramIndex"] = $item['fk_habitat'];
|
||||
$insertParams["lat$paramIndex"] = $addr['gps_lat'];
|
||||
$insertParams["lng$paramIndex"] = $addr['gps_lng'];
|
||||
$insertParams["creat$paramIndex"] = $_SESSION['user_id'] ?? null;
|
||||
@@ -1386,7 +1586,7 @@ class SectorController
|
||||
|
||||
$insertQuery = "INSERT INTO ope_pass
|
||||
(fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis,
|
||||
ville, gps_lat, gps_lng, fk_type, encrypted_name, created_at, fk_user_creat, chk_active)
|
||||
ville, residence, appt, fk_habitat, gps_lat, gps_lng, fk_type, nb_passages, encrypted_name, created_at, fk_user_creat, chk_active)
|
||||
VALUES " . implode(',', $values);
|
||||
|
||||
try {
|
||||
@@ -1401,28 +1601,67 @@ class SectorController
|
||||
}
|
||||
}
|
||||
|
||||
// UPDATE MULTIPLE avec CASE WHEN
|
||||
// UPDATE MULTIPLE avec CASE WHEN (inclut GPS pour uniformisation)
|
||||
if (!empty($toUpdate)) {
|
||||
$updateIds = array_column($toUpdate, 'id');
|
||||
$placeholders = str_repeat('?,', count($updateIds) - 1) . '?';
|
||||
|
||||
$caseWhen = [];
|
||||
$caseWhenHabitat = [];
|
||||
$caseWhenResidence = [];
|
||||
$caseWhenGpsLat = [];
|
||||
$caseWhenGpsLng = [];
|
||||
$updateParams = [];
|
||||
|
||||
foreach ($toUpdate as $upd) {
|
||||
$caseWhen[] = "WHEN id = ? THEN ?";
|
||||
// fk_habitat est toujours présent
|
||||
$caseWhenHabitat[] = "WHEN id = ? THEN ?";
|
||||
$updateParams[] = $upd['id'];
|
||||
$updateParams[] = $upd['fk_adresse'];
|
||||
$updateParams[] = $upd['fk_habitat'];
|
||||
|
||||
// GPS : toujours présent maintenant (uniformisation)
|
||||
if (isset($upd['gps_lat']) && isset($upd['gps_lng'])) {
|
||||
$caseWhenGpsLat[] = "WHEN id = ? THEN ?";
|
||||
$updateParams[] = $upd['id'];
|
||||
$updateParams[] = $upd['gps_lat'];
|
||||
|
||||
$caseWhenGpsLng[] = "WHEN id = ? THEN ?";
|
||||
$updateParams[] = $upd['id'];
|
||||
$updateParams[] = $upd['gps_lng'];
|
||||
}
|
||||
|
||||
// residence est optionnel
|
||||
if (isset($upd['residence'])) {
|
||||
$caseWhenResidence[] = "WHEN id = ? THEN ?";
|
||||
$updateParams[] = $upd['id'];
|
||||
$updateParams[] = $upd['residence'];
|
||||
}
|
||||
}
|
||||
|
||||
$setClause = ["fk_habitat = CASE " . implode(' ', $caseWhenHabitat) . " ELSE fk_habitat END"];
|
||||
if (!empty($caseWhenGpsLat)) {
|
||||
$setClause[] = "gps_lat = CASE " . implode(' ', $caseWhenGpsLat) . " ELSE gps_lat END";
|
||||
}
|
||||
if (!empty($caseWhenGpsLng)) {
|
||||
$setClause[] = "gps_lng = CASE " . implode(' ', $caseWhenGpsLng) . " ELSE gps_lng END";
|
||||
}
|
||||
if (!empty($caseWhenResidence)) {
|
||||
$setClause[] = "residence = CASE " . implode(' ', $caseWhenResidence) . " ELSE residence END";
|
||||
}
|
||||
|
||||
$updateQuery = "UPDATE ope_pass
|
||||
SET fk_adresse = CASE " . implode(' ', $caseWhen) . " END
|
||||
SET " . implode(', ', $setClause) . "
|
||||
WHERE id IN ($placeholders)";
|
||||
|
||||
try {
|
||||
$updateStmt = $this->db->prepare($updateQuery);
|
||||
$updateStmt->execute(array_merge($updateParams, $updateIds));
|
||||
$counters['passages_updated'] = count($toUpdate);
|
||||
|
||||
// Log pour vérifier l'uniformisation GPS (surtout pour immeubles)
|
||||
$this->logService->info('[updatePassagesForSector] Passages mis à jour avec GPS uniformisés', [
|
||||
'nb_updated' => count($toUpdate),
|
||||
'sector_id' => $sectorId
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de la mise à jour multiple des passages', [
|
||||
'sector_id' => $sectorId,
|
||||
@@ -1431,6 +1670,23 @@ class SectorController
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE MULTIPLE en une seule requête
|
||||
if (!empty($toDelete)) {
|
||||
$placeholders = str_repeat('?,', count($toDelete) - 1) . '?';
|
||||
$deleteQuery = "DELETE FROM ope_pass WHERE id IN ($placeholders)";
|
||||
|
||||
try {
|
||||
$deleteStmt = $this->db->prepare($deleteQuery);
|
||||
$deleteStmt->execute($toDelete);
|
||||
$counters['passages_deleted'] += count($toDelete);
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('Erreur lors de la suppression multiple des passages', [
|
||||
'sector_id' => $sectorId,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->logService->warning('[updatePassagesForSector] Pas de création de passages', [
|
||||
'reason' => !$firstUserId ? 'Pas d\'utilisateur affecté' : 'Pas d\'adresses',
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace App\Controllers;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Services\StripeService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\FileService;
|
||||
use App\Services\ApiService;
|
||||
use Session;
|
||||
use Exception;
|
||||
|
||||
@@ -77,7 +80,7 @@ class StripeController extends Controller {
|
||||
$this->requireAuth();
|
||||
|
||||
// Log du début de la requête
|
||||
\LogService::log('Début createOnboardingLink', [
|
||||
LogService::log('Début createOnboardingLink', [
|
||||
'account_id' => $accountId,
|
||||
'user_id' => Session::getUserId()
|
||||
]);
|
||||
@@ -98,7 +101,7 @@ class StripeController extends Controller {
|
||||
$returnUrl = $data['return_url'] ?? '';
|
||||
$refreshUrl = $data['refresh_url'] ?? '';
|
||||
|
||||
\LogService::log('URLs reçues', [
|
||||
LogService::log('URLs reçues', [
|
||||
'return_url' => $returnUrl,
|
||||
'refresh_url' => $refreshUrl
|
||||
]);
|
||||
@@ -110,7 +113,7 @@ class StripeController extends Controller {
|
||||
|
||||
$result = $this->stripeService->createOnboardingLink($accountId, $returnUrl, $refreshUrl);
|
||||
|
||||
\LogService::log('Résultat createOnboardingLink', [
|
||||
LogService::log('Résultat createOnboardingLink', [
|
||||
'success' => $result['success'] ?? false,
|
||||
'has_url' => isset($result['url'])
|
||||
]);
|
||||
@@ -127,7 +130,7 @@ class StripeController extends Controller {
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
\LogService::log('Erreur createOnboardingLink', [
|
||||
LogService::log('Erreur createOnboardingLink', [
|
||||
'level' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
@@ -190,7 +193,7 @@ class StripeController extends Controller {
|
||||
|
||||
// Vérifier que le passage existe et appartient à l'utilisateur
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.*, o.fk_entite
|
||||
SELECT p.*, o.fk_entite, o.id as operation_id
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
WHERE p.id = ? AND p.fk_user = ?
|
||||
@@ -210,13 +213,15 @@ class StripeController extends Controller {
|
||||
}
|
||||
|
||||
// Vérifier que le montant correspond (passage.montant est en euros, amount en centimes)
|
||||
$expectedAmount = (int)($passage['montant'] * 100);
|
||||
$expectedAmount = (int)round($passage['montant'] * 100);
|
||||
if ($amount !== $expectedAmount) {
|
||||
$this->sendError("Le montant ne correspond pas au passage (attendu: {$expectedAmount} centimes, reçu: {$amount} centimes)", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$entiteId = $passage['fk_entite'];
|
||||
$operationId = $passage['operation_id'];
|
||||
$fkUser = $passage['fk_user']; // ope_users.id
|
||||
|
||||
// Déterminer le type de paiement (Tap to Pay ou Web)
|
||||
$paymentMethodTypes = $data['payment_method_types'] ?? ['card_present'];
|
||||
@@ -230,14 +235,16 @@ class StripeController extends Controller {
|
||||
'payment_method_types' => $paymentMethodTypes,
|
||||
'capture_method' => $data['capture_method'] ?? 'automatic',
|
||||
'passage_id' => $passageId,
|
||||
'amicale_id' => $data['amicale_id'] ?? $entiteId,
|
||||
'member_id' => $data['member_id'] ?? Session::getUserId(),
|
||||
'fk_entite' => $data['amicale_id'] ?? $entiteId,
|
||||
'fk_user' => $data['member_id'] ?? $fkUser,
|
||||
'stripe_account' => $data['stripe_account'] ?? null,
|
||||
'metadata' => array_merge(
|
||||
[
|
||||
'passage_id' => (string)$passageId,
|
||||
'operation_id' => (string)$operationId,
|
||||
'amicale_id' => (string)($data['amicale_id'] ?? $entiteId),
|
||||
'member_id' => (string)($data['member_id'] ?? Session::getUserId()),
|
||||
'fk_user' => (string)$fkUser,
|
||||
'created_at' => (string)time(),
|
||||
'type' => $isTapToPay ? 'tap_to_pay' : 'web'
|
||||
],
|
||||
$data['metadata'] ?? []
|
||||
@@ -291,11 +298,12 @@ class StripeController extends Controller {
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT p.*, o.fk_entite,
|
||||
e.encrypted_name as entite_nom,
|
||||
u.first_name as user_prenom, u.sect_name as user_nom
|
||||
ou.first_name as user_prenom, u.sect_name as user_nom
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
LEFT JOIN entites e ON o.fk_entite = e.id
|
||||
LEFT JOIN users u ON p.fk_user = u.id
|
||||
LEFT JOIN ope_users ou ON p.fk_user = ou.id
|
||||
LEFT JOIN users u ON ou.fk_user = u.id
|
||||
WHERE p.stripe_payment_id = :pi_id
|
||||
");
|
||||
$stmt->execute(['pi_id' => $paymentIntentId]);
|
||||
@@ -330,7 +338,7 @@ class StripeController extends Controller {
|
||||
$entiteNom = '';
|
||||
if (!empty($passage['entite_nom'])) {
|
||||
try {
|
||||
$entiteNom = \ApiService::decryptData($passage['entite_nom']);
|
||||
$entiteNom = ApiService::decryptData($passage['entite_nom']);
|
||||
} catch (Exception $e) {
|
||||
$entiteNom = 'Entité inconnue';
|
||||
}
|
||||
@@ -400,6 +408,7 @@ class StripeController extends Controller {
|
||||
$this->sendSuccess([
|
||||
'has_account' => false,
|
||||
'account_id' => null,
|
||||
'location_id' => null,
|
||||
'charges_enabled' => false,
|
||||
'payouts_enabled' => false,
|
||||
'onboarding_completed' => false
|
||||
@@ -415,6 +424,7 @@ class StripeController extends Controller {
|
||||
$this->sendSuccess([
|
||||
'has_account' => true,
|
||||
'account_id' => $account['stripe_account_id'],
|
||||
'location_id' => $account['stripe_location_id'] ?? null,
|
||||
'charges_enabled' => false,
|
||||
'payouts_enabled' => false,
|
||||
'onboarding_completed' => false,
|
||||
@@ -440,6 +450,7 @@ class StripeController extends Controller {
|
||||
$this->sendSuccess([
|
||||
'has_account' => true,
|
||||
'account_id' => $account['stripe_account_id'],
|
||||
'location_id' => $account['stripe_location_id'] ?? null,
|
||||
'charges_enabled' => $stripeAccount->charges_enabled,
|
||||
'payouts_enabled' => $stripeAccount->payouts_enabled,
|
||||
'onboarding_completed' => $stripeAccount->details_submitted,
|
||||
@@ -529,17 +540,17 @@ class StripeController extends Controller {
|
||||
public function getPublicConfig(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
|
||||
$this->sendSuccess([
|
||||
'public_key' => $this->stripeService->getPublicKey(),
|
||||
'test_mode' => $this->stripeService->isTestMode()
|
||||
]);
|
||||
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GET /api/stripe/stats
|
||||
* Récupérer les statistiques de paiement
|
||||
@@ -613,9 +624,164 @@ class StripeController extends Controller {
|
||||
'to' => $dateTo
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/payment-links
|
||||
* Créer un Payment Link Stripe pour paiement par QR Code
|
||||
*
|
||||
* Payload:
|
||||
* {
|
||||
* "amount": 2500,
|
||||
* "currency": "eur",
|
||||
* "description": "Calendrier pompiers",
|
||||
* "passage_id": 789,
|
||||
* "metadata": {...}
|
||||
* }
|
||||
*/
|
||||
public function createPaymentLink(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
$data = $this->getJsonInput();
|
||||
|
||||
// Validation
|
||||
if (!isset($data['amount']) || !isset($data['passage_id'])) {
|
||||
$this->sendError('Montant et passage_id requis', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$amount = (int)$data['amount'];
|
||||
$passageId = (int)$data['passage_id'];
|
||||
|
||||
// Validation du montant (doit être > 0)
|
||||
if ($amount <= 0) {
|
||||
$this->sendError('Le montant doit être supérieur à 0', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que le passage appartient à l'utilisateur ou à son entité
|
||||
$userId = Session::getUserId();
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.*, o.fk_entite, ou.fk_user as ope_user_id
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
JOIN ope_users ou ON p.fk_user = ou.id
|
||||
WHERE p.id = ?
|
||||
');
|
||||
$stmt->execute([$passageId]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if (!$passage) {
|
||||
$this->sendError('Passage non trouvé', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier les droits : soit l'utilisateur est le créateur du passage, soit il appartient à la même entité
|
||||
$userEntityId = Session::getEntityId();
|
||||
if ($passage['ope_user_id'] != $userId && $passage['fk_entite'] != $userEntityId) {
|
||||
$this->sendError('Passage non autorisé', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier qu'il n'y a pas déjà un paiement ou un payment link pour ce passage
|
||||
if (!empty($passage['stripe_payment_id'])) {
|
||||
$this->sendError('Un paiement Stripe existe déjà pour ce passage', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($passage['stripe_payment_link_id'])) {
|
||||
$this->sendError('Un Payment Link existe déjà pour ce passage', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que le montant correspond (passage.montant est en euros, amount en centimes)
|
||||
$expectedAmount = (int)round($passage['montant'] * 100);
|
||||
if ($amount !== $expectedAmount) {
|
||||
$this->sendError("Le montant ne correspond pas au passage (attendu: {$expectedAmount} centimes, reçu: {$amount} centimes)", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Préparer les paramètres
|
||||
$params = [
|
||||
'amount' => $amount,
|
||||
'currency' => $data['currency'] ?? 'eur',
|
||||
'description' => $data['description'] ?? 'Calendrier pompiers',
|
||||
'passage_id' => $passageId,
|
||||
'metadata' => $data['metadata'] ?? []
|
||||
];
|
||||
|
||||
// Créer le Payment Link
|
||||
$result = $this->stripeService->createPaymentLink($params);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->sendSuccess([
|
||||
'payment_link_id' => $result['payment_link_id'],
|
||||
'url' => $result['url'],
|
||||
'amount' => $result['amount'],
|
||||
'passage_id' => $passageId,
|
||||
'type' => 'qr_code'
|
||||
]);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/locations
|
||||
* Créer une Location Stripe Terminal pour une entité (nécessaire pour Tap to Pay)
|
||||
*/
|
||||
public function createLocation(): void {
|
||||
try {
|
||||
$this->requireAuth();
|
||||
|
||||
// Vérifier le rôle de l'utilisateur
|
||||
$userId = Session::getUserId();
|
||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$result = $stmt->fetch();
|
||||
$userRole = $result ? (int)$result['fk_role'] : 0;
|
||||
|
||||
if ($userRole < 2) {
|
||||
$this->sendError('Droits insuffisants - Admin amicale minimum requis', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $this->getJsonInput();
|
||||
$entiteId = $data['fk_entite'] ?? Session::getEntityId();
|
||||
|
||||
if (!$entiteId) {
|
||||
$this->sendError('ID entité requis', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier les droits sur cette entité
|
||||
if (Session::getEntityId() != $entiteId && $userRole < 3) {
|
||||
$this->sendError('Non autorisé pour cette entité', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->stripeService->createLocation($entiteId);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->sendSuccess([
|
||||
'location_id' => $result['location_id'],
|
||||
'message' => $result['message']
|
||||
]);
|
||||
} else {
|
||||
$this->sendError($result['message'], 400);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->sendError('Erreur lors de la création de la location: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,8 @@ class StripeWebhookController extends Controller {
|
||||
}
|
||||
|
||||
// Récupérer le secret webhook selon le mode
|
||||
$stripeConfig = $this->config->get('stripe');
|
||||
$webhookSecret = $this->stripeService->isTestMode()
|
||||
$stripeConfig = $this->config->getStripeConfig();
|
||||
$webhookSecret = $this->stripeService->isTestMode()
|
||||
? $stripeConfig['webhook_secret_test']
|
||||
: $stripeConfig['webhook_secret_live'];
|
||||
|
||||
@@ -95,31 +95,35 @@ class StripeWebhookController extends Controller {
|
||||
case 'account.updated':
|
||||
$this->handleAccountUpdated($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'account.application.authorized':
|
||||
$this->handleAccountAuthorized($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'payment_intent.succeeded':
|
||||
$this->handlePaymentIntentSucceeded($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'payment_intent.payment_failed':
|
||||
$this->handlePaymentIntentFailed($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'checkout.session.completed':
|
||||
$this->handleCheckoutSessionCompleted($event->data->object);
|
||||
break;
|
||||
|
||||
case 'charge.dispute.created':
|
||||
$this->handleChargeDisputeCreated($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'terminal.reader.action_succeeded':
|
||||
$this->handleTerminalReaderActionSucceeded($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
case 'terminal.reader.action_failed':
|
||||
$this->handleTerminalReaderActionFailed($event->data->object);
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
// Événement non géré mais valide
|
||||
error_log("Unhandled Stripe event type: {$event->type}");
|
||||
@@ -278,7 +282,60 @@ class StripeWebhookController extends Controller {
|
||||
|
||||
error_log("Payment failed: {$paymentIntent->id}, reason: " . json_encode($paymentIntent->last_payment_error));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gérer la complétion d'une session de paiement (Payment Link / Checkout)
|
||||
*/
|
||||
private function handleCheckoutSessionCompleted($session): void {
|
||||
$metadata = $session->metadata;
|
||||
|
||||
// Logger l'événement
|
||||
error_log("Checkout session completed: {$session->id}, payment_intent: {$session->payment_intent}");
|
||||
|
||||
// Vérifier si un passage_id est présent dans les metadata
|
||||
if (isset($metadata->passage_id) && !empty($metadata->passage_id)) {
|
||||
$passageId = (int)$metadata->passage_id;
|
||||
|
||||
// Mettre à jour le passage avec le stripe_payment_id
|
||||
$stmt = $this->db->prepare("
|
||||
UPDATE ope_pass
|
||||
SET stripe_payment_id = :payment_intent_id,
|
||||
updated_at = NOW()
|
||||
WHERE id = :passage_id
|
||||
");
|
||||
$stmt->execute([
|
||||
'payment_intent_id' => $session->payment_intent,
|
||||
'passage_id' => $passageId
|
||||
]);
|
||||
|
||||
// Vérifier si la mise à jour a réussi
|
||||
if ($stmt->rowCount() > 0) {
|
||||
error_log("Passage {$passageId} updated with payment_intent {$session->payment_intent}");
|
||||
|
||||
// TODO: Envoyer un email de confirmation avec le reçu fiscal
|
||||
// TODO: Mettre à jour les statistiques en temps réel
|
||||
} else {
|
||||
error_log("Warning: Passage {$passageId} not found or already updated");
|
||||
}
|
||||
} else {
|
||||
error_log("Warning: checkout.session.completed without passage_id in metadata");
|
||||
}
|
||||
|
||||
// Enregistrer l'historique de la session dans stripe_payment_history si nécessaire
|
||||
if (isset($metadata->passage_id)) {
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT id FROM ope_pass WHERE id = :passage_id
|
||||
");
|
||||
$stmt->execute(['passage_id' => $metadata->passage_id]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if ($passage) {
|
||||
// Log dans l'historique
|
||||
error_log("Checkout session completed for passage {$metadata->passage_id}: amount={$session->amount_total}, currency={$session->currency}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gérer un litige (chargeback)
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/EventLogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/PasswordSecurityService.php';
|
||||
|
||||
@@ -15,8 +16,9 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\EventLogService;
|
||||
use App\Services\ApiService;
|
||||
use App\Services\PasswordSecurityService;
|
||||
|
||||
class UserController {
|
||||
@@ -529,16 +531,13 @@ class UserController {
|
||||
]);
|
||||
}
|
||||
|
||||
LogService::log('Utilisateur GeoSector créé', [
|
||||
'level' => 'info',
|
||||
'createdBy' => $currentUserId,
|
||||
'newUserId' => $userId,
|
||||
'email' => !empty($email) ? $email : 'non fourni',
|
||||
'username' => $username,
|
||||
'usernameManual' => $chkUsernameManuel === 1 ? 'oui' : 'non',
|
||||
'passwordManual' => $chkMdpManuel === 1 ? 'oui' : 'non',
|
||||
'emailsSent' => !empty($email) ? '2 emails (username + password)' : 'aucun (pas d\'email)'
|
||||
]);
|
||||
// Log de création utilisateur
|
||||
EventLogService::logUserCreated(
|
||||
(int)$userId,
|
||||
(int)$entiteId,
|
||||
(int)$role,
|
||||
$username
|
||||
);
|
||||
|
||||
// Préparer la réponse avec les informations de connexion si générées automatiquement
|
||||
$responseData = [
|
||||
@@ -762,12 +761,23 @@ class UserController {
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Utilisateur GeoSector mis à jour', [
|
||||
'level' => 'info',
|
||||
'modifiedBy' => $currentUserId,
|
||||
'userId' => $id,
|
||||
'fields' => array_keys($data),
|
||||
]);
|
||||
// Log de mise à jour utilisateur
|
||||
$changes = [];
|
||||
$encryptedFields = ['name', 'email', 'phone', 'mobile', 'username', 'password'];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array($key, $encryptedFields)) {
|
||||
// Champs sensibles : booléen uniquement
|
||||
$changes['encrypted_' . $key] = true;
|
||||
} else {
|
||||
// Champs non sensibles : valeur
|
||||
$changes[$key] = ['new' => $value];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($changes)) {
|
||||
EventLogService::logUserUpdated((int)$id, $changes);
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
@@ -858,24 +868,72 @@ class UserController {
|
||||
|
||||
if ($transferTo) {
|
||||
try {
|
||||
// Transférer TOUS les passages de l'utilisateur vers l'utilisateur désigné
|
||||
$stmt3 = $this->db->prepare('
|
||||
UPDATE ope_pass
|
||||
SET fk_user = :new_user_id
|
||||
WHERE fk_user = :delete_user_id
|
||||
// Transférer les passages opération par opération
|
||||
// (car fk_user dans ope_pass pointe vers ope_users.id, pas users.id)
|
||||
|
||||
// Récupérer toutes les opérations où l'utilisateur à supprimer a des entrées dans ope_users
|
||||
$stmtOps = $this->db->prepare('
|
||||
SELECT DISTINCT fk_operation
|
||||
FROM ope_users
|
||||
WHERE fk_user = ?
|
||||
');
|
||||
$stmt3->execute([
|
||||
'new_user_id' => $transferTo,
|
||||
'delete_user_id' => $id
|
||||
]);
|
||||
|
||||
$transferredCount = $stmt3->rowCount();
|
||||
|
||||
$stmtOps->execute([$id]);
|
||||
$operations = $stmtOps->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
$totalTransferred = 0;
|
||||
|
||||
foreach ($operations as $operationId) {
|
||||
// Trouver ope_users.id de l'utilisateur à supprimer dans cette opération
|
||||
$stmtOldOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtOldOpeUser->execute([$id, $operationId]);
|
||||
$oldOpeUserId = $stmtOldOpeUser->fetchColumn();
|
||||
|
||||
if (!$oldOpeUserId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trouver ope_users.id de l'utilisateur de destination dans cette opération
|
||||
$stmtNewOpeUser = $this->db->prepare('
|
||||
SELECT id FROM ope_users
|
||||
WHERE fk_user = ? AND fk_operation = ?
|
||||
');
|
||||
$stmtNewOpeUser->execute([$transferTo, $operationId]);
|
||||
$newOpeUserId = $stmtNewOpeUser->fetchColumn();
|
||||
|
||||
if (!$newOpeUserId) {
|
||||
LogService::log('Impossible de transférer passages - utilisateur destination absent', [
|
||||
'level' => 'warning',
|
||||
'operation_id' => $operationId,
|
||||
'from_user' => $id,
|
||||
'to_user' => $transferTo
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transférer les passages
|
||||
$stmtTransfer = $this->db->prepare('
|
||||
UPDATE ope_pass
|
||||
SET fk_user = :new_ope_user_id
|
||||
WHERE fk_user = :old_ope_user_id AND fk_operation = :operation_id
|
||||
');
|
||||
$stmtTransfer->execute([
|
||||
'new_ope_user_id' => $newOpeUserId,
|
||||
'old_ope_user_id' => $oldOpeUserId,
|
||||
'operation_id' => $operationId
|
||||
]);
|
||||
|
||||
$totalTransferred += $stmtTransfer->rowCount();
|
||||
}
|
||||
|
||||
LogService::log('Passages transférés avant suppression utilisateur', [
|
||||
'level' => 'info',
|
||||
'from_user' => $id,
|
||||
'to_user' => $transferTo,
|
||||
'passages_transferred' => $transferredCount
|
||||
'operations_count' => count($operations),
|
||||
'passages_transferred' => $totalTransferred
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
Response::json([
|
||||
@@ -890,13 +948,10 @@ class UserController {
|
||||
// —— Suppression réelle de l'utilisateur ——
|
||||
try {
|
||||
// Supprimer les enregistrements dépendants dans ope_users
|
||||
// (CASCADE supprime automatiquement ope_users_sectors et ope_pass)
|
||||
$stmtOpeUsers = $this->db->prepare('DELETE FROM ope_users WHERE fk_user = ?');
|
||||
$stmtOpeUsers->execute([$id]);
|
||||
|
||||
// Supprimer les enregistrements dépendants dans ope_users_sectors
|
||||
$stmtOpeUsersSectors = $this->db->prepare('DELETE FROM ope_users_sectors WHERE fk_user = ?');
|
||||
$stmtOpeUsersSectors->execute([$id]);
|
||||
|
||||
$stmt = $this->db->prepare('DELETE FROM users WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
|
||||
@@ -908,12 +963,8 @@ class UserController {
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Utilisateur GeoSector supprimé', [
|
||||
'level' => 'info',
|
||||
'deletedBy' => $currentUserId,
|
||||
'userId' => $id,
|
||||
'passage_transfer' => $transferTo ? "Tous les passages transférés vers utilisateur $transferTo" : 'Aucun transfert'
|
||||
]);
|
||||
// Log de suppression utilisateur (suppression physique = false pour soft_delete)
|
||||
EventLogService::logUserDeleted((int)$id, false);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
|
||||
@@ -14,8 +14,8 @@ use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
use Exception;
|
||||
|
||||
class VilleController {
|
||||
|
||||
@@ -11,10 +11,11 @@ class Router {
|
||||
'register',
|
||||
'lostpassword',
|
||||
'log',
|
||||
'health', // Health check endpoint pour monitoring et déploiement
|
||||
'villes', // Ajout de la route villes comme endpoint public pour l'autocomplétion du code postal
|
||||
'password/check', // Vérification de la force des mots de passe (public pour l'inscription)
|
||||
'password/compromised', // Vérification si un mot de passe est compromis
|
||||
'stripe/webhook', // Webhook Stripe (doit être public pour recevoir les événements)
|
||||
'stripe/webhooks', // Webhook Stripe (doit être public pour recevoir les événements)
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
@@ -34,6 +35,9 @@ class Router {
|
||||
// Route pour les logs
|
||||
$this->post('log', ['LogController', 'index']);
|
||||
|
||||
// Route health check (monitoring et déploiement)
|
||||
$this->get('health', ['HealthController', 'check']);
|
||||
|
||||
// Routes privées utilisateurs
|
||||
// IMPORTANT: Les routes spécifiques doivent être déclarées AVANT les routes avec paramètres
|
||||
$this->post('users/check-username', ['UserController', 'checkUsername']); // Déplacé avant les routes avec :id
|
||||
@@ -131,12 +135,14 @@ class Router {
|
||||
$this->get('stripe/accounts/:entityId/status', ['StripeController', 'getAccountStatus']);
|
||||
$this->post('stripe/accounts/:accountId/onboarding-link', ['StripeController', 'createOnboardingLink']);
|
||||
|
||||
// Tap to Pay - Vérification compatibilité
|
||||
// Tap to Pay - Vérification compatibilité et configuration
|
||||
$this->post('stripe/devices/check-tap-to-pay', ['StripeController', 'checkTapToPayCapability']);
|
||||
$this->get('stripe/devices/certified-android', ['StripeController', 'getCertifiedAndroidDevices']);
|
||||
$this->post('stripe/locations', ['StripeController', 'createLocation']);
|
||||
|
||||
// Paiements
|
||||
$this->post('stripe/payments/create-intent', ['StripeController', 'createPaymentIntent']);
|
||||
$this->post('stripe/payment-links', ['StripeController', 'createPaymentLink']);
|
||||
$this->get('stripe/payments/:paymentIntentId', ['StripeController', 'getPaymentStatus']);
|
||||
|
||||
// Statistiques et configuration
|
||||
@@ -144,7 +150,21 @@ class Router {
|
||||
$this->get('stripe/config', ['StripeController', 'getPublicConfig']);
|
||||
|
||||
// Webhook (IMPORTANT: pas d'authentification requise pour les webhooks Stripe)
|
||||
$this->post('stripe/webhook', ['StripeWebhookController', 'handleWebhook']);
|
||||
$this->post('stripe/webhooks', ['StripeWebhookController', 'handleWebhook']);
|
||||
|
||||
// Routes Migration (Admin uniquement)
|
||||
$this->get('migrations/test-connections', ['MigrationController', 'testConnections']);
|
||||
$this->get('migrations/entities/available', ['MigrationController', 'getAvailableEntities']);
|
||||
$this->get('migrations/entities/:id', ['MigrationController', 'getEntityDetails']);
|
||||
$this->post('migrations/entity', ['MigrationController', 'migrateEntity']);
|
||||
$this->post('migrations/entity/step', ['MigrationController', 'migrateEntityStep']);
|
||||
$this->get('migrations/entity/:id/status', ['MigrationController', 'getMigrationStatus']);
|
||||
$this->get('migrations/entity/:id/logs', ['MigrationController', 'getMigrationLogs']);
|
||||
$this->get('migrations/entity/:id/report', ['MigrationController', 'getMigrationReport']);
|
||||
$this->get('migrations/entity/:id/compare', ['MigrationController', 'compareEntityData']);
|
||||
$this->get('migrations/entity/:id/verify', ['MigrationController', 'verifyMigration']);
|
||||
$this->delete('migrations/entity/:id', ['MigrationController', 'rollbackEntity']);
|
||||
$this->delete('migrations/entity/:id/step/:step', ['MigrationController', 'rollbackStep']);
|
||||
}
|
||||
|
||||
public function handle(): void {
|
||||
@@ -180,7 +200,6 @@ class Router {
|
||||
|
||||
// Check if endpoint is public
|
||||
if ($this->isPublicEndpoint($endpoint)) {
|
||||
error_log("Public endpoint found: $endpoint");
|
||||
$route = $this->findRoute($method, $endpoint);
|
||||
if ($route) {
|
||||
$this->executeRoute($route);
|
||||
|
||||
@@ -5,22 +5,55 @@ declare(strict_types=1);
|
||||
class Session {
|
||||
public static function start(): void {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
// Configuration d'un répertoire de sessions dédié et persistant
|
||||
$sessionPath = __DIR__ . '/../../sessions';
|
||||
if (!is_dir($sessionPath)) {
|
||||
mkdir($sessionPath, 0700, true);
|
||||
}
|
||||
ini_set('session.save_path', $sessionPath);
|
||||
|
||||
// Configuration des sessions adaptée pour les applications mobiles
|
||||
ini_set('session.use_strict_mode', '1');
|
||||
ini_set('session.cookie_httponly', '1');
|
||||
|
||||
|
||||
// Permettre les connexions non-HTTPS en développement
|
||||
$isProduction = (getenv('APP_ENV') === 'production');
|
||||
ini_set('session.cookie_secure', $isProduction ? '1' : '0');
|
||||
|
||||
|
||||
// SameSite None pour permettre les requêtes cross-origin (applications mobiles)
|
||||
ini_set('session.cookie_samesite', 'None');
|
||||
ini_set('session.gc_maxlifetime', '86400'); // 24 heures
|
||||
|
||||
// Configuration de la durée de vie des sessions : 24 heures
|
||||
$sessionLifetime = 86400; // 24 heures
|
||||
ini_set('session.gc_maxlifetime', (string)$sessionLifetime);
|
||||
ini_set('session.cookie_lifetime', (string)$sessionLifetime);
|
||||
|
||||
// Configuration du garbage collector pour qu'il ne supprime pas trop tôt
|
||||
// gc_probability / gc_divisor = probabilité d'exécution (1/100 = 1%)
|
||||
ini_set('session.gc_probability', '1');
|
||||
ini_set('session.gc_divisor', '100');
|
||||
|
||||
// Récupérer le session_id du Bearer token si présent
|
||||
self::getSessionFromBearer();
|
||||
|
||||
session_start();
|
||||
|
||||
// Log détaillé après le démarrage de la session (DEBUG)
|
||||
$logFile = __DIR__ . '/../../logs/session_' . date('Y-m-d') . '.log';
|
||||
$sessionId = session_id();
|
||||
$sessionExists = isset($_SESSION) && !empty($_SESSION);
|
||||
$sessionData = $sessionExists ? json_encode($_SESSION) : 'empty';
|
||||
$sessionFile = $sessionPath . '/sess_' . $sessionId;
|
||||
$sessionFileExists = file_exists($sessionFile);
|
||||
|
||||
$logMessage = date('Y-m-d H:i:s') . " - Session started\n";
|
||||
$logMessage .= " Session ID: $sessionId\n";
|
||||
$logMessage .= " Session path: $sessionPath\n";
|
||||
$logMessage .= " Session file exists: " . ($sessionFileExists ? 'YES' : 'NO') . "\n";
|
||||
$logMessage .= " Session data exists: " . ($sessionExists ? 'YES' : 'NO') . "\n";
|
||||
$logMessage .= " Session data: $sessionData\n";
|
||||
|
||||
file_put_contents($logFile, $logMessage, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/LogService.php';
|
||||
namespace App\Services;
|
||||
|
||||
class AddressService {
|
||||
use Database;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Service de gestion des adresses
|
||||
*
|
||||
* Ce service interroge la base de données externe 'adresses' pour récupérer
|
||||
* les adresses géographiques dans des secteurs définis.
|
||||
*/
|
||||
class AddressService
|
||||
{
|
||||
private ?PDO $addressesDb = null;
|
||||
private PDO $mainDb;
|
||||
private LogService $logService;
|
||||
|
||||
public function __construct() {
|
||||
private $logService;
|
||||
private $buildingService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->logService = new LogService();
|
||||
|
||||
try {
|
||||
$this->addressesDb = AddressesDatabase::getInstance();
|
||||
$this->addressesDb = \AddressesDatabase::getInstance();
|
||||
$this->logService->info('[AddressService] Connexion à la base d\'adresses réussie');
|
||||
} catch (\Exception $e) {
|
||||
// Si la connexion échoue, on continue sans la base d'adresses
|
||||
@@ -21,53 +39,59 @@ class AddressService {
|
||||
]);
|
||||
$this->addressesDb = null;
|
||||
}
|
||||
$this->mainDb = Database::getInstance();
|
||||
|
||||
$this->mainDb = \Database::getInstance();
|
||||
$this->buildingService = new BuildingService();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Vérifie si la connexion à la base d'adresses est active
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isConnected(): bool {
|
||||
public function isConnected(): bool
|
||||
{
|
||||
return $this->addressesDb !== null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Détermine le département de l'entité courante
|
||||
*
|
||||
*
|
||||
* @param int|null $entityId ID de l'entité
|
||||
* @return string|null Code département (ex: "22", "23")
|
||||
*/
|
||||
private function getDepartmentForEntity(?int $entityId = null): ?string {
|
||||
private function getDepartmentForEntity(?int $entityId = null): ?string
|
||||
{
|
||||
if (!$entityId) {
|
||||
$entityId = $_SESSION['entity_id'] ?? null;
|
||||
}
|
||||
|
||||
|
||||
if (!$entityId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
$query = "SELECT departement FROM entites WHERE id = :entity_id";
|
||||
$stmt = $this->mainDb->prepare($query);
|
||||
$stmt->execute(['entity_id' => $entityId]);
|
||||
$result = $stmt->fetch();
|
||||
|
||||
|
||||
return $result ? $result['departement'] : null;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Récupère toutes les adresses contenues dans un polygone défini par des coordonnées
|
||||
* Gère automatiquement les secteurs multi-départements
|
||||
*
|
||||
*
|
||||
* @param array $coordinates Array de coordonnées [[lat, lng], [lat, lng], ...]
|
||||
* @param int|null $entityId ID de l'entité (pour déterminer le département principal)
|
||||
* @return array Array des adresses trouvées
|
||||
*/
|
||||
public function getAddressesInPolygon(array $coordinates, ?int $entityId = null): array {
|
||||
public function getAddressesInPolygon(array $coordinates, ?int $entityId = null): array
|
||||
{
|
||||
// Si pas de connexion à la base d'adresses, retourner un tableau vide
|
||||
if (!$this->addressesDb) {
|
||||
$this->logService->error('[AddressService] Pas de connexion à la base d\'adresses externe', [
|
||||
@@ -75,21 +99,20 @@ class AddressService {
|
||||
]);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
$this->logService->info('[AddressService] Début recherche adresses', [
|
||||
'entity_id' => $entityId,
|
||||
'nb_coordinates' => count($coordinates)
|
||||
]);
|
||||
|
||||
|
||||
if (count($coordinates) < 3) {
|
||||
throw new InvalidArgumentException("Un polygone doit avoir au moins 3 points");
|
||||
}
|
||||
|
||||
|
||||
// D'abord, déterminer tous les départements touchés par ce secteur
|
||||
require_once __DIR__ . '/DepartmentBoundaryService.php';
|
||||
$boundaryService = new \DepartmentBoundaryService();
|
||||
$boundaryService = new DepartmentBoundaryService();
|
||||
$departmentsTouched = $boundaryService->getDepartmentsForSector($coordinates);
|
||||
|
||||
|
||||
if (empty($departmentsTouched)) {
|
||||
// Si aucun département n'est trouvé par analyse spatiale,
|
||||
// chercher d'abord dans le département de l'entité et ses limitrophes
|
||||
@@ -103,22 +126,22 @@ class AddressService {
|
||||
]);
|
||||
throw new RuntimeException("Impossible de déterminer le département");
|
||||
}
|
||||
|
||||
|
||||
// Obtenir les départements prioritaires (entité + limitrophes)
|
||||
$priorityDepts = $boundaryService->getPriorityDepartments($entityDept);
|
||||
|
||||
|
||||
// Log pour debug
|
||||
$this->logService->warning('[AddressService] Aucun département trouvé par analyse spatiale', [
|
||||
'departements_prioritaires' => implode(', ', $priorityDepts)
|
||||
]);
|
||||
|
||||
|
||||
// Utiliser les départements prioritaires pour la recherche
|
||||
$departmentsTouched = [];
|
||||
foreach ($priorityDepts as $deptCode) {
|
||||
$departmentsTouched[] = ['code_dept' => $deptCode];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Créer le polygone SQL à partir des coordonnées
|
||||
$polygonPoints = [];
|
||||
foreach ($coordinates as $coord) {
|
||||
@@ -127,22 +150,22 @@ class AddressService {
|
||||
}
|
||||
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // MySQL attend longitude latitude
|
||||
}
|
||||
|
||||
|
||||
// Fermer le polygone
|
||||
$polygonPoints[] = $polygonPoints[0];
|
||||
|
||||
|
||||
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
|
||||
|
||||
|
||||
// Collecter les adresses de tous les départements touchés
|
||||
$allAddresses = [];
|
||||
|
||||
|
||||
foreach ($departmentsTouched as $dept) {
|
||||
$deptCode = $dept['code_dept'];
|
||||
$tableName = "cp" . $deptCode;
|
||||
|
||||
|
||||
try {
|
||||
// Requête pour récupérer les adresses dans le polygone pour ce département
|
||||
$sql = "SELECT
|
||||
$sql = "SELECT
|
||||
id,
|
||||
numero,
|
||||
rue as voie,
|
||||
@@ -161,32 +184,32 @@ class AddressService {
|
||||
:dept_code as departement
|
||||
FROM `$tableName`
|
||||
WHERE ST_Contains(
|
||||
ST_GeomFromText(:polygon, 4326),
|
||||
ST_GeomFromText(:polygon, 4326),
|
||||
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8)))
|
||||
)
|
||||
AND gps_lat != ''
|
||||
AND gps_lng != ''";
|
||||
|
||||
|
||||
$stmt = $this->addressesDb->prepare($sql);
|
||||
$stmt->execute([
|
||||
'polygon' => $polygonString,
|
||||
'dept_code' => $deptCode
|
||||
]);
|
||||
|
||||
|
||||
$addresses = $stmt->fetchAll();
|
||||
|
||||
|
||||
// Ajouter les adresses à la collection globale
|
||||
foreach ($addresses as $address) {
|
||||
$allAddresses[] = $address;
|
||||
}
|
||||
|
||||
|
||||
// Log pour debug
|
||||
$this->logService->info('[AddressService] Recherche dans table', [
|
||||
'table' => $tableName,
|
||||
'departement' => $deptCode,
|
||||
'nb_adresses' => count($addresses)
|
||||
]);
|
||||
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Log l'erreur mais continue avec les autres départements
|
||||
$this->logService->error('[AddressService] Erreur SQL', [
|
||||
@@ -197,35 +220,90 @@ class AddressService {
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$this->logService->info('[AddressService] Fin recherche adresses', [
|
||||
'total_adresses' => count($allAddresses)
|
||||
]);
|
||||
return $allAddresses;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enrichit les adresses avec les données bâtiments depuis la base 'batiments'
|
||||
*
|
||||
* Pour chaque adresse trouvée, cette méthode cherche si un bâtiment existe
|
||||
* et ajoute les métadonnées (nb_log, residence, fk_habitat, etc.)
|
||||
*
|
||||
* @param array $addresses Liste d'adresses depuis getAddressesInPolygon()
|
||||
* @param int|null $entityId ID de l'entité (pour logs)
|
||||
* @return array Adresses enrichies avec données bâtiment
|
||||
*/
|
||||
public function enrichAddressesWithBuildings(array $addresses, ?int $entityId = null): array
|
||||
{
|
||||
if (empty($addresses)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->logService->info('[AddressService] Début enrichissement avec bâtiments', [
|
||||
'entity_id' => $entityId,
|
||||
'nb_addresses' => count($addresses)
|
||||
]);
|
||||
|
||||
try {
|
||||
$enrichedAddresses = $this->buildingService->enrichAddresses($addresses);
|
||||
|
||||
// Compter les immeubles vs maisons
|
||||
$nbImmeubles = 0;
|
||||
$nbMaisons = 0;
|
||||
foreach ($enrichedAddresses as $addr) {
|
||||
if (isset($addr['fk_habitat']) && $addr['fk_habitat'] == 2) {
|
||||
$nbImmeubles++;
|
||||
} else {
|
||||
$nbMaisons++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->logService->info('[AddressService] Fin enrichissement avec bâtiments', [
|
||||
'total_adresses' => count($enrichedAddresses),
|
||||
'nb_immeubles' => $nbImmeubles,
|
||||
'nb_maisons' => $nbMaisons
|
||||
]);
|
||||
|
||||
return $enrichedAddresses;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logService->error('[AddressService] Erreur lors de l\'enrichissement', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
// En cas d'erreur, retourner les adresses sans enrichissement
|
||||
return $addresses;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les adresses dans un rayon autour d'un point
|
||||
*
|
||||
*
|
||||
* @param float $latitude Latitude du centre
|
||||
* @param float $longitude Longitude du centre
|
||||
* @param float $radiusMeters Rayon en mètres
|
||||
* @param int|null $entityId ID de l'entité (pour déterminer le département)
|
||||
* @return array Array des adresses trouvées
|
||||
*/
|
||||
public function getAddressesInRadius(float $latitude, float $longitude, float $radiusMeters, ?int $entityId = null): array {
|
||||
public function getAddressesInRadius(float $latitude, float $longitude, float $radiusMeters, ?int $entityId = null): array
|
||||
{
|
||||
// Déterminer le département
|
||||
$dept = $this->getDepartmentForEntity($entityId);
|
||||
if (!$dept) {
|
||||
throw new RuntimeException("Impossible de déterminer le département de l'entité");
|
||||
}
|
||||
|
||||
|
||||
// Nom de la table selon le département
|
||||
$tableName = "cp" . $dept;
|
||||
|
||||
|
||||
try {
|
||||
// Utiliser ST_Distance_Sphere pour calculer la distance en mètres
|
||||
$sql = "SELECT
|
||||
$sql = "SELECT
|
||||
id,
|
||||
numero,
|
||||
rue as voie,
|
||||
@@ -245,45 +323,44 @@ class AddressService {
|
||||
AND gps_lat != ''
|
||||
AND gps_lng != ''
|
||||
ORDER BY distance";
|
||||
|
||||
|
||||
$point = "POINT($longitude $latitude)";
|
||||
|
||||
|
||||
$stmt = $this->addressesDb->prepare($sql);
|
||||
$stmt->execute([
|
||||
'point' => $point,
|
||||
'radius' => $radiusMeters
|
||||
]);
|
||||
|
||||
|
||||
return $stmt->fetchAll();
|
||||
} catch (PDOException $e) {
|
||||
throw new RuntimeException("Erreur lors de la récupération des adresses dans la table $tableName : " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compte le nombre d'adresses dans un polygone
|
||||
* Gère automatiquement les secteurs multi-départements
|
||||
*
|
||||
*
|
||||
* @param array $coordinates Array de coordonnées [[lat, lng], [lat, lng], ...]
|
||||
* @param int|null $entityId ID de l'entité (pour déterminer le département principal)
|
||||
* @return int Nombre d'adresses
|
||||
*/
|
||||
public function countAddressesInPolygon(array $coordinates, ?int $entityId = null): int {
|
||||
public function countAddressesInPolygon(array $coordinates, ?int $entityId = null): int
|
||||
{
|
||||
// Si pas de connexion à la base d'adresses, retourner 0
|
||||
if (!$this->addressesDb) {
|
||||
error_log("AddressService: Pas de connexion à la base d'adresses, retour de 0 adresses");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
if (count($coordinates) < 3) {
|
||||
throw new InvalidArgumentException("Un polygone doit avoir au moins 3 points");
|
||||
}
|
||||
|
||||
|
||||
// D'abord, déterminer tous les départements touchés par ce secteur
|
||||
require_once __DIR__ . '/DepartmentBoundaryService.php';
|
||||
$boundaryService = new \DepartmentBoundaryService();
|
||||
$boundaryService = new DepartmentBoundaryService();
|
||||
$departmentsTouched = $boundaryService->getDepartmentsForSector($coordinates);
|
||||
|
||||
|
||||
if (empty($departmentsTouched)) {
|
||||
// Si aucun département n'est trouvé, utiliser le département de l'entité
|
||||
$dept = $this->getDepartmentForEntity($entityId);
|
||||
@@ -292,7 +369,7 @@ class AddressService {
|
||||
}
|
||||
$departmentsTouched = [['code_dept' => $dept]];
|
||||
}
|
||||
|
||||
|
||||
// Créer le polygone SQL à partir des coordonnées
|
||||
$polygonPoints = [];
|
||||
foreach ($coordinates as $coord) {
|
||||
@@ -301,19 +378,19 @@ class AddressService {
|
||||
}
|
||||
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // MySQL attend longitude latitude
|
||||
}
|
||||
|
||||
|
||||
// Fermer le polygone
|
||||
$polygonPoints[] = $polygonPoints[0];
|
||||
|
||||
|
||||
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
|
||||
|
||||
|
||||
// Compter les adresses dans tous les départements touchés
|
||||
$totalCount = 0;
|
||||
|
||||
|
||||
foreach ($departmentsTouched as $dept) {
|
||||
$deptCode = $dept['code_dept'];
|
||||
$tableName = "cp" . $deptCode;
|
||||
|
||||
|
||||
try {
|
||||
$sql = "SELECT COUNT(*) as count
|
||||
FROM `$tableName`
|
||||
@@ -323,23 +400,20 @@ class AddressService {
|
||||
)
|
||||
AND gps_lat != ''
|
||||
AND gps_lng != ''";
|
||||
|
||||
|
||||
$stmt = $this->addressesDb->prepare($sql);
|
||||
$stmt->execute(['polygon' => $polygonString]);
|
||||
|
||||
|
||||
$result = $stmt->fetch();
|
||||
$deptCount = (int)$result['count'];
|
||||
$totalCount += $deptCount;
|
||||
|
||||
// Log pour debug
|
||||
error_log("Département $deptCode : $deptCount adresses comptées");
|
||||
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Log l'erreur mais continue avec les autres départements
|
||||
error_log("Erreur de comptage pour le département $deptCode : " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $totalCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
use App\Services\PasswordSecurityService;
|
||||
use AppConfig;
|
||||
use App\Services\LogService;
|
||||
|
||||
require_once __DIR__ . '/EmailTemplates.php';
|
||||
require_once __DIR__ . '/PasswordSecurityService.php';
|
||||
@@ -70,11 +73,21 @@ class ApiService {
|
||||
$mail->Body = EmailTemplates::getLostPasswordTemplate($name, $data['username'] ?? '', $data['password']);
|
||||
break;
|
||||
|
||||
case 'password_reset':
|
||||
$mail->Subject = 'Réinitialisation de votre mot de passe GEOSECTOR';
|
||||
$mail->Body = EmailTemplates::getLostPasswordTemplate($name, $data['username'] ?? '', $data['password'] ?? '');
|
||||
break;
|
||||
|
||||
case 'alert':
|
||||
$mail->Subject = $data['subject'] ?? 'Alerte GEOSECTOR';
|
||||
$mail->Body = EmailTemplates::getAlertTemplate($data['subject'] ?? 'Alerte', $data['message'] ?? '');
|
||||
break;
|
||||
|
||||
case 'security_alert':
|
||||
$mail->Subject = $data['subject'] ?? 'Alerte de Sécurité GEOSECTOR';
|
||||
$mail->Body = $data['body'] ?? EmailTemplates::getAlertTemplate($data['subject'] ?? 'Alerte de Sécurité', $data['message'] ?? '');
|
||||
break;
|
||||
|
||||
case 'receipt':
|
||||
$mail->Subject = 'Reçu de passage GEOSECTOR';
|
||||
$mail->Body = EmailTemplates::getReceiptTemplate(
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
require_once __DIR__ . '/../Config/AppConfig.php';
|
||||
require_once __DIR__ . '/LogService.php';
|
||||
|
||||
use Exception;
|
||||
use AppConfig;
|
||||
use App\Services\LogService;
|
||||
|
||||
/**
|
||||
* Service de chiffrement et compression des sauvegardes
|
||||
*
|
||||
*
|
||||
* Ce service gère le processus complet de sécurisation des backups JSON :
|
||||
* 1. Compression GZIP pour réduire la taille
|
||||
* 2. Chiffrement AES-256-CBC pour la sécurité
|
||||
|
||||
319
api/src/Services/BuildingService.php
Normal file
319
api/src/Services/BuildingService.php
Normal file
@@ -0,0 +1,319 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use AppConfig;
|
||||
|
||||
require_once __DIR__ . '/../Config/AppConfig.php';
|
||||
|
||||
/**
|
||||
* Service de gestion des bâtiments
|
||||
*
|
||||
* Ce service enrichit les adresses trouvées par AddressService avec les métadonnées
|
||||
* des bâtiments depuis la base de données 'batiments'.
|
||||
*
|
||||
* Fonctionnalités :
|
||||
* - Enrichissement des adresses avec données bâtiment (nb_log, residence, etc.)
|
||||
* - Identification automatique des immeubles (fk_habitat=2)
|
||||
* - Gestion des adresses sans bâtiment (maisons individuelles, fk_habitat=1)
|
||||
*/
|
||||
class BuildingService
|
||||
{
|
||||
private ?PDO $dbBuildings = null;
|
||||
private bool $connected = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->initConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise la connexion à la base de données batiments
|
||||
*/
|
||||
private function initConnection(): void
|
||||
{
|
||||
try {
|
||||
$config = AppConfig::getInstance()->getBuildingsDatabaseConfig();
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=utf8mb4',
|
||||
$config['host'],
|
||||
$config['name']
|
||||
);
|
||||
|
||||
$this->dbBuildings = new PDO(
|
||||
$dsn,
|
||||
$config['username'],
|
||||
$config['password'],
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]
|
||||
);
|
||||
|
||||
$this->connected = true;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("Erreur de connexion à la base batiments : " . $e->getMessage());
|
||||
$this->connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la connexion à la base batiments est active
|
||||
*
|
||||
* @return bool True si connecté
|
||||
*/
|
||||
public function isConnected(): bool
|
||||
{
|
||||
return $this->connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrichit une liste d'adresses avec les données bâtiment
|
||||
*
|
||||
* Pour chaque adresse trouvée par AddressService, cette méthode cherche
|
||||
* si un bâtiment existe dans bat{dept} avec le lien cle_interop_adr.
|
||||
*
|
||||
* @param array $addresses Liste d'adresses depuis AddressService
|
||||
* Format attendu : ['id' => 'cp22.12345', 'departement' => '22', ...]
|
||||
* @return array Adresses enrichies avec :
|
||||
* - fk_batiment : batiment_groupe_id (ou null)
|
||||
* - fk_habitat : 1=individuel, 2=collectif
|
||||
* - nb_niveau : Nombre d'étages (ou null)
|
||||
* - nb_log : Nombre de logements (ou null)
|
||||
* - residence : Nom de la copropriété (ou '')
|
||||
* - alt_sol : Altitude sol (ou null)
|
||||
*/
|
||||
public function enrichAddresses(array $addresses): array
|
||||
{
|
||||
// Si pas de connexion, retourner les adresses sans enrichissement
|
||||
if (!$this->isConnected() || empty($addresses)) {
|
||||
// Ajouter fk_habitat=1 par défaut (maison individuelle)
|
||||
foreach ($addresses as &$address) {
|
||||
$address['fk_batiment'] = null;
|
||||
$address['fk_habitat'] = 1;
|
||||
$address['nb_niveau'] = null;
|
||||
$address['nb_log'] = null;
|
||||
$address['residence'] = '';
|
||||
$address['alt_sol'] = null;
|
||||
}
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
try {
|
||||
// Grouper les adresses par département
|
||||
$addressesByDept = [];
|
||||
foreach ($addresses as $address) {
|
||||
$dept = $this->extractDepartmentFromAddress($address);
|
||||
if ($dept) {
|
||||
$addressesByDept[$dept][] = $address;
|
||||
}
|
||||
}
|
||||
|
||||
// Enrichir les adresses département par département
|
||||
$enrichedAddresses = [];
|
||||
|
||||
foreach ($addressesByDept as $dept => $deptAddresses) {
|
||||
$enrichedDept = $this->enrichAddressesByDepartment((string)$dept, $deptAddresses);
|
||||
$enrichedAddresses = array_merge($enrichedAddresses, $enrichedDept);
|
||||
}
|
||||
|
||||
return $enrichedAddresses;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("Erreur lors de l'enrichissement des adresses avec batiments : " . $e->getMessage());
|
||||
|
||||
// En cas d'erreur, retourner les adresses avec valeurs par défaut
|
||||
foreach ($addresses as &$address) {
|
||||
$address['fk_batiment'] = null;
|
||||
$address['fk_habitat'] = 1;
|
||||
$address['nb_niveau'] = null;
|
||||
$address['nb_log'] = null;
|
||||
$address['residence'] = '';
|
||||
$address['alt_sol'] = null;
|
||||
}
|
||||
return $addresses;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrichit les adresses d'un département spécifique
|
||||
*
|
||||
* @param string $dept Code du département (ex: '22', '35')
|
||||
* @param array $addresses Liste des adresses du département
|
||||
* @return array Adresses enrichies
|
||||
*/
|
||||
private function enrichAddressesByDepartment(string $dept, array $addresses): array
|
||||
{
|
||||
// Vérifier que la table bat{dept} existe
|
||||
if (!$this->tableExists("bat{$dept}")) {
|
||||
// Table inexistante, retourner avec valeurs par défaut
|
||||
foreach ($addresses as &$address) {
|
||||
$address['fk_batiment'] = null;
|
||||
$address['fk_habitat'] = 1;
|
||||
$address['nb_niveau'] = null;
|
||||
$address['nb_log'] = null;
|
||||
$address['residence'] = '';
|
||||
$address['alt_sol'] = null;
|
||||
}
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
// Créer un mapping address_id => address pour retrouver rapidement
|
||||
$addressMap = [];
|
||||
$addressIds = [];
|
||||
|
||||
foreach ($addresses as $address) {
|
||||
// Extraire l'ID BAN (partie après le point)
|
||||
$addressId = $this->extractAddressId($address['id']);
|
||||
if ($addressId) {
|
||||
$addressIds[] = $addressId;
|
||||
$addressMap[$addressId] = $address;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($addressIds)) {
|
||||
// Pas d'IDs valides, retourner avec valeurs par défaut
|
||||
foreach ($addresses as &$address) {
|
||||
$address['fk_batiment'] = null;
|
||||
$address['fk_habitat'] = 1;
|
||||
$address['nb_niveau'] = null;
|
||||
$address['nb_log'] = null;
|
||||
$address['residence'] = '';
|
||||
$address['alt_sol'] = null;
|
||||
}
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
// Requête pour récupérer les bâtiments
|
||||
$placeholders = str_repeat('?,', count($addressIds) - 1) . '?';
|
||||
$query = "
|
||||
SELECT
|
||||
b.cle_interop_adr,
|
||||
b.batiment_groupe_id,
|
||||
b.nb_niveau,
|
||||
b.nb_log,
|
||||
b.residence,
|
||||
b.altitude_sol_mean
|
||||
FROM bat{$dept} b
|
||||
WHERE b.cle_interop_adr IN ($placeholders)
|
||||
";
|
||||
|
||||
$stmt = $this->dbBuildings->prepare($query);
|
||||
$stmt->execute($addressIds);
|
||||
$buildings = $stmt->fetchAll();
|
||||
|
||||
// Créer un mapping cle_interop_adr => building
|
||||
$buildingMap = [];
|
||||
foreach ($buildings as $building) {
|
||||
$buildingMap[$building['cle_interop_adr']] = $building;
|
||||
}
|
||||
|
||||
// Enrichir les adresses
|
||||
$enrichedAddresses = [];
|
||||
foreach ($addresses as $address) {
|
||||
$addressId = $this->extractAddressId($address['id']);
|
||||
|
||||
if ($addressId && isset($buildingMap[$addressId])) {
|
||||
// Bâtiment trouvé : enrichir avec ses données
|
||||
$building = $buildingMap[$addressId];
|
||||
$address['fk_batiment'] = $building['batiment_groupe_id'];
|
||||
$address['fk_habitat'] = 2; // Collectif
|
||||
$address['nb_niveau'] = $building['nb_niveau'];
|
||||
$address['nb_log'] = $building['nb_log'];
|
||||
$address['residence'] = $building['residence'] ?? '';
|
||||
$address['alt_sol'] = $building['altitude_sol_mean'];
|
||||
} else {
|
||||
// Pas de bâtiment : maison individuelle
|
||||
$address['fk_batiment'] = null;
|
||||
$address['fk_habitat'] = 1; // Individuel
|
||||
$address['nb_niveau'] = null;
|
||||
$address['nb_log'] = null;
|
||||
$address['residence'] = '';
|
||||
$address['alt_sol'] = null;
|
||||
}
|
||||
|
||||
$enrichedAddresses[] = $address;
|
||||
}
|
||||
|
||||
return $enrichedAddresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait le code département depuis une adresse
|
||||
*
|
||||
* @param array $address Adresse depuis AddressService
|
||||
* @return string|null Code département (ex: '22', '35')
|
||||
*/
|
||||
private function extractDepartmentFromAddress(array $address): ?string
|
||||
{
|
||||
// Méthode 1 : Clé 'departement' directement
|
||||
if (isset($address['departement'])) {
|
||||
return $address['departement'];
|
||||
}
|
||||
|
||||
// Méthode 2 : Depuis code_postal
|
||||
if (isset($address['code_postal'])) {
|
||||
$cp = $address['code_postal'];
|
||||
if (strlen($cp) === 4) {
|
||||
return '0' . substr($cp, 0, 1);
|
||||
}
|
||||
return substr($cp, 0, 2);
|
||||
}
|
||||
|
||||
// Méthode 3 : Depuis l'ID (format cp22.12345)
|
||||
if (isset($address['id']) && strpos($address['id'], 'cp') === 0) {
|
||||
preg_match('/^cp(\d{2})\./', $address['id'], $matches);
|
||||
if (isset($matches[1])) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait l'ID BAN depuis l'ID complet (ex: cp22.12345 → 12345)
|
||||
*
|
||||
* @param string $fullId ID complet de l'adresse
|
||||
* @return string|null ID BAN
|
||||
*/
|
||||
private function extractAddressId(string $fullId): ?string
|
||||
{
|
||||
if (strpos($fullId, '.') !== false) {
|
||||
$parts = explode('.', $fullId);
|
||||
return $parts[1] ?? null;
|
||||
}
|
||||
return $fullId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une table existe dans la base batiments
|
||||
*
|
||||
* @param string $tableName Nom de la table (ex: 'bat22')
|
||||
* @return bool True si la table existe
|
||||
*/
|
||||
private function tableExists(string $tableName): bool
|
||||
{
|
||||
try {
|
||||
// SHOW TABLES LIKE ne supporte pas les placeholders PDO
|
||||
// On échappe manuellement le nom de table (alphanumérique uniquement)
|
||||
if (!preg_match('/^[a-zA-Z0-9_]+$/', $tableName)) {
|
||||
error_log("Nom de table invalide : {$tableName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $this->dbBuildings->query("SHOW TABLES LIKE '{$tableName}'");
|
||||
return $stmt && $stmt->rowCount() > 0;
|
||||
} catch (PDOException $e) {
|
||||
error_log("Erreur lors de la vérification de la table {$tableName} : " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Database;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use RuntimeException;
|
||||
|
||||
class DepartmentBoundaryService {
|
||||
private PDO $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance();
|
||||
$this->db = \Database::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class EmailTemplates {
|
||||
/**
|
||||
* Template d'email de bienvenue
|
||||
@@ -12,7 +14,7 @@ class EmailTemplates {
|
||||
Votre compte a été créé avec succès sur <b>GeoSector</b>.<br><br>
|
||||
<b>Identifiant :</b> $username<br>
|
||||
<b>Mot de passe :</b> $password<br><br>
|
||||
Vous pouvez vous connecter dès maintenant sur <a href=\"https://app.geosector.fr\">app.geosector.fr</a><br><br>
|
||||
Vous pouvez vous connecter dès maintenant sur <a href=\"https://app3.geosector.fr\">app3.geosector.fr</a><br><br>
|
||||
À très bientôt,<br>
|
||||
L'équipe GeoSector";
|
||||
}
|
||||
@@ -36,7 +38,7 @@ class EmailTemplates {
|
||||
</p>
|
||||
<p>
|
||||
Une fois que vous aurez reçu votre mot de passe, vous pourrez vous connecter sur
|
||||
<a href=\"https://app.geosector.fr\" style='color:#007bff;'>app.geosector.fr</a>
|
||||
<a href=\"https://app3.geosector.fr\" style='color:#007bff;'>app3.geosector.fr</a>
|
||||
</p>
|
||||
<br>
|
||||
À très bientôt,<br>
|
||||
@@ -59,7 +61,7 @@ class EmailTemplates {
|
||||
</p>
|
||||
<p>
|
||||
Vous pouvez maintenant vous connecter avec votre identifiant (reçu dans un email précédent)
|
||||
et ce mot de passe sur <a href=\"https://app.geosector.fr\" style='color:#007bff;'>app.geosector.fr</a>
|
||||
et ce mot de passe sur <a href=\"https://app3.geosector.fr\" style='color:#007bff;'>app3.geosector.fr</a>
|
||||
</p>
|
||||
<p style='background:#fff3cd; padding:10px; border-radius:5px; margin-top:20px;'>
|
||||
<b>Rappel :</b> Ne communiquez jamais votre mot de passe à un tiers. L'équipe GeoSector
|
||||
@@ -78,7 +80,7 @@ class EmailTemplates {
|
||||
Bonjour $name,<br><br>
|
||||
Vous avez demandé la réinitialisation de votre mot de passe sur <b>GeoSector</b>.<br><br>
|
||||
<b>Nouveau mot de passe :</b> $password<br><br>
|
||||
Vous pouvez vous connecter avec ce nouveau mot de passe sur <a href=\"https://app.geosector.fr\">app.geosector.fr</a><br><br>
|
||||
Vous pouvez vous connecter avec ce nouveau mot de passe sur <a href=\"https://app3.geosector.fr\">app3.geosector.fr</a><br><br>
|
||||
À très bientôt,<br>
|
||||
L'équipe GeoSector";
|
||||
}
|
||||
@@ -95,7 +97,8 @@ class EmailTemplates {
|
||||
}
|
||||
|
||||
/**
|
||||
* Template de reçu de passage
|
||||
* Template de reçu de passage (ancien format simple)
|
||||
* @deprecated Utiliser getReceiptDonationTemplate() pour les reçus de don
|
||||
*/
|
||||
public static function getReceiptTemplate(string $name, string $date, string $address, string $amount, string $paymentMethod): string {
|
||||
return "
|
||||
@@ -126,4 +129,69 @@ class EmailTemplates {
|
||||
<p>À bientôt,</p>
|
||||
<p>L'équipe GeoSector</p>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Template d'email pour reçu de don avec pièce jointe PDF
|
||||
* Utilisé lors de l'envoi automatique des reçus pour les dons (fk_type=1)
|
||||
*
|
||||
* @param array $data Données du reçu (passage_id, entite_name, donor_name, amount, donation_date, payment_method, entite_address, entite_email)
|
||||
* @return string HTML de l'email
|
||||
*/
|
||||
public static function getReceiptDonationTemplate(array $data): string {
|
||||
// Extraction des données avec valeurs par défaut
|
||||
$passageId = $data['passage_id'] ?? '';
|
||||
$entiteName = htmlspecialchars($data['entite_name'] ?? 'Amicale des Sapeurs-Pompiers');
|
||||
$donorName = htmlspecialchars($data['donor_name'] ?? '');
|
||||
$amount = htmlspecialchars($data['amount'] ?? '0,00');
|
||||
$donationDate = htmlspecialchars($data['donation_date'] ?? date('d/m/Y'));
|
||||
$paymentMethod = htmlspecialchars($data['payment_method'] ?? 'Espèces');
|
||||
$entiteAddress = htmlspecialchars($data['entite_address'] ?? '');
|
||||
$entiteEmail = htmlspecialchars($data['entite_email'] ?? '');
|
||||
|
||||
return "
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.passage-id { text-align: right; font-size: 10px; color: #999; margin-bottom: 20px; }
|
||||
.header { background-color: #f4f4f4; padding: 20px; text-align: center; border-radius: 5px; }
|
||||
.content { padding: 20px 0; }
|
||||
.footer { background-color: #f4f4f4; padding: 15px; text-align: center; font-size: 12px; border-radius: 5px; margin-top: 20px; }
|
||||
.amount { font-size: 24px; font-weight: bold; color: #2c5aa0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='container'>
|
||||
<div class='passage-id'>$passageId</div>
|
||||
|
||||
<div class='header'>
|
||||
<h2>$entiteName</h2>
|
||||
</div>
|
||||
|
||||
<div class='content'>
|
||||
<p>Bonjour Mme/M. $donorName,</p>
|
||||
|
||||
<p>Nous vous remercions chaleureusement pour votre don de <span class='amount'>$amount €</span> effectué le $donationDate.</p>
|
||||
|
||||
<p><strong>Mode de paiement :</strong> $paymentMethod</p>
|
||||
|
||||
<p>Vous trouverez ci-joint votre reçu.</p>
|
||||
|
||||
<p>Votre soutien est précieux pour nous permettre de poursuivre nos actions.</p>
|
||||
|
||||
<p>Cordialement,<br>L'équipe de $entiteName</p>
|
||||
</div>
|
||||
|
||||
<div class='footer'>
|
||||
<p><strong>$entiteName</strong><br>
|
||||
$entiteAddress<br>
|
||||
$entiteEmail</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
}
|
||||
}
|
||||
|
||||
533
api/src/Services/EventLogService.php
Normal file
533
api/src/Services/EventLogService.php
Normal file
@@ -0,0 +1,533 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use ClientDetector;
|
||||
use Session;
|
||||
|
||||
/**
|
||||
* EventLogService - Système de logs d'événements JSONL
|
||||
*
|
||||
* Enregistre tous les événements métier (authentification, CRUD passages/secteurs/users/entités/opérations)
|
||||
* dans des fichiers JSONL quotidiens pour statistiques et audit.
|
||||
*
|
||||
* Format : Un événement = une ligne JSON
|
||||
* Stockage : /logs/events/YYYY-MM-DD.jsonl
|
||||
* Rétention : 15 mois (compression après 30 jours)
|
||||
*
|
||||
* @see docs/EVENTS-LOG.md
|
||||
*/
|
||||
class EventLogService
|
||||
{
|
||||
/** @var string Chemin du dossier de logs d'événements */
|
||||
private const LOG_DIR = __DIR__ . '/../../logs/events';
|
||||
|
||||
/** @var int Permissions du dossier */
|
||||
private const DIR_PERMISSIONS = 0750;
|
||||
|
||||
/** @var int Permissions des fichiers */
|
||||
private const FILE_PERMISSIONS = 0640;
|
||||
|
||||
// ==================== MÉTHODES D'AUTHENTIFICATION ====================
|
||||
|
||||
/**
|
||||
* Log une connexion réussie
|
||||
*
|
||||
* @param int $userId ID utilisateur (users.id)
|
||||
* @param int|null $entityId ID entité
|
||||
* @param string $username Nom d'utilisateur
|
||||
*/
|
||||
public static function logLoginSuccess(int $userId, ?int $entityId, string $username): void
|
||||
{
|
||||
self::writeEvent('login_success', [
|
||||
'user_id' => $userId,
|
||||
'entity_id' => $entityId,
|
||||
'username' => $username
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log une tentative de connexion échouée
|
||||
*
|
||||
* @param string $username Nom d'utilisateur tenté
|
||||
* @param string $reason Raison (invalid_password, user_not_found, account_inactive, blocked_ip)
|
||||
* @param int $attempt Numéro de tentative
|
||||
*/
|
||||
public static function logLoginFailed(string $username, string $reason, int $attempt = 1): void
|
||||
{
|
||||
self::writeEvent('login_failed', [
|
||||
'username' => $username,
|
||||
'reason' => $reason,
|
||||
'attempt' => $attempt
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log une déconnexion
|
||||
*
|
||||
* @param int $userId ID utilisateur
|
||||
* @param int|null $entityId ID entité
|
||||
* @param int $sessionDuration Durée session en secondes
|
||||
*/
|
||||
public static function logLogout(int $userId, ?int $entityId, int $sessionDuration = 0): void
|
||||
{
|
||||
self::writeEvent('logout', [
|
||||
'user_id' => $userId,
|
||||
'entity_id' => $entityId,
|
||||
'session_duration' => $sessionDuration
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES PASSAGES ====================
|
||||
|
||||
/**
|
||||
* Log la création d'un passage
|
||||
*
|
||||
* @param int $passageId ID du passage créé
|
||||
* @param int $operationId ID opération
|
||||
* @param int $sectorId ID secteur
|
||||
* @param float $amount Montant
|
||||
* @param string $paymentType Type paiement (cash, stripe, check, etc.)
|
||||
*/
|
||||
public static function logPassageCreated(
|
||||
int $passageId,
|
||||
int $operationId,
|
||||
int $sectorId,
|
||||
float $amount,
|
||||
string $paymentType
|
||||
): void {
|
||||
self::writeEvent('passage_created', [
|
||||
'passage_id' => $passageId,
|
||||
'operation_id' => $operationId,
|
||||
'sector_id' => $sectorId,
|
||||
'amount' => $amount,
|
||||
'payment_type' => $paymentType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la modification d'un passage
|
||||
*
|
||||
* @param int $passageId ID du passage
|
||||
* @param array $changes Tableau des changements ['field' => ['old' => val, 'new' => val]]
|
||||
*/
|
||||
public static function logPassageUpdated(int $passageId, array $changes): void
|
||||
{
|
||||
self::writeEvent('passage_updated', [
|
||||
'passage_id' => $passageId,
|
||||
'changes' => $changes
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la suppression d'un passage
|
||||
*
|
||||
* @param int $passageId ID du passage
|
||||
* @param int $operationId ID opération
|
||||
* @param bool $softDelete Suppression logique ou physique
|
||||
*/
|
||||
public static function logPassageDeleted(int $passageId, int $operationId, bool $softDelete = true): void
|
||||
{
|
||||
$userId = Session::getUserId();
|
||||
self::writeEvent('passage_deleted', [
|
||||
'passage_id' => $passageId,
|
||||
'operation_id' => $operationId,
|
||||
'deleted_by' => $userId,
|
||||
'soft_delete' => $softDelete
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES SECTEURS ====================
|
||||
|
||||
/**
|
||||
* Log la création d'un secteur
|
||||
*
|
||||
* @param int $sectorId ID du secteur créé
|
||||
* @param int $operationId ID opération
|
||||
* @param string $sectorName Nom du secteur
|
||||
*/
|
||||
public static function logSectorCreated(int $sectorId, int $operationId, string $sectorName): void
|
||||
{
|
||||
self::writeEvent('sector_created', [
|
||||
'sector_id' => $sectorId,
|
||||
'operation_id' => $operationId,
|
||||
'sector_name' => $sectorName
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la modification d'un secteur
|
||||
*
|
||||
* @param int $sectorId ID du secteur
|
||||
* @param int $operationId ID opération
|
||||
* @param array $changes Tableau des changements
|
||||
*/
|
||||
public static function logSectorUpdated(int $sectorId, int $operationId, array $changes): void
|
||||
{
|
||||
self::writeEvent('sector_updated', [
|
||||
'sector_id' => $sectorId,
|
||||
'operation_id' => $operationId,
|
||||
'changes' => $changes
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la suppression d'un secteur
|
||||
*
|
||||
* @param int $sectorId ID du secteur
|
||||
* @param int $operationId ID opération
|
||||
* @param bool $softDelete Suppression logique ou physique
|
||||
*/
|
||||
public static function logSectorDeleted(int $sectorId, int $operationId, bool $softDelete = true): void
|
||||
{
|
||||
$userId = Session::getUserId();
|
||||
self::writeEvent('sector_deleted', [
|
||||
'sector_id' => $sectorId,
|
||||
'operation_id' => $operationId,
|
||||
'deleted_by' => $userId,
|
||||
'soft_delete' => $softDelete
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES USERS ====================
|
||||
|
||||
/**
|
||||
* Log la création d'un utilisateur
|
||||
*
|
||||
* @param int $newUserId ID utilisateur créé
|
||||
* @param int $entityId ID entité
|
||||
* @param int $roleId ID rôle
|
||||
* @param string $username Nom d'utilisateur
|
||||
*/
|
||||
public static function logUserCreated(int $newUserId, int $entityId, int $roleId, string $username): void
|
||||
{
|
||||
$createdBy = Session::getUserId();
|
||||
self::writeEvent('user_created', [
|
||||
'new_user_id' => $newUserId,
|
||||
'entity_id' => $entityId,
|
||||
'created_by' => $createdBy,
|
||||
'role_id' => $roleId,
|
||||
'username' => $username
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la modification d'un utilisateur
|
||||
*
|
||||
* @param int $userId ID utilisateur modifié
|
||||
* @param array $changes Tableau des changements (booléen pour champs chiffrés)
|
||||
*/
|
||||
public static function logUserUpdated(int $userId, array $changes): void
|
||||
{
|
||||
$updatedBy = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
|
||||
self::writeEvent('user_updated', [
|
||||
'user_id' => $userId,
|
||||
'entity_id' => $entityId,
|
||||
'updated_by' => $updatedBy,
|
||||
'changes' => $changes
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la suppression d'un utilisateur
|
||||
*
|
||||
* @param int $userId ID utilisateur supprimé
|
||||
* @param bool $softDelete Suppression logique ou physique
|
||||
*/
|
||||
public static function logUserDeleted(int $userId, bool $softDelete = true): void
|
||||
{
|
||||
$deletedBy = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
|
||||
self::writeEvent('user_deleted', [
|
||||
'user_id' => $userId,
|
||||
'entity_id' => $entityId,
|
||||
'deleted_by' => $deletedBy,
|
||||
'soft_delete' => $softDelete
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES ENTITÉS ====================
|
||||
|
||||
/**
|
||||
* Log la création d'une entité
|
||||
*
|
||||
* @param int $entityId ID entité créée
|
||||
* @param int $entityTypeId Type d'entité
|
||||
* @param string $postalCode Code postal
|
||||
*/
|
||||
public static function logEntityCreated(int $entityId, int $entityTypeId, string $postalCode): void
|
||||
{
|
||||
$createdBy = Session::getUserId() ?? 1; // Super-admin par défaut
|
||||
self::writeEvent('entity_created', [
|
||||
'entity_id' => $entityId,
|
||||
'created_by' => $createdBy,
|
||||
'entity_type_id' => $entityTypeId,
|
||||
'postal_code' => $postalCode
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la modification d'une entité
|
||||
*
|
||||
* @param int $entityId ID entité
|
||||
* @param array $changes Tableau des changements (booléen pour champs chiffrés)
|
||||
*/
|
||||
public static function logEntityUpdated(int $entityId, array $changes): void
|
||||
{
|
||||
$updatedBy = Session::getUserId();
|
||||
self::writeEvent('entity_updated', [
|
||||
'entity_id' => $entityId,
|
||||
'user_id' => $updatedBy,
|
||||
'updated_by' => $updatedBy,
|
||||
'changes' => $changes
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la suppression d'une entité
|
||||
*
|
||||
* @param int $entityId ID entité
|
||||
* @param string $reason Raison de la suppression
|
||||
*/
|
||||
public static function logEntityDeleted(int $entityId, string $reason = ''): void
|
||||
{
|
||||
$deletedBy = Session::getUserId() ?? 1;
|
||||
self::writeEvent('entity_deleted', [
|
||||
'entity_id' => $entityId,
|
||||
'deleted_by' => $deletedBy,
|
||||
'soft_delete' => true,
|
||||
'reason' => $reason
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES OPÉRATIONS ====================
|
||||
|
||||
/**
|
||||
* Log la création d'une opération
|
||||
*
|
||||
* @param int $operationId ID opération créée
|
||||
* @param string $dateStart Date début (YYYY-MM-DD)
|
||||
* @param string $dateEnd Date fin (YYYY-MM-DD)
|
||||
*/
|
||||
public static function logOperationCreated(int $operationId, string $dateStart, string $dateEnd): void
|
||||
{
|
||||
$entityId = Session::getEntityId();
|
||||
$createdBy = Session::getUserId();
|
||||
|
||||
self::writeEvent('operation_created', [
|
||||
'operation_id' => $operationId,
|
||||
'entity_id' => $entityId,
|
||||
'created_by' => $createdBy,
|
||||
'date_start' => $dateStart,
|
||||
'date_end' => $dateEnd
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la modification d'une opération
|
||||
*
|
||||
* @param int $operationId ID opération
|
||||
* @param array $changes Tableau des changements
|
||||
*/
|
||||
public static function logOperationUpdated(int $operationId, array $changes): void
|
||||
{
|
||||
$entityId = Session::getEntityId();
|
||||
$updatedBy = Session::getUserId();
|
||||
|
||||
self::writeEvent('operation_updated', [
|
||||
'operation_id' => $operationId,
|
||||
'entity_id' => $entityId,
|
||||
'updated_by' => $updatedBy,
|
||||
'changes' => $changes
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la suppression d'une opération
|
||||
*
|
||||
* @param int $operationId ID opération
|
||||
* @param bool $softDelete Suppression logique ou physique
|
||||
*/
|
||||
public static function logOperationDeleted(int $operationId, bool $softDelete = true): void
|
||||
{
|
||||
$entityId = Session::getEntityId();
|
||||
$deletedBy = Session::getUserId();
|
||||
|
||||
self::writeEvent('operation_deleted', [
|
||||
'operation_id' => $operationId,
|
||||
'entity_id' => $entityId,
|
||||
'deleted_by' => $deletedBy,
|
||||
'soft_delete' => $softDelete
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES PRIVÉES ====================
|
||||
|
||||
/**
|
||||
* Méthode centrale d'écriture d'un événement
|
||||
*
|
||||
* @param string $eventName Nom de l'événement
|
||||
* @param array $data Données spécifiques à l'événement
|
||||
*/
|
||||
private static function writeEvent(string $eventName, array $data): void
|
||||
{
|
||||
try {
|
||||
// Enrichir avec timestamp, user_id, entity_id, IP, platform, app_version
|
||||
$event = self::enrichEvent($eventName, $data);
|
||||
|
||||
// Générer le chemin du fichier quotidien
|
||||
$filename = self::LOG_DIR . '/' . date('Y-m-d') . '.jsonl';
|
||||
|
||||
// Créer le dossier si nécessaire
|
||||
self::ensureLogDirectoryExists();
|
||||
|
||||
// Encoder en JSON compact (une ligne)
|
||||
$jsonLine = json_encode($event, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
|
||||
|
||||
// Écrire en mode append
|
||||
if (file_put_contents($filename, $jsonLine, FILE_APPEND | LOCK_EX) === false) {
|
||||
error_log("EventLogService: Impossible d'écrire dans {$filename}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Appliquer les permissions au fichier
|
||||
if (file_exists($filename)) {
|
||||
@chmod($filename, self::FILE_PERMISSIONS);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Ne jamais bloquer l'application si le logging échoue
|
||||
error_log("EventLogService: Erreur lors de l'écriture de l'événement {$eventName}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrichit un événement avec les métadonnées communes
|
||||
*
|
||||
* @param string $eventName Nom de l'événement
|
||||
* @param array $data Données de l'événement
|
||||
* @return array Événement enrichi
|
||||
*/
|
||||
private static function enrichEvent(string $eventName, array $data): array
|
||||
{
|
||||
// Récupérer les informations client
|
||||
$clientInfo = ClientDetector::getClientInfo();
|
||||
|
||||
// Structure de base
|
||||
$event = [
|
||||
'timestamp' => gmdate('Y-m-d\TH:i:s\Z'), // ISO 8601 UTC
|
||||
'event' => $eventName,
|
||||
];
|
||||
|
||||
// Ajouter user_id si disponible et pas déjà dans $data
|
||||
if (!isset($data['user_id'])) {
|
||||
$userId = Session::getUserId();
|
||||
if ($userId !== null) {
|
||||
$event['user_id'] = $userId;
|
||||
}
|
||||
}
|
||||
|
||||
// Ajouter entity_id si disponible et pas déjà dans $data
|
||||
if (!isset($data['entity_id'])) {
|
||||
$entityId = Session::getEntityId();
|
||||
if ($entityId !== null) {
|
||||
$event['entity_id'] = $entityId;
|
||||
}
|
||||
}
|
||||
|
||||
// Fusionner avec les données spécifiques
|
||||
$event = array_merge($event, $data);
|
||||
|
||||
// Ajouter IP
|
||||
$event['ip'] = $clientInfo['ip'];
|
||||
|
||||
// Ajouter platform
|
||||
$event['platform'] = self::getPlatform($clientInfo);
|
||||
|
||||
// Ajouter app_version si mobile
|
||||
if ($event['platform'] === 'ios' || $event['platform'] === 'android') {
|
||||
$appVersion = self::getAppVersion($clientInfo);
|
||||
if ($appVersion !== null) {
|
||||
$event['app_version'] = $appVersion;
|
||||
}
|
||||
}
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine la plateforme (ios, android, web)
|
||||
*
|
||||
* @param array $clientInfo Informations client de ClientDetector
|
||||
* @return string Platform (ios|android|web)
|
||||
*/
|
||||
private static function getPlatform(array $clientInfo): string
|
||||
{
|
||||
if ($clientInfo['type'] !== 'mobile') {
|
||||
return 'web';
|
||||
}
|
||||
|
||||
$userAgent = $clientInfo['userAgent'];
|
||||
|
||||
// Détection iOS
|
||||
if (stripos($userAgent, 'iOS') !== false ||
|
||||
stripos($userAgent, 'iPhone') !== false ||
|
||||
stripos($userAgent, 'iPad') !== false) {
|
||||
return 'ios';
|
||||
}
|
||||
|
||||
// Détection Android
|
||||
if (stripos($userAgent, 'Android') !== false) {
|
||||
return 'android';
|
||||
}
|
||||
|
||||
// Par défaut mobile générique = web
|
||||
return 'web';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait la version de l'application depuis le User-Agent
|
||||
* Format attendu: AppName/VersionNumber ou Platform/Version AppName/Version
|
||||
*
|
||||
* @param array $clientInfo Informations client
|
||||
* @return string|null Version de l'app ou null
|
||||
*/
|
||||
private static function getAppVersion(array $clientInfo): ?string
|
||||
{
|
||||
$userAgent = $clientInfo['userAgent'];
|
||||
|
||||
// Tentative extraction format: GeoSector/3.3.6
|
||||
if (preg_match('/GeoSector\/([0-9\.]+)/', $userAgent, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// Format alternatif: AppName/Version
|
||||
if (preg_match('/([A-Za-z0-9_]+)\/([0-9\.]+)/', $userAgent, $matches)) {
|
||||
return $matches[2];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée le dossier de logs si nécessaire avec les bonnes permissions
|
||||
*/
|
||||
private static function ensureLogDirectoryExists(): void
|
||||
{
|
||||
if (!is_dir(self::LOG_DIR)) {
|
||||
if (!@mkdir(self::LOG_DIR, self::DIR_PERMISSIONS, true)) {
|
||||
error_log("EventLogService: Impossible de créer le dossier " . self::LOG_DIR);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les permissions
|
||||
if (!is_writable(self::LOG_DIR)) {
|
||||
@chmod(self::LOG_DIR, self::DIR_PERMISSIONS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,24 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xls;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Csv;
|
||||
use PDO;
|
||||
use Database;
|
||||
use Session;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
|
||||
require_once __DIR__ . '/../Services/FileService.php';
|
||||
|
||||
|
||||
class ExportService {
|
||||
private \PDO $db;
|
||||
private PDO $db;
|
||||
private FileService $fileService;
|
||||
|
||||
public function __construct() {
|
||||
@@ -249,10 +256,11 @@ class ExportService {
|
||||
p.fk_habitat, p.appt, p.niveau, p.encrypted_name, p.encrypted_email,
|
||||
p.encrypted_phone, p.montant, p.fk_type_reglement, p.remarque,
|
||||
p.fk_user, p.fk_sector, p.fk_operation,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name, u.sect_name,
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name, u.sect_name,
|
||||
xtr.libelle as reglement_libelle
|
||||
FROM ope_pass p
|
||||
LEFT JOIN users u ON u.id = p.fk_user
|
||||
LEFT JOIN ope_users ou ON ou.id = p.fk_user
|
||||
LEFT JOIN users u ON u.id = ou.fk_user
|
||||
LEFT JOIN x_types_reglements xtr ON xtr.id = p.fk_type_reglement
|
||||
WHERE p.fk_operation = ? AND p.chk_active = 1
|
||||
';
|
||||
@@ -457,10 +465,11 @@ class ExportService {
|
||||
SELECT
|
||||
ous.id, ous.fk_sector, ous.fk_user, ous.created_at, ous.fk_operation,
|
||||
s.libelle as sector_name,
|
||||
u.encrypted_name as user_name, u.first_name
|
||||
u.encrypted_name as user_name, ou.first_name
|
||||
FROM ope_users_sectors ous
|
||||
INNER JOIN ope_sectors s ON s.id = ous.fk_sector
|
||||
INNER JOIN users u ON u.id = ous.fk_user
|
||||
INNER JOIN ope_users ou ON ou.id = ous.fk_user
|
||||
INNER JOIN users u ON u.id = ou.fk_user
|
||||
WHERE ous.fk_operation = ? AND ous.chk_active = 1
|
||||
ORDER BY s.libelle, u.encrypted_name
|
||||
';
|
||||
@@ -619,10 +628,11 @@ class ExportService {
|
||||
p.passed_at, p.fk_type, p.numero, p.rue_bis, p.rue, p.ville,
|
||||
p.fk_habitat, p.appt, p.niveau, p.encrypted_name, p.encrypted_email,
|
||||
p.encrypted_phone, p.montant, p.fk_type_reglement, p.remarque,
|
||||
u.encrypted_name as user_name, u.first_name as user_first_name, u.sect_name,
|
||||
u.encrypted_name as user_name, ou.first_name as user_first_name, u.sect_name,
|
||||
xtr.libelle as reglement_libelle
|
||||
FROM ope_pass p
|
||||
LEFT JOIN users u ON u.id = p.fk_user
|
||||
LEFT JOIN ope_users ou ON ou.id = p.fk_user
|
||||
LEFT JOIN users u ON u.id = ou.fk_user
|
||||
LEFT JOIN x_types_reglements xtr ON xtr.id = p.fk_type_reglement
|
||||
WHERE p.fk_operation = ? AND p.chk_active = 1
|
||||
';
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
use PDO;
|
||||
use Database;
|
||||
use Session;
|
||||
use App\Services\LogService;
|
||||
|
||||
class FileService {
|
||||
private const BASE_UPLOADS_DIR = '/var/www/geosector/api/uploads';
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use AppConfig;
|
||||
use ClientDetector;
|
||||
|
||||
class LogService {
|
||||
public static function log(string $message, array $metadata = []): void {
|
||||
|
||||
791
api/src/Services/MigrationService.php
Normal file
791
api/src/Services/MigrationService.php
Normal file
@@ -0,0 +1,791 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Database;
|
||||
|
||||
require_once __DIR__ . '/LogService.php';
|
||||
require_once __DIR__ . '/ApiService.php';
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Exception;
|
||||
use AppConfig;
|
||||
use App\Services\LogService;
|
||||
use App\Services\ApiService;
|
||||
|
||||
class MigrationService {
|
||||
private ?PDO $sourceDb = null;
|
||||
private PDO $targetDb;
|
||||
private AppConfig $appConfig;
|
||||
private bool $sshTunnelCreated = false;
|
||||
|
||||
// Configuration SSH et base source (TODO: à déplacer dans AppConfig)
|
||||
private const SSH_HOST = '212.83.164.111';
|
||||
private const SSH_PORT = 52266;
|
||||
private const SSH_USER = 'root';
|
||||
private const SSH_KEY_FILE = '/root/.ssh/id_rsa_db2';
|
||||
private const REMOTE_DB_HOST = '127.0.0.1';
|
||||
private const REMOTE_DB_PORT = 3306;
|
||||
private const SOURCE_DB_HOST = '127.0.0.1';
|
||||
private const SOURCE_DB_NAME = 'geosector';
|
||||
private const SOURCE_DB_USER = 'geo_front_user';
|
||||
private const SOURCE_DB_PASS = 'd66,GeoFront.User';
|
||||
private const SOURCE_DB_PORT = 13306;
|
||||
|
||||
// Ordre des étapes de migration
|
||||
private const MIGRATION_STEPS = [
|
||||
'x_devises',
|
||||
'x_entites_types',
|
||||
'x_types_passages',
|
||||
'x_types_reglements',
|
||||
'x_users_roles',
|
||||
'x_pays',
|
||||
'x_regions',
|
||||
'x_departements',
|
||||
'x_villes',
|
||||
'entites',
|
||||
'users',
|
||||
'operations',
|
||||
'ope_sectors',
|
||||
'sectors_adresses',
|
||||
'ope_users',
|
||||
'ope_users_sectors',
|
||||
'ope_pass',
|
||||
'ope_pass_histo',
|
||||
'medias'
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
$this->targetDb = Database::getInstance();
|
||||
$this->appConfig = AppConfig::getInstance();
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->closeSshTunnel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un tunnel SSH vers le serveur distant
|
||||
*/
|
||||
private function createSshTunnel(): bool {
|
||||
if ($this->sshTunnelCreated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vérifier si un tunnel est déjà en cours d'exécution
|
||||
$checkCommand = "ps aux | grep 'ssh -[vf]* -N -L " . self::SOURCE_DB_PORT . ":' | grep -v grep";
|
||||
exec($checkCommand, $output, $return_var);
|
||||
|
||||
if (empty($output)) {
|
||||
// Créer le tunnel SSH
|
||||
$command = sprintf(
|
||||
"ssh -f -N -o StrictHostKeyChecking=no -L %d:%s:%d -p %d %s@%s -i %s 2>&1",
|
||||
self::SOURCE_DB_PORT,
|
||||
self::REMOTE_DB_HOST,
|
||||
self::REMOTE_DB_PORT,
|
||||
self::SSH_PORT,
|
||||
self::SSH_USER,
|
||||
self::SSH_HOST,
|
||||
self::SSH_KEY_FILE
|
||||
);
|
||||
|
||||
exec($command, $output, $return_var);
|
||||
|
||||
if ($return_var !== 0) {
|
||||
LogService::log('Erreur lors de la création du tunnel SSH', [
|
||||
'level' => 'error',
|
||||
'output' => implode("\n", $output)
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attendre que le tunnel soit établi
|
||||
sleep(2);
|
||||
|
||||
// Vérification du tunnel
|
||||
$checkTunnel = "netstat -an | grep " . self::SOURCE_DB_PORT . " | grep LISTEN";
|
||||
exec($checkTunnel, $tunnelOutput);
|
||||
|
||||
if (empty($tunnelOutput)) {
|
||||
LogService::log('Le tunnel SSH semble créé mais le port n\'est pas en écoute', [
|
||||
'level' => 'warning',
|
||||
'port' => self::SOURCE_DB_PORT
|
||||
]);
|
||||
}
|
||||
|
||||
LogService::log('Tunnel SSH établi', [
|
||||
'level' => 'info',
|
||||
'port' => self::SOURCE_DB_PORT
|
||||
]);
|
||||
} else {
|
||||
LogService::log('Un tunnel SSH est déjà en cours d\'exécution', [
|
||||
'level' => 'info'
|
||||
]);
|
||||
}
|
||||
|
||||
$this->sshTunnelCreated = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ferme le tunnel SSH
|
||||
*/
|
||||
private function closeSshTunnel(): void {
|
||||
if (!$this->sshTunnelCreated) {
|
||||
return;
|
||||
}
|
||||
|
||||
$command = "ps aux | grep 'ssh -[vf]* -N -L " . self::SOURCE_DB_PORT . ":' | grep -v grep | awk '{print $2}' | xargs -r kill 2>/dev/null";
|
||||
exec($command);
|
||||
|
||||
$this->sshTunnelCreated = false;
|
||||
$this->sourceDb = null;
|
||||
|
||||
LogService::log('Tunnel SSH fermé', ['level' => 'info']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la connexion à la base source
|
||||
*/
|
||||
private function getSourceConnection(): PDO {
|
||||
if ($this->sourceDb !== null) {
|
||||
return $this->sourceDb;
|
||||
}
|
||||
|
||||
// Établir le tunnel SSH
|
||||
if (!$this->createSshTunnel()) {
|
||||
throw new Exception("Impossible d'établir le tunnel SSH");
|
||||
}
|
||||
|
||||
// Options de connexion PDO
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
|
||||
PDO::ATTR_TIMEOUT => 600
|
||||
];
|
||||
|
||||
if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) {
|
||||
$options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Se connecter à la base spécifique
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;port=%d',
|
||||
self::SOURCE_DB_HOST,
|
||||
self::SOURCE_DB_NAME,
|
||||
self::SOURCE_DB_PORT
|
||||
);
|
||||
|
||||
$this->sourceDb = new PDO($dsn, self::SOURCE_DB_USER, self::SOURCE_DB_PASS, $options);
|
||||
|
||||
LogService::log('Connexion établie à la base source', [
|
||||
'level' => 'info',
|
||||
'database' => self::SOURCE_DB_NAME
|
||||
]);
|
||||
|
||||
return $this->sourceDb;
|
||||
} catch (PDOException $e) {
|
||||
$this->closeSshTunnel();
|
||||
throw new Exception("Erreur de connexion à la base source: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Teste les connexions aux deux bases de données
|
||||
*/
|
||||
public function testConnections(): array {
|
||||
$result = [
|
||||
'source' => ['status' => 'error', 'message' => ''],
|
||||
'target' => ['status' => 'error', 'message' => '']
|
||||
];
|
||||
|
||||
// Test connexion source
|
||||
try {
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
$stmt = $sourceDb->query('SELECT DATABASE() as db, VERSION() as version');
|
||||
$info = $stmt->fetch();
|
||||
|
||||
$result['source'] = [
|
||||
'status' => 'success',
|
||||
'database' => $info['db'],
|
||||
'version' => $info['version'],
|
||||
'message' => 'Connexion réussie'
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$result['source']['message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
// Test connexion cible
|
||||
try {
|
||||
$stmt = $this->targetDb->query('SELECT DATABASE() as db, VERSION() as version');
|
||||
$info = $stmt->fetch();
|
||||
|
||||
$result['target'] = [
|
||||
'status' => 'success',
|
||||
'database' => $info['db'],
|
||||
'version' => $info['version'],
|
||||
'message' => 'Connexion réussie'
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$result['target']['message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les entités disponibles à migrer depuis la base source
|
||||
*/
|
||||
public function getAvailableEntities(): array {
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
|
||||
$stmt = $sourceDb->query("
|
||||
SELECT
|
||||
rowid as id,
|
||||
libelle as name,
|
||||
cp as postal_code,
|
||||
ville as city,
|
||||
active,
|
||||
date_creat as created_at
|
||||
FROM users_entites
|
||||
WHERE active = 1
|
||||
ORDER BY libelle ASC
|
||||
");
|
||||
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les détails d'une entité source
|
||||
*/
|
||||
public function getEntityDetails(int $id): ?array {
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
|
||||
$stmt = $sourceDb->prepare("
|
||||
SELECT
|
||||
e.rowid as id,
|
||||
e.libelle as name,
|
||||
e.cp as postal_code,
|
||||
e.ville as city,
|
||||
e.active,
|
||||
e.date_creat as created_at,
|
||||
(SELECT COUNT(*) FROM users WHERE fk_entite = e.rowid) as users_count,
|
||||
(SELECT COUNT(*) FROM operations WHERE fk_entite = e.rowid) as operations_count,
|
||||
(SELECT COUNT(*) FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.rowid
|
||||
WHERE o.fk_entite = e.rowid) as passages_count
|
||||
FROM users_entites e
|
||||
WHERE e.rowid = ?
|
||||
");
|
||||
|
||||
$stmt->execute([$id]);
|
||||
$entity = $stmt->fetch();
|
||||
|
||||
return $entity ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre une entité complète
|
||||
*/
|
||||
public function migrateEntity(
|
||||
int $entityId,
|
||||
?array $steps = null,
|
||||
bool $dryRun = false,
|
||||
bool $truncate = false
|
||||
): array {
|
||||
$startTime = microtime(true);
|
||||
$migrationId = 'mig_' . time() . '_' . $entityId;
|
||||
|
||||
// Si aucune étape spécifiée, migrer toutes les étapes
|
||||
if ($steps === null) {
|
||||
$steps = self::MIGRATION_STEPS;
|
||||
}
|
||||
|
||||
// Récupérer le nom de l'entité
|
||||
$entityDetails = $this->getEntityDetails($entityId);
|
||||
if (!$entityDetails) {
|
||||
throw new Exception("Entité $entityId non trouvée");
|
||||
}
|
||||
|
||||
$stepsCompleted = [];
|
||||
$totalRecords = 0;
|
||||
$totalErrors = 0;
|
||||
$totalWarnings = 0;
|
||||
|
||||
LogService::log('Début de migration entité', [
|
||||
'level' => 'info',
|
||||
'migration_id' => $migrationId,
|
||||
'entity_id' => $entityId,
|
||||
'entity_name' => $entityDetails['name'],
|
||||
'steps' => $steps,
|
||||
'dry_run' => $dryRun
|
||||
]);
|
||||
|
||||
foreach ($steps as $step) {
|
||||
try {
|
||||
$stepResult = $this->migrateStep($entityId, $step, $dryRun, []);
|
||||
$stepsCompleted[] = [
|
||||
'step' => $step,
|
||||
'status' => 'success',
|
||||
'records_migrated' => $stepResult['records_migrated'],
|
||||
'duration_ms' => $stepResult['duration_ms']
|
||||
];
|
||||
|
||||
$totalRecords += $stepResult['records_migrated'];
|
||||
$totalWarnings += count($stepResult['warnings'] ?? []);
|
||||
} catch (Exception $e) {
|
||||
$stepsCompleted[] = [
|
||||
'step' => $step,
|
||||
'status' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'duration_ms' => 0
|
||||
];
|
||||
$totalErrors++;
|
||||
|
||||
LogService::log('Erreur lors de la migration d\'une étape', [
|
||||
'level' => 'error',
|
||||
'migration_id' => $migrationId,
|
||||
'step' => $step,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
// Arrêter la migration en cas d'erreur critique
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$totalDuration = (microtime(true) - $startTime) * 1000;
|
||||
|
||||
LogService::log('Fin de migration entité', [
|
||||
'level' => 'info',
|
||||
'migration_id' => $migrationId,
|
||||
'entity_id' => $entityId,
|
||||
'total_records' => $totalRecords,
|
||||
'total_errors' => $totalErrors,
|
||||
'duration_ms' => $totalDuration
|
||||
]);
|
||||
|
||||
return [
|
||||
'entity_name' => $entityDetails['name'],
|
||||
'migration_id' => $migrationId,
|
||||
'steps_completed' => $stepsCompleted,
|
||||
'total_duration_ms' => round($totalDuration, 2),
|
||||
'summary' => [
|
||||
'total_records' => $totalRecords,
|
||||
'total_errors' => $totalErrors,
|
||||
'total_warnings' => $totalWarnings
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migre une étape spécifique pour une entité
|
||||
*/
|
||||
public function migrateStep(
|
||||
int $entityId,
|
||||
string $step,
|
||||
bool $dryRun = false,
|
||||
array $options = []
|
||||
): array {
|
||||
$startTime = microtime(true);
|
||||
|
||||
LogService::log("Début migration étape: $step", [
|
||||
'level' => 'info',
|
||||
'entity_id' => $entityId,
|
||||
'step' => $step,
|
||||
'dry_run' => $dryRun
|
||||
]);
|
||||
|
||||
// Appeler la méthode spécifique pour chaque étape
|
||||
$methodName = 'migrate' . $this->snakeToPascal($step);
|
||||
|
||||
if (!method_exists($this, $methodName)) {
|
||||
throw new Exception("Méthode de migration non trouvée pour l'étape: $step");
|
||||
}
|
||||
|
||||
$result = $this->$methodName($entityId, $dryRun, $options);
|
||||
|
||||
$duration = (microtime(true) - $startTime) * 1000;
|
||||
|
||||
LogService::log("Fin migration étape: $step", [
|
||||
'level' => 'info',
|
||||
'entity_id' => $entityId,
|
||||
'records_migrated' => $result['records_migrated'],
|
||||
'duration_ms' => $duration
|
||||
]);
|
||||
|
||||
return [
|
||||
'records_migrated' => $result['records_migrated'],
|
||||
'duration_ms' => round($duration, 2),
|
||||
'warnings' => $result['warnings'] ?? [],
|
||||
'details' => $result['details'] ?? []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit snake_case en PascalCase
|
||||
*/
|
||||
private function snakeToPascal(string $string): string {
|
||||
return str_replace('_', '', ucwords($string, '_'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration des devises (x_devises)
|
||||
*/
|
||||
private function migrateXDevises(int $entityId, bool $dryRun, array $options): array {
|
||||
// Les tables x_* sont globales, pas liées à une entité
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
|
||||
$stmt = $sourceDb->query("SELECT * FROM x_devises WHERE active = 1");
|
||||
$records = $stmt->fetchAll();
|
||||
|
||||
if ($dryRun) {
|
||||
return ['records_migrated' => count($records), 'warnings' => []];
|
||||
}
|
||||
|
||||
$inserted = 0;
|
||||
foreach ($records as $record) {
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO x_devises (id, code, libelle, symbole, chk_active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
code = VALUES(code),
|
||||
libelle = VALUES(libelle),
|
||||
symbole = VALUES(symbole),
|
||||
chk_active = VALUES(chk_active),
|
||||
updated_at = NOW()
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$record['rowid'],
|
||||
$record['code'],
|
||||
$record['libelle'],
|
||||
$record['symbole'],
|
||||
$record['active']
|
||||
]);
|
||||
|
||||
$inserted++;
|
||||
}
|
||||
|
||||
return ['records_migrated' => $inserted, 'warnings' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration des types d'entités (x_entites_types)
|
||||
*/
|
||||
private function migrateXEntitesTypes(int $entityId, bool $dryRun, array $options): array {
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
|
||||
$stmt = $sourceDb->query("SELECT * FROM x_entites_types WHERE active = 1");
|
||||
$records = $stmt->fetchAll();
|
||||
|
||||
if ($dryRun) {
|
||||
return ['records_migrated' => count($records), 'warnings' => []];
|
||||
}
|
||||
|
||||
$inserted = 0;
|
||||
foreach ($records as $record) {
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO x_entites_types (id, libelle, chk_active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
libelle = VALUES(libelle),
|
||||
chk_active = VALUES(chk_active),
|
||||
updated_at = NOW()
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$record['rowid'],
|
||||
$record['libelle'],
|
||||
$record['active']
|
||||
]);
|
||||
|
||||
$inserted++;
|
||||
}
|
||||
|
||||
return ['records_migrated' => $inserted, 'warnings' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthodes de migration pour les autres tables x_* (structure similaire)
|
||||
* TODO: Implémenter les autres méthodes
|
||||
*/
|
||||
private function migrateXTypesPassages(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateXTypesReglements(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateXUsersRoles(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateXPays(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateXRegions(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateXDepartements(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateXVilles(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration de l'entité (users_entites -> entites)
|
||||
*/
|
||||
private function migrateEntites(int $entityId, bool $dryRun, array $options): array {
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
|
||||
$stmt = $sourceDb->prepare("SELECT * FROM users_entites WHERE rowid = ?");
|
||||
$stmt->execute([$entityId]);
|
||||
$entity = $stmt->fetch();
|
||||
|
||||
if (!$entity) {
|
||||
throw new Exception("Entité $entityId non trouvée dans la base source");
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
return ['records_migrated' => 1, 'warnings' => []];
|
||||
}
|
||||
|
||||
// Chiffrement des données sensibles
|
||||
$encryptedName = ApiService::encryptData($entity['libelle']);
|
||||
$encryptedEmail = !empty($entity['email']) ? ApiService::encryptSearchableData($entity['email']) : null;
|
||||
|
||||
// Gestion des téléphones
|
||||
$phone = $entity['tel1'] ?? '';
|
||||
$mobile = $entity['tel2'] ?? '';
|
||||
|
||||
// Détection mobile (commence par 06 ou 07)
|
||||
if (preg_match('/^0[67]/', $phone)) {
|
||||
$mobile = $phone;
|
||||
$phone = $entity['tel2'] ?? '';
|
||||
}
|
||||
|
||||
$encryptedPhone = !empty($phone) ? ApiService::encryptData($phone) : null;
|
||||
$encryptedMobile = !empty($mobile) ? ApiService::encryptData($mobile) : null;
|
||||
$encryptedIban = !empty($entity['iban']) ? ApiService::encryptData($entity['iban']) : null;
|
||||
$encryptedBic = !empty($entity['bic']) ? ApiService::encryptData($entity['bic']) : null;
|
||||
|
||||
$stmt = $this->targetDb->prepare("
|
||||
INSERT INTO entites (
|
||||
id, fk_type, encrypted_name, encrypted_email, encrypted_phone, encrypted_mobile,
|
||||
code_postal, ville, encrypted_iban, encrypted_bic, chk_active, chk_demo,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
fk_type = VALUES(fk_type),
|
||||
encrypted_name = VALUES(encrypted_name),
|
||||
encrypted_email = VALUES(encrypted_email),
|
||||
encrypted_phone = VALUES(encrypted_phone),
|
||||
encrypted_mobile = VALUES(encrypted_mobile),
|
||||
code_postal = VALUES(code_postal),
|
||||
ville = VALUES(ville),
|
||||
encrypted_iban = VALUES(encrypted_iban),
|
||||
encrypted_bic = VALUES(encrypted_bic),
|
||||
chk_active = VALUES(chk_active),
|
||||
updated_at = NOW()
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$entity['rowid'],
|
||||
$entity['fk_type'] ?? 1,
|
||||
$encryptedName,
|
||||
$encryptedEmail,
|
||||
$encryptedPhone,
|
||||
$encryptedMobile,
|
||||
$entity['cp'] ?? null,
|
||||
$entity['ville'] ?? null,
|
||||
$encryptedIban,
|
||||
$encryptedBic,
|
||||
$entity['active']
|
||||
]);
|
||||
|
||||
return ['records_migrated' => 1, 'warnings' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration des utilisateurs
|
||||
*/
|
||||
private function migrateUsers(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter (voir migrate_users.php pour la logique)
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration des opérations
|
||||
*/
|
||||
private function migrateOperations(int $entityId, bool $dryRun, array $options): array {
|
||||
// TODO: À implémenter
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Autres méthodes de migration
|
||||
* TODO: Implémenter toutes les méthodes pour chaque étape
|
||||
*/
|
||||
private function migrateOpeSectors(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateSectorsAdresses(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateOpeUsers(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateOpeUsersSectors(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateOpePass(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateOpePassHisto(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
private function migrateMedias(int $entityId, bool $dryRun, array $options): array {
|
||||
return ['records_migrated' => 0, 'warnings' => ['Non implémenté']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le statut de migration d'une entité
|
||||
*/
|
||||
public function getMigrationStatus(int $entityId): array {
|
||||
// TODO: Implémenter la vérification du statut
|
||||
return [
|
||||
'entity_id' => $entityId,
|
||||
'migrated' => false,
|
||||
'steps_completed' => [],
|
||||
'last_migration_date' => null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les logs de migration d'une entité
|
||||
*/
|
||||
public function getMigrationLogs(int $entityId): array {
|
||||
// TODO: Lire les logs depuis LogService
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un rapport de migration
|
||||
*/
|
||||
public function generateMigrationReport(int $entityId): array {
|
||||
// TODO: Implémenter la génération de rapport
|
||||
return [
|
||||
'entity_id' => $entityId,
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
'summary' => []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare les données source vs cible
|
||||
*/
|
||||
public function compareEntityData(int $entityId): array {
|
||||
$sourceDb = $this->getSourceConnection();
|
||||
|
||||
$comparison = [];
|
||||
|
||||
// Comparer les counts pour chaque table
|
||||
$tables = [
|
||||
'users' => 'fk_entite',
|
||||
'operations' => 'fk_entite'
|
||||
];
|
||||
|
||||
foreach ($tables as $table => $fkColumn) {
|
||||
$sourceTable = $table;
|
||||
$targetTable = $table;
|
||||
|
||||
// Cas spéciaux
|
||||
if ($table === 'users_entites') {
|
||||
$sourceTable = 'users_entites';
|
||||
$targetTable = 'entites';
|
||||
}
|
||||
|
||||
// Count source
|
||||
$stmt = $sourceDb->prepare("SELECT COUNT(*) as count FROM $sourceTable WHERE $fkColumn = ?");
|
||||
$stmt->execute([$entityId]);
|
||||
$sourceCount = $stmt->fetch()['count'];
|
||||
|
||||
// Count cible
|
||||
$stmt = $this->targetDb->prepare("SELECT COUNT(*) as count FROM $targetTable WHERE $fkColumn = ?");
|
||||
$stmt->execute([$entityId]);
|
||||
$targetCount = $stmt->fetch()['count'];
|
||||
|
||||
$comparison[$table] = [
|
||||
'source_count' => $sourceCount,
|
||||
'target_count' => $targetCount,
|
||||
'difference' => $targetCount - $sourceCount,
|
||||
'status' => $sourceCount === $targetCount ? 'ok' : 'warning'
|
||||
];
|
||||
}
|
||||
|
||||
return $comparison;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'intégrité des données migrées
|
||||
*/
|
||||
public function verifyMigration(int $entityId): array {
|
||||
// TODO: Implémenter les vérifications d'intégrité
|
||||
return [
|
||||
'entity_id' => $entityId,
|
||||
'verified_at' => date('Y-m-d H:i:s'),
|
||||
'checks' => [],
|
||||
'errors' => [],
|
||||
'warnings' => []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Annule la migration d'une entité (rollback)
|
||||
*/
|
||||
public function rollbackEntity(int $entityId): array {
|
||||
// TODO: Implémenter le rollback complet
|
||||
$deletedRecords = [];
|
||||
|
||||
// Supprimer dans l'ordre inverse des dépendances
|
||||
$tables = array_reverse(self::MIGRATION_STEPS);
|
||||
|
||||
foreach ($tables as $table) {
|
||||
// Logique de suppression selon la table
|
||||
}
|
||||
|
||||
return ['deleted_records' => $deletedRecords];
|
||||
}
|
||||
|
||||
/**
|
||||
* Annule une étape spécifique de migration
|
||||
*/
|
||||
public function rollbackStep(int $entityId, string $step): array {
|
||||
// TODO: Implémenter le rollback d'une étape
|
||||
return ['deleted_records' => []];
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
require_once __DIR__ . '/ApiService.php';
|
||||
require_once __DIR__ . '/LogService.php';
|
||||
|
||||
use PDO;
|
||||
use ApiService;
|
||||
use LogService;
|
||||
|
||||
class OperationDataService {
|
||||
|
||||
@@ -221,13 +221,13 @@ class OperationDataService {
|
||||
if (!empty($sectorIdsString)) {
|
||||
// Utiliser ope_users au lieu de users pour avoir les données historiques
|
||||
$usersSectorsStmt = $db->prepare(
|
||||
"SELECT DISTINCT ou.fk_user as id, ou.first_name, ou.encrypted_name, ou.sect_name, us.fk_sector
|
||||
"SELECT DISTINCT ou.fk_user as user_id, ou.id as ope_user_id, ou.first_name, ou.encrypted_name, ou.sect_name, us.fk_sector
|
||||
FROM ope_users ou
|
||||
JOIN ope_users_sectors us ON ou.fk_user = us.fk_user AND ou.fk_operation = us.fk_operation
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
AND ou.chk_active = 1
|
||||
JOIN ope_users_sectors us ON ou.id = us.fk_user AND ou.fk_operation = us.fk_operation
|
||||
WHERE us.fk_sector IN ($sectorIdsString)
|
||||
AND us.fk_operation = ?
|
||||
AND us.chk_active = 1
|
||||
AND ou.chk_active = 1
|
||||
AND ou.fk_user != ?" // Exclure l'utilisateur connecté
|
||||
);
|
||||
$usersSectorsStmt->execute([$activeOperationId, $userId]);
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use LogService;
|
||||
use App\Services\LogService;
|
||||
|
||||
require_once __DIR__ . '/LogService.php';
|
||||
|
||||
|
||||
@@ -6,147 +6,99 @@ namespace App\Services;
|
||||
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
use FPDF;
|
||||
use setasign\Fpdi\Fpdi;
|
||||
|
||||
/**
|
||||
* Générateur de reçus PDF avec FPDF
|
||||
* Supporte les logos PNG/JPG
|
||||
* Générateur de reçus PDF avec FPDI (utilise un template PDF)
|
||||
* Génère des PDF légers et valides en format paysage (A4 Landscape)
|
||||
*/
|
||||
class ReceiptPDFGenerator extends FPDF {
|
||||
|
||||
class ReceiptPDFGenerator extends Fpdi {
|
||||
|
||||
private const TEMPLATE_PATH = __DIR__ . '/../../docs/_recu_template.pdf';
|
||||
private const DEFAULT_LOGO_PATH = __DIR__ . '/../../docs/_logo_recu.png';
|
||||
private const LOGO_WIDTH = 40; // Largeur du logo en mm
|
||||
private const LOGO_HEIGHT = 40; // Hauteur du logo en mm
|
||||
|
||||
private const LOGO_WIDTH = 45; // Largeur du logo en mm
|
||||
|
||||
/**
|
||||
* Génère un reçu fiscal PDF
|
||||
* Génère un reçu fiscal PDF et l'enregistre directement dans un fichier
|
||||
*
|
||||
* @param array $data Données du reçu
|
||||
* @param string $outputPath Chemin complet du fichier PDF à créer
|
||||
* @param string|null $logoPath Chemin vers le logo (optionnel)
|
||||
* @return bool True si la génération a réussi
|
||||
*/
|
||||
public function generateReceipt(array $data, ?string $logoPath = null): string {
|
||||
$this->AddPage();
|
||||
$this->SetFont('Arial', '', 12);
|
||||
|
||||
// Déterminer quel logo utiliser
|
||||
$logoToUse = null;
|
||||
if ($logoPath && file_exists($logoPath)) {
|
||||
$logoToUse = $logoPath;
|
||||
} elseif (file_exists(self::DEFAULT_LOGO_PATH)) {
|
||||
$logoToUse = self::DEFAULT_LOGO_PATH;
|
||||
}
|
||||
|
||||
// Ajouter le logo (PNG ou JPG)
|
||||
if ($logoToUse) {
|
||||
try {
|
||||
// Déterminer le type d'image
|
||||
$imageInfo = getimagesize($logoToUse);
|
||||
if ($imageInfo !== false) {
|
||||
$type = '';
|
||||
switch ($imageInfo[2]) {
|
||||
case IMAGETYPE_JPEG:
|
||||
$type = 'JPG';
|
||||
break;
|
||||
case IMAGETYPE_PNG:
|
||||
$type = 'PNG';
|
||||
break;
|
||||
}
|
||||
|
||||
if ($type) {
|
||||
// Position du logo : x=10, y=10, largeur=40mm, hauteur=40mm
|
||||
$this->Image($logoToUse, 10, 10, self::LOGO_WIDTH, self::LOGO_HEIGHT, $type);
|
||||
public function generateReceipt(array $data, string $outputPath, ?string $logoPath = null): bool {
|
||||
try {
|
||||
// Créer la page en orientation paysage (Landscape)
|
||||
$this->AddPage('L');
|
||||
|
||||
// Importer le template PDF
|
||||
if (file_exists(self::TEMPLATE_PATH)) {
|
||||
$this->setSourceFile(self::TEMPLATE_PATH);
|
||||
$tplIdx = $this->importPage(1);
|
||||
$this->useTemplate($tplIdx);
|
||||
}
|
||||
|
||||
// Configuration de base
|
||||
$this->SetFont('Arial');
|
||||
$this->SetFontSize(16);
|
||||
$this->SetTextColor(50, 50, 50);
|
||||
|
||||
// Nom de l'amicale (en haut à droite du template)
|
||||
$this->SetXY(116, 26);
|
||||
$this->Write(0, $this->cleanText($data['entite_city'] ?? ''));
|
||||
|
||||
// Nom du donateur
|
||||
$this->SetXY(35, 41);
|
||||
$this->Write(0, $this->cleanText($data['donor_name'] ?? ''));
|
||||
|
||||
// Adresse du donateur
|
||||
$this->SetXY(35, 55);
|
||||
$this->Write(0, $this->cleanText($data['donor_address'] ?? ''));
|
||||
|
||||
// Montant et mode de règlement
|
||||
$this->SetXY(48, 68);
|
||||
$amount = $data['amount'] ?? '0';
|
||||
$paymentMethod = !empty($data['payment_method'])
|
||||
? ' en ' . mb_strtolower($data['payment_method'], 'UTF-8')
|
||||
: '';
|
||||
$this->Write(0, $this->cleanText($amount . ' euros' . $paymentMethod));
|
||||
|
||||
// Date du don
|
||||
$this->SetXY(20, 82);
|
||||
$this->Write(0, $this->cleanText($data['donation_date'] ?? date('d/m/Y')));
|
||||
|
||||
// Logo de l'entité (en haut à droite)
|
||||
$logoToUse = null;
|
||||
if ($logoPath && file_exists($logoPath)) {
|
||||
$logoToUse = $logoPath;
|
||||
} elseif (file_exists(self::DEFAULT_LOGO_PATH)) {
|
||||
$logoToUse = self::DEFAULT_LOGO_PATH;
|
||||
}
|
||||
|
||||
if ($logoToUse) {
|
||||
try {
|
||||
$this->Image($logoToUse, 245, 8, self::LOGO_WIDTH);
|
||||
} catch (\Exception $e) {
|
||||
// Si erreur avec le logo custom, utiliser le logo par défaut
|
||||
if ($logoToUse !== self::DEFAULT_LOGO_PATH && file_exists(self::DEFAULT_LOGO_PATH)) {
|
||||
try {
|
||||
$this->Image(self::DEFAULT_LOGO_PATH, 245, 8, self::LOGO_WIDTH);
|
||||
} catch (\Exception $e2) {
|
||||
// Continuer sans logo
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Si erreur avec le logo, continuer sans
|
||||
}
|
||||
|
||||
// Écrire directement dans le fichier (mode 'F')
|
||||
$this->Output($outputPath, 'F');
|
||||
|
||||
return true;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
error_log('Erreur génération PDF: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// En-tête à droite du logo
|
||||
$this->SetXY(60, 20);
|
||||
$this->SetFont('Arial', 'B', 14);
|
||||
$this->Cell(130, 6, $this->cleanText(strtoupper($data['entite_name'] ?? '')), 0, 1, 'C');
|
||||
|
||||
if (!empty($data['entite_city'])) {
|
||||
$this->SetX(60);
|
||||
$this->SetFont('Arial', '', 11);
|
||||
$this->Cell(130, 5, $this->cleanText(strtoupper($data['entite_city'])), 0, 1, 'C');
|
||||
}
|
||||
|
||||
if (!empty($data['entite_address'])) {
|
||||
$this->SetX(60);
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$this->Cell(130, 5, $this->cleanText($data['entite_address']), 0, 1, 'C');
|
||||
}
|
||||
|
||||
// Titre du reçu
|
||||
$this->SetY(65);
|
||||
$this->SetFont('Arial', 'B', 16);
|
||||
$this->Cell(0, 10, $this->cleanText('REÇU DE DON'), 0, 1, 'C');
|
||||
|
||||
$this->SetFont('Arial', 'B', 14);
|
||||
$this->Cell(0, 8, $this->cleanText('N° ' . ($data['receipt_number'] ?? '')), 0, 1, 'C');
|
||||
|
||||
// Ligne de séparation
|
||||
$this->Ln(5);
|
||||
$this->Line(20, $this->GetY(), 190, $this->GetY());
|
||||
$this->Ln(8);
|
||||
|
||||
// Informations du donateur
|
||||
$this->SetFont('Arial', 'B', 12);
|
||||
$this->Cell(0, 6, $this->cleanText('DONATEUR :'), 0, 1, 'L');
|
||||
|
||||
$this->SetFont('Arial', '', 11);
|
||||
$this->Cell(0, 6, $this->cleanText($data['donor_name'] ?? ''), 0, 1, 'L');
|
||||
|
||||
if (!empty($data['donor_address'])) {
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$this->Cell(0, 5, $this->cleanText($data['donor_address']), 0, 1, 'L');
|
||||
}
|
||||
|
||||
$this->Ln(8);
|
||||
|
||||
// Cadre pour le montant
|
||||
$this->SetFillColor(240, 240, 240);
|
||||
$this->Rect(20, $this->GetY(), 170, 25, 'F');
|
||||
|
||||
// Montant en gros et centré
|
||||
$this->Ln(5);
|
||||
$this->SetFont('Arial', 'B', 18);
|
||||
$this->Cell(0, 8, $this->cleanText($data['amount'] ?? '0') . $this->cleanText(' euros'), 0, 1, 'C');
|
||||
|
||||
// Date centrée
|
||||
$this->SetFont('Arial', '', 12);
|
||||
$this->Cell(0, 6, $this->cleanText('Don du ' . ($data['donation_date'] ?? date('d/m/Y'))), 0, 1, 'C');
|
||||
|
||||
$this->Ln(10);
|
||||
|
||||
if (!empty($data['payment_method'])) {
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$this->Cell(0, 5, $this->cleanText('Mode de règlement : ') . $this->cleanText($data['payment_method']), 0, 1, 'L');
|
||||
}
|
||||
|
||||
if (!empty($data['operation_name'])) {
|
||||
$this->SetFont('Arial', 'I', 10);
|
||||
$this->Cell(0, 5, $this->cleanText('Campagne : ') . $this->cleanText($data['operation_name']), 0, 1, 'L');
|
||||
}
|
||||
|
||||
// Mention de remerciement
|
||||
$this->Ln(15);
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$this->MultiCell(0, 5, $this->cleanText(
|
||||
"Nous vous remercions pour votre généreux soutien à notre amicale.\n" .
|
||||
"Votre don contribue au financement de nos activités et équipements."
|
||||
), 0, 'C');
|
||||
|
||||
// Signature
|
||||
$this->SetY(-60);
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$this->Cell(0, 5, $this->cleanText('Fait à ' . ($data['entite_city'] ?? '') . ', le ' . ($data['signature_date'] ?? date('d/m/Y'))), 0, 1, 'R');
|
||||
$this->Ln(5);
|
||||
$this->Cell(0, 5, $this->cleanText('Le Président'), 0, 1, 'R');
|
||||
$this->Ln(15);
|
||||
$this->Cell(0, 5, $this->cleanText('(Signature et cachet)'), 0, 1, 'R');
|
||||
|
||||
// Retourner le PDF en string
|
||||
return $this->Output('S');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,12 +8,13 @@ require_once __DIR__ . '/LogService.php';
|
||||
require_once __DIR__ . '/ApiService.php';
|
||||
require_once __DIR__ . '/FileService.php';
|
||||
require_once __DIR__ . '/ReceiptPDFGenerator.php';
|
||||
require_once __DIR__ . '/EmailTemplates.php';
|
||||
|
||||
use PDO;
|
||||
use Database;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use FileService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\FileService;
|
||||
use App\Services\ApiService;
|
||||
use Exception;
|
||||
use DateTime;
|
||||
|
||||
@@ -88,26 +89,28 @@ class ReceiptService {
|
||||
|
||||
// Préparer les données pour la génération du PDF
|
||||
$receiptData = $this->prepareReceiptData($passageData, $operationData, $entiteData, $email);
|
||||
|
||||
// Générer le PDF optimisé
|
||||
$pdfContent = $this->generateOptimizedPDF($receiptData, $logoPath);
|
||||
|
||||
|
||||
// Créer le répertoire de stockage
|
||||
$uploadPath = "/{$operationData['fk_entite']}/recus/{$operationData['id']}";
|
||||
$fullPath = $this->fileService->createDirectory($operationData['fk_entite'], $uploadPath);
|
||||
|
||||
|
||||
// Nom du fichier
|
||||
$fileName = 'recu_' . $passageId . '.pdf';
|
||||
$filePath = $fullPath . '/' . $fileName;
|
||||
|
||||
// Sauvegarder le fichier
|
||||
if (file_put_contents($filePath, $pdfContent) === false) {
|
||||
throw new Exception('Impossible de sauvegarder le fichier PDF');
|
||||
|
||||
// Générer le PDF directement dans le fichier
|
||||
$pdfGenerated = $this->generateOptimizedPDF($receiptData, $filePath, $logoPath);
|
||||
|
||||
if (!$pdfGenerated || !file_exists($filePath)) {
|
||||
throw new Exception('Impossible de générer le fichier PDF');
|
||||
}
|
||||
|
||||
|
||||
// Appliquer les permissions
|
||||
$this->fileService->setFilePermissions($filePath);
|
||||
|
||||
|
||||
// Récupérer la taille du fichier généré
|
||||
$fileSize = filesize($filePath);
|
||||
|
||||
// Enregistrer dans la table medias
|
||||
$mediaId = $this->saveToMedias(
|
||||
$operationData['fk_entite'],
|
||||
@@ -115,21 +118,21 @@ class ReceiptService {
|
||||
$passageId,
|
||||
$fileName,
|
||||
$filePath,
|
||||
strlen($pdfContent)
|
||||
$fileSize
|
||||
);
|
||||
|
||||
|
||||
// Mettre à jour le passage avec les infos du reçu
|
||||
$this->updatePassageReceipt($passageId, $fileName);
|
||||
|
||||
// Ajouter à la queue d'email
|
||||
$this->queueReceiptEmail($passageId, $email, $receiptData, $pdfContent);
|
||||
|
||||
// Ajouter à la queue d'email (le PDF sera lu depuis le fichier)
|
||||
$this->queueReceiptEmail($passageId, $email, $receiptData, $filePath);
|
||||
|
||||
LogService::log('Reçu généré avec succès', [
|
||||
'level' => 'info',
|
||||
'passageId' => $passageId,
|
||||
'mediaId' => $mediaId,
|
||||
'fileName' => $fileName,
|
||||
'fileSize' => strlen($pdfContent)
|
||||
'fileSize' => $fileSize
|
||||
]);
|
||||
|
||||
return true;
|
||||
@@ -146,10 +149,15 @@ class ReceiptService {
|
||||
|
||||
/**
|
||||
* Génère un PDF optimisé avec logo et mise en page épurée
|
||||
*
|
||||
* @param array $data Données du reçu
|
||||
* @param string $outputPath Chemin du fichier PDF à créer
|
||||
* @param string|null $logoPath Chemin du logo
|
||||
* @return bool True si la génération a réussi
|
||||
*/
|
||||
private function generateOptimizedPDF(array $data, ?string $logoPath): string {
|
||||
private function generateOptimizedPDF(array $data, string $outputPath, ?string $logoPath): bool {
|
||||
$pdf = new ReceiptPDFGenerator();
|
||||
return $pdf->generateReceipt($data, $logoPath);
|
||||
return $pdf->generateReceipt($data, $outputPath, $logoPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -158,12 +166,13 @@ class ReceiptService {
|
||||
*/
|
||||
private function getPassageData(int $passageId): ?array {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.*,
|
||||
SELECT p.*,
|
||||
u.encrypted_name as user_encrypted_name,
|
||||
u.encrypted_email as user_encrypted_email,
|
||||
u.encrypted_phone as user_encrypted_phone
|
||||
FROM ope_pass p
|
||||
LEFT JOIN users u ON p.fk_user = u.id
|
||||
LEFT JOIN ope_users ou ON p.fk_user = ou.id
|
||||
LEFT JOIN users u ON ou.fk_user = u.id
|
||||
WHERE p.id = ? AND p.chk_active = 1
|
||||
');
|
||||
$stmt->execute([$passageId]);
|
||||
@@ -345,25 +354,52 @@ class ReceiptService {
|
||||
|
||||
/**
|
||||
* Ajoute le reçu à la queue d'email
|
||||
*
|
||||
* @param int $passageId ID du passage
|
||||
* @param string $email Email du destinataire
|
||||
* @param array $receiptData Données du reçu
|
||||
* @param string $pdfFilePath Chemin vers le fichier PDF
|
||||
*/
|
||||
private function queueReceiptEmail(int $passageId, string $email, array $receiptData, string $pdfContent): void {
|
||||
private function queueReceiptEmail(int $passageId, string $email, array $receiptData, string $pdfFilePath): void {
|
||||
// Lire le contenu du PDF depuis le fichier
|
||||
if (!file_exists($pdfFilePath)) {
|
||||
throw new \Exception('Fichier PDF introuvable pour la mise en queue: ' . $pdfFilePath);
|
||||
}
|
||||
|
||||
$pdfContent = file_get_contents($pdfFilePath);
|
||||
if ($pdfContent === false) {
|
||||
throw new \Exception('Impossible de lire le fichier PDF: ' . $pdfFilePath);
|
||||
}
|
||||
|
||||
// Préparer le sujet
|
||||
$subject = "Votre reçu de don N°" . $receiptData['receipt_number'];
|
||||
|
||||
// Préparer le corps de l'email
|
||||
$body = $this->generateEmailBody($receiptData);
|
||||
|
||||
$subject = "Votre reçu de don - " . $receiptData['entite_name'];
|
||||
|
||||
// Préparer les données pour le template
|
||||
$templateData = [
|
||||
'passage_id' => $passageId,
|
||||
'entite_name' => $receiptData['entite_name'],
|
||||
'donor_name' => $receiptData['donor_name'],
|
||||
'amount' => $receiptData['amount'],
|
||||
'donation_date' => $receiptData['donation_date'],
|
||||
'payment_method' => $receiptData['payment_method'],
|
||||
'entite_address' => $receiptData['entite_address'],
|
||||
'entite_email' => $receiptData['entite_email']
|
||||
];
|
||||
|
||||
// Générer le corps de l'email via le template centralisé
|
||||
$body = EmailTemplates::getReceiptDonationTemplate($templateData);
|
||||
|
||||
// Préparer les headers avec pièce jointe
|
||||
$boundary = md5((string)time());
|
||||
$headers = "MIME-Version: 1.0\r\n";
|
||||
$headers .= "Content-Type: multipart/mixed; boundary=\"$boundary\"\r\n";
|
||||
|
||||
|
||||
// Corps complet avec pièce jointe
|
||||
$fullBody = "--$boundary\r\n";
|
||||
$fullBody .= "Content-Type: text/html; charset=UTF-8\r\n";
|
||||
$fullBody .= "Content-Transfer-Encoding: 7bit\r\n\r\n";
|
||||
$fullBody .= $body . "\r\n\r\n";
|
||||
|
||||
|
||||
// Pièce jointe PDF
|
||||
$fullBody .= "--$boundary\r\n";
|
||||
$fullBody .= "Content-Type: application/pdf; name=\"recu_" . $receiptData['receipt_number'] . ".pdf\"\r\n";
|
||||
@@ -371,14 +407,14 @@ class ReceiptService {
|
||||
$fullBody .= "Content-Disposition: attachment; filename=\"recu_" . $receiptData['receipt_number'] . ".pdf\"\r\n\r\n";
|
||||
$fullBody .= chunk_split(base64_encode($pdfContent)) . "\r\n";
|
||||
$fullBody .= "--$boundary--";
|
||||
|
||||
|
||||
// Insérer dans la queue
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO email_queue (
|
||||
fk_pass, to_email, subject, body, headers, created_at, status
|
||||
) VALUES (?, ?, ?, ?, ?, NOW(), ?)
|
||||
');
|
||||
|
||||
|
||||
$stmt->execute([
|
||||
$passageId,
|
||||
$email,
|
||||
@@ -388,62 +424,7 @@ class ReceiptService {
|
||||
'pending'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère le corps HTML de l'email
|
||||
*/
|
||||
private function generateEmailBody(array $data): string {
|
||||
// Convertir toutes les valeurs en string pour htmlspecialchars
|
||||
$safeData = array_map(function($value) {
|
||||
return is_string($value) ? $value : (string)$value;
|
||||
}, $data);
|
||||
|
||||
$html = '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background-color: #f4f4f4; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; }
|
||||
.footer { background-color: #f4f4f4; padding: 10px; text-align: center; font-size: 12px; }
|
||||
.amount { font-size: 24px; font-weight: bold; color: #2c5aa0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h2>' . htmlspecialchars($safeData['entite_name']) . '</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Bonjour ' . htmlspecialchars($safeData['donor_name']) . ',</p>
|
||||
|
||||
<p>Nous vous remercions chaleureusement pour votre don de <span class="amount">' .
|
||||
htmlspecialchars($safeData['amount']) . ' €</span> effectué le ' .
|
||||
htmlspecialchars($safeData['donation_date']) . '.</p>
|
||||
|
||||
<p>Vous trouverez ci-joint votre reçu fiscal N°' . htmlspecialchars($safeData['receipt_number']) .
|
||||
' qui vous permettra de bénéficier d\'une réduction d\'impôt égale à 66% du montant de votre don.</p>
|
||||
|
||||
<p>Votre soutien est précieux pour nous permettre de poursuivre nos actions.</p>
|
||||
|
||||
<p>Cordialement,<br>
|
||||
L\'équipe de ' . htmlspecialchars($safeData['entite_name']) . '</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Conservez ce reçu pour votre déclaration fiscale</p>
|
||||
<p>' . htmlspecialchars($safeData['entite_name']) . '<br>
|
||||
' . htmlspecialchars($safeData['entite_address']) . '<br>
|
||||
' . htmlspecialchars($safeData['entite_email']) . '</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Met à jour la date d'envoi du reçu
|
||||
*/
|
||||
|
||||
@@ -9,8 +9,9 @@ require_once __DIR__ . '/EmailThrottler.php';
|
||||
|
||||
use PDO;
|
||||
use Database;
|
||||
use ApiService;
|
||||
use App\Services\ApiService;
|
||||
use AppConfig;
|
||||
use App\Services\LogService;
|
||||
|
||||
/**
|
||||
* Service central de gestion des alertes de sécurité et monitoring
|
||||
@@ -94,7 +95,7 @@ class AlertService {
|
||||
$context['request'] = [
|
||||
'uri' => $_SERVER['REQUEST_URI'],
|
||||
'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
|
||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'ip' => \AppConfig::getInstance()->getClientIp(),
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ class PerformanceMonitor {
|
||||
$memoryUsed = $memoryPeak - $memoryStart;
|
||||
|
||||
// Enrichir avec les infos de requête
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? null;
|
||||
$ip = \AppConfig::getInstance()->getClientIp();
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||
$requestSize = strlen(file_get_contents('php://input'));
|
||||
|
||||
|
||||
@@ -74,13 +74,10 @@ class SecurityMonitor {
|
||||
// Critères de détection
|
||||
$isBruteForce = false;
|
||||
$reason = '';
|
||||
|
||||
if ($attempts >= 5) {
|
||||
|
||||
if ($attempts >= 8) {
|
||||
$isBruteForce = true;
|
||||
$reason = "Plus de 5 tentatives en 5 minutes";
|
||||
} elseif ($uniqueUsers >= 3) {
|
||||
$isBruteForce = true;
|
||||
$reason = "Tentatives sur 3 usernames différents";
|
||||
$reason = "Plus de 8 tentatives en 5 minutes";
|
||||
}
|
||||
|
||||
if ($isBruteForce) {
|
||||
@@ -114,15 +111,19 @@ class SecurityMonitor {
|
||||
|
||||
try {
|
||||
// Chercher si le username existe (pour stocker la version chiffrée)
|
||||
require_once __DIR__ . '/../ApiService.php';
|
||||
$encryptedUsername = null;
|
||||
if ($username) {
|
||||
// Chiffrer le username pour la recherche
|
||||
$searchUsername = \ApiService::encryptSearchableData($username);
|
||||
|
||||
$userStmt = $db->prepare('
|
||||
SELECT encrypted_user_name
|
||||
FROM users
|
||||
WHERE username = :username
|
||||
SELECT encrypted_user_name
|
||||
FROM users
|
||||
WHERE encrypted_user_name = :encrypted_username
|
||||
LIMIT 1
|
||||
');
|
||||
$userStmt->execute(['username' => $username]);
|
||||
$userStmt->execute(['encrypted_username' => $searchUsername]);
|
||||
$user = $userStmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($user) {
|
||||
$encryptedUsername = $user['encrypted_user_name'];
|
||||
@@ -178,7 +179,7 @@ class SecurityMonitor {
|
||||
if (isset($_SERVER['REQUEST_URI'])) {
|
||||
$context['endpoint'] = $_SERVER['REQUEST_URI'];
|
||||
$context['method'] = $_SERVER['REQUEST_METHOD'] ?? 'unknown';
|
||||
$context['ip'] = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$context['ip'] = \AppConfig::getInstance()->getClientIp();
|
||||
}
|
||||
|
||||
AlertService::trigger('SQL_INJECTION', $context, 'SECURITY');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user