PHPackages                             gardi/dcb-kit - 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. gardi/dcb-kit

ActiveLibrary[Payment Processing](/categories/payments)

gardi/dcb-kit
=============

A framework-agnostic toolkit for direct carrier billing (DCB): a carrier-gateway interface, normalized callback events, idempotent charging and signature verification.

v0.4.0(today)01↑2900%MITPHPPHP ^8.2CI passing

Since Jun 26Pushed todayCompare

[ Source](https://github.com/NarimanGardi/dcb-kit)[ Packagist](https://packagist.org/packages/gardi/dcb-kit)[ RSS](/packages/gardi-dcb-kit/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (1)Versions (4)Used By (0)

dcb-kit
=======

[](#dcb-kit)

A small, framework-agnostic toolkit for **direct carrier billing (DCB)** in PHP — the part that's the same across every carrier: one gateway interface, **normalized callback events**, idempotent charging, and signature verification. Zero runtime dependencies.

I built and ran **20+ carrier-billing integrations across ~10 countries** on a production platform (subscriptions, one-off charges, and millions of async billing callbacks). The carriers were all different — different APIs, different notification formats, different quirks — but the *shape* of the problem was always the same. `dcb-kit` is that shape, distilled. It is **not** any carrier's proprietary integration (those stay under NDA where they belong); it's the scaffolding you hang your own adapters on.

Install
-------

[](#install)

```
composer require gardi/dcb-kit
```

The idea
--------

[](#the-idea)

A carrier integration always comes down to a few operations — subscribe a number, charge it, cancel — plus a stream of **async notifications** (activated, renewed, charged, out of balance, unsubscribed). Every carrier names these differently; `dcb-kit` normalizes them to one enum so the rest of your app never cares which carrier it's talking to:

```
Subscribed · Renewed · Charged · InsufficientBalance · Unsubscribed · Failed · Unknown

```

Write a carrier
---------------

[](#write-a-carrier)

Implement one interface per carrier:

```
use Gardi\DcbKit\Contracts\CarrierGateway;
use Gardi\DcbKit\Callbacks\{CallbackEvent, CallbackType};
use Gardi\DcbKit\Results\{ChargeResult, SubscriptionResult};
use Gardi\DcbKit\{Money, Support\Signature};

final class AcmeTelecom implements CarrierGateway
{
    public function __construct(private string $apiKey, private string $secret) {}

    public function name(): string { return 'acme'; }

    public function subscribe(string $msisdn, string $plan): SubscriptionResult { /* call carrier */ }

    public function charge(string $msisdn, Money $amount, string $reference): ChargeResult { /* ... */ }

    public function unsubscribe(string $msisdn, string $subscriptionId): void { /* ... */ }

    public function parseCallback(array $payload): CallbackEvent
    {
        $type = match ($payload['event']) {
            'SUB_OK'  => CallbackType::Subscribed,
            'BILL_OK' => CallbackType::Charged,
            'NO_FUND' => CallbackType::InsufficientBalance,
            default   => CallbackType::Unknown,
        };

        return new CallbackEvent($type, $payload['msisdn'], raw: $payload);
    }

    public function verifyCallback(string $rawBody, string $signature): bool
    {
        return Signature::verify($rawBody, $signature, $this->secret);
    }
}
```

Or configure one instead of coding it
-------------------------------------

[](#or-configure-one-instead-of-coding-it)

Most carriers are just a base URL + a status table + an auth scheme + a signature scheme. For those, skip the class — hand `HttpCarrierGateway` the moving parts:

```
use Gardi\DcbKit\{HttpCarrierGateway, CallbackUrl};
use Gardi\DcbKit\Callbacks\{StatusMap, CallbackType};
use Gardi\DcbKit\Contracts\Transport;
use Gardi\DcbKit\Auth\BearerAuth;
use Gardi\DcbKit\Verification\HmacVerifier;

// 1. The HTTP seam — wrap whatever client you use (Guzzle, Laravel Http, curl).
final class GuzzleTransport implements Transport
{
    public function request(string $method, string $url, array $payload, array $headers = []): array
    {
        // ...send it (with $headers) and return the decoded JSON response as an array
    }
}

// 2. Configure the carrier — no subclass needed.
$carriers->extend('acme', fn () => new HttpCarrierGateway(
    name: 'acme',
    baseUrl: $config['acme']['base_url'],            //  [
        'base_url'  => 'https://api.acme.test',
        'auth'      => ['type' => 'bearer', 'token' => $config['acme']['token']],
        'verifier'  => ['type' => 'hmac', 'secret' => $config['acme']['secret']],
        'statuses'  => [
            'ACTIVATION' => 'subscribed',
            'BILL_OK'    => 'charged',
            'NO_FUNDS'   => 'insufficient_balance',
            'CANCEL'     => 'unsubscribed',
        ],
        'status_field'         => 'event',
        'msisdn_field'         => 'phone',
        'transaction_id_field' => 'data.txn.id',
    ],
    // 'mtn' => [ ... ], 'zain' => [ ... ]
], new GuzzleTransport());

$carriers->gateway('acme')->charge(/* ... */);
```

`auth.type` is one of `bearer` / `api_key` / `basic` / `query` / `none`; `verifier.type` is `hmac` / `none`. This maps straight onto a Laravel/Symfony config file — adding a carrier becomes a config edit, not a code deploy.

Use it
------

[](#use-it)

Register your carriers and resolve them by name:

```
use Gardi\DcbKit\{CarrierManager, Money};

$carriers = new CarrierManager();
$carriers->extend('acme', fn () => new AcmeTelecom($apiKey, $secret)); // lazy

$gateway = $carriers->gateway('acme');

// $reference is your idempotency key — never charge the same one twice.
$result = $gateway->charge('9647501234567', Money::of(500, 'IQD'), 'order-42');

// In your webhook controller — verify the RAW body, then parse the decoded array:
if ($gateway->verifyCallback($rawBody, $signature)) {
    $event = $gateway->parseCallback($payload);   // $payload = json_decode($rawBody, true)
    if ($event->isSuccessful()) {
        // mark the subscription/charge as confirmed
    }
}
```

See [`tests/FakeCarrier.php`](tests/FakeCarrier.php) for a complete reference gateway.

Resilience (optional)
---------------------

[](#resilience-optional)

Three opt-in decorators cover the production concerns — all composable with the above.

**Retry transient failures** — wrap your `Transport`. Retrying a charge is safe because the `reference` is the carrier's idempotency key, so the same reference is never a second charge:

```
use Gardi\DcbKit\Transport\RetryingTransport;

$transport = new RetryingTransport(
    new GuzzleTransport(),
    maxAttempts: 3,
    baseDelayMs: 100,         // 100ms, then 200ms, then 400ms ... (exponential)
    retryOn: fn (\Throwable $e) => $e instanceof MyTimeoutException,  // optional: scope it
);
```

**Don't double-charge your own retries** — wrap a gateway with an `IdempotencyStore`. A charge already completed under a reference is replayed from the store instead of charged again (failed charges aren't remembered, so they can be retried):

```
use Gardi\DcbKit\Idempotency\IdempotentGateway;

$gateway = new IdempotentGateway($carriers->gateway('acme'), new RedisIdempotencyStore());
$gateway->charge('9647501234567', Money::of(500, 'IQD'), 'order-42');  // safe to repeat
```

The shipped `InMemoryIdempotencyStore` is for tests / a single process — back the `IdempotencyStore` interface with Redis or a unique-indexed table in production. (This guards against *your app* repeating a charge; the in-flight case — you charged, the response was lost, you retry — is covered by the carrier's own reference idempotency.)

**Handle a webhook in one call** — resolve the carrier, verify the raw body, decode, and parse:

```
use Gardi\DcbKit\Webhooks\WebhookHandler;
use Gardi\DcbKit\Exceptions\InvalidCallbackSignatureException;

$handler = new WebhookHandler($carriers);

try {
    $event = $handler->handle($carrier, $request->getContent(), $request->header('X-Signature'));
    // ... act on $event, then 200
} catch (InvalidCallbackSignatureException) {
    // 403 — spoofed or misconfigured
}
```

What's in the box
-----------------

[](#whats-in-the-box)

- `CarrierGateway` — the per-carrier interface.
- `HttpCarrierGateway` — a configurable base: stand up a carrier from a base URL + `StatusMap` + auth + verifier + (dot-path) field names; override only the unusual bits.
- `CarrierManager` — register/resolve carriers by name (eager or lazy), or `CarrierManager::fromArray()` to build them all from one config array.
- `Authentication` (`Auth\*`) — pluggable outgoing-request auth: bearer, API key, basic, query key, or your own.
- `CallbackVerifier` (`Verification\*`) — pluggable callback verification: HMAC of the raw body, none, or your own.
- `CallbackEvent` / `CallbackType` / `StatusMap` — normalized notifications + the status table that feeds them.
- `Transport` — the HTTP seam (bring your own client; keeps the kit dependency-free).
- `Money`, `SubscriptionResult`, `ChargeResult`, `CallbackUrl` — small value objects.
- `Signature` — the constant-time HMAC primitive behind `HmacVerifier`.
- `RetryingTransport` — a `Transport` decorator: retry transient failures with exponential backoff.
- `IdempotencyStore` + `IdempotentGateway` — dedupe charges by reference (ships an in-memory store; bring your own for production).
- `WebhookHandler` — resolve + verify + parse an incoming carrier webhook in one call.

Limitations
-----------

[](#limitations)

Deliberately a toolkit, not a platform:

- **No real carrier adapters ship with it.** Carrier APIs are proprietary and under contract. `HttpCarrierGateway` gets a standard one running from config; unusual ones you finish by overriding a method.
- **Idempotency is opt-in, and the shipped store isn't persistent.**`IdempotentGateway` dedupes charges by `reference`, but `InMemoryIdempotencyStore`is single-process — back the `IdempotencyStore` interface with Redis or a unique-indexed table in production.
- **Retries, not queueing.** `RetryingTransport` handles transient failures; durable async work (queues/workers) is still yours to wire.

Development
-----------

[](#development)

```
composer install
composer test   # pest
```

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

3

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/110671428?v=4)[Nariman Gardi](/maintainers/NarimanGardi)[@NarimanGardi](https://github.com/NarimanGardi)

---

Top Contributors

[![narimanmuhsin](https://avatars.githubusercontent.com/u/130443709?v=4)](https://github.com/narimanmuhsin "narimanmuhsin (23 commits)")

---

Tags

carrier-billingdcbfintechpaymentsphptelcophppaymentsfintechcarrier billingtelcodcb

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/gardi-dcb-kit/health.svg)

```
[![Health](https://phpackages.com/badges/gardi-dcb-kit/health.svg)](https://phpackages.com/packages/gardi-dcb-kit)
```

###  Alternatives

[kingflamez/laravelrave

A Laravel Package for Flutterwave Rave

152302.5k4](/packages/kingflamez-laravelrave)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
