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:
@@ -1,6 +1,7 @@
|
||||
# PLANNING STRIPE - DÉVELOPPEUR BACKEND PHP
|
||||
## API PHP 8.3 - Intégration Stripe Connect + Terminal
|
||||
## API PHP 8.3 - Intégration Stripe Tap to Pay (Mobile uniquement)
|
||||
### Période : 25/08/2024 - 05/09/2024
|
||||
### Mise à jour : Janvier 2025 - Simplification architecture
|
||||
|
||||
---
|
||||
|
||||
@@ -31,7 +32,13 @@ composer require stripe/stripe-php
|
||||
|
||||
#### ✅ Base de données
|
||||
```sql
|
||||
-- Tables à créer
|
||||
-- Modification de la table ope_pass existante (JANVIER 2025)
|
||||
ALTER TABLE `ope_pass`
|
||||
DROP COLUMN IF EXISTS `is_striped`,
|
||||
ADD COLUMN `stripe_payment_id` VARCHAR(50) DEFAULT NULL COMMENT 'ID du PaymentIntent Stripe (pi_xxx)',
|
||||
ADD INDEX `idx_stripe_payment` (`stripe_payment_id`);
|
||||
|
||||
-- Tables à créer (simplifiées)
|
||||
CREATE TABLE stripe_accounts (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
amicale_id INT NOT NULL,
|
||||
@@ -44,32 +51,8 @@ CREATE TABLE stripe_accounts (
|
||||
FOREIGN KEY (amicale_id) REFERENCES amicales(id)
|
||||
);
|
||||
|
||||
CREATE TABLE payment_intents (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
stripe_payment_intent_id VARCHAR(255) UNIQUE,
|
||||
amicale_id INT NOT NULL,
|
||||
pompier_id INT NOT NULL,
|
||||
amount INT NOT NULL, -- en centimes
|
||||
currency VARCHAR(3) DEFAULT 'eur',
|
||||
status VARCHAR(50),
|
||||
application_fee INT,
|
||||
metadata JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (amicale_id) REFERENCES amicales(id),
|
||||
FOREIGN KEY (pompier_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE terminal_readers (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
stripe_reader_id VARCHAR(255) UNIQUE,
|
||||
amicale_id INT NOT NULL,
|
||||
label VARCHAR(255),
|
||||
location VARCHAR(255),
|
||||
status VARCHAR(50),
|
||||
device_type VARCHAR(50),
|
||||
last_seen_at TIMESTAMP,
|
||||
FOREIGN KEY (amicale_id) REFERENCES amicales(id)
|
||||
);
|
||||
-- NOTE: Table payment_intents SUPPRIMÉE - on utilise directement stripe_payment_id dans ope_pass
|
||||
-- NOTE: Table terminal_readers SUPPRIMÉE - Tap to Pay uniquement, pas de terminaux externes
|
||||
|
||||
CREATE TABLE android_certified_devices (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -162,45 +145,47 @@ public function handleWebhook(Request $request) {
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Terminal Connection Token
|
||||
#### ✅ Configuration Tap to Pay
|
||||
```php
|
||||
// POST /api/terminal/connection-token
|
||||
public function createConnectionToken(Request $request) {
|
||||
$pompier = Auth::user();
|
||||
$amicale = $pompier->amicale;
|
||||
|
||||
$connectionToken = \Stripe\Terminal\ConnectionToken::create([
|
||||
'location' => $amicale->stripe_location_id,
|
||||
], [
|
||||
'stripe_account' => $amicale->stripe_account_id
|
||||
]);
|
||||
|
||||
return ['secret' => $connectionToken->secret];
|
||||
// POST /api/stripe/tap-to-pay/init
|
||||
public function initTapToPay(Request $request) {
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
|
||||
// Vérifier que l'entité a un compte Stripe
|
||||
$account = $this->getStripeAccount($entityId);
|
||||
|
||||
return [
|
||||
'stripe_account_id' => $account->stripe_account_id,
|
||||
'tap_to_pay_enabled' => true
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### 🌆 Après-midi (4h)
|
||||
|
||||
#### ✅ Gestion des Locations
|
||||
#### ✅ Vérification compatibilité Device
|
||||
```php
|
||||
// POST /api/amicales/{id}/create-location
|
||||
public function createLocation($amicaleId) {
|
||||
$amicale = Amicale::find($amicaleId);
|
||||
|
||||
$location = \Stripe\Terminal\Location::create([
|
||||
'display_name' => $amicale->name,
|
||||
'address' => [
|
||||
'line1' => $amicale->address,
|
||||
'city' => $amicale->city,
|
||||
'postal_code' => $amicale->postal_code,
|
||||
'country' => 'FR',
|
||||
],
|
||||
], [
|
||||
'stripe_account' => $amicale->stripe_account_id
|
||||
]);
|
||||
|
||||
$amicale->update(['stripe_location_id' => $location->id]);
|
||||
return $location;
|
||||
// POST /api/stripe/devices/check-tap-to-pay
|
||||
public function checkTapToPayCapability(Request $request) {
|
||||
$platform = $request->input('platform');
|
||||
$model = $request->input('device_model');
|
||||
$osVersion = $request->input('os_version');
|
||||
|
||||
if ($platform === 'iOS') {
|
||||
// iPhone XS et ultérieurs avec iOS 16.4+
|
||||
$supported = $this->checkiOSCompatibility($model, $osVersion);
|
||||
} else {
|
||||
// Android certifié pour la France
|
||||
$supported = $this->checkAndroidCertification($model);
|
||||
}
|
||||
|
||||
return [
|
||||
'tap_to_pay_supported' => $supported,
|
||||
'message' => $supported ?
|
||||
'Tap to Pay disponible' :
|
||||
'Appareil non compatible'
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
@@ -210,24 +195,22 @@ public function createLocation($amicaleId) {
|
||||
|
||||
### 🌅 Matin (4h)
|
||||
|
||||
#### ✅ Création PaymentIntent avec commission
|
||||
#### ✅ Création PaymentIntent avec association au passage
|
||||
```php
|
||||
// POST /api/payments/create-intent
|
||||
public function createPaymentIntent(Request $request) {
|
||||
$validated = $request->validate([
|
||||
'amount' => 'required|integer|min:100', // en centimes
|
||||
'amicale_id' => 'required|exists:amicales,id',
|
||||
'passage_id' => 'required|integer', // ID du passage ope_pass
|
||||
'entity_id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$pompier = Auth::user();
|
||||
$amicale = Amicale::find($validated['amicale_id']);
|
||||
|
||||
// Calculer la commission (2.5% ou 50 centimes minimum)
|
||||
$applicationFee = max(
|
||||
50, // 0.50€ minimum
|
||||
round($validated['amount'] * 0.025) // 2.5%
|
||||
);
|
||||
|
||||
|
||||
$userId = Session::getUserId();
|
||||
$entity = $this->getEntity($validated['entity_id']);
|
||||
|
||||
// Commission à 0% (décision client)
|
||||
$applicationFee = 0;
|
||||
|
||||
$paymentIntent = \Stripe\PaymentIntent::create([
|
||||
'amount' => $validated['amount'],
|
||||
'currency' => 'eur',
|
||||
@@ -235,26 +218,27 @@ public function createPaymentIntent(Request $request) {
|
||||
'capture_method' => 'automatic',
|
||||
'application_fee_amount' => $applicationFee,
|
||||
'transfer_data' => [
|
||||
'destination' => $amicale->stripe_account_id,
|
||||
'destination' => $entity->stripe_account_id,
|
||||
],
|
||||
'metadata' => [
|
||||
'pompier_id' => $pompier->id,
|
||||
'pompier_name' => $pompier->name,
|
||||
'amicale_id' => $amicale->id,
|
||||
'calendrier_annee' => date('Y'),
|
||||
'passage_id' => $validated['passage_id'],
|
||||
'user_id' => $userId,
|
||||
'entity_id' => $entity->id,
|
||||
'year' => date('Y'),
|
||||
],
|
||||
]);
|
||||
|
||||
// Sauvegarder en DB
|
||||
PaymentIntent::create([
|
||||
'stripe_payment_intent_id' => $paymentIntent->id,
|
||||
'amicale_id' => $amicale->id,
|
||||
'pompier_id' => $pompier->id,
|
||||
'amount' => $validated['amount'],
|
||||
'application_fee' => $applicationFee,
|
||||
'status' => $paymentIntent->status,
|
||||
|
||||
// Mise à jour directe dans ope_pass
|
||||
$this->db->prepare("
|
||||
UPDATE ope_pass
|
||||
SET stripe_payment_id = :stripe_id,
|
||||
date_modified = NOW()
|
||||
WHERE id = :passage_id
|
||||
")->execute([
|
||||
':stripe_id' => $paymentIntent->id,
|
||||
':passage_id' => $validated['passage_id']
|
||||
]);
|
||||
|
||||
|
||||
return [
|
||||
'client_secret' => $paymentIntent->client_secret,
|
||||
'payment_intent_id' => $paymentIntent->id,
|
||||
@@ -268,31 +252,59 @@ public function createPaymentIntent(Request $request) {
|
||||
```php
|
||||
// POST /api/payments/{id}/capture
|
||||
public function capturePayment($paymentIntentId) {
|
||||
$localPayment = PaymentIntent::where('stripe_payment_intent_id', $paymentIntentId)->first();
|
||||
|
||||
// Récupérer le passage depuis ope_pass
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT id, stripe_payment_id, montant
|
||||
FROM ope_pass
|
||||
WHERE stripe_payment_id = :stripe_id
|
||||
");
|
||||
$stmt->execute([':stripe_id' => $paymentIntentId]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
$paymentIntent = \Stripe\PaymentIntent::retrieve($paymentIntentId);
|
||||
|
||||
|
||||
if ($paymentIntent->status === 'requires_capture') {
|
||||
$paymentIntent->capture();
|
||||
}
|
||||
|
||||
$localPayment->update(['status' => $paymentIntent->status]);
|
||||
|
||||
// Si succès, envoyer email reçu
|
||||
if ($paymentIntent->status === 'succeeded') {
|
||||
$this->sendReceipt($localPayment);
|
||||
|
||||
// Mettre à jour le statut dans ope_pass si nécessaire
|
||||
if ($paymentIntent->status === 'succeeded' && $passage) {
|
||||
$this->db->prepare("
|
||||
UPDATE ope_pass
|
||||
SET date_stripe_validated = NOW()
|
||||
WHERE id = :passage_id
|
||||
")->execute([':passage_id' => $passage['id']]);
|
||||
|
||||
// Envoyer email reçu si configuré
|
||||
$this->sendReceipt($passage['id']);
|
||||
}
|
||||
|
||||
|
||||
return $paymentIntent;
|
||||
}
|
||||
|
||||
// GET /api/payments/{id}/status
|
||||
public function getPaymentStatus($paymentIntentId) {
|
||||
$payment = PaymentIntent::where('stripe_payment_intent_id', $paymentIntentId)->first();
|
||||
// GET /api/passages/{id}/stripe-status
|
||||
public function getPassageStripeStatus($passageId) {
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT stripe_payment_id, montant, date_creat
|
||||
FROM ope_pass
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmt->execute([':id' => $passageId]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if (!$passage['stripe_payment_id']) {
|
||||
return ['status' => 'no_stripe_payment'];
|
||||
}
|
||||
|
||||
// Récupérer le statut depuis Stripe
|
||||
$paymentIntent = \Stripe\PaymentIntent::retrieve($passage['stripe_payment_id']);
|
||||
|
||||
return [
|
||||
'status' => $payment->status,
|
||||
'amount' => $payment->amount,
|
||||
'created_at' => $payment->created_at,
|
||||
'stripe_payment_id' => $passage['stripe_payment_id'],
|
||||
'status' => $paymentIntent->status,
|
||||
'amount' => $paymentIntent->amount,
|
||||
'currency' => $paymentIntent->currency,
|
||||
'created_at' => $passage['date_creat']
|
||||
];
|
||||
}
|
||||
```
|
||||
@@ -625,14 +637,14 @@ Log::channel('stripe')->info('Payment created', [
|
||||
|
||||
## 🎯 BILAN DÉVELOPPEMENT API (01/09/2024)
|
||||
|
||||
### ✅ ENDPOINTS IMPLÉMENTÉS ET TESTÉS
|
||||
### ✅ ENDPOINTS IMPLÉMENTÉS (TAP TO PAY UNIQUEMENT)
|
||||
|
||||
#### **Stripe Connect - Comptes**
|
||||
- **POST /api/stripe/accounts** ✅
|
||||
- Création compte Stripe Express pour amicales
|
||||
- Gestion déchiffrement données (encrypted_email, encrypted_name)
|
||||
- Support des comptes existants
|
||||
|
||||
|
||||
- **GET /api/stripe/accounts/:entityId/status** ✅
|
||||
- Récupération statut complet du compte
|
||||
- Vérification charges_enabled et payouts_enabled
|
||||
@@ -643,17 +655,6 @@ Log::channel('stripe')->info('Payment created', [
|
||||
- URLs de retour configurées
|
||||
- Gestion des erreurs et timeouts
|
||||
|
||||
#### **Terminal et Locations**
|
||||
- **POST /api/stripe/locations** ✅
|
||||
- Création de locations Terminal
|
||||
- Association avec compte Stripe de l'amicale
|
||||
- ID location retourné : tml_GLJ21w7KCYX4Wj
|
||||
|
||||
- **POST /api/stripe/terminal/connection-token** ✅
|
||||
- Génération tokens de connexion Terminal
|
||||
- Authentification par session
|
||||
- Support multi-amicales
|
||||
|
||||
#### **Configuration et Utilitaires**
|
||||
- **GET /api/stripe/config** ✅
|
||||
- Configuration publique Stripe
|
||||
@@ -728,9 +729,10 @@ Log::channel('stripe')->info('Payment created', [
|
||||
- Public endpoints: webhook uniquement
|
||||
- Pas de stockage clés secrètes en base
|
||||
|
||||
#### **Base de données**
|
||||
#### **Base de données (MISE À JOUR JANVIER 2025)**
|
||||
- **Modification table `ope_pass`** : `stripe_payment_id` VARCHAR(50) remplace `is_striped`
|
||||
- **Table `payment_intents` supprimée** : Intégration directe dans `ope_pass`
|
||||
- Utilisation tables existantes (entites)
|
||||
- Pas de nouvelles tables créées (pas nécessaire pour V1)
|
||||
- Champs encrypted_email et encrypted_name supportés
|
||||
- Déchiffrement automatique avant envoi Stripe
|
||||
|
||||
@@ -743,4 +745,74 @@ Log::channel('stripe')->info('Payment created', [
|
||||
|
||||
---
|
||||
|
||||
*Document créé le 24/08/2024 - Dernière mise à jour : 01/09/2024*
|
||||
## 📱 FLOW TAP TO PAY SIMPLIFIÉ (Janvier 2025)
|
||||
|
||||
### Architecture
|
||||
```
|
||||
Flutter App (Tap to Pay) ↔ API PHP ↔ Stripe API
|
||||
```
|
||||
|
||||
### Étape 1: Création PaymentIntent
|
||||
**Flutter → API**
|
||||
```json
|
||||
POST /api/stripe/payments/create-intent
|
||||
{
|
||||
"amount": 1500,
|
||||
"passage_id": 123,
|
||||
"entity_id": 5
|
||||
}
|
||||
```
|
||||
|
||||
**API → Stripe → Base de données**
|
||||
```php
|
||||
// 1. Créer le PaymentIntent
|
||||
$paymentIntent = Stripe\PaymentIntent::create([...]);
|
||||
|
||||
// 2. Sauvegarder dans ope_pass
|
||||
UPDATE ope_pass SET stripe_payment_id = 'pi_xxx' WHERE id = 123;
|
||||
```
|
||||
|
||||
**API → Flutter**
|
||||
```json
|
||||
{
|
||||
"client_secret": "pi_xxx_secret_yyy",
|
||||
"payment_intent_id": "pi_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
### Étape 2: Collecte du paiement (Flutter)
|
||||
- L'app Flutter utilise le SDK Stripe Terminal
|
||||
- Le téléphone devient le terminal de paiement (Tap to Pay)
|
||||
- Utilise le client_secret pour collecter le paiement
|
||||
|
||||
### Étape 3: Confirmation (Webhook)
|
||||
**Stripe → API**
|
||||
- Event: `payment_intent.succeeded`
|
||||
- Met à jour le statut dans la base de données
|
||||
|
||||
### Tables nécessaires
|
||||
- ✅ `ope_pass.stripe_payment_id` - Association passage/paiement
|
||||
- ✅ `stripe_accounts` - Comptes Connect des amicales
|
||||
- ✅ `android_certified_devices` - Vérification compatibilité
|
||||
- ❌ ~~`stripe_payment_intents`~~ - Supprimée
|
||||
- ❌ ~~`terminal_readers`~~ - Pas de terminaux externes
|
||||
|
||||
### Endpoints essentiels
|
||||
1. `POST /api/stripe/payments/create-intent` - Créer PaymentIntent
|
||||
2. `POST /api/stripe/devices/check-tap-to-pay` - Vérifier compatibilité
|
||||
3. `POST /api/stripe/webhook` - Recevoir confirmations
|
||||
4. `GET /api/passages/{id}/stripe-status` - Vérifier statut
|
||||
|
||||
---
|
||||
|
||||
## 📝 CHANGELOG
|
||||
|
||||
### Janvier 2025 - Refactoring base de données
|
||||
- **Suppression** de la table `payment_intents` (non nécessaire)
|
||||
- **Migration** : `is_striped` → `stripe_payment_id` VARCHAR(50) dans `ope_pass`
|
||||
- **Simplification** : Association directe PaymentIntent ↔ Passage
|
||||
- **Avantage** : Traçabilité directe sans table intermédiaire
|
||||
|
||||
---
|
||||
|
||||
*Document créé le 24/08/2024 - Dernière mise à jour : 09/01/2025*
|
||||
Reference in New Issue
Block a user