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

# Idempotence et Prévention des Doublons

> Empêcher les transactions financières en double grâce aux clés d'idempotence

## Aperçu

L'**idempotence** garantit que l'exécution de la même opération plusieurs fois a le même effet que son exécution une seule fois. Agatabo utilise des clés d'idempotence pour empêcher les transactions financières en double causées par des problèmes réseau, des doubles-clics accidentels ou des relances système.

<Note>
  **Fonctionnement** : Chaque opération financière exige un en-tête `x-idempotency-key` unique. Si la même clé est utilisée deux fois, la deuxième requête est rejetée comme doublon.
</Note>

***

## Pourquoi l'Idempotence est Importante

**Sans protection d'idempotence :**

```
L'utilisateur clique sur "Enregistrer le Dépôt"
  → Timeout réseau (aucune réponse)
  → L'utilisateur clique à nouveau (pensant que cela n'a pas marché)
  → Résultat : Dépôt enregistré DEUX FOIS ❌
```

**Avec l'idempotence d'Agatabo :**

```
L'utilisateur clique sur "Enregistrer le Dépôt" (clé : abc123)
  → Timeout réseau
  → L'utilisateur clique à nouveau (même clé : abc123)
  → Le backend détecte la clé en double
  → Résultat : Dépôt enregistré UNE FOIS ✓
```

***

## Fonctionnement

### Contrainte de Base de Données

Le modèle **JournalEntry** a une contrainte unique :

```prisma theme={null}
@@unique([organizationId, idempotencyKey])
```

**Ce que cela signifie :**

* Dans une organisation, chaque `idempotencyKey` ne peut être utilisée qu'une seule fois
* Toute tentative de créer une écriture comptable en double échoue avec une erreur de contrainte de base de données
* La deuxième requête avec la même clé est rejetée automatiquement

***

### Exigence API

**Toutes les opérations de mutation exigent l'en-tête `x-idempotency-key` :**

```http theme={null}
POST /savings
Headers:
  x-organization-id: org-abc123
  x-idempotency-key: unique-key-here
Body:
  { ... }
```

**Opérations exigeant une clé d'idempotence :**

* Enregistrement de dépôts (`POST /savings`)
* Enregistrement de retraits (`DELETE /savings/{id}`)
* Création de prêts (`POST /loans`)
* Enregistrement de paiements de prêt (`POST /loans/{id}/repay`)
* Enregistrement de dépenses (`POST /expenses`)
* Création d'actifs (`POST /assets`)
* Allocations de réserves (`POST /reserve-allocations`)
* Libérations de réserves (`POST /reserve-allocations/{id}/release`)
* Distributions de dividendes (`POST /dividends/pools`, `POST /dividends/pools/{id}/distribute`)
* Clôture de période (`POST /period-closing/close`, `POST /period-closing/undo`)

**Opérations n'exigeant PAS de clé d'idempotence :**

* Requêtes GET (lecture seule)
* Génération de rapports
* Consultation de données

***

### Validation de Requête

**Le backend valide la clé d'idempotence :**

```typescript theme={null}
if (!idempotencyKey?.trim()) {
  throw new BadRequestException('x-idempotency-key header is required');
}
```

**Clé manquante = erreur 400 Bad Request**

***

## Générer des Clés d'Idempotence

**Responsabilité du frontend** : Générer une clé unique pour chaque opération.

**Format recommandé :**

```typescript theme={null}
// Option 1: UUID v4
const idempotencyKey = crypto.randomUUID();
// Example: "550e8400-e29b-41d4-a716-446655440000"

// Option 2: Timestamp + Random
const idempotencyKey = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// Example: "1686567890123-k3j4h5g6f"

// Option 3: Operation-specific prefix
const idempotencyKey = `deposit-${userId}-${Date.now()}-${Math.random()}`;
// Example: "deposit-user123-1686567890123-0.123456789"
```

**Bonnes pratiques :**

* ✅ Utiliser UUID v4 (aléatoire cryptographiquement)
* ✅ Inclure un timestamp pour faciliter le débogage
* ✅ Stocker la clé dans l'état applicatif pendant la requête
* ✅ Réutiliser la même clé pour les relances de la MÊME requête
* ✅ Générer une nouvelle clé pour les nouvelles opérations
* ❌ Ne jamais réutiliser des clés entre opérations différentes
* ❌ Ne pas utiliser de numéros séquentiels (prévisibles)

***

## Portée de l'Idempotence

**Les clés sont portées à l'organisation :**

```
Organisation A :
  idempotencyKey: "abc123" → Dépôt 10 000 RWF ✓

Organisation B :
  idempotencyKey: "abc123" → Dépôt 5 000 RWF ✓
```

**Différentes organisations peuvent utiliser la même clé** (la contrainte unique inclut `organizationId`)

**Dans la même organisation :**

```
Organisation A :
  idempotencyKey: "abc123" → Dépôt 10 000 RWF ✓
  idempotencyKey: "abc123" → Dépôt 20 000 RWF ❌ DOUBLON
```

***

## Détection des Doublons

**Lorsqu'un doublon est détecté :**

**Requête 1 (première fois) :**

```http theme={null}
POST /savings
Headers:
  x-idempotency-key: abc123

Response: 200 OK
{
  "message": "Savings recorded successfully",
  "data": { ... }
}
```

**Requête 2 (doublon - même clé) :**

```http theme={null}
POST /savings
Headers:
  x-idempotency-key: abc123

Response: 400 Bad Request or 409 Conflict
{
  "message": "Duplicate transaction detected",
  "error": "A transaction with this idempotency key already exists"
}
```

**Erreur de base de données** (violation de contrainte unique) interceptée et retournée comme erreur API.

***

## Scénarios Courants

### Scénario 1 : Timeout Réseau

**Situation :**

1. L'utilisateur soumet le formulaire de dépôt
2. Timeout réseau (30 secondes, aucune réponse)
3. Le frontend relance avec la **même clé d'idempotence**

**Résultat :**

```
Requête 1 : idempotency-key = "xyz789"
  → Traitement... (réseau lent)
  → Succès ! Dépôt créé

Requête 2 (relance) : idempotency-key = "xyz789"
  → Clé en double détectée
  → Rejetée : "Transaction already exists"
  → Dépôt enregistré UNE FOIS ✓
```

**Gestion frontend :**

```typescript theme={null}
const idempotencyKey = generateKey();

try {
  await createDeposit({ idempotencyKey });
} catch (error) {
  if (error.status === 408 || isNetworkTimeout(error)) {
    // Retry with SAME key
    await createDeposit({ idempotencyKey });
  }
}
```

***

### Scénario 2 : Double-Clic Accidentel

**Situation :**

1. L'utilisateur double-clique sur le bouton "Enregistrer le Paiement"
2. Deux requêtes sont envoyées rapidement

**Implémentation frontend :**

```typescript theme={null}
let requestInProgress = false;
const idempotencyKey = generateKey();

async function handleSubmit() {
  if (requestInProgress) return; // Prevent second click

  requestInProgress = true;
  try {
    await recordPayment({ idempotencyKey });
  } finally {
    requestInProgress = false;
  }
}
```

**Même si les deux requêtes atteignent le backend :**

```
Requête 1 : idempotency-key = "abc123"
  → Traitement...
  → Succès ! Paiement créé

Requête 2 : idempotency-key = "abc123"
  → Clé en double détectée
  → Rejetée immédiatement
  → Paiement enregistré UNE FOIS ✓
```

***

### Scénario 3 : Relance d'une Requête Échouée

**Situation :**

1. La transaction échoue à cause d'une erreur de validation
2. L'utilisateur corrige l'erreur et soumet à nouveau

**Peut relancer avec la même clé si la transaction a échoué :**

```typescript theme={null}
const idempotencyKey = generateKey();

// First attempt - validation error
try {
  await createLoan({
    amount: -5000, // Invalid (negative amount)
    idempotencyKey
  });
} catch (error) {
  if (error.status === 400) {
    // Validation error - transaction NOT created
    // Can retry with same key after fixing
    await createLoan({
      amount: 5000, // Fixed
      idempotencyKey // Same key OK - first attempt failed
    });
  }
}
```

**Point clé** : La protection d'idempotence bloque uniquement les transactions **réussies**.

***

### Scénario 4 : Nouvelle Transaction

**Situation :**

1. L'utilisateur enregistre un dépôt avec succès
2. Il veut enregistrer un autre dépôt

**Une nouvelle clé est obligatoire :**

```typescript theme={null}
// First deposit
const key1 = generateKey(); // "abc123"
await createDeposit({ amount: 10000, idempotencyKey: key1 });

// Second deposit - MUST use new key
const key2 = generateKey(); // "def456"
await createDeposit({ amount: 20000, idempotencyKey: key2 });
```

**Utiliser la même clé serait rejeté :**

```typescript theme={null}
// Wrong - reusing key
await createDeposit({ amount: 10000, idempotencyKey: key1 }); // ✓
await createDeposit({ amount: 20000, idempotencyKey: key1 }); // ❌ DUPLICATE
```

***

## Gestion des Erreurs

### Clé d'Idempotence Manquante

**Requête sans clé :**

```http theme={null}
POST /savings
Headers:
  x-organization-id: org-abc123
  # Missing x-idempotency-key

Response: 400 Bad Request
{
  "message": "x-idempotency-key header is required"
}
```

**Le frontend doit inclure une clé pour toutes les mutations.**

***

### Erreur de Clé en Double

**Requête en double détectée :**

```http theme={null}
POST /savings
Headers:
  x-idempotency-key: already-used-key

Response: 400 Bad Request or 409 Conflict
{
  "message": "Duplicate transaction detected",
  "error": "Unique constraint violation on idempotencyKey"
}
```

**Que faire :**

1. Vérifier si la transaction originale a réussi
2. Si elle a réussi : ne pas relancer, afficher un message de succès
3. Si elle a échoué : relancer avec la même clé
4. Si vous créez une nouvelle transaction : générer une nouvelle clé

***

## Exemple d'Implémentation Frontend

**Gestion complète de l'idempotence :**

```typescript theme={null}
import { v4 as uuidv4 } from 'uuid';

class TransactionService {
  private pendingRequests = new Map<string, Promise<any>>();

  async createDeposit(data: DepositData) {
    // Generate idempotency key
    const idempotencyKey = uuidv4();

    // Prevent duplicate in-flight requests
    if (this.pendingRequests.has(idempotencyKey)) {
      return this.pendingRequests.get(idempotencyKey);
    }

    // Create request
    const request = fetch('/savings', {
      method: 'POST',
      headers: {
        'x-organization-id': organizationId,
        'x-idempotency-key': idempotencyKey,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    })
    .then(async (response) => {
      if (!response.ok) {
        const error = await response.json();

        // Check if duplicate
        if (error.message?.includes('duplicate') ||
            error.message?.includes('idempotency')) {
          // Duplicate detected - check if original succeeded
          const original = await this.checkTransactionStatus(idempotencyKey);
          if (original) {
            return original; // Return original transaction
          }
        }

        throw error;
      }

      return response.json();
    })
    .finally(() => {
      this.pendingRequests.delete(idempotencyKey);
    });

    // Store pending request
    this.pendingRequests.set(idempotencyKey, request);

    return request;
  }

  async checkTransactionStatus(idempotencyKey: string) {
    // Query backend for transaction with this key
    // Implementation depends on your API
  }
}
```

***

## Logique de Relance

**Stratégie de relance sûre :**

```typescript theme={null}
async function safeRetry(operation: () => Promise<any>, maxRetries = 3) {
  const idempotencyKey = generateKey();

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation(idempotencyKey);
    } catch (error) {
      // Network errors - retry with same key
      if (isNetworkError(error) && attempt < maxRetries) {
        await delay(1000 * attempt); // Exponential backoff
        continue;
      }

      // Validation errors - don't retry
      if (error.status === 400 && !isDuplicateError(error)) {
        throw error;
      }

      // Duplicate detected - check if original succeeded
      if (isDuplicateError(error)) {
        const original = await checkStatus(idempotencyKey);
        if (original) return original;
      }

      throw error;
    }
  }
}

// Usage
await safeRetry((key) =>
  createDeposit({ amount: 10000, idempotencyKey: key })
);
```

***

## Bonnes Pratiques

<Note>
  **Checklist d'implémentation de l'idempotence :**

  **Génération de clé :**

  * ✅ Utiliser UUID v4 (crypto.randomUUID())
  * ✅ Générer la clé dans le frontend avant la requête
  * ✅ Stocker la clé dans l'état du composant pendant la requête
  * ✅ Inclure la clé dans tous les appels API de mutation
  * ✅ Journaliser les clés pour le débogage (ne pas les exposer aux utilisateurs)

  **Gestion des relances :**

  * ✅ Réutiliser la même clé pour les timeouts réseau
  * ✅ Réutiliser la même clé pour les erreurs serveur 5xx
  * ✅ Générer une nouvelle clé pour les nouvelles opérations
  * ✅ Vérifier le statut de la transaction avant de relancer un doublon
  * ✅ Implémenter un backoff exponentiel pour les relances

  **Gestion des erreurs :**

  * ✅ Détecter les erreurs de doublon (400/409 avec message "idempotency")
  * ✅ Vérifier le statut de la transaction originale avant d'afficher une erreur
  * ✅ Afficher "Transaction déjà enregistrée" au lieu d'une erreur générique
  * ✅ Éviter la confusion utilisateur (ne pas dire "échec" si cela a réussi)

  **UI/UX :**

  * ✅ Désactiver le bouton de soumission pendant la requête (empêcher le double-clic)
  * ✅ Afficher un indicateur de chargement pendant le traitement
  * ✅ Vider le formulaire seulement après confirmation du succès
  * ✅ Gérer les timeouts réseau proprement (relance automatique)
  * ✅ Fournir des messages d'erreur clairs

  **Tests :**

  * ✅ Tester le rejet des clés en double
  * ✅ Tester la relance après timeout réseau
  * ✅ Tester les requêtes concurrentes avec la même clé
  * ✅ Tester l'unicité des clés entre opérations
</Note>

***

## Dépannage

**Q : Erreur "idempotency-key header is required"**

R : Ajoutez l'en-tête `x-idempotency-key` à la requête :

```typescript theme={null}
headers: {
  'x-idempotency-key': crypto.randomUUID()
}
```

Toutes les requêtes POST, PUT, DELETE pour les opérations financières exigent cet en-tête.

***

**Q : Erreur "duplicate transaction" immédiatement**

R : La clé d'idempotence a déjà été utilisée. Soit :

1. La transaction originale a réussi (vérifier l'historique des transactions)
2. La clé d'une opération précédente est réutilisée (générer une nouvelle clé)

**Solution :**

```typescript theme={null}
// Don't reuse keys across operations
const key1 = generateKey();
await createDeposit({ idempotencyKey: key1 }); // ✓

const key2 = generateKey(); // New key!
await createDeposit({ idempotencyKey: key2 }); // ✓
```

***

**Q : Besoin d'enregistrer des transactions identiques**

R : Utilisez des clés d'idempotence différentes :

```typescript theme={null}
// Two identical deposits - different keys
await createDeposit({
  member: "Jane",
  amount: 10000,
  idempotencyKey: generateKey() // Unique key 1
});

await createDeposit({
  member: "Jane",
  amount: 10000,
  idempotencyKey: generateKey() // Unique key 2
});
```

L'idempotence empêche les *requêtes* en double, pas les *transactions* en double.

***

**Q : La transaction a échoué mais je ne peux pas la soumettre à nouveau**

R : Si la transaction a échoué (non créée), vous pouvez relancer avec la **même clé** :

```typescript theme={null}
const key = generateKey();

try {
  await createLoan({ amount: 10000, idempotencyKey: key });
} catch (error) {
  if (isValidationError(error)) {
    // Fix validation issue, retry with same key
    await createLoan({ amount: 10000, idempotencyKey: key }); // OK
  }
}
```

***

**Q : Combien de temps les clés sont-elles valides ?**

R : Les clés d'idempotence sont permanentes. Une fois utilisées avec succès, elles ne peuvent pas être réutilisées dans cette organisation.

**Implication** : N'utilisez pas de clés prévisibles (numéros séquentiels, dates seules).

***

**Q : Puis-je utiliser la même clé dans différentes organisations ?**

R : Oui. La contrainte unique est `(organizationId, idempotencyKey)` :

```typescript theme={null}
// Organization A
await createDeposit({
  organization: "org-A",
  idempotencyKey: "abc123" // ✓
});

// Organization B - same key OK
await createDeposit({
  organization: "org-B",
  idempotencyKey: "abc123" // ✓ Different organization
});
```

***

## Sujets Connexes

<CardGroup cols={2}>
  <Card title="Enregistrement des Dépôts" icon="money-bill-wave" href="/fr/tontines/deposits/recording-deposits">
    API de dépôt avec idempotence
  </Card>

  <Card title="Création de Prêts" icon="hand-holding-dollar" href="/fr/tontines/loans/creating-loans">
    API de prêt avec idempotence
  </Card>

  <Card title="Clôture de Période" icon="lock" href="/fr/tontines/advanced/period-closing">
    Clôture de période avec idempotence
  </Card>

  <Card title="Piste d'Audit" icon="file-magnifying-glass" href="/fr/tontines/advanced/audit-trail">
    Suivre la prévention des doublons
  </Card>
</CardGroup>
