# Écrire un module de paiement

Un moyen de paiement est un module qui se déclare auprès de la
boutique. Le module commerce n'en connaît aucun : désactiver un module
de paiement le retire du tunnel sans qu'un fichier de commerce change.

## Structure minimale

```
modules/paiement-xxx/
    module.php          requires: ['commerce']
    shop-payment.php    la déclaration
    XxxClient.php       appels au prestataire
    Controllers/        réception des notifications
    routes/site.php     adresse de notification
    README.md           où trouver les clés
```

## La déclaration

```php
PaymentGateways::register('xxx', [
    'label'       => 'Nom affiché au client',
    'description' => 'Une ligne sous le nom',
    'icon'        => 'bi-credit-card',

    // Affichée dans les réglages avec un bouton de copie
    'webhook' => '/paiement/xxx/notification',

    // Décrits, pas dessinés : le back-office construit le formulaire
    'settings' => [
        'secret_key' => [
            'type'     => 'text',
            'label'    => 'Clé secrète',
            'required' => true,
            'help'     => 'Où la trouver.',
        ],
    ],

    // Facultatif : masquer selon le montant ou le pays
    'available' => fn(array $ctx, array $config) => $ctx['total'] >= 50,

    // Lancer le paiement
    'start' => function (array $order, array $config): array {
        return ['ok' => true, 'redirect' => 'https://…'];
    },

    // Rembourser
    'refund' => function (array $order, array $config, ?int $amount) {
        return ['ok' => true, 'reference' => '…'];
    },
]);
```

Une passerelle dont un réglage obligatoire est vide **n'apparaît pas**
dans le tunnel : une clé manquante ne doit pas se découvrir au moment
de payer.

## Enregistrer le règlement

C'est le point essentiel. Ne changez **jamais** le statut d'une
commande directement : enregistrez un mouvement au registre, l'état de
paiement s'en déduit.

```php
require_once dirname(__DIR__, 2) . '/commerce/Models/PaymentModel.php';

PaymentModel::record($orderId, [
    'amount'      => 1000,        // centimes, positif
    'fee'         => 59,          // commission du prestataire
    'method'      => 'xxx',
    'kind'        => 'payment',   // payment | refund | chargeback
    'external_id' => 'tx_abc123', // identifiant chez le prestataire
    'occurred_at' => date('Y-m-d H:i:s'),
]);
```

`external_id` est **unique** : une notification rejouée ne crée pas de
double règlement, elle est simplement ignorée.

Un remboursement s'enregistre avec `kind => 'refund'` et un montant
positif — le signe est géré.

## Trois règles de sécurité

**Vérifiez la signature** avant tout traitement. Sans elle, n'importe
qui peut appeler l'adresse de notification et déclarer une commande
payée.

**Vérifiez le montant** reçu contre le total de la commande. Une
session créée pour une commande pourrait être payée pour un autre
montant si la requête a été manipulée.

**Répondez 500 en cas d'échec**, jamais 200. Un succès annoncé sur un
échec fait perdre définitivement la notification.

## La route de notification

```php
[
    'path'       => '/paiement/xxx/notification',
    'method'     => 'POST',
    'action'     => 'XxxController@webhook',

    // Vide : l'appel vient du prestataire, pas d'un navigateur. Il n'a
    // ni session ni jeton. La protection repose sur la signature.
    'middleware' => [],
]
```

## Le rapprochement bancaire

Si le prestataire regroupe les paiements en versements, renseignez
`payout_id` et `payout_at` quand le versement part. C'est ce qui permet
de retrouver les commandes derrière un virement sur le relevé.

## Exemples

`modules/paiement-stripe/` et `modules/paiement-paypal/` sont complets
et commentés. Leurs `README.md` expliquent où trouver chaque clé.
