feat: Version 3.3.4 - Nouvelle architecture pages, optimisations widgets Flutter et API
- Mise à jour VERSION vers 3.3.4 - Optimisations et révisions architecture API (deploy-api.sh, scripts de migration) - Ajout documentation Stripe Tap to Pay complète - Migration vers polices Inter Variable pour Flutter - Optimisations build Android et nettoyage fichiers temporaires - Amélioration système de déploiement avec gestion backups - Ajout scripts CRON et migrations base de données 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -137,111 +137,143 @@ class StripeController extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/stripe/locations
|
||||
* Créer une Location pour Terminal/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', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$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
|
||||
* 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
|
||||
$amount = $data['amount'] ?? 0;
|
||||
|
||||
// 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 > 50000) {
|
||||
$this->sendError('Le montant maximum est de 500€', 400);
|
||||
|
||||
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
|
||||
FROM ope_pass p
|
||||
JOIN operations o ON p.fk_operation = o.id
|
||||
WHERE p.id = ? AND p.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)($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'];
|
||||
|
||||
// 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,
|
||||
'fk_entite' => $data['fk_entite'] ?? Session::getEntityId(),
|
||||
'fk_user' => Session::getUserId(),
|
||||
'metadata' => $data['metadata'] ?? []
|
||||
'currency' => $data['currency'] ?? 'eur',
|
||||
'description' => $data['description'] ?? "Calendrier pompiers - Passage #$passageId",
|
||||
'payment_method_types' => $paymentMethodTypes,
|
||||
'capture_method' => $data['capture_method'] ?? 'automatic',
|
||||
'passage_id' => $passageId,
|
||||
'amicale_id' => $data['amicale_id'] ?? $entiteId,
|
||||
'member_id' => $data['member_id'] ?? Session::getUserId(),
|
||||
'stripe_account' => $data['stripe_account'] ?? null,
|
||||
'metadata' => array_merge(
|
||||
[
|
||||
'passage_id' => (string)$passageId,
|
||||
'amicale_id' => (string)($data['amicale_id'] ?? $entiteId),
|
||||
'member_id' => (string)($data['member_id'] ?? Session::getUserId()),
|
||||
'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'],
|
||||
'application_fee' => $result['application_fee']
|
||||
'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());
|
||||
}
|
||||
@@ -249,60 +281,78 @@ class StripeController extends Controller {
|
||||
|
||||
/**
|
||||
* GET /api/stripe/payments/{paymentIntentId}
|
||||
* Récupérer le statut d'un paiement
|
||||
* Récupérer le statut d'un paiement depuis ope_pass et Stripe
|
||||
*/
|
||||
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"
|
||||
);
|
||||
|
||||
// Récupérer les informations depuis ope_pass
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT p.*, o.fk_entite,
|
||||
e.encrypted_name as entite_nom,
|
||||
u.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 users u ON p.fk_user = u.id
|
||||
WHERE p.stripe_payment_id = :pi_id
|
||||
");
|
||||
$stmt->execute(['pi_id' => $paymentIntentId]);
|
||||
$payment = $stmt->fetch();
|
||||
|
||||
if (!$payment) {
|
||||
$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 ($payment['fk_entite'] != $userEntityId &&
|
||||
$payment['fk_user'] != $userId &&
|
||||
|
||||
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' => $payment['stripe_payment_intent_id'],
|
||||
'status' => $payment['status'],
|
||||
'amount' => $payment['amount'],
|
||||
'currency' => $payment['currency'],
|
||||
'application_fee' => $payment['application_fee'],
|
||||
'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' => $payment['fk_entite'],
|
||||
'nom' => $payment['entite_nom']
|
||||
'id' => $passage['fk_entite'],
|
||||
'nom' => $entiteNom
|
||||
],
|
||||
'user' => [
|
||||
'id' => $payment['fk_user'],
|
||||
'nom' => $payment['user_nom'],
|
||||
'prenom' => $payment['user_prenom']
|
||||
'id' => $passage['fk_user'],
|
||||
'nom' => $passage['user_nom'],
|
||||
'prenom' => $passage['user_prenom']
|
||||
],
|
||||
'created_at' => $payment['created_at']
|
||||
'created_at' => $passage['date_creat'],
|
||||
'stripe_details' => $stripeStatus
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
@@ -419,10 +469,11 @@ class StripeController extends Controller {
|
||||
$platform = $data['platform'] ?? '';
|
||||
|
||||
if ($platform === 'ios') {
|
||||
// Pour iOS, on vérifie côté client (iPhone XS+ avec iOS 15.4+)
|
||||
// Pour iOS, on vérifie côté client (iPhone XS+ avec iOS 16.4+)
|
||||
$this->sendSuccess([
|
||||
'message' => 'Vérification iOS à faire côté client',
|
||||
'requirements' => 'iPhone XS ou plus récent avec iOS 15.4+'
|
||||
'requirements' => 'iPhone XS ou plus récent avec iOS 16.4+',
|
||||
'details' => 'iOS 16.4 minimum requis pour le support PIN complet'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user