feat: synchronisation mode deconnecte fin chat et stats
This commit is contained in:
622
api/docs/PLANNING-STRIPE-API.md
Normal file
622
api/docs/PLANNING-STRIPE-API.md
Normal file
@@ -0,0 +1,622 @@
|
||||
# PLANNING STRIPE - DÉVELOPPEUR BACKEND PHP
|
||||
## API PHP 8.3 - Intégration Stripe Connect + Terminal
|
||||
### Période : 25/08/2024 - 05/09/2024
|
||||
|
||||
---
|
||||
|
||||
## 📅 LUNDI 25/08 - Setup et architecture (8h)
|
||||
|
||||
### 🌅 Matin (4h)
|
||||
```bash
|
||||
# Installation Stripe PHP SDK
|
||||
cd api
|
||||
composer require stripe/stripe-php
|
||||
```
|
||||
|
||||
#### ✅ Configuration environnement
|
||||
- [ ] Créer `config/stripe.php` avec clés TEST
|
||||
- [ ] Ajouter variables `.env` :
|
||||
```env
|
||||
STRIPE_PUBLIC_KEY=pk_test_...
|
||||
STRIPE_SECRET_KEY=sk_test_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
STRIPE_API_VERSION=2024-06-20
|
||||
```
|
||||
- [ ] Créer service `StripeService.php` singleton
|
||||
- [ ] Configurer middleware authentification API
|
||||
|
||||
#### ✅ Base de données
|
||||
```sql
|
||||
-- Tables à créer
|
||||
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)
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
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
|
||||
```php
|
||||
// POST /api/amicales/{id}/stripe-account
|
||||
public function createStripeAccount($amicaleId) {
|
||||
$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);
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Terminal Connection Token
|
||||
```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];
|
||||
}
|
||||
```
|
||||
|
||||
### 🌆 Après-midi (4h)
|
||||
|
||||
#### ✅ Gestion des Locations
|
||||
```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;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 MERCREDI 27/08 - Paiements et fees (8h)
|
||||
|
||||
### 🌅 Matin (4h)
|
||||
|
||||
#### ✅ Création PaymentIntent avec commission
|
||||
```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',
|
||||
]);
|
||||
|
||||
$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%
|
||||
);
|
||||
|
||||
$paymentIntent = \Stripe\PaymentIntent::create([
|
||||
'amount' => $validated['amount'],
|
||||
'currency' => 'eur',
|
||||
'payment_method_types' => ['card_present'],
|
||||
'capture_method' => 'automatic',
|
||||
'application_fee_amount' => $applicationFee,
|
||||
'transfer_data' => [
|
||||
'destination' => $amicale->stripe_account_id,
|
||||
],
|
||||
'metadata' => [
|
||||
'pompier_id' => $pompier->id,
|
||||
'pompier_name' => $pompier->name,
|
||||
'amicale_id' => $amicale->id,
|
||||
'calendrier_annee' => 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,
|
||||
]);
|
||||
|
||||
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) {
|
||||
$localPayment = PaymentIntent::where('stripe_payment_intent_id', $paymentIntentId)->first();
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
return $paymentIntent;
|
||||
}
|
||||
|
||||
// GET /api/payments/{id}/status
|
||||
public function getPaymentStatus($paymentIntentId) {
|
||||
$payment = PaymentIntent::where('stripe_payment_intent_id', $paymentIntentId)->first();
|
||||
return [
|
||||
'status' => $payment->status,
|
||||
'amount' => $payment->amount,
|
||||
'created_at' => $payment->created_at,
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 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
|
||||
|
||||
---
|
||||
|
||||
*Document créé le 24/08/2024 - À mettre à jour quotidiennement*
|
||||
Reference in New Issue
Block a user