mattdi/ethiopia-eims 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

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 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

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

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-13

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固