config = AppConfig::getInstance(); $this->db = Database::getInstance(); // Déterminer le mode (test ou live) $stripeConfig = $this->config->getStripeConfig(); $this->testMode = ($stripeConfig['mode'] ?? 'test') === 'test'; // Initialiser Stripe avec la bonne clé $secretKey = $this->testMode ? $stripeConfig['secret_key_test'] : $stripeConfig['secret_key_live']; if (empty($secretKey) || strpos($secretKey, 'XXXX') !== false) { throw new Exception('Clé Stripe non configurée. Veuillez configurer vos clés dans AppConfig.php'); } $this->stripe = new StripeClient([ 'api_key' => $secretKey, 'stripe_version' => $stripeConfig['api_version'] ]); // Définir la clé API globalement aussi (pour certaines opérations) Stripe::setApiKey($secretKey); Stripe::setApiVersion($stripeConfig['api_version']); } public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Créer un compte Stripe Connect Express pour une amicale */ public function createConnectAccount(int $entiteId): array { try { // Récupérer les infos de l'entité $stmt = $this->db->prepare( "SELECT * FROM entites WHERE id = :id" ); $stmt->execute(['id' => $entiteId]); $entite = $stmt->fetch(PDO::FETCH_ASSOC); if (!$entite) { throw new Exception("Entité non trouvée"); } // Vérifier si un compte existe déjà $stmt = $this->db->prepare( "SELECT * FROM stripe_accounts WHERE fk_entite = :fk_entite" ); $stmt->execute(['fk_entite' => $entiteId]); $existingAccount = $stmt->fetch(PDO::FETCH_ASSOC); if ($existingAccount) { return [ 'success' => false, 'message' => 'Un compte Stripe existe déjà pour cette entité', 'account_id' => $existingAccount['stripe_account_id'] ]; } // Créer le compte Stripe Connect Express $account = $this->stripe->accounts->create([ 'type' => 'express', 'country' => 'FR', 'email' => $entite['email'] ?? null, 'capabilities' => [ 'card_payments' => ['requested' => true], 'transfers' => ['requested' => true], ], 'business_type' => 'non_profit', // Association 'business_profile' => [ 'name' => $entite['nom'], 'product_description' => 'Vente de calendriers des pompiers', 'support_email' => $entite['email'] ?? null, 'url' => $entite['site_web'] ?? null, ], 'metadata' => [ 'entite_id' => $entiteId, 'entite_name' => $entite['nom'] ] ]); // Sauvegarder en base de données $stmt = $this->db->prepare( "INSERT INTO stripe_accounts (fk_entite, stripe_account_id, created_at) VALUES (:fk_entite, :stripe_account_id, NOW())" ); $stmt->execute([ 'fk_entite' => $entiteId, 'stripe_account_id' => $account->id ]); return [ 'success' => true, 'account_id' => $account->id, 'message' => 'Compte Stripe créé avec succès' ]; } catch (ApiErrorException $e) { return [ 'success' => false, 'message' => 'Erreur Stripe: ' . $e->getMessage() ]; } catch (Exception $e) { return [ 'success' => false, 'message' => 'Erreur: ' . $e->getMessage() ]; } } /** * Récupérer les informations d'un compte Stripe Connect */ public function retrieveAccount(string $accountId) { try { return Account::retrieve($accountId); } catch (Exception $e) { Logger::getInstance()->error('Erreur récupération compte Stripe', [ 'account_id' => $accountId, 'error' => $e->getMessage() ]); return null; } } /** * Générer un lien d'onboarding pour finaliser la configuration du compte */ public function createOnboardingLink(string $accountId, string $returnUrl, string $refreshUrl): array { try { $accountLink = $this->stripe->accountLinks->create([ 'account' => $accountId, 'refresh_url' => $refreshUrl, 'return_url' => $returnUrl, 'type' => 'account_onboarding', ]); return [ 'success' => true, 'url' => $accountLink->url ]; } catch (ApiErrorException $e) { return [ 'success' => false, 'message' => 'Erreur Stripe: ' . $e->getMessage() ]; } } /** * Créer une Location pour Terminal/Tap to Pay */ public function createLocation(int $entiteId): array { try { // Récupérer le compte Stripe et l'entité $stmt = $this->db->prepare( "SELECT sa.*, e.* FROM stripe_accounts sa JOIN entites e ON sa.fk_entite = e.id WHERE sa.fk_entite = :fk_entite" ); $stmt->execute(['fk_entite' => $entiteId]); $data = $stmt->fetch(PDO::FETCH_ASSOC); if (!$data) { throw new Exception("Compte Stripe non trouvé pour cette entité"); } // Créer la location $location = $this->stripe->terminal->locations->create([ 'display_name' => $data['nom'], 'address' => [ 'line1' => $data['adresse'] ?? 'Adresse non renseignée', 'city' => $data['ville'] ?? 'Ville', 'postal_code' => $data['code_postal'] ?? '00000', 'country' => 'FR', ], 'metadata' => [ 'entite_id' => $entiteId, 'type' => 'tap_to_pay' ] ], [ 'stripe_account' => $data['stripe_account_id'] ]); // Mettre à jour en base $stmt = $this->db->prepare( "UPDATE stripe_accounts SET stripe_location_id = :location_id, updated_at = NOW() WHERE fk_entite = :fk_entite" ); $stmt->execute([ 'location_id' => $location->id, 'fk_entite' => $entiteId ]); return [ 'success' => true, 'location_id' => $location->id, 'message' => 'Location créée avec succès' ]; } catch (Exception $e) { return [ 'success' => false, 'message' => 'Erreur: ' . $e->getMessage() ]; } } /** * Créer un Connection Token pour Terminal/Tap to Pay */ public function createConnectionToken(int $entiteId): array { try { // Récupérer le compte et la location $stmt = $this->db->prepare( "SELECT * FROM stripe_accounts WHERE fk_entite = :fk_entite" ); $stmt->execute(['fk_entite' => $entiteId]); $account = $stmt->fetch(PDO::FETCH_ASSOC); if (!$account || !$account['stripe_location_id']) { throw new Exception("Location Stripe non configurée pour cette entité"); } // Créer le token $connectionToken = $this->stripe->terminal->connectionTokens->create([ 'location' => $account['stripe_location_id'] ], [ 'stripe_account' => $account['stripe_account_id'] ]); return [ 'success' => true, 'secret' => $connectionToken->secret ]; } catch (Exception $e) { return [ 'success' => false, 'message' => 'Erreur: ' . $e->getMessage() ]; } } /** * Créer une intention de paiement */ public function createPaymentIntent(array $params): array { try { $amount = $params['amount'] ?? 0; $entiteId = $params['fk_entite'] ?? 0; $userId = $params['fk_user'] ?? 0; $metadata = $params['metadata'] ?? []; if ($amount < 100) { throw new Exception("Le montant minimum est de 1€"); } // Récupérer le compte Stripe $stmt = $this->db->prepare( "SELECT * FROM stripe_accounts WHERE fk_entite = :fk_entite" ); $stmt->execute(['fk_entite' => $entiteId]); $account = $stmt->fetch(PDO::FETCH_ASSOC); if (!$account) { throw new Exception("Compte Stripe non trouvé"); } // Calculer la commission (2.5% ou 50 centimes minimum) $stripeConfig = $this->config->getStripeConfig(); $applicationFee = max( $stripeConfig['application_fee_minimum'], round($amount * $stripeConfig['application_fee_percent'] / 100) ); // Créer le PaymentIntent $paymentIntent = $this->stripe->paymentIntents->create([ 'amount' => $amount, 'currency' => 'eur', 'payment_method_types' => ['card_present'], 'capture_method' => 'automatic', 'application_fee_amount' => $applicationFee, 'transfer_data' => [ 'destination' => $account['stripe_account_id'], ], 'metadata' => array_merge($metadata, [ 'entite_id' => $entiteId, 'user_id' => $userId, 'calendrier_annee' => date('Y'), ]), ]); // Sauvegarder en base $stmt = $this->db->prepare( "INSERT INTO stripe_payment_intents (stripe_payment_intent_id, fk_entite, fk_user, amount, currency, status, application_fee, metadata, created_at) VALUES (:pi_id, :fk_entite, :fk_user, :amount, :currency, :status, :app_fee, :metadata, NOW())" ); $stmt->execute([ 'pi_id' => $paymentIntent->id, 'fk_entite' => $entiteId, 'fk_user' => $userId, 'amount' => $amount, 'currency' => 'eur', 'status' => $paymentIntent->status, 'app_fee' => $applicationFee, 'metadata' => json_encode($metadata) ]); return [ 'success' => true, 'client_secret' => $paymentIntent->client_secret, 'payment_intent_id' => $paymentIntent->id, 'amount' => $amount, 'application_fee' => $applicationFee ]; } catch (Exception $e) { return [ 'success' => false, 'message' => 'Erreur: ' . $e->getMessage() ]; } } /** * Vérifier la compatibilité Tap to Pay d'un appareil Android */ public function checkAndroidTapToPayCompatibility(string $manufacturer, string $model): array { try { $stmt = $this->db->prepare( "SELECT * FROM stripe_android_certified_devices WHERE manufacturer = :manufacturer AND model = :model AND tap_to_pay_certified = 1 AND country = 'FR'" ); $stmt->execute([ 'manufacturer' => $manufacturer, 'model' => $model ]); $device = $stmt->fetch(PDO::FETCH_ASSOC); if ($device) { return [ 'success' => true, 'tap_to_pay_supported' => true, 'message' => 'Tap to Pay disponible sur cet appareil', 'min_android_version' => $device['min_android_version'] ]; } return [ 'success' => true, 'tap_to_pay_supported' => false, 'message' => 'Appareil non certifié pour Tap to Pay en France', 'alternative' => 'Utilisez un iPhone XS ou plus récent avec iOS 15.4+' ]; } catch (Exception $e) { return [ 'success' => false, 'message' => 'Erreur: ' . $e->getMessage() ]; } } /** * Récupérer les appareils Android certifiés */ public function getCertifiedAndroidDevices(): array { try { $stmt = $this->db->prepare( "SELECT manufacturer, model, model_identifier, min_android_version FROM stripe_android_certified_devices WHERE tap_to_pay_certified = 1 AND country = 'FR' ORDER BY manufacturer, model" ); $stmt->execute(); return [ 'success' => true, 'devices' => $stmt->fetchAll(PDO::FETCH_ASSOC) ]; } catch (Exception $e) { return [ 'success' => false, 'message' => 'Erreur: ' . $e->getMessage() ]; } } /** * Obtenir le mode actuel (test ou live) */ public function isTestMode(): bool { return $this->testMode; } /** * Obtenir la clé publique pour le frontend */ public function getPublicKey(): string { $stripeConfig = $this->config->getStripeConfig(); return $this->testMode ? $stripeConfig['public_key_test'] : $stripeConfig['public_key_live']; } }