Files
geo/api/src/Controllers/StripeController.php
Pierre 232940b1eb feat: Version 3.6.2 - Correctifs tâches #17-20
- #17: Amélioration gestion des secteurs et statistiques
- #18: Optimisation services API et logs
- #19: Corrections Flutter widgets et repositories
- #20: Fix création passage - détection automatique ope_users.id vs users.id

Suppression dossier web/ (migration vers app Flutter)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 14:11:15 +01:00

837 lines
30 KiB
PHP

<?php
declare(strict_types=1);
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;
/**
* Controller principal pour les opérations Stripe
* Gère les comptes Connect, les paiements et Terminal
*/
class StripeController extends Controller {
private StripeService $stripeService;
public function __construct() {
parent::__construct();
$this->stripeService = StripeService::getInstance();
}
/**
* POST /api/stripe/accounts
* Créer un compte Stripe Connect pour une amicale
*/
public function createAccount(): void {
try {
$this->requireAuth();
// Vérifier le rôle de l'utilisateur (comme dans les autres controllers)
$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->createConnectAccount($entiteId);
if ($result['success']) {
$this->sendSuccess($result);
} else {
$this->sendError($result['message'], 400);
}
} catch (Exception $e) {
$this->sendError('Erreur lors de la création du compte: ' . $e->getMessage());
}
}
/**
* POST /api/stripe/accounts/{accountId}/onboarding-link
* Générer un lien d'onboarding pour finaliser la configuration
*/
public function createOnboardingLink(string $accountId): void {
try {
$this->requireAuth();
// Log du début de la requête
LogService::log('Début createOnboardingLink', [
'account_id' => $accountId,
'user_id' => Session::getUserId()
]);
// 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', 403);
return;
}
$data = $this->getJsonInput();
$returnUrl = $data['return_url'] ?? '';
$refreshUrl = $data['refresh_url'] ?? '';
LogService::log('URLs reçues', [
'return_url' => $returnUrl,
'refresh_url' => $refreshUrl
]);
if (!$returnUrl || !$refreshUrl) {
$this->sendError('URLs de retour requises', 400);
return;
}
$result = $this->stripeService->createOnboardingLink($accountId, $returnUrl, $refreshUrl);
LogService::log('Résultat createOnboardingLink', [
'success' => $result['success'] ?? false,
'has_url' => isset($result['url'])
]);
if ($result['success']) {
$this->sendSuccess([
'status' => 'success',
'url' => $result['url']
]);
exit; // Terminer explicitement après l'envoi de la réponse
} else {
$this->sendError($result['message'], 400);
exit;
}
} catch (Exception $e) {
LogService::log('Erreur createOnboardingLink', [
'level' => 'error',
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
$this->sendError('Erreur: ' . $e->getMessage());
exit;
}
}
/**
* POST /api/stripe/payments/create-intent
* Créer une intention de paiement pour Tap to Pay ou paiement Web
*
* Payload Tap to Pay:
* {
* "amount": 2500,
* "currency": "eur",
* "description": "Calendrier pompiers - Passage #789",
* "payment_method_types": ["card_present"],
* "capture_method": "automatic",
* "passage_id": 789,
* "amicale_id": 42,
* "member_id": 156,
* "stripe_account": "acct_1O3ABC456DEF789",
* "location_id": "tml_FGH123456789",
* "metadata": {...}
* }
*/
public function createPaymentIntent(): void {
try {
$this->requireAuth();
$data = $this->getJsonInput();
// Validation des champs requis
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 passage_id (doit être > 0 car le passage est créé avant)
if ($passageId <= 0) {
$this->sendError('passage_id invalide. Le passage doit être créé avant le paiement', 400);
return;
}
// Validation du montant
if ($amount < 100) {
$this->sendError('Le montant minimum est de 1€ (100 centimes)', 400);
return;
}
if ($amount > 99900) { // 999€ max selon la doc
$this->sendError('Le montant maximum est de 999€', 400);
return;
}
// Vérifier que le passage existe et appartient à l'utilisateur
$stmt = $this->db->prepare('
SELECT p.*, o.fk_entite, o.id as operation_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 = ? AND ou.fk_user = ?
');
$stmt->execute([$passageId, Session::getUserId()]);
$passage = $stmt->fetch();
if (!$passage) {
$this->sendError('Passage non trouvé ou non autorisé', 404);
return;
}
// Vérifier qu'il n'y a pas déjà un paiement Stripe pour ce passage
if (!empty($passage['stripe_payment_id'])) {
$this->sendError('Un paiement Stripe 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;
}
$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'];
$isTapToPay = in_array('card_present', $paymentMethodTypes);
// Préparer les paramètres pour StripeService
$params = [
'amount' => $amount,
'currency' => $data['currency'] ?? 'eur',
'description' => $data['description'] ?? "Calendrier pompiers - Passage #$passageId",
'payment_method_types' => $paymentMethodTypes,
'capture_method' => $data['capture_method'] ?? 'automatic',
'passage_id' => $passageId,
'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),
'fk_user' => (string)$fkUser,
'created_at' => (string)time(),
'type' => $isTapToPay ? 'tap_to_pay' : 'web'
],
$data['metadata'] ?? []
)
];
// Ajouter location_id si fourni (pour Tap to Pay)
if (isset($data['location_id'])) {
$params['location_id'] = $data['location_id'];
}
// Créer le PaymentIntent via StripeService
$result = $this->stripeService->createPaymentIntent($params);
if ($result['success']) {
// Mettre à jour le passage avec le stripe_payment_id
$stmt = $this->db->prepare('
UPDATE ope_pass
SET stripe_payment_id = ?, updated_at = NOW()
WHERE id = ?
');
$stmt->execute([$result['payment_intent_id'], $passageId]);
// Retourner la réponse
$this->sendSuccess([
'client_secret' => $result['client_secret'],
'payment_intent_id' => $result['payment_intent_id'],
'amount' => $result['amount'],
'currency' => $params['currency'],
'passage_id' => $passageId,
'type' => $isTapToPay ? 'tap_to_pay' : 'web'
]);
} else {
$this->sendError($result['message'], 400);
}
} catch (Exception $e) {
$this->sendError('Erreur: ' . $e->getMessage());
}
}
/**
* GET /api/stripe/payments/{paymentIntentId}
* Récupérer le statut d'un paiement depuis ope_pass et Stripe
*/
public function getPaymentStatus(string $paymentIntentId): void {
try {
$this->requireAuth();
// Récupérer les informations depuis ope_pass
$stmt = $this->db->prepare("
SELECT p.*, o.fk_entite,
e.encrypted_name as entite_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 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]);
$passage = $stmt->fetch();
if (!$passage) {
$this->sendError('Paiement non trouvé', 404);
return;
}
// Vérifier les droits
$userEntityId = Session::getEntityId();
$userId = Session::getUserId();
// Récupérer le rôle depuis la base de données
$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 ($passage['fk_entite'] != $userEntityId &&
$passage['fk_user'] != $userId &&
$userRole < 3) {
$this->sendError('Non autorisé', 403);
return;
}
// Récupérer le statut en temps réel depuis Stripe
$stripeStatus = $this->stripeService->getPaymentIntentStatus($paymentIntentId);
// Déchiffrer le nom de l'entité si nécessaire
$entiteNom = '';
if (!empty($passage['entite_nom'])) {
try {
$entiteNom = ApiService::decryptData($passage['entite_nom']);
} catch (Exception $e) {
$entiteNom = 'Entité inconnue';
}
}
$this->sendSuccess([
'payment_intent_id' => $paymentIntentId,
'passage_id' => $passage['id'],
'status' => $stripeStatus['status'] ?? 'unknown',
'amount' => (int)($passage['montant'] * 100), // montant en BDD est en euros, on convertit en centimes
'currency' => 'eur',
'entite' => [
'id' => $passage['fk_entite'],
'nom' => $entiteNom
],
'user' => [
'id' => $passage['fk_user'],
'nom' => $passage['user_nom'],
'prenom' => $passage['user_prenom']
],
'created_at' => $passage['date_creat'],
'stripe_details' => $stripeStatus
]);
} catch (Exception $e) {
$this->sendError('Erreur: ' . $e->getMessage());
}
}
/**
* GET /api/stripe/accounts/{entityId}/status
* Vérifier le statut du compte Stripe d'une entité
*/
public function getAccountStatus(string $entityId): void {
try {
$this->requireAuth();
// Convertir l'entityId en int
$entityId = (int)$entityId;
// Vérifier les droits : admin de l'amicale ou super admin
$userEntityId = Session::getEntityId();
$userId = Session::getUserId();
// Récupérer le rôle depuis la base de données
$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 ($entityId != $userEntityId && $userRole < 3) {
$this->sendError('Non autorisé', 403);
return;
}
// Récupérer le compte Stripe
$stmt = $this->db->prepare(
"SELECT sa.*, e.encrypted_name as entite_nom
FROM stripe_accounts sa
LEFT JOIN entites e ON sa.fk_entite = e.id
WHERE sa.fk_entite = :entity_id"
);
$stmt->execute(['entity_id' => $entityId]);
$account = $stmt->fetch();
if (!$account || !$account['stripe_account_id']) {
$this->sendSuccess([
'has_account' => false,
'account_id' => null,
'location_id' => null,
'charges_enabled' => false,
'payouts_enabled' => false,
'onboarding_completed' => false
]);
return;
}
// Récupérer le statut depuis Stripe
$stripeService = StripeService::getInstance();
$stripeAccount = $stripeService->retrieveAccount($account['stripe_account_id']);
if (!$stripeAccount) {
$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,
'error' => 'Compte non trouvé sur Stripe'
]);
return;
}
// Mettre à jour la base de données avec le statut actuel
$stmt = $this->db->prepare(
"UPDATE stripe_accounts
SET charges_enabled = :charges,
payouts_enabled = :payouts,
updated_at = NOW()
WHERE id = :id"
);
$stmt->execute([
'charges' => $stripeAccount->charges_enabled ? 1 : 0,
'payouts' => $stripeAccount->payouts_enabled ? 1 : 0,
'id' => $account['id']
]);
$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,
'entite' => [
'id' => $entityId,
'nom' => $account['entite_nom']
]
]);
} catch (Exception $e) {
// Logger::getInstance()->error('Erreur statut compte Stripe', [
// 'entity_id' => $entityId,
// 'error' => $e->getMessage()
// ]);
$this->sendError('Erreur: ' . $e->getMessage());
}
}
/**
* GET /api/stripe/config
* Récupérer la configuration publique Stripe
*/
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
*/
public function getPaymentStats(): void {
try {
$this->requireAuth();
$entiteId = $_GET['fk_entite'] ?? Session::getEntityId();
$userId = $_GET['fk_user'] ?? null;
$dateFrom = $_GET['date_from'] ?? date('Y-m-01');
$dateTo = $_GET['date_to'] ?? date('Y-m-d');
// Vérifier les droits
// Récupérer le rôle pour vérifier les droits
$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 ($entiteId != Session::getEntityId() && $userRole < 3) {
$this->sendError('Non autorisé', 403);
return;
}
$query = "SELECT
COUNT(CASE WHEN status = 'succeeded' THEN 1 END) as total_ventes,
SUM(CASE WHEN status = 'succeeded' THEN amount ELSE 0 END) as total_montant,
SUM(CASE WHEN status = 'succeeded' THEN application_fee ELSE 0 END) as total_commissions,
DATE(created_at) as date_vente
FROM stripe_payment_intents
WHERE fk_entite = :entite_id
AND DATE(created_at) BETWEEN :date_from AND :date_to";
$params = [
'entite_id' => $entiteId,
'date_from' => $dateFrom,
'date_to' => $dateTo
];
if ($userId) {
$query .= " AND fk_user = :user_id";
$params['user_id'] = $userId;
}
$query .= " GROUP BY DATE(created_at) ORDER BY date_vente DESC";
$stmt = $this->db->prepare($query);
$stmt->execute($params);
$stats = $stmt->fetchAll();
// Calculer les totaux
$totals = [
'total_ventes' => 0,
'total_montant' => 0,
'total_commissions' => 0
];
foreach ($stats as $stat) {
$totals['total_ventes'] += $stat['total_ventes'];
$totals['total_montant'] += $stat['total_montant'];
$totals['total_commissions'] += $stat['total_commissions'];
}
$this->sendSuccess([
'stats' => $stats,
'totals' => $totals,
'period' => [
'from' => $dateFrom,
'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());
}
}
/**
* POST /api/stripe/terminal/connection-token
* Créer un Connection Token pour Stripe Terminal/Tap to Pay
* Requis par le SDK Stripe Terminal pour se connecter aux readers
*/
public function createConnectionToken(): void {
try {
$this->requireAuth();
$data = $this->getJsonInput();
$entiteId = $data['amicale_id'] ?? Session::getEntityId();
if (!$entiteId) {
$this->sendError('ID entité requis', 400);
return;
}
// Vérifier les droits sur cette entité
$userRole = Session::getRole() ?? 0;
if (Session::getEntityId() != $entiteId && $userRole < 3) {
$this->sendError('Non autorisé pour cette entité', 403);
return;
}
$result = $this->stripeService->createConnectionToken($entiteId);
if ($result['success']) {
$this->sendSuccess([
'secret' => $result['secret']
]);
} else {
$this->sendError($result['message'], 400);
}
} catch (Exception $e) {
$this->sendError('Erreur lors de la création du connection token: ' . $e->getMessage());
}
}
/**
* POST /api/stripe/payments/cancel
* Annuler un PaymentIntent Stripe
*
* Payload:
* {
* "payment_intent_id": "pi_3SWvho2378xpV4Rn1J43Ks7M"
* }
*/
public function cancelPayment(): void {
try {
$this->requireAuth();
$data = $this->getJsonInput();
// Validation
if (!isset($data['payment_intent_id'])) {
$this->sendError('payment_intent_id requis', 400);
return;
}
$paymentIntentId = $data['payment_intent_id'];
// Vérifier que le passage existe et appartient à l'utilisateur
$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.stripe_payment_id = ?
');
$stmt->execute([$paymentIntentId]);
$passage = $stmt->fetch();
if (!$passage) {
$this->sendError('Paiement non trouvé', 404);
return;
}
// Vérifier les droits
$userId = Session::getUserId();
$userEntityId = Session::getEntityId();
if ($passage['ope_user_id'] != $userId && $passage['fk_entite'] != $userEntityId) {
$this->sendError('Non autorisé', 403);
return;
}
// Annuler le PaymentIntent via StripeService
$result = $this->stripeService->cancelPaymentIntent($paymentIntentId);
if ($result['success']) {
// Retirer le stripe_payment_id du passage
$stmt = $this->db->prepare('
UPDATE ope_pass
SET stripe_payment_id = NULL, updated_at = NOW()
WHERE id = ?
');
$stmt->execute([$passage['id']]);
$this->sendSuccess([
'status' => 'canceled',
'payment_intent_id' => $paymentIntentId,
'passage_id' => $passage['id']
]);
} else {
$this->sendError($result['message'], 400);
}
} catch (Exception $e) {
$this->sendError('Erreur: ' . $e->getMessage());
}
}
}