Files
geo/api/docs/PLANNING-STRIPE-API.md
pierre b6584c83fa 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>
2025-10-05 20:11:15 +02:00

25 KiB

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)

# Installation Stripe PHP SDK
cd api
composer require stripe/stripe-php

Configuration environnement

  • Créer configuration Stripe dans AppConfig.php avec clés TEST
  • Ajouter variables de configuration :
    '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'
    ]
    
  • Créer service StripeService.php singleton
  • Configurer authentification Session-based API

Base de données

-- 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)

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

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é

// 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

// 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

/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

{
    "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:
    $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:
    // 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

POST /api/stripe/payments/create-intent
{
    "amount": 1500,
    "passage_id": 123,
    "entity_id": 5
}

API → Stripe → Base de données

// 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

{
    "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_stripedstripe_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