# Codes de réduction
Source: https://docs.lomi.africa/build/billing/discount-coupons

Créez, validez, cumulez et suivez les coupons selon les mêmes règles qu’au tunnel de paiement.

***

title: Codes de réduction
description: Créez, validez, cumulez et suivez les coupons selon les mêmes règles qu’au tunnel de paiement.
-----------------------------------------------------------------------------------------------------------

import { Tabs, Tab } from 'fumadocs-ui/components/tabs';

L’API Discount coupons assure une validation serveur stricte et un suivi d’usage pour les tunnels.

Cette page décrit les règles de validation et d’application des coupons utilisées au tunnel et par l’API, notamment :

* validation à la création ;
* validation au tunnel (client, périmètre, quantité, fréquence) ;
* application mono ou multi-coupon ;
* réservation d’usage dédupliquée et liaison transaction.

## Fonctionnement de la logique coupon (bout en bout)

En résumé :

1. Création via **`POST /coupons`** avec vos contraintes.
2. Validation au tunnel (règles serveur : client, périmètre, quantité, fréquence).
3. Calcul et application de la remise sur la session checkout, y compris cumul lorsque autorisé.
4. Réservation d’usage liée à la session jusqu’au paiement.
5. Liaison de la réservation à la transaction finale en cas de succès.
6. Incrément des compteurs d’usage lorsque la transaction passe en **`completed`**.

Cela aligne l’aperçu tableau de bord, le tunnel hébergé et les intégrations API.

## Créer un coupon de réduction

### Corps de la requête

| Champ                   | Type      | Obligatoire    | Description                                                 |
| ----------------------- | --------- | -------------- | ----------------------------------------------------------- |
| `code`                  | `string`  | **Oui**        | Code unique (passé en majuscules automatiquement)           |
| `discount_type`         | `string`  | Non            | `percentage` ou `fixed` (défaut : `percentage`)             |
| `discount_percentage`   | `number`  | Si pourcentage | Pourcentage (`> 0` et `<= 100`)                             |
| `discount_fixed_amount` | `number`  | Si fixe        | Montant fixe                                                |
| `description`           | `string`  | Non            | Description                                                 |
| `is_active`             | `boolean` | Non            | Actif (défaut : `true`)                                     |
| `max_uses`              | `number`  | Non            | Plafond total d’utilisations                                |
| `max_quantity_per_use`  | `number`  | Non            | Quantité max par utilisation                                |
| `valid_from`            | `string`  | Non            | Début (ISO 8601)                                            |
| `expires_at`            | `string`  | Non            | Fin (ISO 8601)                                              |
| `customer_type`         | `string`  | Non            | `all`, `new`, `returning`                                   |
| `usage_frequency_limit` | `string`  | Non            | `total`, `per_customer`, `per_day`, `per_week`, `per_month` |
| `usage_limit_value`     | `number`  | Conditionnel   | Requis si `usage_frequency_limit != total`                  |
| `scope_type`            | `string`  | Non            | `organization_wide`, `specific_products`, `specific_prices` |
| `product_ids`           | `array`   | Non            | IDs produits (si périmètre spécifique)                      |

### Règles à la création

À la création, l’API impose :

* unicité du code par organisation ;
* exclusion mutuelle pourcentage vs fixe ;
* `valid_from < expires_at` lorsque les deux existent ;
* `usage_limit_value` requis pour les modes autre que `total` ;
* pour `specific_products` / `specific_prices`, produits liés à la même organisation.

<Tabs items={["TypeScript", "Python", "cURL"]}>
  <Tab value="TypeScript">
    ```typescript
    import { LomiSDK } from '@lomi./sdk';

    const lomi = new LomiSDK({
      apiKey: process.env.LOMI_SECRET_KEY!,
      environment: 'live',
    });

    // Percentage discount
    const coupon = await lomi.coupons.create({
      code: 'SAVE20',
      discount_type: 'percentage',
      discount_percentage: 20,
      description: '20% off all products',
      max_uses: 100,
      expires_at: '2024-12-31T23:59:59Z',
    });

    // Fixed amount discount
    const fixedCoupon = await lomi.coupons.create({
      code: 'FLAT5000',
      discount_type: 'fixed',
      discount_fixed_amount: 5000,
      description: '5000 XOF off',
      customer_type: 'new',
    });

    console.log(`Coupon created: ${coupon.code}`);
    ```
  </Tab>

  <Tab value="Python">
    ```python
    from lomi import LomiClient
    import os

    client = LomiClient(
        api_key=os.environ["LOMI_SECRET_KEY"],
        environment="test"
    )

    coupon = client.discount_coupons.create({
        "code": "SAVE20",
        "discount_type": "percentage",
        "discount_percentage": 20,
        "description": "20% off all products",
        "max_uses": 100,
        "expires_at": "2024-12-31T23:59:59Z"
    })

    print(f"Coupon created: {coupon['code']}")
    ```
  </Tab>

  <Tab value="cURL">
    ```bash
    curl -X POST "https://api.lomi.africa/coupons" \
      -H "X-API-KEY: $LOMI_SECRET_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "code": "SAVE20",
        "discount_type": "percentage",
        "discount_percentage": 20,
        "description": "20% off all products",
        "max_uses": 100,
        "expires_at": "2024-12-31T23:59:59Z"
      }'
    ```
  </Tab>
</Tabs>

***

## Validation au tunnel

La validation combine contrôles structurels et métier. Un coupon doit satisfaire tous les points suivants :

1. **Existe et actif** pour la même organisation.
2. **Fenêtre temporelle :** `valid_from` démarrée et `expires_at` non dépassée.
3. **Plafond global :** `current_uses < max_uses` (si `max_uses`).
4. **Fréquence** client (si activée) : par client/jour/semaine/mois.
5. **Quantité :** quantité du tunnel ≤ `max_quantity_per_use`.
6. **Éligibilité client :**
   * `new` : aucune transaction de paiement ou d’échéance complétée dans l’org ;
   * `returning` : au moins une telle transaction ;
   * `all` : sans restriction d’historique.
7. **Périmètre :**
   * `organization_wide` : tous les produits ;
   * `specific_products` / `specific_prices` : liaison dans `coupon_product_links`.

En cas d’échec, l’API renvoie un message explicite.

## Calcul de la réduction

`calculate_coupon_discount` applique la remise sur le montant de base hors frais optionnels :

* `base_price = p_base_amount - p_fees_amount` ;
* coupon pourcentage : `discount = base_price * percentage` ;
* coupon fixe : `discount = fixed_amount` ;
* multiplication par quantité si pertinent ;
* plafonnement pour ne pas dépasser la base éligible ;
* montant final `(base_price - discount) + frais`.

Cela évite totaux négatifs et sur-remises.

## Cumul multi-coupons (séquentiel)

Les coupons s’appliquent dans l’ordre sur un montant courant :

* A sur le montant initial ;
* B sur le montant après A ;

C’est un **cumul séquentiel**, pas une somme parallèle. La réponse détaille pour chaque coupon le montant d’origine, la remise et le résultat.

Si un coupon du tableau échoue à la validation, tout le calcul multi-coupon échoue.

## Suivi d’usage et déduplication

L’usage est enregistré dans `coupon_usage` avec garde-fous contre les réservations en double :

* réservation unique par `(coupon_id, checkout_session_id)` tant que `transaction_id IS NULL` ;
* nettoyage des doublons obsolètes avant l’index unique partiel ;
* à paiement terminé, liaison à la transaction (sans doublon) ;
* sinon insertion directe liée à la transaction.

Comportement attendu pour nouvelles tentatives prestataire et courses entre webhooks.

## Lister les coupons

Récupère tous les coupons de réduction de votre organisation.

<Tabs items={["TypeScript", "Python", "cURL"]}>
  <Tab value="TypeScript">
    ```typescript
    const coupons = await lomi.coupons.list();
    ```
  </Tab>

  <Tab value="Python">
    ```python
    coupons = client.discount_coupons.list()
    ```
  </Tab>

  <Tab value="cURL">
    ```bash
    curl -X GET "https://api.lomi.africa/coupons" \
      -H "X-API-KEY: $LOMI_SECRET_KEY"
    ```
  </Tab>
</Tabs>

***

## Obtenir un coupon

Récupère le détail d’un coupon.

<Tabs items={["TypeScript", "Python", "cURL"]}>
  <Tab value="TypeScript">
    ```typescript
    const coupon = await lomi.coupons.get('dc_abc123...');
    ```
  </Tab>

  <Tab value="Python">
    ```python
    coupon = client.discount_coupons.get('dc_abc123...')
    ```
  </Tab>

  <Tab value="cURL">
    ```bash
    curl -X GET "https://api.lomi.africa/coupons/dc_abc123..." \
      -H "X-API-KEY: $LOMI_SECRET_KEY"
    ```
  </Tab>
</Tabs>

***

## Indicateurs de performance d’un coupon

Statistiques d’usage et impact chiffre d’affaires pour un coupon (transactions `completed` uniquement).

<Tabs items={["TypeScript", "Python", "cURL"]}>
  <Tab value="TypeScript">
    ```typescript
    const performance = await lomi.coupons.getPerformance('dc_abc123...');

    console.log(`Total uses: ${performance.total_uses}`);
    console.log(`Total discounted: ${performance.total_discount_amount}`);
    console.log(`Revenue generated: ${performance.total_revenue}`);
    console.log(`Avg order value: ${performance.average_order_value}`);
    ```
  </Tab>

  <Tab value="Python">
    ```python
    performance = client.discount_coupons.get_performance('dc_abc123...')
    print(f"Total uses: {performance['total_uses']}")
    ```
  </Tab>

  <Tab value="cURL">
    ```bash
    curl -X GET "https://api.lomi.africa/coupons/dc_abc123.../performance" \
      -H "X-API-KEY: $LOMI_SECRET_KEY"
    ```
  </Tab>
</Tabs>

### Réponse

```json
{
  "total_uses": 45,
  "total_discounts": 25000,
  "total_revenue": 150000,
  "average_discount": 555.56,
  "unique_customers": 38
}
```

***

## Objet Discount Coupon

| Champ                          | Type      | Description                                                 |
| ------------------------------ | --------- | ----------------------------------------------------------- |
| `id`                           | `string`  | Identifiant unique                                          |
| `code`                         | `string`  | Code                                                        |
| `discount_type`                | `string`  | `percentage` ou `fixed`                                     |
| `discount_percentage`          | `number`  | Valeur pourcentage                                          |
| `discount_fixed_amount`        | `number`  | Montant fixe                                                |
| `customer_type`                | `string`  | `all`, `new`, `returning`                                   |
| `usage_frequency_limit`        | `string`  | `total`, `per_customer`, `per_day`, `per_week`, `per_month` |
| `usage_limit_value`            | `number`  | Plafond de fréquence le cas échéant                         |
| `is_active`                    | `boolean` | Actif                                                       |
| `max_uses`                     | `number`  | Maximum d’utilisations                                      |
| `current_uses`                 | `number`  | Utilisations courantes                                      |
| `max_quantity_per_use`         | `number`  | Quantité max par utilisation                                |
| `valid_from`                   | `string`  | Début de validité                                           |
| `expires_at`                   | `string`  | Expiration                                                  |
| `scope_type`                   | `string`  | Périmètre                                                   |
| `product_links`                | `array`   | Produits liés si périmètre spécifique                       |
| `completed_redemptions`        | `number`  | Utilisations terminées                                      |
| `distinct_customers_completed` | `number`  | Clients distincts avec utilisations complétées              |
| `created_at`                   | `string`  | Création                                                    |

***

## Modèles d’implémentation courants

### Valider avant d’appliquer

1. valider le coupon (endpoint de pré-validation),
2. afficher un aperçu de la remise,
3. appliquer à la création ou confirmation du tunnel.

### Gérer les relances prestataires

Les callbacks peuvent être renvoyés. La réservation / liaison coupon est pensée pour être idempotente par paire session + coupon.

### Fournir le client quand c’est possible

Pour les coupons `new` et `returning`, envoyez `customer_id` pour une évaluation d’éligibilité correcte.

***

## Réponses d’erreur

| Statut | Description                       |
| ------ | --------------------------------- |
| `400`  | Entrée invalide ou code en double |
| `401`  | Clé API invalide ou manquante     |
| `404`  | Coupon introuvable                |

## Exemples concrets

### Coupon réservé aux nouveaux clients

Configuration :

* `customer_type = new`
* `scope_type = organization_wide`
* `discount_type = percentage`
* `discount_percentage = 20`

Comportement :

* Client sans transaction de paiement / échéance complétée dans l’organisation : coupon valide.
* Client ayant au moins une transaction de paiement / échéance complétée : coupon refusé (non éligible).
* Sans contexte client, les contrôles d’éligibilité pour les types restreints peuvent échouer.

### Coupon réservé aux clients existants

Configuration :

* `customer_type = returning`
* `usage_frequency_limit = per_month`
* `usage_limit_value = 1`

Comportement :

* Un client existant peut l’utiliser une fois par mois.
* Une deuxième utilisation le même mois est refusée.
* Un nouveau mois autorise à nouveau une utilisation (sous réserve des autres limites).

### Coupon limité à des produits

Configuration :

* `scope_type = specific_products`
* `product_ids = [Produit A, Produit B]`

Comportement :

* Tunnel pour A ou B : le périmètre passe.
* Tunnel pour C : refusé (`coupon is not applicable to this product`).
* Les coupons organisation-wide ignorent les liens produit.

### Coupon plafonné en quantité

Configuration :

* `max_quantity_per_use = 2`

Comportement :

* Quantité `1` ou `2` : valide.
* Quantité `3+` : refusée.

### Remise fixe plafonnée au montant de base

Tunnel :

* Montant de base : `3 000`
* Frais : `500`
* Prix éligible : `2 500`

Coupon :

* `discount_type = fixed`
* `discount_fixed_amount = 5 000`

Résultat :

* La remise est plafonnée à `2 500`.
* Montant final : `(2 500 - 2 500) + 500 = 500`.

### Cumul séquentiel (deux coupons)

Montant : `10 000`

Ordre :

1. `SAVE20` (20 %)
2. `FLAT1000` (fixe 1 000)

Calcul :

* Après `SAVE20` : remise `2 000`, montant `8 000`
* Après `FLAT1000` : remise `1 000`, montant `7 000`
* Remise totale : `3 000`

Le second coupon s’applique au montant déjà réduit, pas au montant d’origine.

### Réservation en attente et liaison transaction

1. Le coupon est appliqué à la session checkout.
2. Une réservation `coupon_usage` en attente est enregistrée pour `(coupon_id, checkout_session_id)`.
3. Le callback prestataire crée / finalise la transaction.
4. La réservation est liée à la transaction.

Cela évite les lignes d’usage en double lors des relances webhook, et garde le reporting exact.

### Relances sûres

Si le même événement prestataire est relancé :

* la réservation en attente est mise à jour / liée quand c’est possible,
* les insertions en double sont empêchées par l’unicité et la gestion des conflits,
* le comptage d’usage reste cohérent quand la finalisation de transaction s’exécute.

### Ordre de cumul et total

Les mêmes deux coupons, ordre inversé :

1. `FLAT1000` d’abord : remise `1 000`, montant `9 000`
2. `SAVE20` ensuite : 20 % de `9 000` = `1 800`, montant `7 200`
3. **Remise totale : `2 800`** (pas `3 000`)

Affichez l’ordre appliqué dans l’UI et conservez le même ordre côté serveur.

### Fenêtre glissante vs calendrier

Selon le chemin de validation, les limites peuvent être :

* une fenêtre **glissante** (par exemple les 24 dernières heures pour `per_day`) ;
* des bornes **calendaires** pour `per_day` / `per_week` / `per_month`.

Pour un coupon « une fois par jour », un client à 23:59 peut ou non l’utiliser à 00:01. **Traitez la limite comme « au plus N utilisations sur la période configurée »** et testez en sandbox.

### Réappliquer un coupon sur la même session

Si la session a déjà une ligne `coupon_usage` en attente, le système peut **mettre à jour** cette ligne au lieu d’en insérer une autre, tant que la transaction n’est pas finalisée.

Après **paiement réussi**, l’usage repose sur la transaction.

### Tunnels gratuits ou entièrement remisés

Quand la remise ramène le payable à **zéro**, la plateforme peut enregistrer un parcours **gratuit** pour garder grand livre et webhooks cohérents. Les rapports doivent toujours montrer **prix catalogue**, **remise** et **net**.

### Relances côté client

Votre app relance `POST /checkout-sessions` ou l’application de coupon à cause d’un réseau instable :

* Utilisez la **même clé d’idempotence** ou le même id de session pour ne pas créer de sessions parallèles.
* Côté serveur, les webhooks de finalisation en double ne doivent pas incrémenter l’usage deux fois si la complétion de transaction est idempotente ; évitez tout de même les applications **initiées par le client** en double.

### Notes d’intégration

* Validez toujours avant l’application finale.
* Envoyez le contexte client pour les coupons `new` / `returning`.
* Rendez l’application de coupon idempotente côté client.
* Pour plusieurs coupons, conservez l’ordre prévu.
