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