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'].
Data model
Section titled “Data model”EtimsStoreConfig, per-store settings:is_enabled,submission_mode(EtimsSubmissionModeenum),is_vat_registered,branch_id(the KRA branch code).EtimsItemMapping, per-product fiscal mapping:stock_id(belongs toNewStock),classification_code,tax_category,origin_country,package_unit,quantity_unit, plus sync tracking (etims_item_id,last_sync_statusviaItemSyncStatusenum,last_synced_at,sync_error).NewStock::etimsMapping()is ahasOneback-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 categoryB= 16% VAT standard-rated, originKE).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 hardcodedFALLBACKconstant 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.
Store config (EtimsStoreConfigController)
Section titled “Store config (EtimsStoreConfigController)”show returns (and lazily creates, defaulted is_enabled: false) the store’s EtimsStoreConfig. update writes branch_id, submission_mode, and is_vat_registered.
Submission (EtimsSubmissionController)
Section titled “Submission (EtimsSubmissionController)”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.
Callback (EtimsCallbackController)
Section titled “Callback (EtimsCallbackController)”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.
Route surface
Section titled “Route surface”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.
Related
Section titled “Related”- Inventory:
NewStock/category source for item and category mappings - Sales: owns the
Salemodel and theetims_*columns this module reads/writes;ProcessSaleStockJobtriggers automatic submission - ReceiptSettings
- Tax