Skip to content

eTIMS

Location: dg_smart_pos_api/modules/Etims. Handles Kenya Revenue Authority (KRA) eTIMS fiscal reporting: mapping products to eTIMS classification/tax codes, submitting paid sales to KRA through the third-party DigiTax API, and tracking submission status. It reads from Inventory (NewStock, categories) and writes fiscal state onto Sales’ own Sale model rather than owning a submission model of its own; see below.

Etims is enabled in config/modules.php and is a tenant route module, required directly from routes/tenant.php (require base_path('modules/Etims/Routes/api.php')), registered once, not duplicated the way Sales’ routes are. config/modules.php records its dependencies as ['Inventory', 'ReceiptSettings', 'Sales'].

  • EtimsStoreConfig, per-store settings: is_enabled, submission_mode (EtimsSubmissionMode enum), is_vat_registered, branch_id (the KRA branch code).
  • EtimsItemMapping, per-product fiscal mapping: stock_id (belongs to NewStock), classification_code, tax_category, origin_country, package_unit, quantity_unit, plus sync tracking (etims_item_id, last_sync_status via ItemSyncStatus enum, last_synced_at, sync_error). NewStock::etimsMapping() is a hasOne back-reference defined on the Inventory model itself.
  • EtimsCategoryMapping, the same fiscal fields as above, but per stock category rather than per item; used to fill in an item’s mapping when it has none. EtimsCategoryMapping::resolveForStock() implements the actual priority order: category mapping, then tenant-wide defaults, then a hardcoded fallback (SYSTEM_DEFAULTS, e.g. tax category B = 16% VAT standard-rated, origin KE).
  • EtimsDefaultSetting, the tenant-wide fallback row used when an item has no category or the category has no mapping (EtimsDefaultSetting::resolve()), itself falling back to a hardcoded FALLBACK constant if no row exists yet.

Item mapping and sync (EtimsItemController)

Section titled “Item mapping and sync (EtimsItemController)”

index returns every NewStock with its EtimsItemMapping (or an unsaved placeholder if none exists yet). update edits an item’s mapping fields directly (firstOrCreate by stock_id). sync/syncAll push one or all unsynced items to DigiTax: both resolve a config via EtimsSubmissionService::getTenantConfig() (aborting 422 if eTIMS isn’t configured/enabled for the tenant), firstOrCreate a mapping pre-filled from EtimsCategoryMapping::resolveForStock() if none exists, then call EtimsSubmissionService::syncItem(). syncAll skips items whose mapping isSynced() already, and collects per-item errors rather than aborting the whole batch on one failure.

Category and default mappings (EtimsCategoryController)

Section titled “Category and default mappings (EtimsCategoryController)”

Standard CRUD over EtimsCategoryMapping (index, update, bulkUpdate) plus defaults/updateDefaults for the tenant-wide EtimsDefaultSetting row used when an item’s category has no mapping of its own.

show returns (and lazily creates, defaulted is_enabled: false) the store’s EtimsStoreConfig. update writes branch_id, submission_mode, and is_vat_registered.

index/show list/read sales that have a non-null etims_status, ordered by etims_submitted_at. submit manually submits one paid sale (SaleStatus::Paid required; refuses a sale already submitted unless its prior attempt failed) by setting submit_to_etims = true and dispatching SubmitSaleToEtimsJob. retry re-dispatches the same job for a sale whose isEtimsFailed() is true, after clearing its error state. bulkSubmit does the same for every paid, not-yet-submitted sale in a store in one call.

Submission is not purely manual: Modules\Sales\Jobs\ProcessSaleStockJob (the job that runs after a sale’s stock has been deducted) dispatches SubmitSaleToEtimsJob itself whenever $sale->submit_to_etims is true, falling back to GenerateReceiptJob (skipping eTIMS) otherwise. So a store with eTIMS enabled and configured for automatic submission has sales flow straight from stock deduction into an eTIMS submission attempt, with the controller endpoints above serving as the manual/retry/bulk path for whatever didn’t go automatically or failed.

POST /api/etims/callback/{tenantId} is registered withoutMiddleware(['auth:sanctum']) since DigiTax calls it directly, not a logged-in user. It resolves the Tenant by the ID in the URL and runs the handler inside $tenant->run(...) to get proper tenant-DB context, then delegates to EtimsSubmissionService::handleSaleCallback() for a sale.sync event (any other event is acknowledged and ignored). On internal failure it still returns HTTP 200 with status: false, deliberately, so DigiTax doesn’t retry indefinitely against a tenant-not-found or transient error.

Registered via modules/Etims/Routes/api.php (required once from routes/tenant.php, no duplicate registration found elsewhere):

Method Path Controller method Auth
POST etims/callback/{tenantId} EtimsCallbackController@handle none (withoutMiddleware(['auth:sanctum']))
GET etims/stores/{store}/config EtimsStoreConfigController@show auth:sanctum
PUT etims/stores/{store}/config EtimsStoreConfigController@update auth:sanctum
GET etims/stores/{store}/items EtimsItemController@index auth:sanctum
POST etims/stores/{store}/items/sync-all EtimsItemController@syncAll auth:sanctum
PUT etims/stores/{store}/items/{stock} EtimsItemController@update auth:sanctum
POST etims/stores/{store}/items/{stock}/sync EtimsItemController@sync auth:sanctum
GET etims/stores/{store}/submissions EtimsSubmissionController@index auth:sanctum
GET etims/stores/{store}/submissions/{sale} EtimsSubmissionController@show auth:sanctum
POST etims/stores/{store}/submissions/{sale}/retry EtimsSubmissionController@retry auth:sanctum
POST etims/stores/{store}/submissions/bulk-submit EtimsSubmissionController@bulkSubmit auth:sanctum
POST etims/stores/{store}/sales/{sale}/submit EtimsSubmissionController@submit auth:sanctum
GET etims/stores/{store}/categories EtimsCategoryController@index auth:sanctum
PUT etims/stores/{store}/categories/{category} EtimsCategoryController@update auth:sanctum
PUT etims/stores/{store}/categories-bulk EtimsCategoryController@bulkUpdate auth:sanctum
GET etims/stores/{store}/defaults EtimsCategoryController@defaults auth:sanctum
PUT etims/stores/{store}/defaults EtimsCategoryController@updateDefaults auth:sanctum

Central-level, per-tenant eTIMS configuration (the DigiTax credentials, enable/disable for the whole tenant) is a separate surface entirely: routes/admin.php registers etims, etims/enable, etims/disable, etims/stores, etims/stores/{store} against App\Http\Controllers\Admin\AdminEtimsController, which operates on the central App\Models\EtimsConfig model, not anything in this module. That’s the Admin app’s territory, not a tenant-facing route.

  • Inventory: NewStock/category source for item and category mappings
  • Sales: owns the Sale model and the etims_* columns this module reads/writes; ProcessSaleStockJob triggers automatic submission
  • ReceiptSettings
  • Tax