Fix: Corriger le type PDO dans StripeService et retirer getConnection()
This commit is contained in:
@@ -25,7 +25,8 @@ class AppConfig {
|
||||
$this->currentHost = $_SERVER['SERVER_NAME'] ?? $_SERVER['HTTP_HOST'] ?? '';
|
||||
|
||||
// Récupérer les autres en-têtes pour une utilisation ultérieure si nécessaire
|
||||
$this->headers = getallheaders();
|
||||
// getallheaders() n'existe pas en CLI, donc on vérifie
|
||||
$this->headers = function_exists('getallheaders') ? getallheaders() : [];
|
||||
|
||||
// Déterminer l'adresse IP du client
|
||||
$this->clientIp = $this->getClientIpAddress();
|
||||
@@ -63,8 +64,16 @@ class AppConfig {
|
||||
'api_key' => '', // À remplir avec la clé API Mapbox
|
||||
],
|
||||
'stripe' => [
|
||||
'api_key' => '', // À remplir avec la clé API Stripe
|
||||
'webhook_secret' => '', // À remplir avec le secret du webhook Stripe
|
||||
'public_key_test' => 'pk_test_51QwoVN00pblGEgsXkf8qlXmLGEpxDQcG0KLRpjrGLjJHd7AVZ4Iwd6ChgdjO0w0n3vRqwNCEW8KnHUe5eh3uIlkV00k07kCBmd', // À remplacer par votre clé publique TEST
|
||||
'secret_key_test' => 'sk_test_51QwoVN00pblGEgsXnvqi8qfYpzHtesWWclvK3lzQjPNoHY0dIyOpJmxIkoLqsbmRMEUZpKS5MQ7iFDRlSqVyTo9c006yWetbsd', // À remplacer par votre clé secrète TEST
|
||||
'public_key_live' => 'pk_live_XXXXXXXXXXXX', // À remplacer par votre clé publique LIVE
|
||||
'secret_key_live' => 'sk_live_XXXXXXXXXXXX', // À remplacer par votre clé secrète LIVE
|
||||
'webhook_secret_test' => 'whsec_test_XXXXXXXXXXXX', // À remplacer après création webhook TEST
|
||||
'webhook_secret_live' => 'whsec_live_XXXXXXXXXXXX', // À remplacer après création webhook LIVE
|
||||
'api_version' => '2024-06-20',
|
||||
'application_fee_percent' => 2.5, // Commission de 2.5%
|
||||
'application_fee_minimum' => 50, // Commission minimum 50 centimes
|
||||
'mode' => 'test', // 'test' ou 'live'
|
||||
],
|
||||
'sms' => [
|
||||
'provider' => 'ovh', // Comme mentionné dans le cahier des charges
|
||||
@@ -131,11 +140,6 @@ class AppConfig {
|
||||
],
|
||||
// Vous pouvez activer des fonctionnalités de débogage en développement
|
||||
'debug' => true,
|
||||
// Configurez des endpoints de test pour Stripe, etc.
|
||||
'stripe' => [
|
||||
'api_key' => 'pk_test_...', // Clé de test Stripe
|
||||
'webhook_secret' => 'whsec_test_...', // Secret de test
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
493
api/src/Controllers/StripeController.php
Normal file
493
api/src/Controllers/StripeController.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
335
api/src/Controllers/StripeWebhookController.php
Normal file
335
api/src/Controllers/StripeWebhookController.php
Normal 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}");
|
||||
}
|
||||
}
|
||||
99
api/src/Core/Controller.php
Normal file
99
api/src/Core/Controller.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Database;
|
||||
use Session;
|
||||
use Response;
|
||||
use Request;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Classe de base pour tous les controllers
|
||||
* Fournit des méthodes communes pour l'authentification et les réponses
|
||||
*/
|
||||
abstract class Controller {
|
||||
protected PDO $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifier que l'utilisateur est authentifié
|
||||
*/
|
||||
protected function requireAuth(): void {
|
||||
if (!Session::isAuthenticated()) {
|
||||
$this->sendError('Non authentifié', 401);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifier que l'utilisateur a le rôle minimum requis
|
||||
* @param int $minRole Rôle minimum requis (1=membre, 2=admin amicale, 3=super admin)
|
||||
*/
|
||||
protected function requireRole(int $minRole): void {
|
||||
$this->requireAuth();
|
||||
|
||||
$userRole = Session::getRole();
|
||||
if ($userRole < $minRole) {
|
||||
$this->sendError('Droits insuffisants', 403);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer les données JSON de la requête
|
||||
*/
|
||||
protected function getJsonInput(): array {
|
||||
return Request::getJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoyer une réponse de succès
|
||||
*/
|
||||
protected function sendSuccess($data = null, int $code = 200): void {
|
||||
if ($data === null) {
|
||||
Response::json(['status' => 'success'], $code);
|
||||
} else {
|
||||
Response::json($data, $code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoyer une réponse d'erreur
|
||||
*/
|
||||
protected function sendError(string $message, int $code = 500): void {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => $message
|
||||
], $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valider qu'un tableau contient les clés requises
|
||||
*/
|
||||
protected function validateRequired(array $data, array $requiredFields): bool {
|
||||
foreach ($requiredFields as $field) {
|
||||
if (!isset($data[$field]) || $data[$field] === '') {
|
||||
$this->sendError("Le champ '$field' est requis", 400);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoyer et valider un ID
|
||||
*/
|
||||
protected function validateId($id): ?int {
|
||||
if (!is_numeric($id) || $id <= 0) {
|
||||
$this->sendError('ID invalide', 400);
|
||||
return null;
|
||||
}
|
||||
return (int) $id;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ class Router {
|
||||
'villes', // Ajout de la route villes comme endpoint public pour l'autocomplétion du code postal
|
||||
'password/check', // Vérification de la force des mots de passe (public pour l'inscription)
|
||||
'password/compromised', // Vérification si un mot de passe est compromis
|
||||
'stripe/webhook', // Webhook Stripe (doit être public pour recevoir les événements)
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
@@ -121,6 +122,29 @@ class Router {
|
||||
$this->get('admin/security-report', ['SecurityController', 'getSecurityReport']);
|
||||
$this->post('admin/cleanup', ['SecurityController', 'cleanup']);
|
||||
$this->post('admin/test-alert', ['SecurityController', 'testAlert']);
|
||||
|
||||
// Routes Stripe
|
||||
// Configuration et onboarding
|
||||
$this->post('stripe/accounts', ['StripeController', 'createAccount']);
|
||||
$this->get('stripe/accounts/:entityId/status', ['StripeController', 'getAccountStatus']);
|
||||
$this->post('stripe/accounts/:accountId/onboarding-link', ['StripeController', 'createOnboardingLink']);
|
||||
$this->post('stripe/locations', ['StripeController', 'createLocation']);
|
||||
|
||||
// Terminal et Tap to Pay
|
||||
$this->post('stripe/terminal/connection-token', ['StripeController', 'createConnectionToken']);
|
||||
$this->post('stripe/devices/check-tap-to-pay', ['StripeController', 'checkTapToPayCapability']);
|
||||
$this->get('stripe/devices/certified-android', ['StripeController', 'getCertifiedAndroidDevices']);
|
||||
|
||||
// Paiements
|
||||
$this->post('stripe/payments/create-intent', ['StripeController', 'createPaymentIntent']);
|
||||
$this->get('stripe/payments/:paymentIntentId', ['StripeController', 'getPaymentStatus']);
|
||||
|
||||
// Statistiques et configuration
|
||||
$this->get('stripe/stats', ['StripeController', 'getPaymentStats']);
|
||||
$this->get('stripe/config', ['StripeController', 'getPublicConfig']);
|
||||
|
||||
// Webhook (IMPORTANT: pas d'authentification requise pour les webhooks Stripe)
|
||||
$this->post('stripe/webhook', ['StripeWebhookController', 'handleWebhook']);
|
||||
}
|
||||
|
||||
public function handle(): void {
|
||||
|
||||
446
api/src/Services/StripeService.php
Normal file
446
api/src/Services/StripeService.php
Normal file
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Stripe\Stripe;
|
||||
use Stripe\StripeClient;
|
||||
use Stripe\Exception\ApiErrorException;
|
||||
use AppConfig;
|
||||
use Database;
|
||||
use PDO;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Service principal pour gérer l'intégration Stripe
|
||||
* Gère Stripe Connect, Terminal et les paiements
|
||||
*/
|
||||
class StripeService {
|
||||
private static ?self $instance = null;
|
||||
private StripeClient $stripe;
|
||||
private AppConfig $config;
|
||||
private PDO $db;
|
||||
private bool $testMode;
|
||||
|
||||
private function __construct() {
|
||||
$this->config = AppConfig::getInstance();
|
||||
$this->db = Database::getInstance();
|
||||
|
||||
// Déterminer le mode (test ou live)
|
||||
$stripeConfig = $this->config->get('stripe');
|
||||
$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->get('stripe');
|
||||
$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->get('stripe');
|
||||
return $this->testMode
|
||||
? $stripeConfig['public_key_test']
|
||||
: $stripeConfig['public_key_live'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user