PHPackages                             ghanem/bee - 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. [API Development](/categories/api)
4. /
5. ghanem/bee

Abandoned → [ghanem/basata](/?search=ghanem%2Fbasata)Library[API Development](/categories/api)

ghanem/bee
==========

A Laravel package that provides an interface to the Basata payment services API.

v3.0.0(2w ago)01661MITPHPPHP ^8.1CI passing

Since Sep 27Pushed 2w ago1 watchersCompare

[ Source](https://github.com/gaitco/basata)[ Packagist](https://packagist.org/packages/ghanem/bee)[ Docs](https://github.com/gaitco/basata)[ RSS](/packages/ghanem-bee/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (4)Dependencies (10)Versions (5)Used By (0)

Basata
======

[](#basata)

[![Latest Stable Version](https://camo.githubusercontent.com/ccc9fdeb821c602d71d86581cf16bafe4cf44c2ceca59f2474dd6aad449ccfd2/68747470733a2f2f706f7365722e707567782e6f72672f6768616e656d2f6261736174612f762f737461626c652e737667)](https://packagist.org/packages/ghanem/basata) [![License](https://camo.githubusercontent.com/b19da58b31573f4a76bfa28af4828859be994e1ae143f2ace9eabd60a4e7ff5e/68747470733a2f2f706f7365722e707567782e6f72672f6768616e656d2f6261736174612f6c6963656e73652e737667)](https://packagist.org/packages/ghanem/basata) [![Total Downloads](https://camo.githubusercontent.com/c1ff1981bcfa44571b3a76315b3ae81f27ef10a7229f711cfe8cecc4f3947dec/68747470733a2f2f706f7365722e707567782e6f72672f6768616e656d2f6261736174612f646f776e6c6f6164732e737667)](https://packagist.org/packages/ghanem/basata)

A Laravel package that provides an interface to the Basata *Cash Collector Channel API* (spec v3.0.8) payment services.

Requirements
------------

[](#requirements)

- PHP 8.1+
- Laravel 10, 11, 12, or 13

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

[](#installation)

```
composer require ghanem/basata
```

Publish the configuration file:

```
php artisan vendor:publish --provider="Ghanem\Basata\BasataServiceProvider" --tag="basata-config"
```

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

[](#configuration)

Add the following to your `.env` file:

```
BASATA_USERNAME=your-username
BASATA_PASSWORD=your-password
BASATA_URL=https://your-basata-api-url.com/
BASATA_TERMINAL_ID=your-terminal-id
```

`BASATA_TERMINAL_ID` is **required**. The Basata API requires a unique External Terminal ID per terminal (spec FAQ Q3) — it identifies which physical/logical terminal is making the request, not a login credential. If it is empty, every request throws immediately with API error code `1024`(`TerminalIdRequired`) rather than being silently sent without one.

> **Coming from `ghanem/bee`?** This field used to be hardcoded to the literal string `'1'` for every request, for every installation. That was a bug — see [Migrating from `ghanem/bee`](#migrating-from-ghanembee) below.

Usage
-----

[](#usage)

You can use the `Basata` facade or resolve `BasataService` from the container.

### Service &amp; Category Information

[](#service--category-information)

```
use Ghanem\Basata\Facades\Basata;

// Get all categories
$categories = Basata::getCategoryList();

// Get category service list
$categoryServices = Basata::getCategoryServiceList();

// Get the provider list (spec 5.1 takes no filter — the old $categoryId
// argument was accepted and silently ignored, and is gone in this release)
$providers = Basata::getProviderList();

// Get all services
$services = Basata::getServiceList();

// Get service input/output parameters
$inputParams = Basata::getServiceInputParameterList();
$outputParams = Basata::getServiceOutputParameterList();
```

### Transactions

[](#transactions)

```
// Transaction inquiry — account_number and service_id are required (plus
// service_version, auto-filled below); a missing one throws
// BasataValidationException (code 1008) instead of silently defaulting.
$inquiry = Basata::transactionInquiry([
    'account_number' => '12345',
    'service_id' => 10,
    'input_parameter_list' => [
        ['key' => 'phone', 'value' => '0912345678'],
    ],
]);

// Transaction payment — account_number, service_id, external_id, amount,
// total_amount and quantity are all required (plus service_version,
// auto-filled below).
$payment = Basata::transactionPayment([
    'account_number' => '12345',
    'service_id' => 10,
    'external_id' => 'order-001',
    'amount' => 100,
    'service_charge' => 5,
    'total_amount' => 105,
    'quantity' => 1,
    'inquiry_transaction_id' => $inquiry['data']['transaction_id'],
    'input_parameter_list' => [],
]);

// Get transaction details by ID
$transaction = Basata::getTransaction(123);

// Get transaction by external ID
$transaction = Basata::getTransaction('order-001', 'external_id');
```

`Basata::transactionInquiry()`/`Basata::transactionPayment()` fill in `service_version` automatically from `getProviderList()` before sending the request, so you don't need to pass it yourself. This auto-fill only happens through the facade/`BasataService` — `getBillsAmount()` and any direct `ApiClient` usage do not get it and must supply `service_version` explicitly.

**Pass your own `service_version` when you have one.** `GetProviderList`sends `service_version: 0`, which spec 5.1 defines as *"force update the service list"* and FAQ A1 explicitly tells terminals not to do routinely ("store this value… check it periodically"). So:

- a `service_version` you pass in the `$data` array wins and skips the lookup entirely;
- otherwise the lookup runs, but the response is cached like the other catalogue calls (`provider_list_{lang}`), so it is not one force-refresh per transaction. Call `Basata::clearCache('provider_list_en')` (or the API's error 1025 "Incorrect service version") to refresh it.

### Prepaid Card Recharge Confirmation

[](#prepaid-card-recharge-confirmation)

Per spec 5.9, a successful `transactionPayment()` for a service whose input parameters include a `card_data` record must be confirmed afterwards:

```
use Ghanem\Basata\Enums\OperationStatus;

Basata::confirmPrepaidCardRecharge(
    paymentTransactionId: $payment['data']['transaction_id'],
    status: OperationStatus::Success, // or OperationStatus::Fail
);
```

### Account &amp; Billing

[](#account--billing)

```
// Get account info
$account = Basata::getAccountInfo();

// Get bills amount (performs an inquiry and returns the amount).
// Unlike transactionInquiry(), getBillsAmount() does NOT auto-fill
// service_version from getProviderList() — pass it yourself, or it throws
// BasataValidationException (code 1008).
$bills = Basata::getBillsAmount([
    'service_version' => 0,
    'service_id' => 10,
    'account_number' => '12345',
]);
```

### Service Charge Calculation

[](#service-charge-calculation)

```
// Calculate service charge for an amount
$result = Basata::calculateServiceCharge([
    'service_id' => 10,
    'amount' => 100,
]);
// Returns: ['service_id' => 10, 'amount' => 100, 'service_charge' => 5, 'total_amount' => 105]

// Reverse calculate (from total amount back to base amount)
$result = Basata::calculateServiceChargeReverse([
    'service_id' => 10,
    'amount' => 105, // total amount including charge
]);
// Returns: ['service_id' => 10, 'amount' => 95.45, 'service_charge' => 9.55, 'total_amount' => 105]
```

Both are client-side calculations over the service's `service_charge_list`(from the cached `getServiceList()`), and both fail loudly rather than guessing:

- an unknown `service_id` throws `BasataNotFoundException` (code 1018);
- an amount outside every charge band throws `BasataValidationException`(code 1022) instead of returning a zero charge that would then be posted.

`calculateServiceChargeReverse()` honours the band's `percentage` flag: a percentage charge is extracted out of the total, a fixed charge is subtracted from it. The band itself is matched on the resulting *net* amount, and `total_amount` always round-trips back to the total you passed in.

### All actions at a glance

[](#all-actions-at-a-glance)

MethodMaps to API action`getCategoryList()``GetCategoryList``getCategoryServiceList()``GetCategoryServiceList``getProviderList()``GetProviderList``getServiceList()``GetServiceList``getServiceInputParameterList()``GetServiceInputParameterList``getServiceOutputParameterList()``GetServiceOutputParameterList``getTransaction($id, 'id')``GetTransactionDetails``getTransaction($id, 'external_id')``GetTransactionByExternalId``getAccountInfo()``GetAccountInfo``transactionInquiry()``TransactionInquiry``transactionPayment()``TransactionPayment``confirmPrepaidCardRecharge()``ConfirmPrepaidCardRecharge``calculateServiceCharge()` / `calculateServiceChargeReverse()`client-side calculation built on `getServiceList()` — not a separate API action`getBillsAmount()`client-side helper built on `transactionInquiry()` — not a separate API action`confirmPrepaidCardRecharge()` is new in this release; every other method existed already and was re-verified field-by-field against the v3.0.8 spec.

### Language Support

[](#language-support)

Most methods accept a language parameter (defaults to `'en'`, or `BASATA_LANGUAGE` if set):

```
$categories = Basata::getCategoryList('ar');
$services = Basata::getServiceList('ar');
```

### DTOs (Typed Responses)

[](#dtos-typed-responses)

Use `*Dto` methods for typed response objects instead of raw arrays/collections:

```
use Ghanem\Basata\DTOs\ApiResponse;
use Ghanem\Basata\DTOs\TransactionResult;
use Ghanem\Basata\DTOs\ServiceChargeResult;

// API response DTO
$response = Basata::getCategoryListDto(); // returns ApiResponse
$response->success;    // bool
$response->data;       // array
$response->statusCode; // int
$response->get('categories.0.name'); // dot notation access

// Transaction DTO
$tx = Basata::getTransactionDto(123); // returns TransactionResult
$tx->transactionId; // int|string|null — the spec types transaction_id as a
                    // String, so it is passed through verbatim (never cast)
$tx->amount;        // ?float
$tx->serviceCharge; // ?float
$tx->totalAmount;   // ?float
$tx->raw;           // the full `data` block
// GetTransactionDetails / GetTransactionByExternalId nest the record under
// `data.transaction_details` (a Transaction Detail, PDF 4.10) while
// TransactionInquiry / TransactionPayment return their fields flat in `data`.
// TransactionResult reads the nested record first and falls back to the flat
// level, so both shapes populate the same DTO. A Transaction Detail carries no
// transaction_id or service_charge, so those are null for the report actions —
// read the rest of the detail (status, provider_name, details_list, …) off
// `$tx->raw['transaction_details']`.

$inquiry = Basata::transactionInquiryDto($data);  // TransactionResult
$payment = Basata::transactionPaymentDto($data);  // TransactionResult

// Service charge DTO
$charge = Basata::calculateServiceChargeDto([
    'service_id' => 10,
    'amount' => 100,
]); // returns ServiceChargeResult
$charge->serviceId;     // int
$charge->amount;        // float
$charge->serviceCharge; // float
$charge->totalAmount;   // float
```

Error Handling
--------------

[](#error-handling)

The Basata API returns **HTTP 200 even for a business failure** — for example insufficient balance or a transaction already in progress. Success is never inferred from the HTTP status; it's read from the response body (`"success": true`). Anything else — `"success": false`, a missing `success`key, an empty body, a scalar body, or a non-JSON body — is treated as a failure.

**Exactly one thing means success: an HTTP 2xx whose body says `"success": true`.** Everything else — a business failure, *and* a transport or server failure (any non-2xx: 401, 404, 502, 504, …) — goes through the same error layer and obeys the same `basata.errors.throw` setting. There is no path where a 502 quietly returns an array that reads like a response, so `$payment['data']['transaction_id']` can never be silently `null` because the gateway died — which matters most on `transactionPayment()`, where a 5xx is exactly the case where the payment may already have executed.

For a non-2xx the exception's `apiCode` is the API's own error code when the body carries one, and otherwise the HTTP status (e.g. `502`); a bare status matches no documented code, so it surfaces as `BasataServerException`. The `payload` always includes `status_code`, `link`, and the request `params`**with `login`/`password` stripped**.

By default, a failure throws a typed exception carrying the error code, message, and full payload:

```
use Ghanem\Basata\Exceptions\BasataException;
use Ghanem\Basata\Exceptions\BasataInsufficientBalanceException;

try {
    Basata::transactionPayment($data);
} catch (BasataInsufficientBalanceException $e) {
    // $e->apiCode  — int, e.g. 1016
    // $e->getMessage() — the API's message text
    // $e->payload  — array, the raw response body
} catch (BasataException $e) {
    // catches every Basata exception — they all extend this base class
}
```

Set `basata.errors.throw` to `false` (env `BASATA_ERRORS_THROW=false`) to get the raw response payload back instead of an exception — useful for call sites written against the old array-return contract:

```
BASATA_ERRORS_THROW=false
```

`basata.errors.throw` governs how a *failed request* is handled — a business failure in the body, a non-2xx transport/server failure, or the client-side rate limiter. It does **not** cover pre-flight validation that runs before a request is ever sent — a missing `BASATA_TERMINAL_ID` (code 1024) or a missing required field on `transactionInquiry()`/`transactionPayment()` (code 1008/1017) always throws, regardless of this setting, because there is no API response to fall back to.

### Exception classes

[](#exception-classes)

Every documented error code (spec section 6) maps to one of these, via `Ghanem\Basata\Enums\ErrorCode::exceptionClass()`. An undocumented/unknown code falls back to `BasataServerException` rather than being swallowed.

ExceptionExample codes`BasataAuthenticationException`1001 login required, 1002 password required, 1003 incorrect credentials, 1010 invalid user, 1012 change password required, 1013 permission denied`BasataValidationException`1004–1009, 1011 (language required), 1017 (wrong amount), 1020, 1022, 1024 (**terminal\_id required**), 1025, 1019, 1028, 1029, 2001–2005`BasataDuplicateTransactionIdException` (**extends** `BasataValidationException`)1023 — see [Ambiguous payments](#ambiguous-payments-retries-error-1023-and-faq-a10)`BasataInsufficientBalanceException`1016`BasataRateLimitException`1033`BasataTransactionInProgressException`1034`BasataNotFoundException`1014 account not found, 1015 receiver account not found, 1018 unknown service, 1021 inquiry transaction not found, 1026 transaction not found, 1027 Beecard not found`BasataServerException`2000, 20000, and any code not in the table above> Codes 1027–1029 refer to "Beecard" — the API's own product name for a physical prepaid card. That naming is kept verbatim rather than renamed to "Basatacard".

### Ambiguous payments: retries, error 1023, and FAQ A10

[](#ambiguous-payments-retries-error-1023-and-faq-a10)

Requests are retried on **connection failure** (see [Retry Mechanism](#retry-mechanism)) — including payments. If the connection drops *after* Basata processed the payment, the retry re-posts the same `external_id` and the API answers **1023 "Duplicate transaction ID"**. That looks like a validation error but very often means *your payment succeeded*.

Per spec FAQ A10 (p.21), resolve the ambiguity by asking for the transaction by your own ID rather than assuming either outcome:

```
use Ghanem\Basata\Exceptions\BasataDuplicateTransactionIdException;

try {
    $payment = Basata::transactionPayment($data); // $data['external_id'] = 'order-001'
} catch (BasataDuplicateTransactionIdException $e) {
    // Do NOT re-submit with a new external_id — ask what happened first.
    $existing = Basata::getTransaction('order-001', 'external_id');
    $status = $existing['data']['transaction_details']['status'] ?? null;
    // SUCCESS -> the payment went through; ERROR/DEPOSIT_ERROR -> it did not.
}
```

`BasataDuplicateTransactionIdException` extends `BasataValidationException`, so existing `catch (BasataValidationException)` blocks still catch it — but catching it on its own lets you run the reconciliation above instead of treating it as a caller bug. The same lookup is the right response to any payment whose outcome you are unsure of (a timeout, a 5xx, a lost response): always use a stable, caller-generated `external_id` so it stays answerable.

### Main error codes

[](#main-error-codes)

CodeNameMeaning1008DataRequiredA required `data` field is missing (used by the client-side validation on `transactionInquiry`/`transactionPayment`)1011LanguageRequired`language` was not sent — this package always sends it1016InsufficientBalanceTerminal balance too low for the transaction1017WrongAmount`amount`/`total_amount` missing or invalid1022WrongServiceChargeSubmitted `service_charge` doesn't match the server's calculation1023DuplicateTransactionId`external_id` was already used — **may mean the payment succeeded**, see [Ambiguous payments](#ambiguous-payments-retries-error-1023-and-faq-a10)1024TerminalIdRequired`terminal_id` missing — thrown client-side before the request is even sent if `BASATA_TERMINAL_ID` is unset1026TransactionNotFoundNo transaction matches the given ID1033RateLimitExceededClient-side rate limiter tripped (see [Rate Limiting](#rate-limiting))1034TransactionInProgressThe transaction is still processing; retry the inquiry later2000 / 20000InternalServerError / AmbiguousServerErrorBasata-side failureSee `Ghanem\Basata\Enums\ErrorCode` for the full list of ~35 codes and their exact spec wording.

Transaction Status
------------------

[](#transaction-status)

`Ghanem\Basata\Enums\TransactionStatus` models the transaction lifecycle:

```
use Ghanem\Basata\Enums\TransactionStatus;

$transaction = Basata::getTransaction(123);

// GetTransactionDetails nests the record under `transaction_details`
// (spec 5.11) — NOT directly under `data`.
$status = TransactionStatus::from($transaction['data']['transaction_details']['status']);

if ($status->isFinal()) {
    // stop polling
}
```

> **This enum models the string statuses only** — the ones returned by `GetTransactionDetails`/`GetTransactionByExternalId` and by `TransactionPayment` (spec 4.9/4.10/5.11). Spec §5.7 types the **`TransactionInquiry`** response's `status` as an *Integer* (`0, 1, 2, 3, 5, 6`), so `TransactionStatus::from()` will not parse an inquiry response. Don't pass one to it.

StatusFinal?`NEW`No`IN_PROGRESS`No`SUCCESS`**Yes**`ERROR`**Yes**`DEPOSIT_ERROR`**Yes**`CANCELLED`Yes (not enumerated in the spec's finality table, but a cancelled transaction will not progress further)Use `isFinal()` to decide whether to keep polling `getTransaction()` for a pending transaction.

### Retry Mechanism

[](#retry-mechanism)

Failed API requests are automatically retried with exponential backoff:

```
BASATA_RETRY_TRIES=3       # Number of retry attempts
BASATA_RETRY_DELAY=100     # Initial delay in milliseconds
BASATA_RETRY_MULTIPLIER=2  # Backoff multiplier
```

### Request/Response Logging

[](#requestresponse-logging)

Enable logging to debug API calls. `login`/`password` are dropped from every log line and from the error payload returned to the caller:

```
BASATA_LOG_ENABLED=true
BASATA_LOG_CHANNEL=stack   # Optional: specific log channel
```

Secrets in the payload itself are masked, on both the request and the response side — `GetTransactionDetails` returns the voucher PIN and expiry date in `details_list` (FAQ A10) and `input_parameter_list` can carry `card_data`(§5.9). The value is replaced with `[REDACTED]`, the field itself stays, so the log is still useful. Matching is a case-insensitive substring test on the key name (and on the `key` of a `{"key": …, "value": …}` pair), driven by `config('basata.logging.redact')`:

```
// config/basata.php
'logging' => [
    'redact' => ['pin', 'card', 'voucher', 'serial', 'secret', 'password', 'expiry', 'account_number'],
],
```

Add your service's own parameter names to that list; remove an entry to un-redact it.

### Caching

[](#caching)

The provider, service and category lists are automatically cached to reduce API calls (transactions and reports never are):

```
BASATA_CACHE_ENABLED=true    # Enabled by default
BASATA_CACHE_TTL=3600        # Cache lifetime in seconds
BASATA_CACHE_STORE=redis     # Optional: specific cache store
```

```
// Clear all cached data
Basata::clearCache();

// Clear specific cache key
Basata::clearCache('category_list_en');
```

### Rate Limiting

[](#rate-limiting)

Limit the number of API requests per minute:

```
BASATA_RATE_LIMIT_ENABLED=true
BASATA_RATE_LIMIT_MAX=60      # Max requests per minute
```

When the limit is hit, the request throws `BasataRateLimitException` (code 1033) instead of hitting the network — unless `basata.errors.throw` is `false`, in which case the same shape payload is returned as an array.

### Webhooks

[](#webhooks)

Receive transaction status updates via webhooks:

```
BASATA_WEBHOOK_ENABLED=true
BASATA_WEBHOOK_PATH=basata/webhook
BASATA_WEBHOOK_SECRET=your-secret  # Optional: signature validation
```

Listen for webhook events in your application:

```
use Ghanem\Basata\Events\BasataWebhookReceived;
use Ghanem\Basata\Events\TransactionStatusUpdated;

// Listen to all webhook events
Event::listen(BasataWebhookReceived::class, function ($event) {
    // $event->event   - event name (e.g. 'transaction.completed')
    // $event->payload - full webhook payload
});

// Listen specifically to transaction status changes
Event::listen(TransactionStatusUpdated::class, function ($event) {
    // $event->transactionId
    // $event->status
    // $event->payload
});
```

### Async / Queue Support

[](#async--queue-support)

Process transactions asynchronously using Laravel queues:

```
BASATA_QUEUE_CONNECTION=redis   # Optional: queue connection
BASATA_QUEUE_NAME=payments      # Optional: queue name
```

The queued jobs run the *same* validation as the synchronous calls, so every required field must be present or the job throws on the worker:

```
// Dispatch a single payment to the queue
Basata::transactionPaymentAsync([
    'account_number' => '12345',
    'service_id' => 10,
    'external_id' => 'order-001',
    'amount' => 100,
    'service_charge' => 5,
    'total_amount' => 105,
    'quantity' => 1,
]);

// Batch multiple transactions
$batch = Basata::batchTransactions([
    ['action' => 'payment', 'data' => [
        'account_number' => '12345',
        'service_id' => 10,
        'external_id' => 'order-002',
        'amount' => 100,
        'total_amount' => 105,
        'quantity' => 1,
    ]],
    ['action' => 'inquiry', 'data' => [
        'account_number' => '123',
        'service_id' => 11,
    ]],
    ['action' => 'payment', 'data' => [
        'account_number' => '12345',
        'service_id' => 12,
        'external_id' => 'order-003',
        'amount' => 200,
        'total_amount' => 210,
        'quantity' => 1,
    ], 'lang' => 'ar'],
]);

// Batch with callback event
Basata::batchTransactions($transactions, App\Events\TransactionProcessed::class);
```

`transactionPaymentAsync()` dispatches `TransactionStatusUpdated` carrying the API's **actual** status (`SUCCESS`, `IN_PROGRESS`, `ERROR`, `DEPOSIT_ERROR`) — `success: true` only means the request was accepted, so listeners must check `$event->status` (see [Transaction Status](#transaction-status)) before treating a payment as done. A response with no status at all is reported as `IN_PROGRESS`.

Migrating from `ghanem/bee`
---------------------------

[](#migrating-from-ghanembee)

`ghanem/basata` is a republish, not a drop-in upgrade — Packagist names are permanent, and this package renames every symbol to match the product's actual name (the spec itself says "Bee" was only ever an internal codename). There are no backwards-compatibility aliases. Update every reference below deliberately.

### Rename map

[](#rename-map)

Old (`ghanem/bee`)New (`ghanem/basata`)`composer require ghanem/bee``composer require ghanem/basata``Ghanem\Bee\``Ghanem\Basata\``Ghanem\Bee\BeeService``Ghanem\Basata\BasataService``Ghanem\Bee\BeeServiceProvider``Ghanem\Basata\BasataServiceProvider``Ghanem\Bee\Facades\Bee` / `Bee::``Ghanem\Basata\Facades\Basata` / `Basata::``Ghanem\Bee\Http\BeeWebhookController``Ghanem\Basata\Http\BasataWebhookController``Ghanem\Bee\Events\BeeWebhookReceived``Ghanem\Basata\Events\BasataWebhookReceived``config/bee.php`, `config('bee.*')``config/basata.php`, `config('basata.*')`Cache key prefix `bee_``basata_`Webhook path `bee/webhook``basata/webhook`Webhook signature header `X-Bee-Signature``X-Basata-Signature`### Environment variables

[](#environment-variables)

OldNew`BEE_USERNAME``BASATA_USERNAME``BEE_PASSWORD``BASATA_PASSWORD``BEE_URL``BASATA_URL``BEE_TERMINAL_ID` (config key existed but was ignored — see below)`BASATA_TERMINAL_ID` **(now required and actually used)**`BEE_LANGUAGE``BASATA_LANGUAGE``BEE_RETRY_TRIES` / `_DELAY` / `_MULTIPLIER``BASATA_RETRY_TRIES` / `_DELAY` / `_MULTIPLIER``BEE_LOG_ENABLED` / `_CHANNEL``BASATA_LOG_ENABLED` / `_CHANNEL``BEE_CACHE_ENABLED` / `_TTL` / `_STORE``BASATA_CACHE_ENABLED` / `_TTL` / `_STORE``BEE_RATE_LIMIT_ENABLED` / `_MAX``BASATA_RATE_LIMIT_ENABLED` / `_MAX``BEE_WEBHOOK_ENABLED` / `_PATH` / `_SECRET``BASATA_WEBHOOK_ENABLED` / `_PATH` / `_SECRET``BEE_QUEUE_CONNECTION` / `_NAME``BASATA_QUEUE_CONNECTION` / `_NAME`— (did not exist)`BASATA_ERRORS_THROW` **(new, default `true`)**### ⚠️ `terminal_id` is now required — read this before upgrading

[](#️-terminal_id-is-now-required--read-this-before-upgrading)

`ghanem/bee` shipped a `BEE_TERMINAL_ID` config key, but the actual request code never read it — every action method **hardcoded `terminal_id` to the literal string `'1'`**, on every single request, for every installation, regardless of what you set. That was a bug, not a default: the spec requires a unique External Terminal ID per terminal (FAQ Q3), and sending `'1'` from every installation is indistinguishable from not identifying your terminal at all.

`ghanem/basata` removes the hardcoded value. You **must** set `BASATA_TERMINAL_ID` in your `.env` to your actual terminal ID before upgrading — if it is empty, every API call now throws `BasataValidationException` (API code 1024) instead of silently sending `1`.

### ⚠️ Business failures now throw

[](#️-business-failures-now-throw)

`ghanem/bee` only checked the HTTP status code. A `200 OK` response with `"success": false` in the body (e.g. insufficient balance, transaction in progress) was returned to your code as if it had succeeded. Any code that inspected `$result['success']` or relied on exceptions never being thrown for these cases must be updated — see [Error Handling](#error-handling), or set `BASATA_ERRORS_THROW=false` to keep the old array-return behavior while you migrate call sites incrementally.

### ⚠️ `getProviderList()` no longer takes a category ID

[](#️-getproviderlist-no-longer-takes-a-category-id)

`getProviderList(int $categoryId = 2, ?string $lang = null)` is now `getProviderList(?string $lang = null)`. Spec 5.1 defines the action as taking `service_version` only — the argument was accepted, documented, and never sent anywhere. Drop it from your call sites; a positional `getProviderList(2)` now passes `2` as the language.

### ⚠️ Missing required transaction fields now throw

[](#️-missing-required-transaction-fields-now-throw)

`ghanem/bee`'s `transactionInquiry()`/`transactionPayment()` silently defaulted a missing `amount` to `1.5` and a missing `service_id` to `14` if the caller forgot to pass them — meaning a bug in caller code could submit a real 1.5 EGP payment against the wrong service instead of failing loudly. `ghanem/basata` throws `BasataValidationException` for any missing required field instead.

Testing
-------

[](#testing)

```
composer test
```

Sponsor
-------

[](#sponsor)

[Become a Sponsor](https://github.com/sponsors/AbdullahGhanem)

License
-------

[](#license)

MIT

###  Health Score

48

—

FairBetter than 94% of packages

Maintenance97

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity59

Maturing project, gaining track record

 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 ~468 days

Total

4

Last Release

16d ago

Major Versions

v1.0 → V2.02026-03-08

V2.1 → v3.0.02026-08-02

PHP version history (2 changes)v1.0PHP ^7.3|^8.0

V2.0PHP ^8.1

### Community

Maintainers

![](https://www.gravatar.com/avatar/fafeedb68c6ad2ddbb249caed67289c8c74073d25fcfb210534a9769d393dda4?d=identicon)[ghanem](/maintainers/ghanem)

---

Top Contributors

[![AbdullahGhanem](https://avatars.githubusercontent.com/u/5055892?v=4)](https://github.com/AbdullahGhanem "AbdullahGhanem (26 commits)")

---

Tags

basatabill-paymentcash-collectoregyptlaravellaravel-packagepayment-gatewaypaymentsphpphpapilaravelpaymentbasata

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/ghanem-bee/health.svg)

```
[![Health](https://phpackages.com/badges/ghanem-bee/health.svg)](https://phpackages.com/packages/ghanem-bee)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[defstudio/telegraph

A laravel facade to interact with Telegram Bots

818355.4k3](/packages/defstudio-telegraph)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5226.7k](/packages/simplestats-io-laravel-client)[forjedio/inertia-table

Backend-driven dynamic tables for Laravel + Inertia.js

272.0k](/packages/forjedio-inertia-table)

PHPackages © 2026

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