flavytech/laravel-etims

Production-grade Laravel SDK for Kenya's KRA eTIMS / Gava Connect API. Handles invoice submission, queue-based sync, retries, multi-tenancy, and offline resilience.

Maintainers

Package info

github.com/flavian-ndunda/laravel-etims

pkg:composer/flavytech/laravel-etims

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.2 2026-05-26 19:10 UTC

This package is auto-updated.

Last update: 2026-05-26 19:11:59 UTC


README

Latest Version PHP Version Laravel Version Tests License

Production-grade Laravel SDK for Kenya's KRA eTIMS / Gava Connect API. Built for POS systems, ERP platforms, and SaaS products operating in Kenya and East Africa.

Why This SDK?

Integrating with KRA eTIMS is non-trivial in real production environments:

  • The API goes down. Your invoices must not be lost.
  • Networks in Kenya are intermittent. Retries must be smart, not dumb.
  • POS systems process hundreds of transactions per hour. Your queue must be resilient.
  • You may serve multiple businesses from one Laravel installation. Multi-tenancy must be first-class.

This SDK handles all of that so you can focus on your product.

Features

Feature Status
Invoice submission (sync + async) ✅ Phase 1
Auth token management + auto-refresh ✅ Phase 1
Exponential backoff retry ✅ Phase 1
Idempotency protection ✅ Phase 1
Audit trail (DB) ✅ Phase 1
Laravel event system integration ✅ Phase 1
Sandbox/production mode switching ✅ Phase 1
Testing fake + assertion API ✅ Phase 1
KRA PIN validation ✅ Phase 1
Dead letter queue + failed invoice recovery ✅ Phase 1
Multi-tenant SaaS support ✅ Phase 1
Stock synchronization 🔜 Phase 2
Credit/debit note handling 🔜 Phase 2
Webhook handling 🔜 Phase 2
QR code + thermal receipt support 🔜 Phase 3
Branch management 🔜 Phase 3

Installation

composer require flavytech/laravel-etims

Publish the configuration and migrations:

php artisan vendor:publish --tag=etims-config
php artisan vendor:publish --tag=etims-migrations
php artisan migrate

Configuration

Add these variables to your .env file:

# Mode: sandbox | production
ETIMS_MODE=sandbox

# Your KRA-issued credentials
ETIMS_PIN=P000000000A
ETIMS_BRANCH_ID=00
ETIMS_DEVICE_SERIAL=your-device-serial
ETIMS_SECRET=your-api-secret

# Queue (recommended: use Redis)
ETIMS_QUEUE_CONNECTION=redis
ETIMS_QUEUE_NAME=etims
ETIMS_MAX_TRIES=5

# Optional tuning
ETIMS_TIMEOUT=30
ETIMS_LOG_CHANNEL=daily

Never commit real credentials to version control. Use .env only.

Quick Start

Submitting an Invoice Synchronously

Use this when you need an immediate KRA response (e.g. a POS checkout flow):

use Flavytech\Etims\Facades\Etims;
use Flavytech\Etims\DTOs\InvoiceDTO;
use Flavytech\Etims\DTOs\InvoiceLineDTO;

$invoice = InvoiceDTO::make([
    'invoice_number' => 'INV-2024-001',
    'supplier_pin'   => config('etims.credentials.pin'),
    'buyer_pin'      => 'P000000000B',    // buyer's KRA PIN
    'buyer_name'     => 'Acme Ltd',
    'total_amount'   => 11600.00,
    'vat_amount'     => 1600.00,
    'taxable_amount' => 10000.00,
    'invoice_date'   => now()->toDateString(),
    'invoice_type'   => 'S',              // S = Sale
    'payment_type'   => 'CASH',
    'items'          => [
        InvoiceLineDTO::make([
            'item_number'    => 1,
            'item_code'      => 'ITEM-001',
            'item_name'      => 'Widget Pro',
            'quantity'       => 2,
            'unit_price'     => 5000.00,
            'taxable_amount' => 10000.00,
            'vat_amount'     => 1600.00,
            'total_amount'   => 11600.00,
            'tax_type_code'  => 'A',       // A = Standard 16% VAT
        ]),
    ],
]);

$response = Etims::submitInvoice($invoice);

if ($response->isSuccessful()) {
    // Print the KRA receipt number and QR code
    $receiptNumber = $response->receiptNumber;
    $qrCode        = $response->qrCode;
}

Queuing an Invoice (Recommended for Most Cases)

This is the preferred approach for production. The invoice is dispatched to a durable background job with automatic retries:

// Fire and forget — returns immediately
Etims::queueInvoice($invoice);

// Your application continues while KRA processes in the background.
// Listen to events to know the outcome:

Listen to the outcome events in your EventServiceProvider:

protected $listen = [
    \Flavytech\Etims\Events\InvoiceSubmitted::class => [
        \App\Listeners\GenerateKraReceipt::class,    // generate receipt PDF
        \App\Listeners\UpdateOrderStatus::class,     // mark order as fiscalized
    ],
    \Flavytech\Etims\Events\InvoiceFailed::class => [
        \App\Listeners\AlertOperationsTeam::class,   // notify via Slack/email
        \App\Listeners\FlagOrderForReview::class,    // mark in your system
    ],
    \Flavytech\Etims\Events\InvoiceQueued::class => [
        \App\Listeners\ShowPendingStatus::class,     // update POS UI immediately
    ],
];

Validating a KRA PIN

$result = Etims::validatePin('P000000000B');

if ($result->isValid()) {
    echo "Buyer: {$result->taxpayerName}";
} else {
    // PIN not found in KRA registry
    return back()->withError('Invalid buyer PIN. Please verify and try again.');
}

Checking Invoice Status

$status = Etims::getInvoiceStatus('INV-2024-001');

if ($status->isSuccessful()) {
    echo "Receipt: {$status->receiptNumber}";
} elseif ($status->isPending()) {
    echo "KRA is still processing this invoice.";
}

Working with Failed Invoices

// Get all permanently failed invoices for review
$failedInvoices = Etims::failedInvoices();

foreach ($failedInvoices as $invoice) {
    echo "{$invoice->invoice_number}: {$invoice->failure_reason}";
}

// Re-queue a failed invoice after fixing the issue
Etims::retryFailedInvoice($invoice->id);

Invoice Types

Code Description Use Case
S Sale Standard sales invoice
C Credit Note Refund / reversal of a sale
D Debit Note Additional charge on a sale

Tax Type Codes

Code Description VAT Rate
A Standard rated 16%
B Zero rated 0%
C VAT exempt N/A
D Non-VATable N/A
E Excisable (with VAT) 16% + Excise

Payment Type Codes

Code Description
CASH Cash payment
CREDIT Credit terms
MPESA M-Pesa mobile money
BANK Bank transfer
CHEQUE Cheque
OTHER Other payment methods

Multi-Tenant SaaS Setup

When serving multiple KRA-registered businesses from one Laravel installation:

Step 1: Enable multi-tenancy in config:

// config/etims.php
'multi_tenancy' => [
    'enabled'         => true,
    'tenant_resolver' => \App\Services\EtimsTenantResolver::class,
],

Step 2: Implement the TenantResolverContract:

use Flavytech\Etims\Contracts\TenantResolverContract;

class EtimsTenantResolver implements TenantResolverContract
{
    public function resolve(): array
    {
        // Resolve from your tenancy system
        $tenant = app('currentTenant'); // e.g. spatie/laravel-multitenancy

        return [
            'pin'           => $tenant->kra_pin,
            'branch_id'     => $tenant->etims_branch_id,
            'device_serial' => $tenant->etims_device_serial,
            'secret'        => decrypt($tenant->etims_secret), // store encrypted!
            'mode'          => $tenant->etims_mode,            // per-tenant sandbox/prod
        ];
    }

    public function tenantId(): string|int
    {
        return app('currentTenant')->id;
    }
}

Step 3: Bind it in your AppServiceProvider:

$this->app->bind(
    \Flavytech\Etims\Contracts\TenantResolverContract::class,
    \App\Services\EtimsTenantResolver::class
);

That's it. Every SDK call now automatically uses the correct credentials for the active tenant.

Testing

The SDK provides a first-class fake for testing without any real HTTP calls.

use Flavytech\Etims\Facades\Etims;

beforeEach(function () {
    Etims::fake();
});

it('fiscalizes an order on checkout', function () {
    $order = Order::factory()->create(['total' => 11600]);

    $this->post("/orders/{$order->id}/checkout");

    Etims::assertInvoiceSubmitted("INV-{$order->id}");
});

it('queues the invoice for background processing', function () {
    Queue::fake();

    Etims::queueInvoice(makeTestInvoice('INV-001'));

    Etims::assertInvoiceQueued('INV-001');
});

it('handles KRA downtime gracefully', function () {
    Etims::fake()->failWith(
        new \Flavytech\Etims\Exceptions\EtimsApiException('KRA is down', 503)
    );

    // Your app should still return 200 — it queues for retry
    $this->post('/checkout/1')->assertStatus(202);
});

it('validates correct invoice data', function () {
    $fake = Etims::fake();

    $this->post('/checkout/1');

    $fake->assertSubmittedMatching(
        fn($invoice) => $invoice->totalAmount === 11600.00
            && $invoice->invoiceType === 'S'
    );
});

it('rejects an invalid buyer PIN', function () {
    Etims::fake()->withInvalidPins(['P999999999Z']);

    $response = Etims::validatePin('P999999999Z');

    expect($response->isValid())->toBeFalse();
});

Stubbing Specific Responses

use Flavytech\Etims\DTOs\InvoiceResponseDTO;

$stubbedResponse = InvoiceResponseDTO::fromKraResponse([
    'resultCd' => '000',
    'resultMsg' => 'OK',
    'data' => [
        'rcptNo'    => 'RCPT-MY-TEST',
        'qrCodeUrl' => 'https://test.kra.go.ke/qr/test',
    ],
]);

Etims::fake()->respondTo('INV-SPECIFIC-001', $stubbedResponse);

$response = Etims::submitInvoice($invoice); // returns RCPT-MY-TEST

Running the SDK Test Suite

composer test
composer test-coverage
composer analyse   # PHPStan static analysis
composer format    # PHP CS Fixer

Queue Worker Setup

Run a dedicated worker for the eTIMS queue in production:

# Supervisor config for dedicated eTIMS worker
php artisan queue:work redis \
    --queue=etims \
    --tries=5 \
    --backoff=10,30,60,120,300 \
    --timeout=60 \
    --sleep=3

For failed job monitoring:

# View failed eTIMS jobs
php artisan queue:failed | grep etims

# Retry all failed jobs
php artisan queue:retry all

Architecture Overview

Facade (Etims::)
    └── EtimsManager         (orchestration, idempotency, events, multi-tenancy)
            └── EtimsClient  (API contract implementation)
                    └── EtimsHttpClient  (HTTP, auth tokens, retries, logging)
                                └── KRA Gava Connect API

Async path:
    Etims::queueInvoice() → SubmitInvoiceJob → Queue Worker → EtimsClient

Error Handling Reference

Exception Cause Retryable
EtimsApiException KRA API error or network failure Depends on HTTP status
EtimsAuthException Invalid credentials or expired token No — fix credentials
EtimsValidationException Invalid DTO data (client-side) No — fix data
EtimsIdempotencyException Duplicate invoice detected No — already submitted
EtimsConfigException Missing or invalid SDK config No — fix config

Changelog

See CHANGELOG.md.

Contributing

Contributions welcome. Please read CONTRIBUTING.md first.

License

MIT. See LICENSE.

Built with care for the Kenyan and East African developer ecosystem. 🇰🇪