> ## Documentation Index
> Fetch the complete documentation index at: https://docs.colismove.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Paiements & KYC

> Sc 8, 9, 10, 11, 19, 20 — Stripe Connect, 3DS DSP2, KYC, wallet, reconciliation cron.

Six scénarios couvrent la chaîne de paiement Stripe Connect, l'authentification forte 3DS DSP2, la vérification KYC et le wallet carrier.

## Sc 8 — Pays non supporté côté transporteur

Si le carrier appartient à un pays non whitelisté côté Stripe Connect, l'onboarding est bloqué dès la création de l'annonce.

```mermaid theme={null}
flowchart TD
    A[Carrier crée annonce] --> B{Pays carrier whitelist ?}
    B -->|Non| C[HTTP 422<br/>PAYS_NON_SUPPORTE]
    B -->|Oui| D{Stripe Connect actif ?}
    D -->|Non| E[Redirect onboarding]
    D -->|Oui| F[Annonce créée]
    C --> G[Frontend affiche page<br/>'pays bientôt disponible']
    G --> H[Waitlist signup<br/>commit 31c791b]
```

**Référence** : commit `a783274`

## Sc 9 — Carte refusée 3DS DSP2

Authentification forte (Strong Customer Authentication) requise par PSD2. Le frontend doit gérer 3 codes Stripe :

```mermaid theme={null}
sequenceDiagram
    actor S as Sender
    participant FE as Frontend
    participant API as Backend
    participant Stripe

    S->>FE: Confirme paiement
    FE->>Stripe: PaymentIntent confirm
    Stripe-->>FE: requires_action (3DS challenge)
    FE->>FE: Stripe.js handleCardAction()
    S->>FE: Saisit code 3DS
    alt Authentification réussie
        Stripe-->>API: webhook payment_intent.succeeded
        API->>API: → RESERVATION_PAYEE
    else Authentification échouée
        Stripe-->>FE: payment_intent_authentication_failure
        FE-->>S: "Veuillez réessayer"
    else Carte refusée
        Stripe-->>FE: requires_payment_method
        FE-->>S: "Carte refusée, choisissez une autre"
    end
```

### Codes Stripe gérés

| Code                                    | UX frontend                    | Backend                    |
| --------------------------------------- | ------------------------------ | -------------------------- |
| `requires_action`                       | Stripe.js `handleCardAction()` | Pas de transition          |
| `payment_intent_authentication_failure` | Message rejet 3DS              | Stay `EN_ATTENTE_PAIEMENT` |
| `requires_payment_method`               | Demander nouvelle carte        | Stay `EN_ATTENTE_PAIEMENT` |

### Idempotence

Webhook `payment_intent.succeeded` protégé par clé idempotency stockée dans `processed_webhook_events` (table dédiée).

**Référence** : commit `d6ccab9`

## Sc 10 — Cumul LCB-FT silencieux → Stripe Identity (Pivot Flynanga)

**Décision archi explicite** (ADR `2026-05-01-pivot-flynanga-stripe-identity.md`) : drop Didit V2, bascule sur Stripe Identity quand le seuil LCB-FT est atteint.

```mermaid theme={null}
flowchart TD
    A[User effectue transactions] --> B[Compteur LCB-FT cumul mensuel]
    B --> C{Cumul ≥ seuil ?}
    C -->|Non| D[Continuer, pas de KYC]
    C -->|Oui| E[Trigger Stripe Identity session]
    E --> F[User redirige Stripe Identity]
    F --> G{Identité vérifiée ?}
    G -->|Oui| H[ROLE_VERIFIED_USER]
    G -->|Non| I[Block transactions futures]
```

### Migration

* Compteur mensuel via `UtilisateurLcbftJpaEntity` (5 colonnes + id, `@DynamicUpdate`, table partagée `utilisateur` ADR-008)
* V16 migration LCB-FT déployée
* Webhooks Stripe Identity configurés
* Statut runbook : `runbooks/2026-05-01-rollout-didit-disabled-lcbft.md` ready

## Sc 11 — Webhook perdu / reconciliation cron

Si un webhook Stripe est perdu (réseau, downtime), un scheduler répare l'incohérence en interrogeant l'API Stripe directement.

```mermaid theme={null}
sequenceDiagram
    participant Cron as StripePaymentReconciliationScheduler
    participant API as Backend
    participant Stripe

    Note over Cron: Toutes les 15 min, initialDelay 1 min
    Cron->>API: findStaleEnAttentePaiement(cutoff = now - 5min)
    API-->>Cron: List<PaymentIntent en suspens>
    loop Pour chaque PaymentIntent
        Cron->>Stripe: paymentIntents.retrieve(id)
        alt status = succeeded
            Cron->>API: replay webhook payment_intent.succeeded
            Note over API: idempotence processed_webhook_events<br/>source 'stripe-cron'<br/>LCB-FT increment mirror
        else status = requires_capture
            Cron->>API: replay capture
        else status = processing
            Note over Cron: skip
        end
    end
```

### Logique

* Lookup JPQL : `findStaleEnAttentePaiement(cutoff)`
* Replay idempotent (clé event Stripe ne re-déclenche pas)
* 6 tests Mockito (zero, succeeded+LCB-FT, requires\_capture, processing skip, déjà reconcilié, continue après failure)
* `PaymentLog` enregistré avec source `stripe-cron`

**Référence** : commit `d0de512`

## Sc 19 — Wallet transporteur (Stripe Express direct)

Plutôt que de maintenir un dashboard wallet custom, ColisMove redirige le carrier directement vers le **Stripe Express dashboard** (solde, payouts, fiscal docs natifs).

```mermaid theme={null}
sequenceDiagram
    actor C as Carrier
    participant FE as Frontend
    participant API as Backend
    participant Stripe

    C->>FE: Clic "Mon wallet"
    FE->>API: POST /v1/api/stripe-connect/wallet-link
    alt Wallet trouvé + Stripe Connect actif
        API->>Stripe: accounts.loginLinks.create(accountId)
        Stripe-->>API: {url: 'https://connect.stripe.com/express/...'}
        API-->>FE: 200 {url}
        FE->>C: Redirect Stripe Express
    else Wallet absent
        API-->>FE: 400 STRIPE_CONNECT_REQUIRED
    else Erreur Stripe
        API-->>FE: 502 STRIPE_ERROR
    end
```

### Avantages

* Solde, payouts, fiscal docs natifs Stripe
* Zéro maintenance ColisMove
* Conforme PSD2 (Stripe gère)

**Référence** : commit `4e873f4`

## Sc 20 — Compte transporteur Stripe fermé

Si Stripe désactive un compte carrier (suspension, fraude), le webhook `account.updated` détecte la transition `chargesEnabled: true → false` et bloque les nouvelles réservations.

```mermaid theme={null}
sequenceDiagram
    participant Stripe
    participant API as Backend
    actor C as Carrier
    actor S as Sender

    Stripe->>API: webhook account.updated (charges_enabled=false)
    API->>API: prior chargesEnabled=true detected
    API->>API: Publish StripeAccountStatusChangedEvent.disabled
    API->>API: Carrier annonces → invisibles
    API-->>C: Email rouge "Compte désactivé" + disabledReason

    Note over S,API: Plus tard, sender essaie de booker
    S->>API: POST /v1/api/bookings
    API->>API: AnnonceSnapshot.createurStripeChargesEnabled = false
    API-->>S: 422 CARRIER_TEMPORARILY_DISABLED

    Note over Stripe: Plus tard, Stripe réactive
    Stripe->>API: webhook account.updated (charges_enabled=true)
    API-->>C: Email vert "Compte réactivé"
```

### Logique technique

* `StripeAccountStatusChangedEvent` shared-domain (factories `disabled` / `reactivated`)
* Détection idempotente via prior chargesEnabled lookup
* Carrier gate dans `ReservationService.creer` lève `TransporteurIndisponibleException` HTTP 422
* 16e champ `AnnonceSnapshot.createurStripeChargesEnabled`
* 7 tests Mockito (5 transitions service + 2 carrier gate)

**Référence** : commit `2216fd0`

## Récap effets Stripe

| Scénario | Webhook                             | Action backend                  |
| -------- | ----------------------------------- | ------------------------------- |
| Sc 9     | `payment_intent.succeeded` (3DS OK) | → RESERVATION\_PAYEE            |
| Sc 11    | `payment_intent.succeeded` (replay) | Idempotent → RESERVATION\_PAYEE |
| Sc 20    | `account.updated`                   | Disable/enable carrier annonces |

| Scénario | Endpoint Stripe SDK                               |
| -------- | ------------------------------------------------- |
| Sc 19    | `accounts.loginLinks.create()`                    |
| Sc 6/16  | `refunds.create()` + `transfers.createReversal()` |
