mattdi/ethiopia-eims
Composer 安装命令:
composer require mattdi/ethiopia-eims
包简介
Framework-agnostic PHP client for the Ethiopian Ministry of Revenue Electronic Invoice Management System (EIMS), with an optional Laravel bridge.
README 文档
README
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
- PHP 8.2+
guzzlehttp/guzzle^7.5- A PSR-16 cache (optional, for token caching) — Laravel's cache works out of the box.
Installation
composer require mattdi/ethiopia-eims
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
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 & retries
Pass a 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
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
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
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
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
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
| Method | Endpoint | Returns |
|---|---|---|
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<irn, VerifyResult|EimsException> |
registerReceipt(ReceiptRequest) |
POST /v1/receipt/sales |
ReceiptResult |
status(irn) |
GET /v1/invoices/status/{irn} |
StatusResult |
Enums
CancellationReason(Duplicate,DataEntryError,OrderCancelled,Other) can be passed tocancel()instead of a raw string.InvoiceStatus(Pending,Registered,Failed,Cancelled,Unknown) withInvoiceStatus::fromString()and predicatesisRegistered()/isCancelled(). Read it fromStatusResult::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
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
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
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)
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
MIT
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-13