Files
geo/api/docs/STRIPE-BACKEND-MIGRATION.md
pierre 2f5946a184 feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles
- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD)
  * DEV: Clés TEST Pierre (mode test)
  * REC: Clés TEST Client (mode test)
  * PROD: Clés LIVE Client (mode live)
- Ajout de la gestion des bases de données immeubles/bâtiments
  * Configuration buildings_database pour DEV/REC/PROD
  * Service BuildingService pour enrichissement des adresses
- Optimisations pages et améliorations ergonomie
- Mises à jour des dépendances Composer
- Nettoyage des fichiers obsolètes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 18:26:27 +01:00

14 KiB
Raw Blame History

🔧 Migration Backend Stripe - Option A (Tout en 1)

📋 Objectif

Optimiser la création de compte Stripe Connect en 1 seule requête côté Flutter qui crée :

  1. Le compte Stripe Connect
  2. La Location Terminal
  3. Le lien d'onboarding

🗄️ 1. Modification de la base de données

Ajouter la colonne stripe_location_id

ALTER TABLE amicales
ADD COLUMN stripe_location_id VARCHAR(255) NULL
AFTER stripe_id;

Vérification :

DESCRIBE amicales;

Doit afficher :

+-------------------+--------------+------+-----+---------+-------+
| Field             | Type         | Null | Key | Default | Extra |
+-------------------+--------------+------+-----+---------+-------+
| stripe_id         | varchar(255) | YES  |     | NULL    |       |
| stripe_location_id| varchar(255) | YES  |     | NULL    |       |
+-------------------+--------------+------+-----+---------+-------+

🔧 2. Modification de l'endpoint POST /stripe/accounts

Fichier : app/Http/Controllers/StripeController.php (ou similaire)

Méthode : createAccount() ou store()

Code proposé :

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Models\Amicale;

/**
 * Créer un compte Stripe Connect avec Location Terminal et lien d'onboarding
 *
 * @param Request $request
 * @return \Illuminate\Http\JsonResponse
 */
public function createStripeAccount(Request $request)
{
    $request->validate([
        'fk_entite' => 'required|integer|exists:amicales,id',
        'return_url' => 'required|string|url',
        'refresh_url' => 'required|string|url',
    ]);

    $fkEntite = $request->fk_entite;
    $amicale = Amicale::findOrFail($fkEntite);

    // Vérifier si un compte existe déjà
    if (!empty($amicale->stripe_id)) {
        return $this->handleExistingAccount($amicale, $request);
    }

    DB::beginTransaction();

    try {
        // Configurer la clé Stripe (selon environnement)
        \Stripe\Stripe::setApiKey(config('services.stripe.secret'));

        // 1⃣ Créer le compte Stripe Connect Express
        $account = \Stripe\Account::create([
            'type' => 'express',
            'country' => 'FR',
            'email' => $amicale->email,
            'business_type' => 'non_profit', // ou 'company' selon le cas
            'business_profile' => [
                'name' => $amicale->name,
                'url' => config('app.url'),
            ],
            'capabilities' => [
                'card_payments' => ['requested' => true],
                'transfers' => ['requested' => true],
            ],
        ]);

        \Log::info('Stripe account created', [
            'amicale_id' => $amicale->id,
            'account_id' => $account->id,
        ]);

        // 2⃣ Créer la Location Terminal pour Tap to Pay
        $location = \Stripe\Terminal\Location::create([
            'display_name' => $amicale->name,
            'address' => [
                'line1' => $amicale->adresse1 ?: 'Non renseigné',
                'line2' => $amicale->adresse2,
                'city' => $amicale->ville ?: 'Non renseigné',
                'postal_code' => $amicale->code_postal ?: '00000',
                'country' => 'FR',
            ],
        ], [
            'stripe_account' => $account->id, // ← Important : Connect account
        ]);

        \Log::info('Stripe Terminal Location created', [
            'amicale_id' => $amicale->id,
            'location_id' => $location->id,
        ]);

        // 3⃣ Créer le lien d'onboarding
        $accountLink = \Stripe\AccountLink::create([
            'account' => $account->id,
            'refresh_url' => $request->refresh_url,
            'return_url' => $request->return_url,
            'type' => 'account_onboarding',
        ]);

        \Log::info('Stripe onboarding link created', [
            'amicale_id' => $amicale->id,
            'account_id' => $account->id,
        ]);

        // 4⃣ Sauvegarder TOUT en base de données
        $amicale->stripe_id = $account->id;
        $amicale->stripe_location_id = $location->id;
        $amicale->chk_stripe = true;
        $amicale->save();

        DB::commit();

        // 5⃣ Retourner TOUTES les informations
        return response()->json([
            'success' => true,
            'account_id' => $account->id,
            'location_id' => $location->id,
            'onboarding_url' => $accountLink->url,
            'charges_enabled' => $account->charges_enabled,
            'payouts_enabled' => $account->payouts_enabled,
            'existing' => false,
            'message' => 'Compte Stripe Connect créé avec succès',
        ], 201);

    } catch (\Stripe\Exception\ApiErrorException $e) {
        DB::rollBack();

        \Log::error('Stripe API error', [
            'amicale_id' => $amicale->id,
            'error' => $e->getMessage(),
            'type' => get_class($e),
        ]);

        return response()->json([
            'success' => false,
            'message' => 'Erreur Stripe : ' . $e->getMessage(),
        ], 500);

    } catch (\Exception $e) {
        DB::rollBack();

        \Log::error('Stripe account creation failed', [
            'amicale_id' => $amicale->id,
            'error' => $e->getMessage(),
            'trace' => $e->getTraceAsString(),
        ]);

        return response()->json([
            'success' => false,
            'message' => 'Erreur lors de la création du compte Stripe',
        ], 500);
    }
}

/**
 * Gérer le cas d'un compte Stripe existant
 */
private function handleExistingAccount(Amicale $amicale, Request $request)
{
    try {
        \Stripe\Stripe::setApiKey(config('services.stripe.secret'));

        // Récupérer les infos du compte existant
        $account = \Stripe\Account::retrieve($amicale->stripe_id);

        // Si pas de location_id, la créer maintenant
        if (empty($amicale->stripe_location_id)) {
            $location = \Stripe\Terminal\Location::create([
                'display_name' => $amicale->name,
                'address' => [
                    'line1' => $amicale->adresse1 ?: 'Non renseigné',
                    'city' => $amicale->ville ?: 'Non renseigné',
                    'postal_code' => $amicale->code_postal ?: '00000',
                    'country' => 'FR',
                ],
            ], [
                'stripe_account' => $amicale->stripe_id,
            ]);

            $amicale->stripe_location_id = $location->id;
            $amicale->save();

            \Log::info('Location created for existing account', [
                'amicale_id' => $amicale->id,
                'location_id' => $location->id,
            ]);
        }

        // Si le compte est déjà complètement configuré
        if ($account->charges_enabled && $account->payouts_enabled) {
            return response()->json([
                'success' => true,
                'account_id' => $amicale->stripe_id,
                'location_id' => $amicale->stripe_location_id,
                'onboarding_url' => null,
                'charges_enabled' => true,
                'payouts_enabled' => true,
                'existing' => true,
                'message' => 'Compte Stripe déjà configuré et actif',
            ]);
        }

        // Compte existant mais configuration incomplète : générer un nouveau lien
        $accountLink = \Stripe\AccountLink::create([
            'account' => $amicale->stripe_id,
            'refresh_url' => $request->refresh_url,
            'return_url' => $request->return_url,
            'type' => 'account_onboarding',
        ]);

        return response()->json([
            'success' => true,
            'account_id' => $amicale->stripe_id,
            'location_id' => $amicale->stripe_location_id,
            'onboarding_url' => $accountLink->url,
            'charges_enabled' => $account->charges_enabled,
            'payouts_enabled' => $account->payouts_enabled,
            'existing' => true,
            'message' => 'Compte existant, configuration à finaliser',
        ]);

    } catch (\Exception $e) {
        \Log::error('Error handling existing account', [
            'amicale_id' => $amicale->id,
            'error' => $e->getMessage(),
        ]);

        return response()->json([
            'success' => false,
            'message' => 'Erreur lors de la vérification du compte existant',
        ], 500);
    }
}

📡 3. Modification de l'endpoint GET /stripe/accounts/{id}/status

Ajouter location_id dans la réponse :

public function checkAccountStatus($amicaleId)
{
    $amicale = Amicale::findOrFail($amicaleId);

    if (empty($amicale->stripe_id)) {
        return response()->json([
            'has_account' => false,
            'account_id' => null,
            'location_id' => null,
            'charges_enabled' => false,
            'payouts_enabled' => false,
            'onboarding_completed' => false,
        ]);
    }

    try {
        \Stripe\Stripe::setApiKey(config('services.stripe.secret'));
        $account = \Stripe\Account::retrieve($amicale->stripe_id);

        return response()->json([
            'has_account' => true,
            'account_id' => $amicale->stripe_id,
            'location_id' => $amicale->stripe_location_id, // ← Ajouté
            'charges_enabled' => $account->charges_enabled,
            'payouts_enabled' => $account->payouts_enabled,
            'onboarding_completed' => $account->details_submitted,
        ]);

    } catch (\Exception $e) {
        return response()->json([
            'has_account' => false,
            'error' => $e->getMessage(),
        ], 500);
    }
}

🗑️ 4. Endpoint à SUPPRIMER (devenu inutile)

POST /stripe/locations

Cet endpoint n'est plus nécessaire car la Location est créée automatiquement dans POST /stripe/accounts.

Option 1 : Supprimer complètement Option 2 : Le garder pour compatibilité temporaire (si utilisé ailleurs)


📝 5. Modification du modèle Eloquent

Fichier : app/Models/Amicale.php

Ajouter le champ stripe_location_id :

protected $fillable = [
    // ... autres champs
    'stripe_id',
    'stripe_location_id', // ← Ajouté
    'chk_stripe',
];

protected $casts = [
    'chk_stripe' => 'boolean',
];

6. Tests à effectuer

Test 1 : Nouvelle amicale

curl -X POST http://localhost/api/stripe/accounts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {token}" \
  -d '{
    "fk_entite": 123,
    "return_url": "https://app.geosector.fr/stripe/success",
    "refresh_url": "https://app.geosector.fr/stripe/refresh"
  }'

Réponse attendue :

{
  "success": true,
  "account_id": "acct_xxxxxxxxxxxxx",
  "location_id": "tml_xxxxxxxxxxxxx",
  "onboarding_url": "https://connect.stripe.com/setup/...",
  "charges_enabled": false,
  "payouts_enabled": false,
  "existing": false,
  "message": "Compte Stripe Connect créé avec succès"
}

Test 2 : Amicale avec compte existant

curl -X POST http://localhost/api/stripe/accounts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {token}" \
  -d '{
    "fk_entite": 456,
    "return_url": "https://app.geosector.fr/stripe/success",
    "refresh_url": "https://app.geosector.fr/stripe/refresh"
  }'

Réponse attendue :

{
  "success": true,
  "account_id": "acct_xxxxxxxxxxxxx",
  "location_id": "tml_xxxxxxxxxxxxx",
  "onboarding_url": null,
  "charges_enabled": true,
  "payouts_enabled": true,
  "existing": true,
  "message": "Compte Stripe déjà configuré et actif"
}

Test 3 : Vérifier la BDD

SELECT id, name, stripe_id, stripe_location_id, chk_stripe
FROM amicales
WHERE id = 123;

Résultat attendu :

+-----+------------------+-------------------+-------------------+------------+
| id  | name             | stripe_id         | stripe_location_id| chk_stripe |
+-----+------------------+-------------------+-------------------+------------+
| 123 | Pompiers Paris15 | acct_xxxxxxxxxxxxx| tml_xxxxxxxxxxxxx | 1          |
+-----+------------------+-------------------+-------------------+------------+

🚀 7. Déploiement

Étapes :

  1. Appliquer la migration SQL
  2. Déployer le code Backend modifié
  3. Tester avec Postman/curl
  4. Déployer le code Flutter modifié
  5. Tester le flow complet depuis l'app

📊 Comparaison Avant/Après

Aspect Avant Après
Appels API Flutter → Backend 3 1
Appels Backend → Stripe 3 3 (mais atomiques)
Latence totale ~3-5s ~1-2s
Gestion erreurs Complexe Simplifié avec transaction
Atomicité Non Oui (DB transaction)
Location ID sauvegardé Non Oui

🎯 Bénéfices

  1. Performance : Latence divisée par 2-3
  2. Fiabilité : Transaction BDD garantit la cohérence
  3. Simplicité : Code Flutter plus simple
  4. Maintenance : Moins de code à maintenir
  5. Traçabilité : Logs centralisés côté Backend
  6. Tap to Pay prêt : location_id disponible immédiatement

⚠️ Points d'attention

  1. Rollback : Si la transaction échoue, rien n'est sauvegardé (bon comportement)
  2. Logs : Bien logger chaque étape pour le debug
  3. Stripe Connect limitations : Respecter les rate limits Stripe
  4. Tests : Tester avec des comptes Stripe de test d'abord

📚 Ressources