PHPackages                             vnuswilliams/subscription-kpay - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. vnuswilliams/subscription-kpay

ActiveLibrary[Payment Processing](/categories/payments)

vnuswilliams/subscription-kpay
==============================

Driver de paiement KPay (Mobile Money &amp; Cartes) pour vnuswilliams/laravel-subscription

1.1.2(1mo ago)020↓83.3%MITPHPPHP ^8.2

Since Jul 4Pushed 1mo agoCompare

[ Source](https://github.com/vnuswilliams/subscription-kpay)[ Packagist](https://packagist.org/packages/vnuswilliams/subscription-kpay)[ RSS](/packages/vnuswilliams-subscription-kpay/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (6)Versions (8)Used By (0)

Laravel Subscription — KPay Driver
==================================

[](#laravel-subscription--kpay-driver)

[![Latest Version on Packagist](https://camo.githubusercontent.com/84102ccc9164a1a49f40ff9ad06b3577bdc828e3c41d56b646b79cf80ced8e89/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f766e757377696c6c69616d732f737562736372697074696f6e2d6b7061792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/vnuswilliams/subscription-kpay)[![Total Downloads](https://camo.githubusercontent.com/2187a68ad14de34f2aacc54c6a600b8090882b3f07cf06d3a19ddaf40adde148/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f766e757377696c6c69616d732f737562736372697074696f6e2d6b7061792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/vnuswilliams/subscription-kpay)[![Software License](https://camo.githubusercontent.com/55c0218c8f8009f06ad4ddae837ddd05301481fcf0dff8e0ed9dadda8780713e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d627269676874677265656e2e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)

Intégration [KPay](https://kpay.site/documentation) (Mobile Money &amp; Cartes) pour [`vnuswilliams/laravel-subscription`](https://github.com/vnuswilliams/laravel-subscription).

Ce package est un **satellite de paiement**, entièrement découplé du cœur du système d'abonnement. Il écoute les événements émis par `laravel-subscription`, déclenche le paiement via l'API KPay, et confirme ou suspend la souscription en fonction du résultat — **sans jamais modifier la structure ou le comportement du package core**.

---

Sommaire
--------

[](#sommaire)

1. [Philosophie &amp; Architecture](#philosophie--architecture)
2. [Prérequis](#pr%C3%A9requis)
3. [Installation](#installation)
4. [Configuration](#configuration)
5. [Préparation des Modèles](#pr%C3%A9paration-des-mod%C3%A8les)
6. [Flux de paiement (bout en bout)](#flux-de-paiement-bout-en-bout)
7. [Configuration du Webhook KPay](#configuration-du-webhook-kpay)
8. [Flux de retour (mode Gateway)](#flux-de-retour-mode-gateway)
9. [Utilisation de l'API](#utilisation-de-lapi)
10. [Middleware](#middleware)
11. [Événements (Laravel Events)](#%C3%A9v%C3%A9nements-laravel-events)
12. [Base de données](#base-de-donn%C3%A9es)
13. [Notes d'architecture importantes](#notes-darchitecture-importantes)
14. [Licence](#licence)

---

Philosophie &amp; Architecture
------------------------------

[](#philosophie--architecture)

`laravel-subscription` reste **totalement agnostique** au paiement : `subscribeTo()` crée une souscription **active immédiatement**, exactement comme si aucun package de paiement n'était installé. `subscription-kpay` n'ajoute **aucune colonne**, **aucun statut d'enum**, **aucune méthode** au core. Il se contente d'observer et de réagir, en s'appuyant uniquement sur l'API publique déjà exposée par le core (notamment `suppress()`).

```
┌─────────────────────────┐        SubscriptionCreated        ┌──────────────────────────┐
│   laravel-subscription  │ ─────────────────────────────────▶ │    subscription-kpay     │
│   (agnostique, inchangé)│                                     │   (écoute & réagit)      │
└─────────────────────────┘                                     └──────────────────────────┘
                                                                            │
                                                                            ▼
                                                              Initie le paiement KPay
                                                              (montant = subscription->price)
                                                                            │
                            ┌───────────────────────────────────────────────┼───────────────────────────────────────────────┐
                            ▼                                               ▼                                               ▼
                  Webhook KPay : payment.completed              Webhook KPay : payment.failed                 Webhook KPay : payment.cancelled
                  → transaction → success                       → transaction → failed                        → transaction → cancelled
                  → paid_at renseigné                            → subscription->suppress()                    → subscription->suppress()
                  → event KPayPaymentCompleted                   → event KPayPaymentFailed                     → event KPayPaymentCancelled

```

**Principe directeur : c'est à KPay de s'adapter au core, jamais l'inverse.** Si demain un autre moyen de paiement doit être intégré (Stripe, PayPal...), il suivra exactement le même schéma : son propre package, sa propre table, ses propres events — sans jamais toucher à `laravel-subscription`.

---

Prérequis
---------

[](#prérequis)

- PHP `^8.2`
- Laravel `^11.0 | ^12.0 | ^13.0`
- `vnuswilliams/laravel-subscription` installé et configuré, avec les colonnes `price` présentes sur les tables `plans` et `subscriptions` (voir son [README](https://github.com/vnuswilliams/laravel-subscription))
- Un compte marchand KPay actif ([documentation officielle](https://kpay.site/documentation))

---

Installation
------------

[](#installation)

```
composer require vnuswilliams/subscription-kpay
```

Publiez le fichier de configuration :

```
php artisan kpay:publish-config
```

La commande `kpay:publish-config` est interactive : si un fichier `config/kpay.php` existe déjà, elle vous demandera confirmation avant de l'écraser. Si vous préférez la méthode non interactive, la commande legacy `vendor:publish` reste disponible :

```
php artisan vendor:publish --provider="Vnuswilliams\SubscriptionKpay\SubscriptionKpayServiceProvider" --tag=kpay-config --force
```

Si la nouvelle commande artisan n'est pas trouvée après installation, exécutez :

```
composer dump-autoload
```

Exécutez les migrations. Le package crée **une seule table**, `kpay_transactions`, sans toucher au schéma du core :

```
php artisan migrate
```

---

Configuration
-------------

[](#configuration)

Ajoutez vos clés KPay dans le fichier `.env` de votre application :

```
KPAY_BASE_URL=https://admin.kpay.site
KPAY_API_KEY=votre_api_key
KPAY_SECRET_KEY=votre_secret_key
KPAY_WEBHOOK_SECRET=votre_webhook_secret
KPAY_RETURN_SECRET=votre_return_secret

KPAY_CURRENCY=XAF
KPAY_DEFAULT_MODE=gateway
KPAY_MIN_AMOUNT=50

KPAY_RETURN_URL=https://votre-app.test/paiement/retour
KPAY_CANCEL_URL=https://votre-app.test/paiement/annule
```

> **Note sur `KPAY_CURRENCY`** : l'API KPay ne prend pas de paramètre `currency` à l'initiation du paiement — la devise réelle est déduite automatiquement par KPay selon l'opérateur/le pays (`XAF`, `XOF`, `KES`, `ZMW`...). `KPAY_CURRENCY` sert donc uniquement de valeur d'affichage et de stockage par défaut dans `kpay_transactions.currency`, pas à un appel API. La valeur par défaut est `XAF`, cohérente avec la zone FCFA/Cameroun.

Le fichier `config/kpay.php` publié expose tous ces paramètres, plus :

CléDescriptionDéfaut`timeout`Timeout HTTP (secondes) des appels à l'API KPay`10``webhook_route_prefix`Chemin de la route webhook`kpay/webhook``return_route_prefix`Chemin de la route de retour (mode gateway)`kpay/return``payment_pending_route`Route de redirection si paiement non confirmé (contexte web)`home``min_amount`Montant minimum accepté par KPay (zone Cameroun), validé avant tout appel API`50`---

Préparation des Modèles
-----------------------

[](#préparation-des-modèles)

Le modèle souscripteur (`Company`, `User`, `Team`, etc.) doit déjà utiliser `HasSubscriptions` du core. Ajoutez simplement `HasKPayBilling` par-dessus :

```
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Vnuswilliams\Subscription\Traits\HasSubscriptions;
use Vnuswilliams\SubscriptionKpay\Traits\HasKPayBilling;

class Company extends Model
{
    use HasSubscriptions;
    use HasKPayBilling;
}
```

`HasKPayBilling` reste un **proxy fin** — aucune logique métier n'y vit, tout est délégué à `KPayBillingService`, conformément au pattern déjà en place dans le core.

---

Flux de paiement (bout en bout)
-------------------------------

[](#flux-de-paiement-bout-en-bout)

1. **Souscription** — `$company->subscribeTo($plan)` crée la souscription côté core (statut `active`, comportement inchangé) et émet `SubscriptionCreated`.
2. **Déclenchement** — Le listener `InitiateKPayPaymentOnSubscriptionCreated` intercepte l'événement :
    - Si `subscription->price price` est inférieur à `config('kpay.min_amount')`, l'appel est rejeté avant tout envoi à l'API (log + pas de transaction créée).
    - Sinon, `KPayBillingService::initiatePayment()` appelle l'API KPay avec le montant exact de `subscription->price` (prix figé au moment de la souscription, indépendant du prix courant du plan) et crée une ligne `kpay_transactions` en statut `pending`, avec `external_id` = ID de la souscription.
3. **Paiement** — L'utilisateur finalise le paiement côté KPay (USSD ou page hébergée selon `KPAY_DEFAULT_MODE`).
4. **Webhook** — KPay notifie votre application de manière asynchrone (source d'autorité) :
    - **`payment.completed`** → la transaction passe à `success`, `paid_at` est renseigné, l'événement `KPayPaymentCompleted` est émis. La souscription reste telle quelle côté core.
    - **`payment.failed`** → la transaction passe à `failed`, l'événement `KPayPaymentFailed` est émis, puis `subscription->suppress()` est appelé pour couper l'accès immédiatement.
    - **`payment.cancelled`** → la transaction passe à `cancelled` (statut distinct, pas confondu avec `failed`), l'événement `KPayPaymentCancelled` est émis, puis `subscription->suppress()` est appelé.

> **Pas de suppression en base.** À la différence d'une première version de cette architecture, ni la souscription ni la transaction ne sont supprimées en cas d'échec/annulation : on utilise `suppress()` (déjà fourni par le core) qui coupe l'accès sans détruire les enregistrements. L'historique complet reste disponible dans `kpay_transactions` pour l'audit, le support et le rapprochement comptable.

> **Pas de tâche planifiée.** Ce package ne fait aucune hypothèse sur un scheduler : toute la logique de confirmation/suspension repose exclusivement sur la réception du webhook.

---

Configuration du Webhook KPay
-----------------------------

[](#configuration-du-webhook-kpay)

Dans votre dashboard KPay, configurez l'URL de callback vers :

```
https://votre-app.test/kpay/webhook

```

La route est enregistrée automatiquement par le package et exemptée de la protection CSRF. La signature de chaque requête entrante est vérifiée via HMAC-SHA256 (en-tête `X-KPAY-Signature`, comparaison à temps constant) avant tout traitement — toute signature invalide renvoie `401` sans effet de bord.

---

Flux de retour (mode Gateway)
-----------------------------

[](#flux-de-retour-mode-gateway)

En mode `gateway`, après avoir finalisé (ou annulé) le paiement sur la page hébergée KPay, le client est redirigé vers `KPAY_RETURN_URL` avec des paramètres de requête signés :

```
https://votre-app.test/paiement/retour?status=COMPLETED&reference=...&externalId=...&ts=...&sig=...

```

Cette signature est **distincte** de celle du webhook (secret `KPAY_RETURN_SECRET`, format `status|reference|externalId|ts`) et comporte une fenêtre anti-replay de 10 minutes basée sur `ts`.

Comportement de la route `kpay/return` fournie par le package :

1. Vérifie la signature et la fraîcheur du timestamp (`401` si invalide ou expiré).
2. Effectue un `GET /api/v1/payments/{id}` auprès de KPay pour confirmer l'état réel du paiement — **le contenu de l'URL de retour n'est jamais considéré comme fiable à lui seul**, seul le webhook (ou cette vérification active) fait foi.
3. Affiche une page adaptée selon le résultat :
    - Paiement déjà confirmé par le webhook → page de succès.
    - Paiement encore `pending`/`processing` → page d'attente (le webhook n'est pas encore arrivé), redirection possible vers `config('kpay.payment_pending_route')`.
    - Paiement `failed`/`cancelled` → page d'échec/annulation.

Le webhook reste dans tous les cas la seule source qui déclenche les changements d'état en base ; la route de retour ne fait qu'informer l'utilisateur.

---

Utilisation de l'API
--------------------

[](#utilisation-de-lapi)

### Vérifier si la souscription active est payée

[](#vérifier-si-la-souscription-active-est-payée)

```
if ($company->isCurrentSubscriptionPaid()) {
    // Le paiement KPay a été confirmé
}
```

### Consulter l'historique des transactions

[](#consulter-lhistorique-des-transactions)

```
$transactions = $company->kpayTransactions();

foreach ($transactions as $transaction) {
    echo $transaction->status->value; // pending | processing | success | failed | cancelled | expired
}
```

### Récupérer la dernière transaction de la souscription active

[](#récupérer-la-dernière-transaction-de-la-souscription-active)

```
$latest = $company->latestKPayTransaction();
```

---

Middleware
----------

[](#middleware)

Le paiement étant confirmé de façon asynchrone (webhook), il existe une fenêtre entre la création de la souscription (déjà `active` côté core) et la confirmation KPay. Pour bloquer l'accès pendant cette fenêtre, empilez `kpay.paid` par-dessus le middleware `subscribed` du core :

```
Route::middleware(['subscribed', 'kpay.paid'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
});
```

Comportement :

- Requête JSON → `402 Payment Required`
- Requête web → redirection vers `config('kpay.payment_pending_route')` avec un message flash `error`

> Une fois qu'un paiement échoue ou est annulé, la souscription est **suspendue** via `suppress()` par le webhook — le middleware `subscribed` seul suffit alors à couper l'accès (il s'appuie sur `hasAccess()`, qui tient compte de la suspension). `kpay.paid` sert uniquement à la fenêtre d'attente de confirmation, avant tout webhook.

---

Événements (Laravel Events)
---------------------------

[](#événements-laravel-events)

ÉvénementDéclenché quandPayload`KPayPaymentInitiated`Le paiement vient d'être créé côté KPay`KPayTransaction $transaction``KPayPaymentCompleted`Le webhook confirme un paiement réussi`KPayTransaction $transaction``KPayPaymentFailed`Le webhook signale un échec`KPayTransaction $transaction`, `array $payload``KPayPaymentCancelled`Le webhook signale une annulation côté client`KPayTransaction $transaction`, `array $payload`Exemple d'utilisation dans votre `EventServiceProvider` :

```
use Vnuswilliams\SubscriptionKpay\Events\KPayPaymentFailed;

Event::listen(KPayPaymentFailed::class, function (KPayPaymentFailed $event) {
    Notification::route('mail', $event->payload['customer_email'] ?? null)
        ->notify(new PaymentFailedNotification());
});
```

---

Base de données
---------------

[](#base-de-données)

Le package crée une seule table, indépendante du schéma du core :

### `kpay_transactions`

[](#kpay_transactions)

ColonneTypeDescription`subscription_id``unsignedBigInteger`Référence applicative vers `subscriptions` (pas de contrainte FK — voir notes d'architecture)`external_id``string`, uniqueIdentifiant envoyé à KPay lors de l'initiation (= ID de la souscription), utilisé pour le rapprochement et l'idempotence côté init`kpay_payment_id``string`, uniqueIdentifiant du paiement côté KPay (`id` retourné à l'initiation)`kpay_reference``string`, nullableRéférence propre à KPay (ex: `KPAY-20260514-ABC123`), utile pour le support et le rapprochement comptable`amount``unsignedBigInteger`Montant, copié depuis `subscription->price` au moment de l'initiation`currency``string(3)`Devise déduite/affichée (`XAF` par défaut)`status``string``pending` | `processing` | `success` | `failed` | `cancelled` | `expired``raw_payload``json`, nullableRéponse brute de l'API / webhook, pour audit`paid_at``timestamp`, nullableRenseigné à la confirmation du paiement---

Notes d'architecture importantes
--------------------------------

[](#notes-darchitecture-importantes)

- **Aucune dépendance de schéma dure** : `subscription_id` n'est pas une clé étrangère contrainte, car le nom de la table `subscriptions` est configurable côté core (`config('subscriptions.table_names.subscriptions')`). La cohérence est garantie applicativement, pas au niveau base de données.
- **Prix figé (snapshot)** : le montant facturé provient toujours de `subscription->price`, jamais de `plan->price`. Un changement de tarif sur un plan n'affecte donc jamais les souscriptions déjà créées.
- **Échec/annulation = suspension, pas de suppression** : par choix assumé, une souscription dont le paiement échoue ou est annulé est **suspendue** via `suppress()` (API publique du core), et sa transaction conserve son statut final (`failed` ou `cancelled`) — aucune donnée n'est détruite, l'historique complet reste disponible pour l'audit et le support.
- **Statuts distincts pour échec et annulation** : `failed` (paiement refusé/erreur) et `cancelled` (abandon volontaire côté client) sont deux statuts et deux événements séparés, pour ne pas mélanger deux causes différentes dans les rapports/notifications.
- **Idempotence du webhook** : une transaction déjà `success`, `failed` ou `cancelled` ignore tout nouvel appel webhook — évite les doubles traitements en cas de retry côté KPay.
- **Plans gratuits ignorés** : `subscription->price
