- 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>
818 lines
25 KiB
Markdown
818 lines
25 KiB
Markdown
# PLANNING STRIPE - DÉVELOPPEUR BACKEND PHP
|
|
## 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
|
|
|
|
---
|
|
|
|
## 📅 LUNDI 25/08 - Setup et architecture (8h)
|
|
|
|
### 🌅 Matin (4h)
|
|
```bash
|
|
# Installation Stripe PHP SDK
|
|
cd api
|
|
composer require stripe/stripe-php
|
|
```
|
|
|
|
#### ✅ Configuration environnement
|
|
- [x] Créer configuration Stripe dans `AppConfig.php` avec clés TEST
|
|
- [x] Ajouter variables de configuration :
|
|
```php
|
|
'stripe' => [
|
|
'public_key_test' => 'pk_test_51QwoVN00pblGEgsXkf8qlXm...',
|
|
'secret_key_test' => 'sk_test_51QwoVN00pblGEgsXnvqi8qf...',
|
|
'webhook_secret_test' => 'whsec_test_...',
|
|
'api_version' => '2024-06-20',
|
|
'application_fee_percent' => 0, // DECISION: 0% commission
|
|
'mode' => 'test'
|
|
]
|
|
```
|
|
- [x] Créer service `StripeService.php` singleton
|
|
- [x] Configurer authentification Session-based API
|
|
|
|
#### ✅ Base de données
|
|
```sql
|
|
-- 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,
|
|
stripe_account_id VARCHAR(255) UNIQUE,
|
|
charges_enabled BOOLEAN DEFAULT FALSE,
|
|
payouts_enabled BOOLEAN DEFAULT FALSE,
|
|
onboarding_completed BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_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,
|
|
manufacturer VARCHAR(100),
|
|
model VARCHAR(200),
|
|
model_identifier VARCHAR(200),
|
|
tap_to_pay_certified BOOLEAN DEFAULT FALSE,
|
|
certification_date DATE,
|
|
min_android_version INT,
|
|
country VARCHAR(2) DEFAULT 'FR',
|
|
last_verified TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_manufacturer_model (manufacturer, model)
|
|
);
|
|
```
|
|
|
|
### 🌆 Après-midi (4h)
|
|
|
|
#### ✅ Endpoints Connect - Onboarding (RÉALISÉS)
|
|
```php
|
|
// POST /api/stripe/accounts - IMPLEMENTED
|
|
public function createAccount() {
|
|
$amicale = Amicale::find($amicaleId);
|
|
|
|
$account = \Stripe\Account::create([
|
|
'type' => 'express',
|
|
'country' => 'FR',
|
|
'email' => $amicale->email,
|
|
'capabilities' => [
|
|
'card_payments' => ['requested' => true],
|
|
'transfers' => ['requested' => true],
|
|
],
|
|
'business_type' => 'non_profit',
|
|
'business_profile' => [
|
|
'name' => $amicale->name,
|
|
'product_description' => 'Vente de calendriers des pompiers',
|
|
],
|
|
]);
|
|
|
|
// Sauvegarder stripe_account_id
|
|
return $account;
|
|
}
|
|
|
|
// GET /api/amicales/{id}/onboarding-link
|
|
public function getOnboardingLink($amicaleId) {
|
|
$accountLink = \Stripe\AccountLink::create([
|
|
'account' => $amicale->stripe_account_id,
|
|
'refresh_url' => config('app.url') . '/stripe/refresh',
|
|
'return_url' => config('app.url') . '/stripe/success',
|
|
'type' => 'account_onboarding',
|
|
]);
|
|
|
|
return ['url' => $accountLink->url];
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📅 MARDI 26/08 - Webhooks et Terminal (8h)
|
|
|
|
### 🌅 Matin (4h)
|
|
|
|
#### ✅ Webhooks handler
|
|
```php
|
|
// POST /api/webhooks/stripe
|
|
public function handleWebhook(Request $request) {
|
|
$payload = $request->getContent();
|
|
$sig_header = $request->header('Stripe-Signature');
|
|
|
|
try {
|
|
$event = \Stripe\Webhook::constructEvent(
|
|
$payload, $sig_header, config('stripe.webhook_secret')
|
|
);
|
|
} catch(\Exception $e) {
|
|
return response('Invalid signature', 400);
|
|
}
|
|
|
|
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->handlePaymentSucceeded($event->data->object);
|
|
break;
|
|
}
|
|
|
|
return response('Webhook handled', 200);
|
|
}
|
|
```
|
|
|
|
#### ✅ Configuration Tap to Pay
|
|
```php
|
|
// 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)
|
|
|
|
#### ✅ Vérification compatibilité Device
|
|
```php
|
|
// 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'
|
|
];
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📅 MERCREDI 27/08 - Paiements et fees (8h)
|
|
|
|
### 🌅 Matin (4h)
|
|
|
|
#### ✅ 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
|
|
'passage_id' => 'required|integer', // ID du passage ope_pass
|
|
'entity_id' => 'required|integer',
|
|
]);
|
|
|
|
$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',
|
|
'payment_method_types' => ['card_present'],
|
|
'capture_method' => 'automatic',
|
|
'application_fee_amount' => $applicationFee,
|
|
'transfer_data' => [
|
|
'destination' => $entity->stripe_account_id,
|
|
],
|
|
'metadata' => [
|
|
'passage_id' => $validated['passage_id'],
|
|
'user_id' => $userId,
|
|
'entity_id' => $entity->id,
|
|
'year' => date('Y'),
|
|
],
|
|
]);
|
|
|
|
// 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,
|
|
];
|
|
}
|
|
```
|
|
|
|
### 🌆 Après-midi (4h)
|
|
|
|
#### ✅ Capture et confirmation
|
|
```php
|
|
// POST /api/payments/{id}/capture
|
|
public function capturePayment($paymentIntentId) {
|
|
// 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();
|
|
}
|
|
|
|
// 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/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 [
|
|
'stripe_payment_id' => $passage['stripe_payment_id'],
|
|
'status' => $paymentIntent->status,
|
|
'amount' => $paymentIntent->amount,
|
|
'currency' => $paymentIntent->currency,
|
|
'created_at' => $passage['date_creat']
|
|
];
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📅 JEUDI 28/08 - Reporting et Android compatibility (8h)
|
|
|
|
### 🌅 Matin (4h)
|
|
|
|
#### ✅ Gestion appareils Android certifiés
|
|
```php
|
|
// POST /api/devices/check-tap-to-pay
|
|
public function checkTapToPayCapability(Request $request) {
|
|
$validated = $request->validate([
|
|
'platform' => 'required|in:ios,android',
|
|
'manufacturer' => 'required_if:platform,android',
|
|
'model' => 'required_if:platform,android',
|
|
'os_version' => 'required',
|
|
]);
|
|
|
|
if ($validated['platform'] === 'ios') {
|
|
// iPhone XS et ultérieurs avec iOS 15.4+
|
|
$supportedModels = ['iPhone11,', 'iPhone12,', 'iPhone13,', 'iPhone14,', 'iPhone15,', 'iPhone16,'];
|
|
$modelSupported = false;
|
|
|
|
foreach ($supportedModels as $prefix) {
|
|
if (str_starts_with($validated['model'], $prefix)) {
|
|
$modelSupported = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$osVersion = explode('.', $validated['os_version']);
|
|
$osSupported = $osVersion[0] > 15 ||
|
|
($osVersion[0] == 15 && isset($osVersion[1]) && $osVersion[1] >= 4);
|
|
|
|
return [
|
|
'tap_to_pay_supported' => $modelSupported && $osSupported,
|
|
'message' => $modelSupported && $osSupported ?
|
|
'Tap to Pay disponible' :
|
|
'iPhone XS ou ultérieur avec iOS 15.4+ requis'
|
|
];
|
|
}
|
|
|
|
// Android - vérifier dans la base de données
|
|
$device = DB::table('android_certified_devices')
|
|
->where('manufacturer', $validated['manufacturer'])
|
|
->where('model', $validated['model'])
|
|
->where('tap_to_pay_certified', true)
|
|
->first();
|
|
|
|
return [
|
|
'tap_to_pay_supported' => $device !== null,
|
|
'message' => $device ?
|
|
'Tap to Pay disponible sur cet appareil' :
|
|
'Appareil non certifié pour Tap to Pay en France',
|
|
'alternative' => !$device ? 'Utilisez un iPhone compatible' : null
|
|
];
|
|
}
|
|
|
|
// GET /api/devices/certified-android
|
|
public function getCertifiedAndroidDevices() {
|
|
return DB::table('android_certified_devices')
|
|
->where('tap_to_pay_certified', true)
|
|
->where('country', 'FR')
|
|
->orderBy('manufacturer')
|
|
->orderBy('model')
|
|
->get();
|
|
}
|
|
```
|
|
|
|
#### ✅ Seeder pour appareils certifiés
|
|
```php
|
|
// database/seeders/AndroidCertifiedDevicesSeeder.php
|
|
public function run() {
|
|
$devices = [
|
|
// Samsung
|
|
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21', 'model_identifier' => 'SM-G991B', 'min_android_version' => 11],
|
|
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21+', 'model_identifier' => 'SM-G996B', 'min_android_version' => 11],
|
|
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21 Ultra', 'model_identifier' => 'SM-G998B', 'min_android_version' => 11],
|
|
['manufacturer' => 'Samsung', 'model' => 'Galaxy S22', 'model_identifier' => 'SM-S901B', 'min_android_version' => 12],
|
|
['manufacturer' => 'Samsung', 'model' => 'Galaxy S23', 'model_identifier' => 'SM-S911B', 'min_android_version' => 13],
|
|
['manufacturer' => 'Samsung', 'model' => 'Galaxy S24', 'model_identifier' => 'SM-S921B', 'min_android_version' => 14],
|
|
// Google Pixel
|
|
['manufacturer' => 'Google', 'model' => 'Pixel 6', 'model_identifier' => 'oriole', 'min_android_version' => 12],
|
|
['manufacturer' => 'Google', 'model' => 'Pixel 6 Pro', 'model_identifier' => 'raven', 'min_android_version' => 12],
|
|
['manufacturer' => 'Google', 'model' => 'Pixel 7', 'model_identifier' => 'panther', 'min_android_version' => 13],
|
|
['manufacturer' => 'Google', 'model' => 'Pixel 8', 'model_identifier' => 'shiba', 'min_android_version' => 14],
|
|
];
|
|
|
|
foreach ($devices as $device) {
|
|
DB::table('android_certified_devices')->insert([
|
|
'manufacturer' => $device['manufacturer'],
|
|
'model' => $device['model'],
|
|
'model_identifier' => $device['model_identifier'],
|
|
'tap_to_pay_certified' => true,
|
|
'certification_date' => now(),
|
|
'min_android_version' => $device['min_android_version'],
|
|
'country' => 'FR',
|
|
]);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### ✅ Endpoints statistiques
|
|
```php
|
|
// GET /api/amicales/{id}/stats
|
|
public function getAmicaleStats($amicaleId) {
|
|
$stats = DB::table('payment_intents')
|
|
->where('amicale_id', $amicaleId)
|
|
->where('status', 'succeeded')
|
|
->selectRaw('
|
|
COUNT(*) as total_ventes,
|
|
SUM(amount) as total_montant,
|
|
SUM(application_fee) as total_commissions,
|
|
DATE(created_at) as date
|
|
')
|
|
->groupBy('date')
|
|
->get();
|
|
|
|
return $stats;
|
|
}
|
|
|
|
// GET /api/pompiers/{id}/ventes
|
|
public function getPompierVentes($pompierId) {
|
|
return PaymentIntent::where('pompier_id', $pompierId)
|
|
->where('status', 'succeeded')
|
|
->orderBy('created_at', 'desc')
|
|
->paginate(20);
|
|
}
|
|
```
|
|
|
|
### 🌆 Après-midi (4h)
|
|
|
|
#### ✅ Gestion des remboursements
|
|
```php
|
|
// POST /api/payments/{id}/refund
|
|
public function refundPayment($paymentIntentId, Request $request) {
|
|
$validated = $request->validate([
|
|
'amount' => 'integer|min:100', // optionnel, remboursement partiel
|
|
'reason' => 'string|in:duplicate,fraudulent,requested_by_customer',
|
|
]);
|
|
|
|
$payment = PaymentIntent::where('stripe_payment_intent_id', $paymentIntentId)->first();
|
|
|
|
$refund = \Stripe\Refund::create([
|
|
'payment_intent' => $paymentIntentId,
|
|
'amount' => $validated['amount'] ?? null, // null = remboursement total
|
|
'reason' => $validated['reason'] ?? 'requested_by_customer',
|
|
'reverse_transfer' => true, // Important pour Connect
|
|
'refund_application_fee' => true, // Rembourser aussi la commission
|
|
]);
|
|
|
|
$payment->update(['status' => 'refunded']);
|
|
|
|
return $refund;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📅 VENDREDI 29/08 - Mode offline et sync (8h)
|
|
|
|
### 🌅 Matin (4h)
|
|
|
|
#### ✅ Queue de synchronisation
|
|
```php
|
|
// POST /api/payments/batch-sync
|
|
public function batchSync(Request $request) {
|
|
$validated = $request->validate([
|
|
'transactions' => 'required|array',
|
|
'transactions.*.local_id' => 'required|string',
|
|
'transactions.*.amount' => 'required|integer',
|
|
'transactions.*.created_at' => 'required|date',
|
|
'transactions.*.payment_method' => 'required|in:card,cash',
|
|
]);
|
|
|
|
$results = [];
|
|
|
|
foreach ($validated['transactions'] as $transaction) {
|
|
if ($transaction['payment_method'] === 'cash') {
|
|
// Enregistrer paiement cash uniquement en DB
|
|
$results[] = $this->recordCashPayment($transaction);
|
|
} else {
|
|
// Créer PaymentIntent a posteriori (si possible)
|
|
$results[] = $this->createOfflinePayment($transaction);
|
|
}
|
|
}
|
|
|
|
return ['synced' => $results];
|
|
}
|
|
```
|
|
|
|
### 🌆 Après-midi (4h)
|
|
|
|
#### ✅ Tests unitaires critiques
|
|
```php
|
|
class StripePaymentTest extends TestCase {
|
|
public function test_create_payment_intent_with_fees() {
|
|
// Test création PaymentIntent avec commission
|
|
}
|
|
|
|
public function test_webhook_signature_validation() {
|
|
// Test sécurité webhook
|
|
}
|
|
|
|
public function test_refund_reverses_transfer() {
|
|
// Test remboursement avec annulation virement
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📅 LUNDI 01/09 - Sécurité et optimisations (8h)
|
|
|
|
### 🌅 Matin (4h)
|
|
|
|
#### ✅ Rate limiting et sécurité
|
|
```php
|
|
// Middleware RateLimiter pour endpoints sensibles
|
|
Route::middleware(['throttle:10,1'])->group(function () {
|
|
Route::post('/payments/create-intent', 'PaymentController@createIntent');
|
|
});
|
|
|
|
// Validation des montants
|
|
public function validateAmount($amount) {
|
|
if ($amount < 100 || $amount > 50000) { // 1€ - 500€
|
|
throw new ValidationException('Montant invalide');
|
|
}
|
|
}
|
|
```
|
|
|
|
### 🌆 Après-midi (4h)
|
|
|
|
#### ✅ Logs et monitoring
|
|
```php
|
|
// Logger tous les événements Stripe
|
|
Log::channel('stripe')->info('Payment created', [
|
|
'payment_intent_id' => $paymentIntent->id,
|
|
'amount' => $paymentIntent->amount,
|
|
'pompier_id' => $pompier->id,
|
|
]);
|
|
```
|
|
|
|
---
|
|
|
|
## 📅 MARDI 02/09 - Documentation API (4h)
|
|
|
|
#### ✅ Documentation OpenAPI/Swagger
|
|
```yaml
|
|
/api/payments/create-intent:
|
|
post:
|
|
summary: Créer une intention de paiement
|
|
parameters:
|
|
- name: amount
|
|
type: integer
|
|
required: true
|
|
description: Montant en centimes
|
|
responses:
|
|
200:
|
|
description: PaymentIntent créé
|
|
```
|
|
|
|
---
|
|
|
|
## 📅 MERCREDI 03/09 - Tests d'intégration (8h)
|
|
|
|
#### ✅ Tests end-to-end
|
|
- [ ] Parcours complet onboarding amicale
|
|
- [ ] Création paiement → capture → confirmation
|
|
- [ ] Test remboursement complet et partiel
|
|
- [ ] Test webhooks avec ngrok
|
|
|
|
---
|
|
|
|
## 📅 JEUDI 04/09 - Mise en production (8h)
|
|
|
|
---
|
|
|
|
## 📅 VENDREDI 05/09 - Support et livraison finale (8h)
|
|
|
|
### 🌅 Matin (4h)
|
|
|
|
#### ✅ Déploiement final
|
|
- [ ] Migration DB production
|
|
- [ ] Variables environnement LIVE
|
|
- [ ] Smoke tests production
|
|
- [ ] Vérification des webhooks en production
|
|
|
|
### 🌆 Après-midi (4h)
|
|
|
|
#### ✅ Support et monitoring
|
|
- [ ] Monitoring des premiers paiements réels
|
|
- [ ] Support hotline pour équipes terrain
|
|
- [ ] Documentation de passation
|
|
- [ ] Réunion de clôture et retour d'expérience
|
|
|
|
---
|
|
|
|
## 📊 RÉCAPITULATIF
|
|
|
|
- **Total heures** : 72h sur 10 jours
|
|
- **Endpoints créés** : 15
|
|
- **Tables DB** : 3
|
|
- **Tests** : 20+
|
|
|
|
## 🔧 DÉPENDANCES
|
|
|
|
```json
|
|
{
|
|
"require": {
|
|
"php": "^8.3",
|
|
"stripe/stripe-php": "^13.0",
|
|
"laravel/framework": "^10.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
## ⚠️ CHECKLIST SÉCURITÉ
|
|
|
|
- [ ] ❌ JAMAIS logger les clés secrètes
|
|
- [ ] ✅ TOUJOURS valider signature webhooks
|
|
- [ ] ✅ TOUJOURS utiliser HTTPS
|
|
- [ ] ✅ Rate limiting sur endpoints paiement
|
|
- [ ] ✅ Logs détaillés pour audit
|
|
|
|
---
|
|
|
|
## 🎯 BILAN DÉVELOPPEMENT API (01/09/2024)
|
|
|
|
### ✅ 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
|
|
- Retour JSON avec informations détaillées
|
|
|
|
- **POST /api/stripe/accounts/:accountId/onboarding-link** ✅
|
|
- Génération liens d'onboarding Stripe
|
|
- URLs de retour configurées
|
|
- Gestion des erreurs et timeouts
|
|
|
|
#### **Configuration et Utilitaires**
|
|
- **GET /api/stripe/config** ✅
|
|
- Configuration publique Stripe
|
|
- Clés publiques et paramètres client
|
|
- Adaptation par environnement
|
|
|
|
- **POST /api/stripe/webhook** ✅
|
|
- Réception événements Stripe
|
|
- Vérification signatures webhook
|
|
- Traitement des événements Connect
|
|
|
|
### 🔧 CORRECTIONS TECHNIQUES RÉALISÉES
|
|
|
|
#### **StripeController.php**
|
|
- Fixed `Database::getInstance()` → `$this->db`
|
|
- Fixed `$db->prepare()` → `$this->db->prepare()`
|
|
- Removed `details_submitted` column from SQL UPDATE
|
|
- Added proper exit statements after JSON responses
|
|
- Commented out Logger class calls (class not found)
|
|
|
|
#### **StripeService.php**
|
|
- Added proper Stripe SDK imports (`use Stripe\Account`)
|
|
- Fixed `Account::retrieve()` → `$this->stripe->accounts->retrieve()`
|
|
- **CRUCIAL**: Added data decryption support:
|
|
```php
|
|
$nom = !empty($entite['encrypted_name']) ?
|
|
\ApiService::decryptData($entite['encrypted_name']) : '';
|
|
$email = !empty($entite['encrypted_email']) ?
|
|
\ApiService::decryptSearchableData($entite['encrypted_email']) : null;
|
|
```
|
|
- Fixed address mapping (adresse1, adresse2 vs adresse)
|
|
- **REMOVED commission calculation - set to 0%**
|
|
|
|
#### **Router.php**
|
|
- Commented out excessive debug logging causing nginx 502 errors:
|
|
```php
|
|
// error_log("Recherche de route pour: méthode=$method, uri=$uri");
|
|
// error_log("Test pattern: $pattern contre uri: $uri");
|
|
```
|
|
|
|
#### **AppConfig.php**
|
|
- Set `application_fee_percent` to 0 (was 2.5)
|
|
- Set `application_fee_minimum` to 0 (was 50)
|
|
- **Policy**: 100% of payments go to amicales
|
|
|
|
### 📊 TESTS ET VALIDATION
|
|
|
|
#### **Tests Réussis**
|
|
1. **POST /api/stripe/accounts** → 200 OK (Compte créé: acct_1S2YfNP63A07c33Y)
|
|
2. **GET /api/stripe/accounts/5/status** → 200 OK (charges_enabled: true)
|
|
3. **POST /api/stripe/locations** → 200 OK (Location: tml_GLJ21w7KCYX4Wj)
|
|
4. **POST /api/stripe/accounts/.../onboarding-link** → 200 OK (Link generated)
|
|
5. **Onboarding Stripe** → Completed successfully by user
|
|
|
|
#### **Erreurs Résolues**
|
|
- ❌ 500 "Class App\Controllers\Database not found" → ✅ Fixed
|
|
- ❌ 400 "Invalid email address: " → ✅ Fixed (decryption added)
|
|
- ❌ 502 "upstream sent too big header" → ✅ Fixed (logs removed)
|
|
- ❌ SQL "Column not found: details_submitted" → ✅ Fixed
|
|
|
|
### 🚀 ARCHITECTURE TECHNIQUE
|
|
|
|
#### **Services Implémentés**
|
|
- **StripeService**: Singleton pour interactions Stripe API
|
|
- **StripeController**: Endpoints REST avec gestion sessions
|
|
- **StripeWebhookController**: Handler événements webhook
|
|
- **ApiService**: Déchiffrement données encrypted fields
|
|
|
|
#### **Sécurité**
|
|
- Validation signatures webhook Stripe
|
|
- Authentification session-based pour APIs privées
|
|
- Public endpoints: webhook uniquement
|
|
- Pas de stockage clés secrètes en base
|
|
|
|
#### **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)
|
|
- Champs encrypted_email et encrypted_name supportés
|
|
- Déchiffrement automatique avant envoi Stripe
|
|
|
|
### 🎯 PROCHAINES ÉTAPES API
|
|
1. **Tests paiements réels** avec PaymentIntents
|
|
2. **Endpoints statistiques** pour dashboard amicales
|
|
3. **Webhooks production** avec clés live
|
|
4. **Monitoring et logs** des transactions
|
|
5. **Rate limiting** sur endpoints sensibles
|
|
|
|
---
|
|
|
|
## 📱 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* |