PHPackages                             mattdi/ethiopia-eims - 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. mattdi/ethiopia-eims

ActiveLibrary[Payment Processing](/categories/payments)

mattdi/ethiopia-eims
====================

Framework-agnostic PHP client for the Ethiopian Ministry of Revenue Electronic Invoice Management System (EIMS), with an optional Laravel bridge.

v1.1.0(1mo ago)04MITPHPPHP ^8.2CI passing

Since Jul 13Pushed 1mo agoCompare

[ Source](https://github.com/Matt-di/ethiopia-eims)[ Packagist](https://packagist.org/packages/mattdi/ethiopia-eims)[ Docs](https://github.com/Matt-di/ethiopia-eims)[ RSS](/packages/mattdi-ethiopia-eims/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (5)Versions (3)Used By (0)

Ethiopia EIMS PHP Client
========================

[](#ethiopia-eims-php-client)

A framework-agnostic PHP client for the **Ethiopian Ministry of Revenue (MoR) Electronic Invoice Management System (EIMS)**, with an optional Laravel bridge.

It lets any ERP register invoices, receipts, and query/verify/cancel documents against the EIMS API. The package speaks **plain DTOs and returns result objects** — it does not touch your database, models, or tenancy. Mapping your domain (sales, customers, products) to an `InvoiceRequest` and persisting results is your application's job.

> **Status:** `0.x`, API may change. Ethiopian e-invoicing is pre-mandate; the request/response shapes reflect the current EIMS API and may evolve.

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

[](#requirements)

- PHP 8.2+
- `guzzlehttp/guzzle` ^7.5
- A PSR-16 cache (optional, for token caching) — Laravel's cache works out of the box.

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

[](#installation)

```
composer require mattdi/ethiopia-eims
```

Plain PHP usage
---------------

[](#plain-php-usage)

```
use Mattdi\Eims\EimsClient;
use Mattdi\Eims\Config\EimsCredentials;
use Mattdi\Eims\Config\SellerConfig;
use Mattdi\Eims\Data\{InvoiceRequest, BuyerDetails, InvoiceItem, ValueDetails, DocumentDetails, SourceSystem};
use Mattdi\Eims\Enums\TransactionType;

$client = new EimsClient(
    credentials: new EimsCredentials(
        clientId:     getenv('EIMS_CLIENT_ID'),
        clientSecret: getenv('EIMS_CLIENT_SECRET'),
        apiKey:       getenv('EIMS_API_KEY'),
        baseUrl:      'http://core.mor.gov.et',
    ),
    seller: new SellerConfig(
        tin:        '0001234567',
        legalName:  'Acme Pharmacy PLC',
        regionCode: '14',
        city:       'Addis Ababa',
        vatNumber:  'VAT-123',
    ),
    // cache:          $psr16Cache,     // optional
    // cacheNamespace: 'store_42',      // optional; defaults to the TIN
);

$result = $client->register(new InvoiceRequest(
    buyer: BuyerDetails::walkIn(),
    items: [
        new InvoiceItem(
            productDescription: 'Paracetamol 500mg',
            quantity: 2,
            unitPrice: 25.00,
            preTaxValue: 50.00,
            taxAmount: 0.00,
            totalLineAmount: 50.00,
        ),
    ],
    values: new ValueDetails(totalValue: 50.00, taxValue: 0.00),
    document: new DocumentDetails(documentNumber: 1001, date: date('d-m-Y\TH:i:s')),
    sourceSystem: new SourceSystem(invoiceCounter: 1001, systemNumber: 'SYS-1'),
    transactionType: TransactionType::B2C,
));

echo $result->irn;
echo $result->qrCode;
echo $result->signedInvoice;
```

### Building invoices the easy way

[](#building-invoices-the-easy-way)

`Invoice` + `InvoiceItem::make()` compute line totals and the `ValueDetails`for you:

```
use Mattdi\Eims\Data\{Invoice, InvoiceItem, BuyerDetails};
use Mattdi\Eims\Enums\{TransactionType, PaymentMode, DocumentType};

$invoice = Invoice::for(BuyerDetails::walkIn())
    ->document(1001, date('d-m-Y\TH:i:s'), DocumentType::Invoice)
    ->source(1001, systemNumber: 'SYS-1')
    ->transactionType(TransactionType::B2C)
    ->payment(PaymentMode::Cash)
    ->addItem(InvoiceItem::make('Paracetamol 500mg', quantity: 2, unitPrice: 25.00, taxRate: 0.15))
    ->addItem(InvoiceItem::make('Vitamin C', quantity: 1, unitPrice: 20.00, taxRate: 0.15, discount: 5.00))
    ->build(); // throws if missing items / document / source

$result = $client->register($invoice);
```

`InvoiceItem::make()` returns an immutable `InvoiceItem` where `preTaxValue`, `taxAmount`, and `totalLineAmount` are derived from the inputs.

Resilience: logging &amp; retries
---------------------------------

[](#resilience-logging--retries)

Pass a [PSR-3](https://www.php-fig.org/psr/psr-3/) logger to see every request and response, and configure automatic retry on connection failures / 5xx:

```
use Mattdi\Eims\Config\EimsCredentials;
use Mattdi\Eims\Support\HttpClientFactory;

$credentials = new EimsCredentials(
    clientId: '...', clientSecret: '...', apiKey: '...',
    baseUrl: 'http://core.mor.gov.et',
    maxRetries: 2,        // transient failures retried with exponential backoff
    retryDelayMs: 500,
);

$http = HttpClientFactory::make($credentials, $psr3Logger);
$client = new EimsClient(credentials: $credentials, seller: $seller, http: $http);
```

In Laravel, enable logging via config and the package wires the logger for you (see below). `EimsCredentials::validate()` fails fast if any required value is missing.

Testing your integration
------------------------

[](#testing-your-integration)

`Mattdi\Eims\Testing\FakeEimsClient` records calls and lets you assert on them:

```
use Mattdi\Eims\Testing\FakeEimsClient;
use Mattdi\Eims\Data\Responses\RegisterResult;

$fake = new FakeEimsClient();
$fake->pushRegister(new RegisterResult('FAKE-IRN', null, null, null));

$result = $fake->register($invoice);

$fake->assertRegistered();
$fake->assertRegisteredCount(1);
```

### In Laravel

[](#in-laravel)

`Eims::fake()` swaps the resolved client for a `FakeEimsClient`:

```
use Mattdi\Eims\Laravel\Facades\Eims;

Eims::fake();

// ... exercise your code that registers an invoice ...

Eims::fake()->assertRegistered();
```

Laravel usage
-------------

[](#laravel-usage)

Publish the config (optional):

```
php artisan vendor:publish --tag=eims-config
```

Set env vars:

```
EIMS_BASE_URL=http://core.mor.gov.et
EIMS_CLIENT_ID=...
EIMS_CLIENT_SECRET=...
EIMS_API_KEY=...

# optional
EIMS_RETRY_TIMES=2
EIMS_RETRY_DELAY_MS=500
EIMS_LOGGING=false
EIMS_LOG_CHANNEL=
EIMS_SELLER_RESOLVER=App\Eims\StoreSellerResolver
```

Resolve a client for a taxpayer via the facade — global credentials and the cache are wired for you; you supply the per-taxpayer `SellerConfig`:

```
use Mattdi\Eims\Laravel\Facades\Eims;
use Mattdi\Eims\Config\SellerConfig;

$client = Eims::for(
    new SellerConfig(tin: $store->tin, legalName: $store->legal_name, /* ... */),
    cacheNamespace: "store_{$store->id}",   // keeps tokens isolated per taxpayer
);

$result = $client->register($invoiceRequest);
```

### Resolving the seller from your domain

[](#resolving-the-seller-from-your-domain)

Implement `Mattdi\Eims\Contracts\SellerConfigResolver` and register its class via `EIMS_SELLER_RESOLVER` (or `config('eims.seller_resolver')`). Then call `Eims::client($key)` and the package builds the `SellerConfig` for you — e.g. from the current tenant/store:

```
use Mattdi\Eims\Contracts\SellerConfigResolver;
use Mattdi\Eims\Config\SellerConfig;

class StoreSellerResolver implements SellerConfigResolver
{
    public function resolve(mixed $key = null): SellerConfig
    {
        $store = Store::findOrFail($key); // your model

        return new SellerConfig(
            tin: $store->tin,
            legalName: $store->legal_name,
            regionCode: $store->region_code,
            city: $store->city,
        );
    }

    public function cacheNamespace(mixed $key = null): ?string
    {
        return 'store_' . $key; // isolates auth tokens per taxpayer
    }
}

// Elsewhere:
$client = Eims::client($store->id);
```

### Artisan commands

[](#artisan-commands)

```
php artisan eims:test-connection [key]   # authenticate to verify credentials
php artisan eims:verify {irn} [key]       # verify an invoice by IRN
php artisan eims:status {irn} [key]       # fetch registration status
```

`[key]` is forwarded to your `SellerConfigResolver` (omitted when using `Eims::for()` directly).

Operations
----------

[](#operations)

MethodEndpointReturns`register(InvoiceRequest)``POST /v1/register``RegisterResult``cancel(irn, reasonCode, remark)``POST /v1/cancel``CancelResult``verify(irn)``GET /v1/verify/``VerifyResult``bulkVerify(irns[])`loops `verify``array``registerReceipt(ReceiptRequest)``POST /v1/receipt/sales``ReceiptResult``status(irn)``GET /v1/invoices/status/{irn}``StatusResult`### Enums

[](#enums)

- `CancellationReason` (`Duplicate`, `DataEntryError`, `OrderCancelled`, `Other`) can be passed to `cancel()` instead of a raw string.
- `InvoiceStatus` (`Pending`, `Registered`, `Failed`, `Cancelled`, `Unknown`) with `InvoiceStatus::fromString()` and predicates `isRegistered()` / `isCancelled()`. Read it from `StatusResult::statusEnum()`.

```
use Mattdi\Eims\Enums\{CancellationReason, InvoiceStatus};

$client->cancel($irn, CancellationReason::Duplicate, 'issued in error');
$status = $client->status($irn);
if ($status->statusEnum()->isCancelled()) { /* ... */ }
```

Error handling
--------------

[](#error-handling)

```
use Mattdi\Eims\Exceptions\{EimsApiException, EimsAuthenticationException, EimsConnectionException};

try {
    $result = $client->register($invoice);
} catch (EimsAuthenticationException $e) {
    // Bad/expired credentials
} catch (EimsApiException $e) {
    // EIMS rejected the request: $e->getMessage(), $e->body, $e->statusCode
} catch (EimsConnectionException $e) {
    // Network/transport failure or non-JSON response
}
```

Multi-tenant note
-----------------

[](#multi-tenant-note)

The client is stateless per instance and represents a single taxpayer. To serve many taxpayers (e.g. a chain of stores), create one client per taxpayer and pass a unique `cacheNamespace` so their auth tokens never collide. The package never needs to know about your "store"/"branch"/tenant concepts.

Bring your own persistence
--------------------------

[](#bring-your-own-persistence)

The client returns result objects only. Store the IRN, QR code, signed invoice, and raw responses in whatever table/model your app uses.

Signing / encryption (experimental)
-----------------------------------

[](#signing--encryption-experimental)

The current EIMS register flow accepts plain JSON. `Support\Canonicalizer` and `Support\Signer` are provided for integrations issued signing keys, but are not wired into the request flow.

License
-------

[](#license)

MIT

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance90

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity47

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

Total

2

Last Release

48d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/52673343?v=4)[Matt](/maintainers/Matt-di)[@Matt-di](https://github.com/Matt-di)

---

Top Contributors

[![Matt-di](https://avatars.githubusercontent.com/u/52673343?v=4)](https://github.com/Matt-di "Matt-di (5 commits)")

---

Tags

laravelinvoiceE-InvoicetaxEthiopiamoreims

###  Code Quality

TestsPest

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/mattdi-ethiopia-eims/health.svg)

```
[![Health](https://phpackages.com/badges/mattdi-ethiopia-eims/health.svg)](https://phpackages.com/packages/mattdi-ethiopia-eims)
```

###  Alternatives

[laravel/framework

The Laravel Framework.

35.4k569.8M21.9k](/packages/laravel-framework)[civicrm/civicrm-core

Open source constituent relationship management for non-profits, NGOs and advocacy organizations.

769306.5k56](/packages/civicrm-civicrm-core)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)[nutgram/nutgram

The Telegram bot library that doesn't drive you nuts

744343.6k8](/packages/nutgram-nutgram)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

605.9M711](/packages/shopware-core)[tempest/framework

The PHP framework that gets out of your way.

2.3k42.4k21](/packages/tempest-framework)

PHPackages © 2026

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