- Mise à jour VERSION vers 3.3.4 - Optimisations et révisions architecture API (deploy-api.sh, scripts de migration) - Ajout documentation Stripe Tap to Pay complète - Migration vers polices Inter Variable pour Flutter - Optimisations build Android et nettoyage fichiers temporaires - Amélioration système de déploiement avec gestion backups - Ajout scripts CRON et migrations base de données 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
25 KiB
25 KiB
PLANNING STRIPE - DÉVELOPPEUR BACKEND PHP
API PHP 8.3 - Intégration Stripe Tap to Pay (Mobile uniquement)
Période : 25/08/2024 - 05/09/2024
Mise à jour : Janvier 2025 - Simplification architecture
📅 LUNDI 25/08 - Setup et architecture (8h)
🌅 Matin (4h)
# Installation Stripe PHP SDK
cd api
composer require stripe/stripe-php
✅ Configuration environnement
- Créer configuration Stripe dans
AppConfig.phpavec clés TEST - Ajouter variables de configuration :
'stripe' => [ 'public_key_test' => 'pk_test_51QwoVN00pblGEgsXkf8qlXm...', 'secret_key_test' => 'sk_test_51QwoVN00pblGEgsXnvqi8qf...', 'webhook_secret_test' => 'whsec_test_...', 'api_version' => '2024-06-20', 'application_fee_percent' => 0, // DECISION: 0% commission 'mode' => 'test' ] - Créer service
StripeService.phpsingleton - Configurer authentification Session-based API
✅ Base de données
-- Modification de la table ope_pass existante (JANVIER 2025)
ALTER TABLE `ope_pass`
DROP COLUMN IF EXISTS `is_striped`,
ADD COLUMN `stripe_payment_id` VARCHAR(50) DEFAULT NULL COMMENT 'ID du PaymentIntent Stripe (pi_xxx)',
ADD INDEX `idx_stripe_payment` (`stripe_payment_id`);
-- Tables à créer (simplifiées)
CREATE TABLE stripe_accounts (
id INT PRIMARY KEY AUTO_INCREMENT,
amicale_id INT NOT NULL,
stripe_account_id VARCHAR(255) UNIQUE,
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 (amicale_id) REFERENCES amicales(id)
);
-- NOTE: Table payment_intents SUPPRIMÉE - on utilise directement stripe_payment_id dans ope_pass
-- NOTE: Table terminal_readers SUPPRIMÉE - Tap to Pay uniquement, pas de terminaux externes
CREATE TABLE android_certified_devices (
id INT 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',
last_verified TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_manufacturer_model (manufacturer, model)
);
🌆 Après-midi (4h)
✅ Endpoints Connect - Onboarding (RÉALISÉS)
// POST /api/stripe/accounts - IMPLEMENTED
public function createAccount() {
$amicale = Amicale::find($amicaleId);
$account = \Stripe\Account::create([
'type' => 'express',
'country' => 'FR',
'email' => $amicale->email,
'capabilities' => [
'card_payments' => ['requested' => true],
'transfers' => ['requested' => true],
],
'business_type' => 'non_profit',
'business_profile' => [
'name' => $amicale->name,
'product_description' => 'Vente de calendriers des pompiers',
],
]);
// Sauvegarder stripe_account_id
return $account;
}
// GET /api/amicales/{id}/onboarding-link
public function getOnboardingLink($amicaleId) {
$accountLink = \Stripe\AccountLink::create([
'account' => $amicale->stripe_account_id,
'refresh_url' => config('app.url') . '/stripe/refresh',
'return_url' => config('app.url') . '/stripe/success',
'type' => 'account_onboarding',
]);
return ['url' => $accountLink->url];
}
📅 MARDI 26/08 - Webhooks et Terminal (8h)
🌅 Matin (4h)
✅ Webhooks handler
// POST /api/webhooks/stripe
public function handleWebhook(Request $request) {
$payload = $request->getContent();
$sig_header = $request->header('Stripe-Signature');
try {
$event = \Stripe\Webhook::constructEvent(
$payload, $sig_header, config('stripe.webhook_secret')
);
} catch(\Exception $e) {
return response('Invalid signature', 400);
}
switch ($event->type) {
case 'account.updated':
$this->handleAccountUpdated($event->data->object);
break;
case 'account.application.authorized':
$this->handleAccountAuthorized($event->data->object);
break;
case 'payment_intent.succeeded':
$this->handlePaymentSucceeded($event->data->object);
break;
}
return response('Webhook handled', 200);
}
✅ Configuration Tap to Pay
// POST /api/stripe/tap-to-pay/init
public function initTapToPay(Request $request) {
$userId = Session::getUserId();
$entityId = Session::getEntityId();
// Vérifier que l'entité a un compte Stripe
$account = $this->getStripeAccount($entityId);
return [
'stripe_account_id' => $account->stripe_account_id,
'tap_to_pay_enabled' => true
];
}
🌆 Après-midi (4h)
✅ Vérification compatibilité Device
// POST /api/stripe/devices/check-tap-to-pay
public function checkTapToPayCapability(Request $request) {
$platform = $request->input('platform');
$model = $request->input('device_model');
$osVersion = $request->input('os_version');
if ($platform === 'iOS') {
// iPhone XS et ultérieurs avec iOS 16.4+
$supported = $this->checkiOSCompatibility($model, $osVersion);
} else {
// Android certifié pour la France
$supported = $this->checkAndroidCertification($model);
}
return [
'tap_to_pay_supported' => $supported,
'message' => $supported ?
'Tap to Pay disponible' :
'Appareil non compatible'
];
}
📅 MERCREDI 27/08 - Paiements et fees (8h)
🌅 Matin (4h)
✅ Création PaymentIntent avec association au passage
// POST /api/payments/create-intent
public function createPaymentIntent(Request $request) {
$validated = $request->validate([
'amount' => 'required|integer|min:100', // en centimes
'passage_id' => 'required|integer', // ID du passage ope_pass
'entity_id' => 'required|integer',
]);
$userId = Session::getUserId();
$entity = $this->getEntity($validated['entity_id']);
// Commission à 0% (décision client)
$applicationFee = 0;
$paymentIntent = \Stripe\PaymentIntent::create([
'amount' => $validated['amount'],
'currency' => 'eur',
'payment_method_types' => ['card_present'],
'capture_method' => 'automatic',
'application_fee_amount' => $applicationFee,
'transfer_data' => [
'destination' => $entity->stripe_account_id,
],
'metadata' => [
'passage_id' => $validated['passage_id'],
'user_id' => $userId,
'entity_id' => $entity->id,
'year' => date('Y'),
],
]);
// Mise à jour directe dans ope_pass
$this->db->prepare("
UPDATE ope_pass
SET stripe_payment_id = :stripe_id,
date_modified = NOW()
WHERE id = :passage_id
")->execute([
':stripe_id' => $paymentIntent->id,
':passage_id' => $validated['passage_id']
]);
return [
'client_secret' => $paymentIntent->client_secret,
'payment_intent_id' => $paymentIntent->id,
];
}
🌆 Après-midi (4h)
✅ Capture et confirmation
// POST /api/payments/{id}/capture
public function capturePayment($paymentIntentId) {
// Récupérer le passage depuis ope_pass
$stmt = $this->db->prepare("
SELECT id, stripe_payment_id, montant
FROM ope_pass
WHERE stripe_payment_id = :stripe_id
");
$stmt->execute([':stripe_id' => $paymentIntentId]);
$passage = $stmt->fetch();
$paymentIntent = \Stripe\PaymentIntent::retrieve($paymentIntentId);
if ($paymentIntent->status === 'requires_capture') {
$paymentIntent->capture();
}
// Mettre à jour le statut dans ope_pass si nécessaire
if ($paymentIntent->status === 'succeeded' && $passage) {
$this->db->prepare("
UPDATE ope_pass
SET date_stripe_validated = NOW()
WHERE id = :passage_id
")->execute([':passage_id' => $passage['id']]);
// Envoyer email reçu si configuré
$this->sendReceipt($passage['id']);
}
return $paymentIntent;
}
// GET /api/passages/{id}/stripe-status
public function getPassageStripeStatus($passageId) {
$stmt = $this->db->prepare("
SELECT stripe_payment_id, montant, date_creat
FROM ope_pass
WHERE id = :id
");
$stmt->execute([':id' => $passageId]);
$passage = $stmt->fetch();
if (!$passage['stripe_payment_id']) {
return ['status' => 'no_stripe_payment'];
}
// Récupérer le statut depuis Stripe
$paymentIntent = \Stripe\PaymentIntent::retrieve($passage['stripe_payment_id']);
return [
'stripe_payment_id' => $passage['stripe_payment_id'],
'status' => $paymentIntent->status,
'amount' => $paymentIntent->amount,
'currency' => $paymentIntent->currency,
'created_at' => $passage['date_creat']
];
}
📅 JEUDI 28/08 - Reporting et Android compatibility (8h)
🌅 Matin (4h)
✅ Gestion appareils Android certifiés
// POST /api/devices/check-tap-to-pay
public function checkTapToPayCapability(Request $request) {
$validated = $request->validate([
'platform' => 'required|in:ios,android',
'manufacturer' => 'required_if:platform,android',
'model' => 'required_if:platform,android',
'os_version' => 'required',
]);
if ($validated['platform'] === 'ios') {
// iPhone XS et ultérieurs avec iOS 15.4+
$supportedModels = ['iPhone11,', 'iPhone12,', 'iPhone13,', 'iPhone14,', 'iPhone15,', 'iPhone16,'];
$modelSupported = false;
foreach ($supportedModels as $prefix) {
if (str_starts_with($validated['model'], $prefix)) {
$modelSupported = true;
break;
}
}
$osVersion = explode('.', $validated['os_version']);
$osSupported = $osVersion[0] > 15 ||
($osVersion[0] == 15 && isset($osVersion[1]) && $osVersion[1] >= 4);
return [
'tap_to_pay_supported' => $modelSupported && $osSupported,
'message' => $modelSupported && $osSupported ?
'Tap to Pay disponible' :
'iPhone XS ou ultérieur avec iOS 15.4+ requis'
];
}
// Android - vérifier dans la base de données
$device = DB::table('android_certified_devices')
->where('manufacturer', $validated['manufacturer'])
->where('model', $validated['model'])
->where('tap_to_pay_certified', true)
->first();
return [
'tap_to_pay_supported' => $device !== null,
'message' => $device ?
'Tap to Pay disponible sur cet appareil' :
'Appareil non certifié pour Tap to Pay en France',
'alternative' => !$device ? 'Utilisez un iPhone compatible' : null
];
}
// GET /api/devices/certified-android
public function getCertifiedAndroidDevices() {
return DB::table('android_certified_devices')
->where('tap_to_pay_certified', true)
->where('country', 'FR')
->orderBy('manufacturer')
->orderBy('model')
->get();
}
✅ Seeder pour appareils certifiés
// database/seeders/AndroidCertifiedDevicesSeeder.php
public function run() {
$devices = [
// Samsung
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21', 'model_identifier' => 'SM-G991B', 'min_android_version' => 11],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21+', 'model_identifier' => 'SM-G996B', 'min_android_version' => 11],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21 Ultra', 'model_identifier' => 'SM-G998B', 'min_android_version' => 11],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S22', 'model_identifier' => 'SM-S901B', 'min_android_version' => 12],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S23', 'model_identifier' => 'SM-S911B', 'min_android_version' => 13],
['manufacturer' => 'Samsung', 'model' => 'Galaxy S24', 'model_identifier' => 'SM-S921B', 'min_android_version' => 14],
// Google Pixel
['manufacturer' => 'Google', 'model' => 'Pixel 6', 'model_identifier' => 'oriole', 'min_android_version' => 12],
['manufacturer' => 'Google', 'model' => 'Pixel 6 Pro', 'model_identifier' => 'raven', 'min_android_version' => 12],
['manufacturer' => 'Google', 'model' => 'Pixel 7', 'model_identifier' => 'panther', 'min_android_version' => 13],
['manufacturer' => 'Google', 'model' => 'Pixel 8', 'model_identifier' => 'shiba', 'min_android_version' => 14],
];
foreach ($devices as $device) {
DB::table('android_certified_devices')->insert([
'manufacturer' => $device['manufacturer'],
'model' => $device['model'],
'model_identifier' => $device['model_identifier'],
'tap_to_pay_certified' => true,
'certification_date' => now(),
'min_android_version' => $device['min_android_version'],
'country' => 'FR',
]);
}
}
✅ Endpoints statistiques
// GET /api/amicales/{id}/stats
public function getAmicaleStats($amicaleId) {
$stats = DB::table('payment_intents')
->where('amicale_id', $amicaleId)
->where('status', 'succeeded')
->selectRaw('
COUNT(*) as total_ventes,
SUM(amount) as total_montant,
SUM(application_fee) as total_commissions,
DATE(created_at) as date
')
->groupBy('date')
->get();
return $stats;
}
// GET /api/pompiers/{id}/ventes
public function getPompierVentes($pompierId) {
return PaymentIntent::where('pompier_id', $pompierId)
->where('status', 'succeeded')
->orderBy('created_at', 'desc')
->paginate(20);
}
🌆 Après-midi (4h)
✅ Gestion des remboursements
// POST /api/payments/{id}/refund
public function refundPayment($paymentIntentId, Request $request) {
$validated = $request->validate([
'amount' => 'integer|min:100', // optionnel, remboursement partiel
'reason' => 'string|in:duplicate,fraudulent,requested_by_customer',
]);
$payment = PaymentIntent::where('stripe_payment_intent_id', $paymentIntentId)->first();
$refund = \Stripe\Refund::create([
'payment_intent' => $paymentIntentId,
'amount' => $validated['amount'] ?? null, // null = remboursement total
'reason' => $validated['reason'] ?? 'requested_by_customer',
'reverse_transfer' => true, // Important pour Connect
'refund_application_fee' => true, // Rembourser aussi la commission
]);
$payment->update(['status' => 'refunded']);
return $refund;
}
📅 VENDREDI 29/08 - Mode offline et sync (8h)
🌅 Matin (4h)
✅ Queue de synchronisation
// POST /api/payments/batch-sync
public function batchSync(Request $request) {
$validated = $request->validate([
'transactions' => 'required|array',
'transactions.*.local_id' => 'required|string',
'transactions.*.amount' => 'required|integer',
'transactions.*.created_at' => 'required|date',
'transactions.*.payment_method' => 'required|in:card,cash',
]);
$results = [];
foreach ($validated['transactions'] as $transaction) {
if ($transaction['payment_method'] === 'cash') {
// Enregistrer paiement cash uniquement en DB
$results[] = $this->recordCashPayment($transaction);
} else {
// Créer PaymentIntent a posteriori (si possible)
$results[] = $this->createOfflinePayment($transaction);
}
}
return ['synced' => $results];
}
🌆 Après-midi (4h)
✅ Tests unitaires critiques
class StripePaymentTest extends TestCase {
public function test_create_payment_intent_with_fees() {
// Test création PaymentIntent avec commission
}
public function test_webhook_signature_validation() {
// Test sécurité webhook
}
public function test_refund_reverses_transfer() {
// Test remboursement avec annulation virement
}
}
📅 LUNDI 01/09 - Sécurité et optimisations (8h)
🌅 Matin (4h)
✅ Rate limiting et sécurité
// Middleware RateLimiter pour endpoints sensibles
Route::middleware(['throttle:10,1'])->group(function () {
Route::post('/payments/create-intent', 'PaymentController@createIntent');
});
// Validation des montants
public function validateAmount($amount) {
if ($amount < 100 || $amount > 50000) { // 1€ - 500€
throw new ValidationException('Montant invalide');
}
}
🌆 Après-midi (4h)
✅ Logs et monitoring
// Logger tous les événements Stripe
Log::channel('stripe')->info('Payment created', [
'payment_intent_id' => $paymentIntent->id,
'amount' => $paymentIntent->amount,
'pompier_id' => $pompier->id,
]);
📅 MARDI 02/09 - Documentation API (4h)
✅ Documentation OpenAPI/Swagger
/api/payments/create-intent:
post:
summary: Créer une intention de paiement
parameters:
- name: amount
type: integer
required: true
description: Montant en centimes
responses:
200:
description: PaymentIntent créé
📅 MERCREDI 03/09 - Tests d'intégration (8h)
✅ Tests end-to-end
- Parcours complet onboarding amicale
- Création paiement → capture → confirmation
- Test remboursement complet et partiel
- Test webhooks avec ngrok
📅 JEUDI 04/09 - Mise en production (8h)
📅 VENDREDI 05/09 - Support et livraison finale (8h)
🌅 Matin (4h)
✅ Déploiement final
- Migration DB production
- Variables environnement LIVE
- Smoke tests production
- Vérification des webhooks en production
🌆 Après-midi (4h)
✅ Support et monitoring
- Monitoring des premiers paiements réels
- Support hotline pour équipes terrain
- Documentation de passation
- Réunion de clôture et retour d'expérience
📊 RÉCAPITULATIF
- Total heures : 72h sur 10 jours
- Endpoints créés : 15
- Tables DB : 3
- Tests : 20+
🔧 DÉPENDANCES
{
"require": {
"php": "^8.3",
"stripe/stripe-php": "^13.0",
"laravel/framework": "^10.0"
}
}
⚠️ CHECKLIST SÉCURITÉ
- ❌ JAMAIS logger les clés secrètes
- ✅ TOUJOURS valider signature webhooks
- ✅ TOUJOURS utiliser HTTPS
- ✅ Rate limiting sur endpoints paiement
- ✅ Logs détaillés pour audit
🎯 BILAN DÉVELOPPEMENT API (01/09/2024)
✅ ENDPOINTS IMPLÉMENTÉS (TAP TO PAY UNIQUEMENT)
Stripe Connect - Comptes
-
POST /api/stripe/accounts ✅
- Création compte Stripe Express pour amicales
- Gestion déchiffrement données (encrypted_email, encrypted_name)
- Support des comptes existants
-
GET /api/stripe/accounts/:entityId/status ✅
- Récupération statut complet du compte
- Vérification charges_enabled et payouts_enabled
- Retour JSON avec informations détaillées
-
POST /api/stripe/accounts/:accountId/onboarding-link ✅
- Génération liens d'onboarding Stripe
- URLs de retour configurées
- Gestion des erreurs et timeouts
Configuration et Utilitaires
-
GET /api/stripe/config ✅
- Configuration publique Stripe
- Clés publiques et paramètres client
- Adaptation par environnement
-
POST /api/stripe/webhook ✅
- Réception événements Stripe
- Vérification signatures webhook
- Traitement des événements Connect
🔧 CORRECTIONS TECHNIQUES RÉALISÉES
StripeController.php
- Fixed
Database::getInstance()→$this->db - Fixed
$db->prepare()→$this->db->prepare() - Removed
details_submittedcolumn from SQL UPDATE - Added proper exit statements after JSON responses
- Commented out Logger class calls (class not found)
StripeService.php
- Added proper Stripe SDK imports (
use Stripe\Account) - Fixed
Account::retrieve()→$this->stripe->accounts->retrieve() - CRUCIAL: Added data decryption support:
$nom = !empty($entite['encrypted_name']) ? \ApiService::decryptData($entite['encrypted_name']) : ''; $email = !empty($entite['encrypted_email']) ? \ApiService::decryptSearchableData($entite['encrypted_email']) : null; - Fixed address mapping (adresse1, adresse2 vs adresse)
- REMOVED commission calculation - set to 0%
Router.php
- Commented out excessive debug logging causing nginx 502 errors:
// error_log("Recherche de route pour: méthode=$method, uri=$uri"); // error_log("Test pattern: $pattern contre uri: $uri");
AppConfig.php
- Set
application_fee_percentto 0 (was 2.5) - Set
application_fee_minimumto 0 (was 50) - Policy: 100% of payments go to amicales
📊 TESTS ET VALIDATION
Tests Réussis
- POST /api/stripe/accounts → 200 OK (Compte créé: acct_1S2YfNP63A07c33Y)
- GET /api/stripe/accounts/5/status → 200 OK (charges_enabled: true)
- POST /api/stripe/locations → 200 OK (Location: tml_GLJ21w7KCYX4Wj)
- POST /api/stripe/accounts/.../onboarding-link → 200 OK (Link generated)
- Onboarding Stripe → Completed successfully by user
Erreurs Résolues
- ❌ 500 "Class App\Controllers\Database not found" → ✅ Fixed
- ❌ 400 "Invalid email address: " → ✅ Fixed (decryption added)
- ❌ 502 "upstream sent too big header" → ✅ Fixed (logs removed)
- ❌ SQL "Column not found: details_submitted" → ✅ Fixed
🚀 ARCHITECTURE TECHNIQUE
Services Implémentés
- StripeService: Singleton pour interactions Stripe API
- StripeController: Endpoints REST avec gestion sessions
- StripeWebhookController: Handler événements webhook
- ApiService: Déchiffrement données encrypted fields
Sécurité
- Validation signatures webhook Stripe
- Authentification session-based pour APIs privées
- Public endpoints: webhook uniquement
- Pas de stockage clés secrètes en base
Base de données (MISE À JOUR JANVIER 2025)
- Modification table
ope_pass:stripe_payment_idVARCHAR(50) remplaceis_striped - Table
payment_intentssupprimée : Intégration directe dansope_pass - Utilisation tables existantes (entites)
- Champs encrypted_email et encrypted_name supportés
- Déchiffrement automatique avant envoi Stripe
🎯 PROCHAINES ÉTAPES API
- Tests paiements réels avec PaymentIntents
- Endpoints statistiques pour dashboard amicales
- Webhooks production avec clés live
- Monitoring et logs des transactions
- Rate limiting sur endpoints sensibles
📱 FLOW TAP TO PAY SIMPLIFIÉ (Janvier 2025)
Architecture
Flutter App (Tap to Pay) ↔ API PHP ↔ Stripe API
Étape 1: Création PaymentIntent
Flutter → API
POST /api/stripe/payments/create-intent
{
"amount": 1500,
"passage_id": 123,
"entity_id": 5
}
API → Stripe → Base de données
// 1. Créer le PaymentIntent
$paymentIntent = Stripe\PaymentIntent::create([...]);
// 2. Sauvegarder dans ope_pass
UPDATE ope_pass SET stripe_payment_id = 'pi_xxx' WHERE id = 123;
API → Flutter
{
"client_secret": "pi_xxx_secret_yyy",
"payment_intent_id": "pi_xxx"
}
Étape 2: Collecte du paiement (Flutter)
- L'app Flutter utilise le SDK Stripe Terminal
- Le téléphone devient le terminal de paiement (Tap to Pay)
- Utilise le client_secret pour collecter le paiement
Étape 3: Confirmation (Webhook)
Stripe → API
- Event:
payment_intent.succeeded - Met à jour le statut dans la base de données
Tables nécessaires
- ✅
ope_pass.stripe_payment_id- Association passage/paiement - ✅
stripe_accounts- Comptes Connect des amicales - ✅
android_certified_devices- Vérification compatibilité - ❌
- Suppriméestripe_payment_intents - ❌
- Pas de terminaux externesterminal_readers
Endpoints essentiels
POST /api/stripe/payments/create-intent- Créer PaymentIntentPOST /api/stripe/devices/check-tap-to-pay- Vérifier compatibilitéPOST /api/stripe/webhook- Recevoir confirmationsGET /api/passages/{id}/stripe-status- Vérifier statut
📝 CHANGELOG
Janvier 2025 - Refactoring base de données
- Suppression de la table
payment_intents(non nécessaire) - Migration :
is_striped→stripe_payment_idVARCHAR(50) dansope_pass - Simplification : Association directe PaymentIntent ↔ Passage
- Avantage : Traçabilité directe sans table intermédiaire
Document créé le 24/08/2024 - Dernière mise à jour : 09/01/2025