Fix: Corriger le type PDO dans StripeService et retirer getConnection()

This commit is contained in:
2025-09-01 15:23:48 +02:00
parent f597c9aeb5
commit a548ef8890
545 changed files with 189339 additions and 130108 deletions

View File

@@ -0,0 +1,493 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Core\Controller;
use App\Services\StripeService;
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();
$this->requireRole(2); // Admin amicale minimum
$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 && Session::getRole() < 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();
$this->requireRole(2);
$data = $this->getJsonInput();
$returnUrl = $data['return_url'] ?? '';
$refreshUrl = $data['refresh_url'] ?? '';
if (!$returnUrl || !$refreshUrl) {
$this->sendError('URLs de retour requises', 400);
return;
}
$result = $this->stripeService->createOnboardingLink($accountId, $returnUrl, $refreshUrl);
if ($result['success']) {
$this->sendSuccess(['url' => $result['url']]);
} else {
$this->sendError($result['message'], 400);
}
} catch (Exception $e) {
$this->sendError('Erreur: ' . $e->getMessage());
}
}
/**
* POST /api/stripe/locations
* Créer une Location pour Terminal/Tap to Pay
*/
public function createLocation(): void {
try {
$this->requireAuth();
$this->requireRole(2);
$data = $this->getJsonInput();
$entiteId = $data['fk_entite'] ?? Session::getEntityId();
$result = $this->stripeService->createLocation($entiteId);
if ($result['success']) {
$this->sendSuccess($result);
} else {
$this->sendError($result['message'], 400);
}
} catch (Exception $e) {
$this->sendError('Erreur: ' . $e->getMessage());
}
}
/**
* POST /api/stripe/terminal/connection-token
* Créer un token de connexion pour Terminal/Tap to Pay
*/
public function createConnectionToken(): void {
try {
$this->requireAuth();
$entiteId = Session::getEntityId();
if (!$entiteId) {
$this->sendError('Entité non définie', 400);
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: ' . $e->getMessage());
}
}
/**
* POST /api/stripe/payments/create-intent
* Créer une intention de paiement
*/
public function createPaymentIntent(): void {
try {
$this->requireAuth();
$data = $this->getJsonInput();
// Validation
$amount = $data['amount'] ?? 0;
if ($amount < 100) {
$this->sendError('Le montant minimum est de 1€ (100 centimes)', 400);
return;
}
if ($amount > 50000) {
$this->sendError('Le montant maximum est de 500€', 400);
return;
}
$params = [
'amount' => $amount,
'fk_entite' => $data['fk_entite'] ?? Session::getEntityId(),
'fk_user' => Session::getUserId(),
'metadata' => $data['metadata'] ?? []
];
$result = $this->stripeService->createPaymentIntent($params);
if ($result['success']) {
$this->sendSuccess([
'client_secret' => $result['client_secret'],
'payment_intent_id' => $result['payment_intent_id'],
'amount' => $result['amount'],
'application_fee' => $result['application_fee']
]);
} 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
*/
public function getPaymentStatus(string $paymentIntentId): void {
try {
$this->requireAuth();
$stmt = $this->db->prepare(
"SELECT spi.*, e.nom as entite_nom, u.nom as user_nom, u.prenom as user_prenom
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
WHERE spi.stripe_payment_intent_id = :pi_id"
);
$stmt->execute(['pi_id' => $paymentIntentId]);
$payment = $stmt->fetch();
if (!$payment) {
$this->sendError('Paiement non trouvé', 404);
return;
}
// Vérifier les droits
$userEntityId = Session::getEntityId();
$userRole = Session::getRole();
$userId = Session::getUserId();
if ($payment['fk_entite'] != $userEntityId &&
$payment['fk_user'] != $userId &&
$userRole < 3) {
$this->sendError('Non autorisé', 403);
return;
}
$this->sendSuccess([
'payment_intent_id' => $payment['stripe_payment_intent_id'],
'status' => $payment['status'],
'amount' => $payment['amount'],
'currency' => $payment['currency'],
'application_fee' => $payment['application_fee'],
'entite' => [
'id' => $payment['fk_entite'],
'nom' => $payment['entite_nom']
],
'user' => [
'id' => $payment['fk_user'],
'nom' => $payment['user_nom'],
'prenom' => $payment['user_prenom']
],
'created_at' => $payment['created_at']
]);
} 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(int $entityId): void {
try {
$this->requireAuth();
// Vérifier les droits : admin de l'amicale ou super admin
$userEntityId = Session::getEntityId();
$userRole = Session::getRole();
if ($entityId != $userEntityId && $userRole < 3) {
$this->sendError('Non autorisé', 403);
return;
}
$db = Database::getInstance();
// Récupérer le compte Stripe
$stmt = $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,
'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'],
'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 = $db->prepare(
"UPDATE stripe_accounts
SET charges_enabled = :charges,
payouts_enabled = :payouts,
details_submitted = :details,
updated_at = NOW()
WHERE id = :id"
);
$stmt->execute([
'charges' => $stripeAccount->charges_enabled ? 1 : 0,
'payouts' => $stripeAccount->payouts_enabled ? 1 : 0,
'details' => $stripeAccount->details_submitted ? 1 : 0,
'id' => $account['id']
]);
$this->sendSuccess([
'has_account' => true,
'account_id' => $account['stripe_account_id'],
'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());
}
}
/**
* POST /api/stripe/devices/check-tap-to-pay
* Vérifier la compatibilité Tap to Pay d'un appareil
*/
public function checkTapToPayCapability(): void {
try {
$data = $this->getJsonInput();
$platform = $data['platform'] ?? '';
if ($platform === 'ios') {
// Pour iOS, on vérifie côté client (iPhone XS+ avec iOS 15.4+)
$this->sendSuccess([
'message' => 'Vérification iOS à faire côté client',
'requirements' => 'iPhone XS ou plus récent avec iOS 15.4+'
]);
return;
}
if ($platform === 'android') {
$manufacturer = $data['manufacturer'] ?? '';
$model = $data['model'] ?? '';
if (!$manufacturer || !$model) {
$this->sendError('Manufacturer et model requis pour Android', 400);
return;
}
$result = $this->stripeService->checkAndroidTapToPayCompatibility($manufacturer, $model);
if ($result['success']) {
$this->sendSuccess($result);
} else {
$this->sendError($result['message'], 400);
}
} else {
$this->sendError('Platform doit être ios ou android', 400);
}
} catch (Exception $e) {
$this->sendError('Erreur: ' . $e->getMessage());
}
}
/**
* GET /api/stripe/devices/certified-android
* Récupérer la liste des appareils Android certifiés
*/
public function getCertifiedAndroidDevices(): void {
try {
$result = $this->stripeService->getCertifiedAndroidDevices();
if ($result['success']) {
$this->sendSuccess(['devices' => $result['devices']]);
} else {
$this->sendError($result['message'], 400);
}
} catch (Exception $e) {
$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
if ($entiteId != Session::getEntityId() && Session::getRole() < 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());
}
}
}

View File

@@ -0,0 +1,335 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Core\Controller;
use App\Services\StripeService;
use Stripe\Webhook;
use Stripe\Exception\SignatureVerificationException;
use AppConfig;
use Exception;
use PDO;
/**
* Controller pour gérer les webhooks Stripe
* Point d'entrée pour tous les événements Stripe
*/
class StripeWebhookController extends Controller {
private StripeService $stripeService;
private AppConfig $config;
public function __construct() {
parent::__construct();
$this->stripeService = StripeService::getInstance();
$this->config = AppConfig::getInstance();
}
/**
* POST /api/stripe/webhook
* Point d'entrée principal pour les webhooks Stripe
*/
public function handleWebhook(): void {
try {
// Récupérer le payload et la signature
$payload = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
if (empty($sigHeader)) {
http_response_code(400);
echo 'Missing Stripe signature';
exit;
}
// Récupérer le secret webhook selon le mode
$stripeConfig = $this->config->get('stripe');
$webhookSecret = $this->stripeService->isTestMode()
? $stripeConfig['webhook_secret_test']
: $stripeConfig['webhook_secret_live'];
if (empty($webhookSecret) || strpos($webhookSecret, 'XXXX') !== false) {
http_response_code(500);
echo 'Webhook secret not configured';
exit;
}
// Vérifier la signature
try {
$event = Webhook::constructEvent($payload, $sigHeader, $webhookSecret);
} catch (SignatureVerificationException $e) {
http_response_code(400);
echo 'Invalid signature';
exit;
}
// Vérifier si l'événement a déjà été traité (idempotence)
$stmt = $this->db->prepare(
"SELECT id FROM stripe_webhooks WHERE stripe_event_id = :event_id"
);
$stmt->execute(['event_id' => $event->id]);
if ($stmt->fetch()) {
// Événement déjà traité
http_response_code(200);
echo 'Event already processed';
exit;
}
// Enregistrer l'événement
$stmt = $this->db->prepare(
"INSERT INTO stripe_webhooks (stripe_event_id, event_type, livemode, payload, created_at)
VALUES (:event_id, :event_type, :livemode, :payload, NOW())"
);
$stmt->execute([
'event_id' => $event->id,
'event_type' => $event->type,
'livemode' => $event->livemode ? 1 : 0,
'payload' => $payload
]);
$webhookId = $this->db->lastInsertId();
// Traiter l'événement selon son type
try {
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->handlePaymentIntentSucceeded($event->data->object);
break;
case 'payment_intent.payment_failed':
$this->handlePaymentIntentFailed($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}");
}
// Marquer comme traité
$stmt = $this->db->prepare(
"UPDATE stripe_webhooks
SET processed = 1, processed_at = NOW()
WHERE id = :id"
);
$stmt->execute(['id' => $webhookId]);
} catch (Exception $e) {
// Enregistrer l'erreur
$stmt = $this->db->prepare(
"UPDATE stripe_webhooks
SET error_message = :error
WHERE id = :id"
);
$stmt->execute([
'error' => $e->getMessage(),
'id' => $webhookId
]);
throw $e;
}
// Réponse de succès
http_response_code(200);
echo 'Webhook handled';
} catch (Exception $e) {
error_log('Stripe webhook error: ' . $e->getMessage());
http_response_code(500);
echo 'Webhook handler error';
}
}
/**
* Gérer la mise à jour d'un compte Connect
*/
private function handleAccountUpdated($account): void {
$stmt = $this->db->prepare(
"UPDATE stripe_accounts
SET charges_enabled = :charges_enabled,
payouts_enabled = :payouts_enabled,
onboarding_completed = :onboarding_completed,
updated_at = NOW()
WHERE stripe_account_id = :account_id"
);
$stmt->execute([
'charges_enabled' => $account->charges_enabled ? 1 : 0,
'payouts_enabled' => $account->payouts_enabled ? 1 : 0,
'onboarding_completed' => ($account->charges_enabled && $account->payouts_enabled) ? 1 : 0,
'account_id' => $account->id
]);
// Log pour suivi
error_log("Account updated: {$account->id}, charges: {$account->charges_enabled}, payouts: {$account->payouts_enabled}");
}
/**
* Gérer l'autorisation d'un compte Connect
*/
private function handleAccountAuthorized($account): void {
// Similaire à account.updated mais spécifique à l'autorisation
$this->handleAccountUpdated($account);
// Potentiellement envoyer un email de confirmation
// TODO: Implémenter notification email
}
/**
* Gérer un paiement réussi
*/
private function handlePaymentIntentSucceeded($paymentIntent): void {
// Mettre à jour le statut en base
$stmt = $this->db->prepare(
"UPDATE stripe_payment_intents
SET status = :status, updated_at = NOW()
WHERE stripe_payment_intent_id = :pi_id"
);
$stmt->execute([
'status' => 'succeeded',
'pi_id' => $paymentIntent->id
]);
// Enregistrer dans l'historique
$stmt = $this->db->prepare(
"SELECT id FROM stripe_payment_intents WHERE stripe_payment_intent_id = :pi_id"
);
$stmt->execute(['pi_id' => $paymentIntent->id]);
$localPayment = $stmt->fetch();
if ($localPayment) {
$stmt = $this->db->prepare(
"INSERT INTO stripe_payment_history
(fk_payment_intent, event_type, event_data, created_at)
VALUES (:fk_pi, 'succeeded', :data, NOW())"
);
$stmt->execute([
'fk_pi' => $localPayment['id'],
'data' => json_encode([
'amount' => $paymentIntent->amount,
'currency' => $paymentIntent->currency,
'payment_method' => $paymentIntent->payment_method,
'charges' => $paymentIntent->charges->data
])
]);
}
// TODO: Envoyer un reçu par email
// TODO: Mettre à jour les statistiques en temps réel
error_log("Payment succeeded: {$paymentIntent->id} for {$paymentIntent->amount} {$paymentIntent->currency}");
}
/**
* Gérer un paiement échoué
*/
private function handlePaymentIntentFailed($paymentIntent): void {
// Mettre à jour le statut
$stmt = $this->db->prepare(
"UPDATE stripe_payment_intents
SET status = :status, updated_at = NOW()
WHERE stripe_payment_intent_id = :pi_id"
);
$stmt->execute([
'status' => 'failed',
'pi_id' => $paymentIntent->id
]);
// Enregistrer dans l'historique avec la raison de l'échec
$stmt = $this->db->prepare(
"SELECT id FROM stripe_payment_intents WHERE stripe_payment_intent_id = :pi_id"
);
$stmt->execute(['pi_id' => $paymentIntent->id]);
$localPayment = $stmt->fetch();
if ($localPayment) {
$stmt = $this->db->prepare(
"INSERT INTO stripe_payment_history
(fk_payment_intent, event_type, event_data, created_at)
VALUES (:fk_pi, 'failed', :data, NOW())"
);
$stmt->execute([
'fk_pi' => $localPayment['id'],
'data' => json_encode([
'error' => $paymentIntent->last_payment_error,
'cancellation_reason' => $paymentIntent->cancellation_reason
])
]);
}
error_log("Payment failed: {$paymentIntent->id}, reason: " . json_encode($paymentIntent->last_payment_error));
}
/**
* Gérer un litige (chargeback)
*/
private function handleChargeDisputeCreated($dispute): void {
// Trouver le paiement concerné
$chargeId = $dispute->charge;
// TODO: Implémenter la gestion des litiges
// - Notifier l'admin
// - Bloquer les fonds si nécessaire
// - Créer une tâche de suivi
error_log("Dispute created: {$dispute->id} for charge {$chargeId}, amount: {$dispute->amount}");
// Envoyer une alerte urgente
// TODO: Implémenter système d'alertes
}
/**
* Gérer une action réussie sur un Terminal reader
*/
private function handleTerminalReaderActionSucceeded($reader): void {
// Mettre à jour le statut du reader
$stmt = $this->db->prepare(
"UPDATE stripe_terminal_readers
SET status = :status, last_seen_at = NOW()
WHERE stripe_reader_id = :reader_id"
);
$stmt->execute([
'status' => 'online',
'reader_id' => $reader->id
]);
error_log("Terminal reader action succeeded: {$reader->id}");
}
/**
* Gérer une action échouée sur un Terminal reader
*/
private function handleTerminalReaderActionFailed($reader): void {
// Mettre à jour le statut du reader
$stmt = $this->db->prepare(
"UPDATE stripe_terminal_readers
SET status = :status, last_seen_at = NOW()
WHERE stripe_reader_id = :reader_id"
);
$stmt->execute([
'status' => 'error',
'reader_id' => $reader->id
]);
error_log("Terminal reader action failed: {$reader->id}");
}
}