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:
pierre
2025-11-09 18:26:27 +01:00
parent 21657a3820
commit 2f5946a184
812 changed files with 142105 additions and 25992 deletions

View File

@@ -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());
}
}
}