Initial release: qbirr/sdk v1.0.0
This commit is contained in:
@@ -0,0 +1,68 @@
|
|||||||
|
# qbirr/sdk (PHP)
|
||||||
|
|
||||||
|
Verify Ethiopian bank transfer receipts in real time.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require qbirr/sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires PHP 7.4+ and the `curl` + `json` extensions (standard on almost every host).
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Qbirr\Qbirr;
|
||||||
|
|
||||||
|
$qb = new Qbirr($_ENV['QBIRR_API_KEY']);
|
||||||
|
|
||||||
|
$r = $qb->verify([
|
||||||
|
'provider' => 'cbe',
|
||||||
|
'ref' => 'FT23001234ABC',
|
||||||
|
'amount' => 500,
|
||||||
|
'receiver_name' => 'YOUR FULL NAME',
|
||||||
|
'receiver_account' => '1000017692643',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($r['verified']) {
|
||||||
|
echo "Paid by " . $r['payer'] . ", amount: " . $r['amount'];
|
||||||
|
} else {
|
||||||
|
echo "Failed: " . $r['error'];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Drop-in for WordPress / Laravel / vanilla PHP
|
||||||
|
|
||||||
|
Works the same in every framework — it's just curl under the hood.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// WordPress / WooCommerce — inside a payment gateway
|
||||||
|
$r = (new Qbirr\Qbirr(get_option('qbirr_api_key')))->verify([...]);
|
||||||
|
|
||||||
|
// Laravel
|
||||||
|
$r = (new \Qbirr\Qbirr(config('services.qbirr.key')))->verify([...]);
|
||||||
|
|
||||||
|
// Vanilla
|
||||||
|
require 'vendor/autoload.php';
|
||||||
|
$r = (new Qbirr\Qbirr(getenv('QBIRR_API_KEY')))->verify([...]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Qbirr\Qbirr;
|
||||||
|
use Qbirr\QbirrException;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$r = $qb->verify([ ... ]);
|
||||||
|
} catch (QbirrException $e) {
|
||||||
|
error_log("qbirr error " . $e->status . ": " . $e->getMessage());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Verification failures (wrong amount, wrong receiver) are **not** exceptions — they're returned as `['verified' => false, 'error' => '...']`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "qbirr/sdk",
|
||||||
|
"description": "qbirr.com — Ethiopian bank verification SDK (CBE, Telebirr, Awash, Dashen, M-Pesa, Abyssinia, eBirr)",
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": ["ethiopia", "cbe", "telebirr", "awash", "dashen", "mpesa", "abyssinia", "ebirr", "payment", "verification"],
|
||||||
|
"homepage": "https://qbirr.com",
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.4",
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-json": "*"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Qbirr\\": "src/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* qbirr — PHP client for the qbirr payment verification API.
|
||||||
|
*
|
||||||
|
* $qb = new Qbirr\Qbirr('your_api_key');
|
||||||
|
* $r = $qb->verify([
|
||||||
|
* 'provider' => 'cbe',
|
||||||
|
* 'ref' => 'FT23001234ABC',
|
||||||
|
* 'amount' => 500,
|
||||||
|
* 'receiver_name' => 'YOUR NAME',
|
||||||
|
* 'receiver_account' => '1000017692643',
|
||||||
|
* ]);
|
||||||
|
* if ($r['verified']) echo "Paid by " . $r['payer'];
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Qbirr;
|
||||||
|
|
||||||
|
class QbirrException extends \RuntimeException {
|
||||||
|
public ?int $status;
|
||||||
|
public $body;
|
||||||
|
public function __construct(string $msg, ?int $status = null, $body = null) {
|
||||||
|
parent::__construct($msg);
|
||||||
|
$this->status = $status;
|
||||||
|
$this->body = $body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Qbirr {
|
||||||
|
public const VERSION = '1.0.0';
|
||||||
|
public const DEFAULT_BASE_URL = 'https://verify.qbirr.com';
|
||||||
|
|
||||||
|
private string $apiKey;
|
||||||
|
private string $baseUrl;
|
||||||
|
private int $timeoutSec;
|
||||||
|
|
||||||
|
public function __construct(string $apiKey, string $baseUrl = self::DEFAULT_BASE_URL, int $timeoutSec = 30) {
|
||||||
|
if ($apiKey === '') throw new QbirrException('apiKey is required');
|
||||||
|
$this->apiKey = $apiKey;
|
||||||
|
$this->baseUrl = rtrim($baseUrl, '/');
|
||||||
|
$this->timeoutSec = $timeoutSec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a transaction.
|
||||||
|
*
|
||||||
|
* @param array $req {
|
||||||
|
* provider: cbe | telebirr | abyssinia | dashen | mpesa | awash | ebirr
|
||||||
|
* ref: Transaction ref. For awash, SMS URL or token. For ebirr, "tenant/token" or URL.
|
||||||
|
* amount: Expected payment in ETB.
|
||||||
|
* receiver_name: Your receiving account name.
|
||||||
|
* receiver_account: Required for cbe and abyssinia.
|
||||||
|
* }
|
||||||
|
* @return array { verified: bool, payer: string, amount: float, error: string }
|
||||||
|
*/
|
||||||
|
public function verify(array $req): array {
|
||||||
|
$body = [
|
||||||
|
'provider' => $req['provider'] ?? '',
|
||||||
|
'ref' => $req['ref'] ?? '',
|
||||||
|
'amount' => $req['amount'] ?? 0,
|
||||||
|
'receiver_name' => $req['receiver_name'] ?? '',
|
||||||
|
];
|
||||||
|
if (!empty($req['receiver_account'])) $body['receiver_account'] = $req['receiver_account'];
|
||||||
|
|
||||||
|
$data = $this->request('POST', '/api/v1/verify', $body);
|
||||||
|
return [
|
||||||
|
'verified' => (bool) ($data['verified'] ?? false),
|
||||||
|
'payer' => (string) ($data['payer'] ?? ''),
|
||||||
|
'amount' => (float) ($data['amount'] ?? 0),
|
||||||
|
'error' => (string) ($data['error'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array { used, limit, remaining, plan, period_start, period_end }
|
||||||
|
*/
|
||||||
|
public function usage(): array {
|
||||||
|
return $this->request('GET', '/api/v1/usage');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify by uploading a receipt PDF or image.
|
||||||
|
*
|
||||||
|
* @param array $req { provider, amount, receiver_name, [receiver_account], filePath }
|
||||||
|
* OR { provider, ..., fileContent, fileName, mimeType }
|
||||||
|
*/
|
||||||
|
public function verifyUpload(array $req): array {
|
||||||
|
if (isset($req['filePath'])) {
|
||||||
|
$fileContent = file_get_contents($req['filePath']);
|
||||||
|
if ($fileContent === false) throw new QbirrException("Could not read file: " . $req['filePath']);
|
||||||
|
$fileName = basename($req['filePath']);
|
||||||
|
$mimeType = mime_content_type($req['filePath']) ?: 'application/octet-stream';
|
||||||
|
} else {
|
||||||
|
$fileContent = $req['fileContent'] ?? '';
|
||||||
|
$fileName = $req['fileName'] ?? 'upload.pdf';
|
||||||
|
$mimeType = $req['mimeType'] ?? 'application/octet-stream';
|
||||||
|
}
|
||||||
|
if ($fileContent === '') throw new QbirrException("Empty file content");
|
||||||
|
|
||||||
|
$boundary = '----qbirrphp' . bin2hex(random_bytes(8));
|
||||||
|
$body = '';
|
||||||
|
foreach (['provider', 'amount', 'receiver_name', 'receiver_account'] as $k) {
|
||||||
|
if (isset($req[$k]) && $req[$k] !== '') {
|
||||||
|
$body .= "--{$boundary}\r\n"
|
||||||
|
. "Content-Disposition: form-data; name=\"{$k}\"\r\n\r\n"
|
||||||
|
. $req[$k] . "\r\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$body .= "--{$boundary}\r\n"
|
||||||
|
. "Content-Disposition: form-data; name=\"file\"; filename=\"{$fileName}\"\r\n"
|
||||||
|
. "Content-Type: {$mimeType}\r\n\r\n"
|
||||||
|
. $fileContent . "\r\n"
|
||||||
|
. "--{$boundary}--\r\n";
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_URL => $this->baseUrl . '/api/v1/verify/upload',
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => $this->timeoutSec + 30,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'X-API-Key: ' . $this->apiKey,
|
||||||
|
'Content-Type: multipart/form-data; boundary=' . $boundary,
|
||||||
|
'Accept: application/json',
|
||||||
|
'User-Agent: qbirr-php/' . self::VERSION,
|
||||||
|
],
|
||||||
|
CURLOPT_POSTFIELDS => $body,
|
||||||
|
]);
|
||||||
|
$raw = curl_exec($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($raw === false) throw new QbirrException("Network error: $err");
|
||||||
|
$data = json_decode((string) $raw, true);
|
||||||
|
if (!is_array($data)) throw new QbirrException("Non-JSON response (HTTP $code)", $code, $raw);
|
||||||
|
if ($code < 200 || $code >= 300) {
|
||||||
|
throw new QbirrException((string)($data['error'] ?? "HTTP $code"), $code, $data);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'verified' => (bool) ($data['verified'] ?? false),
|
||||||
|
'payer' => (string) ($data['payer'] ?? ''),
|
||||||
|
'amount' => (float) ($data['amount'] ?? 0),
|
||||||
|
'error' => (string) ($data['error'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function request(string $method, string $path, ?array $body = null): array {
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_URL => $this->baseUrl . $path,
|
||||||
|
CURLOPT_CUSTOMREQUEST => $method,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => $this->timeoutSec,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'X-API-Key: ' . $this->apiKey,
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Accept: application/json',
|
||||||
|
'User-Agent: qbirr-php/' . self::VERSION,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
if ($body !== null) {
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = curl_exec($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($raw === false) throw new QbirrException("Network error: $err");
|
||||||
|
|
||||||
|
$data = json_decode((string) $raw, true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
throw new QbirrException("Non-JSON response (HTTP $code)", $code, $raw);
|
||||||
|
}
|
||||||
|
if ($code < 200 || $code >= 300) {
|
||||||
|
$msg = $data['error'] ?? "HTTP $code";
|
||||||
|
throw new QbirrException((string) $msg, $code, $data);
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user