- #17: Amélioration gestion des secteurs et statistiques - #18: Optimisation services API et logs - #19: Corrections Flutter widgets et repositories - #20: Fix création passage - détection automatique ope_users.id vs users.id Suppression dossier web/ (migration vers app Flutter) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
837 lines
30 KiB
PHP
837 lines
30 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Core\Controller;
|
|
use App\Services\StripeService;
|
|
use App\Services\LogService;
|
|
use App\Services\FileService;
|
|
use App\Services\ApiService;
|
|
use Session;
|
|
use Exception;
|
|
|
|
/**
|
|
* Controller principal pour les opérations Stripe
|
|
* Gère les comptes Connect, les paiements et Terminal
|
|
*/
|
|
class StripeController extends Controller {
|
|
private StripeService $stripeService;
|
|
|
|
public function __construct() {
|
|
parent::__construct();
|
|
$this->stripeService = StripeService::getInstance();
|
|
}
|
|
|
|
/**
|
|
* POST /api/stripe/accounts
|
|
* Créer un compte Stripe Connect pour une amicale
|
|
*/
|
|
public function createAccount(): void {
|
|
try {
|
|
$this->requireAuth();
|
|
|
|
// Vérifier le rôle de l'utilisateur (comme dans les autres controllers)
|
|
$userId = Session::getUserId();
|
|
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
|
$stmt->execute([$userId]);
|
|
$result = $stmt->fetch();
|
|
$userRole = $result ? (int)$result['fk_role'] : 0;
|
|
|
|
if ($userRole < 2) {
|
|
$this->sendError('Droits insuffisants - Admin amicale minimum requis', 403);
|
|
return;
|
|
}
|
|
|
|
$data = $this->getJsonInput();
|
|
$entiteId = $data['fk_entite'] ?? Session::getEntityId();
|
|
|
|
if (!$entiteId) {
|
|
$this->sendError('ID entité requis', 400);
|
|
return;
|
|
}
|
|
|
|
// Vérifier les droits sur cette entité
|
|
if (Session::getEntityId() != $entiteId && $userRole < 3) {
|
|
$this->sendError('Non autorisé pour cette entité', 403);
|
|
return;
|
|
}
|
|
|
|
$result = $this->stripeService->createConnectAccount($entiteId);
|
|
|
|
if ($result['success']) {
|
|
$this->sendSuccess($result);
|
|
} else {
|
|
$this->sendError($result['message'], 400);
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Erreur lors de la création du compte: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/stripe/accounts/{accountId}/onboarding-link
|
|
* Générer un lien d'onboarding pour finaliser la configuration
|
|
*/
|
|
public function createOnboardingLink(string $accountId): void {
|
|
try {
|
|
$this->requireAuth();
|
|
|
|
// Log du début de la requête
|
|
LogService::log('Début createOnboardingLink', [
|
|
'account_id' => $accountId,
|
|
'user_id' => Session::getUserId()
|
|
]);
|
|
|
|
// Vérifier le rôle de l'utilisateur
|
|
$userId = Session::getUserId();
|
|
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
|
$stmt->execute([$userId]);
|
|
$result = $stmt->fetch();
|
|
$userRole = $result ? (int)$result['fk_role'] : 0;
|
|
|
|
if ($userRole < 2) {
|
|
$this->sendError('Droits insuffisants', 403);
|
|
return;
|
|
}
|
|
|
|
$data = $this->getJsonInput();
|
|
$returnUrl = $data['return_url'] ?? '';
|
|
$refreshUrl = $data['refresh_url'] ?? '';
|
|
|
|
LogService::log('URLs reçues', [
|
|
'return_url' => $returnUrl,
|
|
'refresh_url' => $refreshUrl
|
|
]);
|
|
|
|
if (!$returnUrl || !$refreshUrl) {
|
|
$this->sendError('URLs de retour requises', 400);
|
|
return;
|
|
}
|
|
|
|
$result = $this->stripeService->createOnboardingLink($accountId, $returnUrl, $refreshUrl);
|
|
|
|
LogService::log('Résultat createOnboardingLink', [
|
|
'success' => $result['success'] ?? false,
|
|
'has_url' => isset($result['url'])
|
|
]);
|
|
|
|
if ($result['success']) {
|
|
$this->sendSuccess([
|
|
'status' => 'success',
|
|
'url' => $result['url']
|
|
]);
|
|
exit; // Terminer explicitement après l'envoi de la réponse
|
|
} else {
|
|
$this->sendError($result['message'], 400);
|
|
exit;
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
LogService::log('Erreur createOnboardingLink', [
|
|
'level' => 'error',
|
|
'message' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
$this->sendError('Erreur: ' . $e->getMessage());
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/stripe/payments/create-intent
|
|
* Créer une intention de paiement pour Tap to Pay ou paiement Web
|
|
*
|
|
* Payload Tap to Pay:
|
|
* {
|
|
* "amount": 2500,
|
|
* "currency": "eur",
|
|
* "description": "Calendrier pompiers - Passage #789",
|
|
* "payment_method_types": ["card_present"],
|
|
* "capture_method": "automatic",
|
|
* "passage_id": 789,
|
|
* "amicale_id": 42,
|
|
* "member_id": 156,
|
|
* "stripe_account": "acct_1O3ABC456DEF789",
|
|
* "location_id": "tml_FGH123456789",
|
|
* "metadata": {...}
|
|
* }
|
|
*/
|
|
public function createPaymentIntent(): void {
|
|
try {
|
|
$this->requireAuth();
|
|
|
|
$data = $this->getJsonInput();
|
|
|
|
// Validation des champs requis
|
|
if (!isset($data['amount']) || !isset($data['passage_id'])) {
|
|
$this->sendError('Montant et passage_id requis', 400);
|
|
return;
|
|
}
|
|
|
|
$amount = (int)$data['amount'];
|
|
$passageId = (int)$data['passage_id'];
|
|
|
|
// Validation du passage_id (doit être > 0 car le passage est créé avant)
|
|
if ($passageId <= 0) {
|
|
$this->sendError('passage_id invalide. Le passage doit être créé avant le paiement', 400);
|
|
return;
|
|
}
|
|
|
|
// Validation du montant
|
|
if ($amount < 100) {
|
|
$this->sendError('Le montant minimum est de 1€ (100 centimes)', 400);
|
|
return;
|
|
}
|
|
|
|
if ($amount > 99900) { // 999€ max selon la doc
|
|
$this->sendError('Le montant maximum est de 999€', 400);
|
|
return;
|
|
}
|
|
|
|
// Vérifier que le passage existe et appartient à l'utilisateur
|
|
$stmt = $this->db->prepare('
|
|
SELECT p.*, o.fk_entite, o.id as operation_id
|
|
FROM ope_pass p
|
|
JOIN operations o ON p.fk_operation = o.id
|
|
JOIN ope_users ou ON p.fk_user = ou.id
|
|
WHERE p.id = ? AND ou.fk_user = ?
|
|
');
|
|
$stmt->execute([$passageId, Session::getUserId()]);
|
|
$passage = $stmt->fetch();
|
|
|
|
if (!$passage) {
|
|
$this->sendError('Passage non trouvé ou non autorisé', 404);
|
|
return;
|
|
}
|
|
|
|
// Vérifier qu'il n'y a pas déjà un paiement Stripe pour ce passage
|
|
if (!empty($passage['stripe_payment_id'])) {
|
|
$this->sendError('Un paiement Stripe existe déjà pour ce passage', 400);
|
|
return;
|
|
}
|
|
|
|
// Vérifier que le montant correspond (passage.montant est en euros, amount en centimes)
|
|
$expectedAmount = (int)round($passage['montant'] * 100);
|
|
if ($amount !== $expectedAmount) {
|
|
$this->sendError("Le montant ne correspond pas au passage (attendu: {$expectedAmount} centimes, reçu: {$amount} centimes)", 400);
|
|
return;
|
|
}
|
|
|
|
$entiteId = $passage['fk_entite'];
|
|
$operationId = $passage['operation_id'];
|
|
$fkUser = $passage['fk_user']; // ope_users.id
|
|
|
|
// Déterminer le type de paiement (Tap to Pay ou Web)
|
|
$paymentMethodTypes = $data['payment_method_types'] ?? ['card_present'];
|
|
$isTapToPay = in_array('card_present', $paymentMethodTypes);
|
|
|
|
// Préparer les paramètres pour StripeService
|
|
$params = [
|
|
'amount' => $amount,
|
|
'currency' => $data['currency'] ?? 'eur',
|
|
'description' => $data['description'] ?? "Calendrier pompiers - Passage #$passageId",
|
|
'payment_method_types' => $paymentMethodTypes,
|
|
'capture_method' => $data['capture_method'] ?? 'automatic',
|
|
'passage_id' => $passageId,
|
|
'fk_entite' => $data['amicale_id'] ?? $entiteId,
|
|
'fk_user' => $data['member_id'] ?? $fkUser,
|
|
'stripe_account' => $data['stripe_account'] ?? null,
|
|
'metadata' => array_merge(
|
|
[
|
|
'passage_id' => (string)$passageId,
|
|
'operation_id' => (string)$operationId,
|
|
'amicale_id' => (string)($data['amicale_id'] ?? $entiteId),
|
|
'fk_user' => (string)$fkUser,
|
|
'created_at' => (string)time(),
|
|
'type' => $isTapToPay ? 'tap_to_pay' : 'web'
|
|
],
|
|
$data['metadata'] ?? []
|
|
)
|
|
];
|
|
|
|
// Ajouter location_id si fourni (pour Tap to Pay)
|
|
if (isset($data['location_id'])) {
|
|
$params['location_id'] = $data['location_id'];
|
|
}
|
|
|
|
// Créer le PaymentIntent via StripeService
|
|
$result = $this->stripeService->createPaymentIntent($params);
|
|
|
|
if ($result['success']) {
|
|
// Mettre à jour le passage avec le stripe_payment_id
|
|
$stmt = $this->db->prepare('
|
|
UPDATE ope_pass
|
|
SET stripe_payment_id = ?, updated_at = NOW()
|
|
WHERE id = ?
|
|
');
|
|
$stmt->execute([$result['payment_intent_id'], $passageId]);
|
|
|
|
// Retourner la réponse
|
|
$this->sendSuccess([
|
|
'client_secret' => $result['client_secret'],
|
|
'payment_intent_id' => $result['payment_intent_id'],
|
|
'amount' => $result['amount'],
|
|
'currency' => $params['currency'],
|
|
'passage_id' => $passageId,
|
|
'type' => $isTapToPay ? 'tap_to_pay' : 'web'
|
|
]);
|
|
} else {
|
|
$this->sendError($result['message'], 400);
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Erreur: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/stripe/payments/{paymentIntentId}
|
|
* Récupérer le statut d'un paiement depuis ope_pass et Stripe
|
|
*/
|
|
public function getPaymentStatus(string $paymentIntentId): void {
|
|
try {
|
|
$this->requireAuth();
|
|
|
|
// Récupérer les informations depuis ope_pass
|
|
$stmt = $this->db->prepare("
|
|
SELECT p.*, o.fk_entite,
|
|
e.encrypted_name as entite_nom,
|
|
ou.first_name as user_prenom, u.sect_name as user_nom
|
|
FROM ope_pass p
|
|
JOIN operations o ON p.fk_operation = o.id
|
|
LEFT JOIN entites e ON o.fk_entite = e.id
|
|
LEFT JOIN ope_users ou ON p.fk_user = ou.id
|
|
LEFT JOIN users u ON ou.fk_user = u.id
|
|
WHERE p.stripe_payment_id = :pi_id
|
|
");
|
|
$stmt->execute(['pi_id' => $paymentIntentId]);
|
|
$passage = $stmt->fetch();
|
|
|
|
if (!$passage) {
|
|
$this->sendError('Paiement non trouvé', 404);
|
|
return;
|
|
}
|
|
|
|
// Vérifier les droits
|
|
$userEntityId = Session::getEntityId();
|
|
$userId = Session::getUserId();
|
|
|
|
// Récupérer le rôle depuis la base de données
|
|
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
|
$stmt->execute([$userId]);
|
|
$result = $stmt->fetch();
|
|
$userRole = $result ? (int)$result['fk_role'] : 0;
|
|
|
|
if ($passage['fk_entite'] != $userEntityId &&
|
|
$passage['fk_user'] != $userId &&
|
|
$userRole < 3) {
|
|
$this->sendError('Non autorisé', 403);
|
|
return;
|
|
}
|
|
|
|
// Récupérer le statut en temps réel depuis Stripe
|
|
$stripeStatus = $this->stripeService->getPaymentIntentStatus($paymentIntentId);
|
|
|
|
// Déchiffrer le nom de l'entité si nécessaire
|
|
$entiteNom = '';
|
|
if (!empty($passage['entite_nom'])) {
|
|
try {
|
|
$entiteNom = ApiService::decryptData($passage['entite_nom']);
|
|
} catch (Exception $e) {
|
|
$entiteNom = 'Entité inconnue';
|
|
}
|
|
}
|
|
|
|
$this->sendSuccess([
|
|
'payment_intent_id' => $paymentIntentId,
|
|
'passage_id' => $passage['id'],
|
|
'status' => $stripeStatus['status'] ?? 'unknown',
|
|
'amount' => (int)($passage['montant'] * 100), // montant en BDD est en euros, on convertit en centimes
|
|
'currency' => 'eur',
|
|
'entite' => [
|
|
'id' => $passage['fk_entite'],
|
|
'nom' => $entiteNom
|
|
],
|
|
'user' => [
|
|
'id' => $passage['fk_user'],
|
|
'nom' => $passage['user_nom'],
|
|
'prenom' => $passage['user_prenom']
|
|
],
|
|
'created_at' => $passage['date_creat'],
|
|
'stripe_details' => $stripeStatus
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Erreur: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/stripe/accounts/{entityId}/status
|
|
* Vérifier le statut du compte Stripe d'une entité
|
|
*/
|
|
public function getAccountStatus(string $entityId): void {
|
|
try {
|
|
$this->requireAuth();
|
|
|
|
// Convertir l'entityId en int
|
|
$entityId = (int)$entityId;
|
|
|
|
// Vérifier les droits : admin de l'amicale ou super admin
|
|
$userEntityId = Session::getEntityId();
|
|
$userId = Session::getUserId();
|
|
|
|
// Récupérer le rôle depuis la base de données
|
|
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
|
$stmt->execute([$userId]);
|
|
$result = $stmt->fetch();
|
|
$userRole = $result ? (int)$result['fk_role'] : 0;
|
|
|
|
if ($entityId != $userEntityId && $userRole < 3) {
|
|
$this->sendError('Non autorisé', 403);
|
|
return;
|
|
}
|
|
|
|
// Récupérer le compte Stripe
|
|
$stmt = $this->db->prepare(
|
|
"SELECT sa.*, e.encrypted_name as entite_nom
|
|
FROM stripe_accounts sa
|
|
LEFT JOIN entites e ON sa.fk_entite = e.id
|
|
WHERE sa.fk_entite = :entity_id"
|
|
);
|
|
$stmt->execute(['entity_id' => $entityId]);
|
|
$account = $stmt->fetch();
|
|
|
|
if (!$account || !$account['stripe_account_id']) {
|
|
$this->sendSuccess([
|
|
'has_account' => false,
|
|
'account_id' => null,
|
|
'location_id' => null,
|
|
'charges_enabled' => false,
|
|
'payouts_enabled' => false,
|
|
'onboarding_completed' => false
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// Récupérer le statut depuis Stripe
|
|
$stripeService = StripeService::getInstance();
|
|
$stripeAccount = $stripeService->retrieveAccount($account['stripe_account_id']);
|
|
|
|
if (!$stripeAccount) {
|
|
$this->sendSuccess([
|
|
'has_account' => true,
|
|
'account_id' => $account['stripe_account_id'],
|
|
'location_id' => $account['stripe_location_id'] ?? null,
|
|
'charges_enabled' => false,
|
|
'payouts_enabled' => false,
|
|
'onboarding_completed' => false,
|
|
'error' => 'Compte non trouvé sur Stripe'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// Mettre à jour la base de données avec le statut actuel
|
|
$stmt = $this->db->prepare(
|
|
"UPDATE stripe_accounts
|
|
SET charges_enabled = :charges,
|
|
payouts_enabled = :payouts,
|
|
updated_at = NOW()
|
|
WHERE id = :id"
|
|
);
|
|
$stmt->execute([
|
|
'charges' => $stripeAccount->charges_enabled ? 1 : 0,
|
|
'payouts' => $stripeAccount->payouts_enabled ? 1 : 0,
|
|
'id' => $account['id']
|
|
]);
|
|
|
|
$this->sendSuccess([
|
|
'has_account' => true,
|
|
'account_id' => $account['stripe_account_id'],
|
|
'location_id' => $account['stripe_location_id'] ?? null,
|
|
'charges_enabled' => $stripeAccount->charges_enabled,
|
|
'payouts_enabled' => $stripeAccount->payouts_enabled,
|
|
'onboarding_completed' => $stripeAccount->details_submitted,
|
|
'entite' => [
|
|
'id' => $entityId,
|
|
'nom' => $account['entite_nom']
|
|
]
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
// Logger::getInstance()->error('Erreur statut compte Stripe', [
|
|
// 'entity_id' => $entityId,
|
|
// 'error' => $e->getMessage()
|
|
// ]);
|
|
$this->sendError('Erreur: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/stripe/config
|
|
* Récupérer la configuration publique Stripe
|
|
*/
|
|
public function getPublicConfig(): void {
|
|
try {
|
|
$this->requireAuth();
|
|
|
|
$this->sendSuccess([
|
|
'public_key' => $this->stripeService->getPublicKey(),
|
|
'test_mode' => $this->stripeService->isTestMode()
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Erreur: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/stripe/stats
|
|
* Récupérer les statistiques de paiement
|
|
*/
|
|
public function getPaymentStats(): void {
|
|
try {
|
|
$this->requireAuth();
|
|
|
|
$entiteId = $_GET['fk_entite'] ?? Session::getEntityId();
|
|
$userId = $_GET['fk_user'] ?? null;
|
|
$dateFrom = $_GET['date_from'] ?? date('Y-m-01');
|
|
$dateTo = $_GET['date_to'] ?? date('Y-m-d');
|
|
|
|
// Vérifier les droits
|
|
// Récupérer le rôle pour vérifier les droits
|
|
$userId = Session::getUserId();
|
|
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
|
$stmt->execute([$userId]);
|
|
$result = $stmt->fetch();
|
|
$userRole = $result ? (int)$result['fk_role'] : 0;
|
|
|
|
if ($entiteId != Session::getEntityId() && $userRole < 3) {
|
|
$this->sendError('Non autorisé', 403);
|
|
return;
|
|
}
|
|
|
|
$query = "SELECT
|
|
COUNT(CASE WHEN status = 'succeeded' THEN 1 END) as total_ventes,
|
|
SUM(CASE WHEN status = 'succeeded' THEN amount ELSE 0 END) as total_montant,
|
|
SUM(CASE WHEN status = 'succeeded' THEN application_fee ELSE 0 END) as total_commissions,
|
|
DATE(created_at) as date_vente
|
|
FROM stripe_payment_intents
|
|
WHERE fk_entite = :entite_id
|
|
AND DATE(created_at) BETWEEN :date_from AND :date_to";
|
|
|
|
$params = [
|
|
'entite_id' => $entiteId,
|
|
'date_from' => $dateFrom,
|
|
'date_to' => $dateTo
|
|
];
|
|
|
|
if ($userId) {
|
|
$query .= " AND fk_user = :user_id";
|
|
$params['user_id'] = $userId;
|
|
}
|
|
|
|
$query .= " GROUP BY DATE(created_at) ORDER BY date_vente DESC";
|
|
|
|
$stmt = $this->db->prepare($query);
|
|
$stmt->execute($params);
|
|
$stats = $stmt->fetchAll();
|
|
|
|
// Calculer les totaux
|
|
$totals = [
|
|
'total_ventes' => 0,
|
|
'total_montant' => 0,
|
|
'total_commissions' => 0
|
|
];
|
|
|
|
foreach ($stats as $stat) {
|
|
$totals['total_ventes'] += $stat['total_ventes'];
|
|
$totals['total_montant'] += $stat['total_montant'];
|
|
$totals['total_commissions'] += $stat['total_commissions'];
|
|
}
|
|
|
|
$this->sendSuccess([
|
|
'stats' => $stats,
|
|
'totals' => $totals,
|
|
'period' => [
|
|
'from' => $dateFrom,
|
|
'to' => $dateTo
|
|
]
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Erreur: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/stripe/payment-links
|
|
* Créer un Payment Link Stripe pour paiement par QR Code
|
|
*
|
|
* Payload:
|
|
* {
|
|
* "amount": 2500,
|
|
* "currency": "eur",
|
|
* "description": "Calendrier pompiers",
|
|
* "passage_id": 789,
|
|
* "metadata": {...}
|
|
* }
|
|
*/
|
|
public function createPaymentLink(): void {
|
|
try {
|
|
$this->requireAuth();
|
|
|
|
$data = $this->getJsonInput();
|
|
|
|
// Validation
|
|
if (!isset($data['amount']) || !isset($data['passage_id'])) {
|
|
$this->sendError('Montant et passage_id requis', 400);
|
|
return;
|
|
}
|
|
|
|
$amount = (int)$data['amount'];
|
|
$passageId = (int)$data['passage_id'];
|
|
|
|
// Validation du montant (doit être > 0)
|
|
if ($amount <= 0) {
|
|
$this->sendError('Le montant doit être supérieur à 0', 400);
|
|
return;
|
|
}
|
|
|
|
// Vérifier que le passage appartient à l'utilisateur ou à son entité
|
|
$userId = Session::getUserId();
|
|
$stmt = $this->db->prepare('
|
|
SELECT p.*, o.fk_entite, ou.fk_user as ope_user_id
|
|
FROM ope_pass p
|
|
JOIN operations o ON p.fk_operation = o.id
|
|
JOIN ope_users ou ON p.fk_user = ou.id
|
|
WHERE p.id = ?
|
|
');
|
|
$stmt->execute([$passageId]);
|
|
$passage = $stmt->fetch();
|
|
|
|
if (!$passage) {
|
|
$this->sendError('Passage non trouvé', 404);
|
|
return;
|
|
}
|
|
|
|
// Vérifier les droits : soit l'utilisateur est le créateur du passage, soit il appartient à la même entité
|
|
$userEntityId = Session::getEntityId();
|
|
if ($passage['ope_user_id'] != $userId && $passage['fk_entite'] != $userEntityId) {
|
|
$this->sendError('Passage non autorisé', 403);
|
|
return;
|
|
}
|
|
|
|
// Vérifier qu'il n'y a pas déjà un paiement ou un payment link pour ce passage
|
|
if (!empty($passage['stripe_payment_id'])) {
|
|
$this->sendError('Un paiement Stripe existe déjà pour ce passage', 400);
|
|
return;
|
|
}
|
|
|
|
if (!empty($passage['stripe_payment_link_id'])) {
|
|
$this->sendError('Un Payment Link existe déjà pour ce passage', 400);
|
|
return;
|
|
}
|
|
|
|
// Vérifier que le montant correspond (passage.montant est en euros, amount en centimes)
|
|
$expectedAmount = (int)round($passage['montant'] * 100);
|
|
if ($amount !== $expectedAmount) {
|
|
$this->sendError("Le montant ne correspond pas au passage (attendu: {$expectedAmount} centimes, reçu: {$amount} centimes)", 400);
|
|
return;
|
|
}
|
|
|
|
// Préparer les paramètres
|
|
$params = [
|
|
'amount' => $amount,
|
|
'currency' => $data['currency'] ?? 'eur',
|
|
'description' => $data['description'] ?? 'Calendrier pompiers',
|
|
'passage_id' => $passageId,
|
|
'metadata' => $data['metadata'] ?? []
|
|
];
|
|
|
|
// Créer le Payment Link
|
|
$result = $this->stripeService->createPaymentLink($params);
|
|
|
|
if ($result['success']) {
|
|
$this->sendSuccess([
|
|
'payment_link_id' => $result['payment_link_id'],
|
|
'url' => $result['url'],
|
|
'amount' => $result['amount'],
|
|
'passage_id' => $passageId,
|
|
'type' => 'qr_code'
|
|
]);
|
|
} else {
|
|
$this->sendError($result['message'], 400);
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Erreur: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/stripe/locations
|
|
* Créer une Location Stripe Terminal pour une entité (nécessaire pour Tap to Pay)
|
|
*/
|
|
public function createLocation(): void {
|
|
try {
|
|
$this->requireAuth();
|
|
|
|
// Vérifier le rôle de l'utilisateur
|
|
$userId = Session::getUserId();
|
|
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
|
$stmt->execute([$userId]);
|
|
$result = $stmt->fetch();
|
|
$userRole = $result ? (int)$result['fk_role'] : 0;
|
|
|
|
if ($userRole < 2) {
|
|
$this->sendError('Droits insuffisants - Admin amicale minimum requis', 403);
|
|
return;
|
|
}
|
|
|
|
$data = $this->getJsonInput();
|
|
$entiteId = $data['fk_entite'] ?? Session::getEntityId();
|
|
|
|
if (!$entiteId) {
|
|
$this->sendError('ID entité requis', 400);
|
|
return;
|
|
}
|
|
|
|
// Vérifier les droits sur cette entité
|
|
if (Session::getEntityId() != $entiteId && $userRole < 3) {
|
|
$this->sendError('Non autorisé pour cette entité', 403);
|
|
return;
|
|
}
|
|
|
|
$result = $this->stripeService->createLocation($entiteId);
|
|
|
|
if ($result['success']) {
|
|
$this->sendSuccess([
|
|
'location_id' => $result['location_id'],
|
|
'message' => $result['message']
|
|
]);
|
|
} else {
|
|
$this->sendError($result['message'], 400);
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Erreur lors de la création de la location: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/stripe/terminal/connection-token
|
|
* Créer un Connection Token pour Stripe Terminal/Tap to Pay
|
|
* Requis par le SDK Stripe Terminal pour se connecter aux readers
|
|
*/
|
|
public function createConnectionToken(): void {
|
|
try {
|
|
$this->requireAuth();
|
|
|
|
$data = $this->getJsonInput();
|
|
$entiteId = $data['amicale_id'] ?? Session::getEntityId();
|
|
|
|
if (!$entiteId) {
|
|
$this->sendError('ID entité requis', 400);
|
|
return;
|
|
}
|
|
|
|
// Vérifier les droits sur cette entité
|
|
$userRole = Session::getRole() ?? 0;
|
|
if (Session::getEntityId() != $entiteId && $userRole < 3) {
|
|
$this->sendError('Non autorisé pour cette entité', 403);
|
|
return;
|
|
}
|
|
|
|
$result = $this->stripeService->createConnectionToken($entiteId);
|
|
|
|
if ($result['success']) {
|
|
$this->sendSuccess([
|
|
'secret' => $result['secret']
|
|
]);
|
|
} else {
|
|
$this->sendError($result['message'], 400);
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Erreur lors de la création du connection token: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/stripe/payments/cancel
|
|
* Annuler un PaymentIntent Stripe
|
|
*
|
|
* Payload:
|
|
* {
|
|
* "payment_intent_id": "pi_3SWvho2378xpV4Rn1J43Ks7M"
|
|
* }
|
|
*/
|
|
public function cancelPayment(): void {
|
|
try {
|
|
$this->requireAuth();
|
|
|
|
$data = $this->getJsonInput();
|
|
|
|
// Validation
|
|
if (!isset($data['payment_intent_id'])) {
|
|
$this->sendError('payment_intent_id requis', 400);
|
|
return;
|
|
}
|
|
|
|
$paymentIntentId = $data['payment_intent_id'];
|
|
|
|
// Vérifier que le passage existe et appartient à l'utilisateur
|
|
$stmt = $this->db->prepare('
|
|
SELECT p.*, o.fk_entite, ou.fk_user as ope_user_id
|
|
FROM ope_pass p
|
|
JOIN operations o ON p.fk_operation = o.id
|
|
JOIN ope_users ou ON p.fk_user = ou.id
|
|
WHERE p.stripe_payment_id = ?
|
|
');
|
|
$stmt->execute([$paymentIntentId]);
|
|
$passage = $stmt->fetch();
|
|
|
|
if (!$passage) {
|
|
$this->sendError('Paiement non trouvé', 404);
|
|
return;
|
|
}
|
|
|
|
// Vérifier les droits
|
|
$userId = Session::getUserId();
|
|
$userEntityId = Session::getEntityId();
|
|
|
|
if ($passage['ope_user_id'] != $userId && $passage['fk_entite'] != $userEntityId) {
|
|
$this->sendError('Non autorisé', 403);
|
|
return;
|
|
}
|
|
|
|
// Annuler le PaymentIntent via StripeService
|
|
$result = $this->stripeService->cancelPaymentIntent($paymentIntentId);
|
|
|
|
if ($result['success']) {
|
|
// Retirer le stripe_payment_id du passage
|
|
$stmt = $this->db->prepare('
|
|
UPDATE ope_pass
|
|
SET stripe_payment_id = NULL, updated_at = NOW()
|
|
WHERE id = ?
|
|
');
|
|
$stmt->execute([$passage['id']]);
|
|
|
|
$this->sendSuccess([
|
|
'status' => 'canceled',
|
|
'payment_intent_id' => $paymentIntentId,
|
|
'passage_id' => $passage['id']
|
|
]);
|
|
} else {
|
|
$this->sendError($result['message'], 400);
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->sendError('Erreur: ' . $e->getMessage());
|
|
}
|
|
}
|
|
} |