kra-etims-sdk 0.1.1__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/PKG-INFO +120 -252
  2. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/README.md +119 -251
  3. kra_etims_sdk-0.1.3/kra_etims_sdk/base_client.py +149 -0
  4. kra_etims_sdk-0.1.3/kra_etims_sdk/client.py +95 -0
  5. kra_etims_sdk-0.1.3/kra_etims_sdk/schemas.py +308 -0
  6. kra_etims_sdk-0.1.3/kra_etims_sdk/validator.py +21 -0
  7. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/PKG-INFO +120 -252
  8. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/pyproject.toml +1 -1
  9. kra_etims_sdk-0.1.3/tests/test_etims.py +543 -0
  10. kra_etims_sdk-0.1.1/kra_etims_sdk/base_client.py +0 -67
  11. kra_etims_sdk-0.1.1/kra_etims_sdk/client.py +0 -106
  12. kra_etims_sdk-0.1.1/kra_etims_sdk/schemas.py +0 -212
  13. kra_etims_sdk-0.1.1/kra_etims_sdk/validator.py +0 -47
  14. kra_etims_sdk-0.1.1/tests/test_etims.py +0 -222
  15. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/LICENSE +0 -0
  16. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk/__init__.py +0 -0
  17. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk/auth.py +0 -0
  18. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk/exceptions.py +0 -0
  19. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/SOURCES.txt +0 -0
  20. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/dependency_links.txt +0 -0
  21. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/requires.txt +0 -0
  22. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/top_level.txt +0 -0
  23. {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kra-etims-sdk
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Python SDK for KRA eTIMS OSCU API
5
5
  Author: Emmanuel Bartile
6
6
  License: MIT
@@ -39,7 +39,7 @@ Dynamic: license-file
39
39
  ![Postman Compliant](https://img.shields.io/badge/Postman-Compliant-FF6C37?logo=postman)
40
40
  ![Pytest Tested](https://img.shields.io/badge/Tests-Pytest-3776AB?logo=pytest)
41
41
 
42
- A production-ready **Python SDK** for integrating with the Kenya Revenue Authority (KRA) **eTIMS OSCU** (Online Sales Control Unit) API. Built to match the official Postman collection specifications with strict header compliance, token management, and comprehensive Pydantic validation.
42
+ A production-ready **Python SDK** for integrating with the Kenya Revenue Authority (KRA) **eTIMS OSCU** (Online Sales Control Unit) API. Built to match the official Postman collection specifications with strict header compliance, token management, and comprehensive payload validation.
43
43
 
44
44
  > ⚠️ **Critical Note**: This SDK implements the **new OSCU specification** (KRA-hosted), *not* the VSCU eTIMS API. OSCU requires device registration, headers, and `cmcKey` lifecycle management.
45
45
 
@@ -79,17 +79,6 @@ KRA's **Electronic Tax Invoice Management System (eTIMS)** uses **OSCU** (Online
79
79
  - Communication key (`cmcKey`) lifecycle management
80
80
  - Strict payload schema compliance per KRA specifications
81
81
 
82
- ### OSCU vs VSCU eTIMS
83
-
84
- | Feature | OSCU (This SDK) | VSCU eTIMS |
85
- |---------|-----------------|--------------|
86
- | **Hosting** | KRA-hosted (cloud) | Self-hosted (on-premise) |
87
- | **Device Registration** | Mandatory pre-registration | Not required |
88
- | **Authentication** | Bearer token | Basic auth only |
89
- | **Communication Key** | `cmcKey` required after init | Not applicable |
90
- | **API Base URL** | `sbx.kra.go.ke/etims-oscu/api/v1` | `etims-api-sbx.kra.go.ke` |
91
- | **Header Requirements** | Strict 6-header compliance | Minimal headers |
92
-
93
82
  ### Receipt Types & Labels Matrix
94
83
 
95
84
  Each receipt is formed from a combination of receipt type and transaction type:
@@ -191,7 +180,8 @@ flowchart TD
191
180
  Before integration, you **MUST** complete these prerequisites:
192
181
 
193
182
  ### 1. Device Registration (MANDATORY)
194
- - Register OSCU device via [eTIMS Taxpayer Sandbox Portal](https://sbx.kra.go.ke)
183
+ - Register OSCU device via [eTIMS Taxpayer Production Portal](https://etims.kra.go.ke)
184
+ - Register OSCU device via [eTIMS Taxpayer Sandbox Portal](https://etims-sbx.kra.go.ke)
195
185
  - Obtain **approved device serial number** (`dvcSrlNo`)
196
186
  - ⚠️ **Dynamic/unregistered device serials fail with `resultCd: 901`** ("It is not valid device")
197
187
 
@@ -199,24 +189,23 @@ Before integration, you **MUST** complete these prerequisites:
199
189
  ```python
200
190
  # 1. Initialize FIRST (returns cmcKey)
201
191
  response = etims.select_init_osdc_info({
202
- "tin": config.oscu["tin"],
203
- "bhfId": config.oscu["bhf_id"],
204
- "dvcSrlNo": "dvcv1130", # KRA-approved serial
192
+ "tin": config["oscu"]["tin"],
193
+ "bhfId": config["oscu"]["bhf_id"],
194
+ "dvcSrlNo": config["oscu"]["device_serial"], # KRA-approved serial
205
195
  })
206
196
 
207
- # 2. Extract cmcKey (sandbox returns at root level)
208
- cmc_key = response.get("cmcKey") or response.get("data", {}).get("cmcKey")
197
+ # 2. Extract cmcKey
198
+ cmc_key = response.get("cmcKey")
209
199
 
210
200
  # 3. Update config IMMEDIATELY
211
- config.oscu["cmc_key"] = cmc_key
201
+ config["oscu"]["cmc_key"] = cmc_key
212
202
 
213
203
  # 4. Recreate client with updated config (critical!)
214
204
  etims = EtimsClient(config, auth)
215
-
216
- # 5. ALL subsequent requests require cmcKey in headers
217
- etims.select_code_list(...)
218
205
  ```
219
206
 
207
+ > 🔔 Note: `cmcKey` is only required for certain write operations (branch/user/insurance), not all endpoints.
208
+
220
209
  ### 3. Invoice Numbering Rules
221
210
  - **MUST be sequential integers** (1, 2, 3...) – **NOT strings** (`INV001`)
222
211
  - Must be unique per branch office (`bhfId`)
@@ -268,94 +257,52 @@ etims.select_code_list(...)
268
257
 
269
258
  ```bash
270
259
  pip install kra-etims-sdk
271
- # OR with dev dependencies
272
- pip install "kra-etims-sdk[dev]"
273
260
  ```
274
261
 
275
262
  ### Requirements
276
263
  - Python 3.9+
277
264
  - `requests` (≥2.31)
278
265
  - `pydantic` (≥2.0)
279
- - `typing-extensions` (for Python <3.10)
266
+
267
+ > 💡 The SDK uses plain dictionaries for configuration — no custom config class required.
280
268
 
281
269
  ---
282
270
 
283
271
  ## Configuration
284
272
 
273
+ Define your config as a **plain Python dictionary**:
274
+
285
275
  ```python
286
- from kra_etims_sdk import KraEtimsConfig
287
276
  import os
288
-
289
- config = KraEtimsConfig(
290
- env="sandbox", # "sandbox" | "production"
291
- cache_file="./.kra_token.json",
292
-
293
- auth={
294
- "sandbox": {
295
- "token_url": "https://sbx.kra.go.ke/v1/token/generate".strip(),
296
- "consumer_key": os.environ["KRA_CONSUMER_KEY"],
297
- "consumer_secret": os.environ["KRA_CONSUMER_SECRET"],
298
- },
299
- "production": {
300
- "token_url": "https://kra.go.ke/v1/token/generate".strip(),
301
- "consumer_key": os.environ["KRA_PROD_CONSUMER_KEY"],
302
- "consumer_secret": os.environ["KRA_PROD_CONSUMER_SECRET"],
277
+ import tempfile
278
+
279
+ config = {
280
+ 'env': 'sbx', # 'sbx' = sandbox, 'prod' = production
281
+ 'cache_file': os.path.join(tempfile.gettempdir(), 'kra_etims_token.json'),
282
+ 'auth': {
283
+ 'sbx': {
284
+ 'token_url': 'https://sbx.kra.go.ke/v1/token/generate',
285
+ 'consumer_key': os.getenv('KRA_CONSUMER_KEY'),
286
+ 'consumer_secret': os.getenv('KRA_CONSUMER_SECRET'),
303
287
  }
304
288
  },
305
-
306
- api={
307
- "sandbox": {"base_url": "https://sbx.kra.go.ke/etims-oscu/api/v1".strip()},
308
- "production": {"base_url": "https://api.developer.go.ke/etims-oscu/api/v1".strip()}
309
- },
310
-
311
- oscu={
312
- "tin": os.environ["KRA_TIN"],
313
- "bhf_id": os.environ["KRA_BHF_ID"],
314
- "cmc_key": "", # Set AFTER initialization
315
- },
316
-
317
- endpoints={
318
- # INITIALIZATION (ONLY endpoint without tin/bhfId/cmcKey headers)
319
- "selectInitOsdcInfo": "/selectInitOsdcInfo",
320
-
321
- # DATA MANAGEMENT
322
- "selectCodeList": "/selectCodeList",
323
- "selectItemClsList": "/selectItemClass",
324
- "selectBhfList": "/branchList",
325
- "selectTaxpayerInfo": "/selectTaxpayerInfo",
326
- "selectCustomerList": "/selectCustomerList",
327
- "selectNoticeList": "/selectNoticeList",
328
-
329
- # BRANCH MANAGEMENT
330
- "branchInsuranceInfo": "/branchInsuranceInfo",
331
- "branchUserAccount": "/branchUserAccount",
332
- "branchSendCustomerInfo": "/branchSendCustomerInfo",
333
-
334
- # ITEM MANAGEMENT
335
- "saveItem": "/saveItem",
336
- "itemInfo": "/itemInfo",
337
-
338
- # PURCHASE MANAGEMENT
339
- "selectPurchaseTrns": "/getPurchaseTransactionInfo",
340
- "sendPurchaseTransactionInfo": "/sendPurchaseTransactionInfo",
341
-
342
- # SALES MANAGEMENT
343
- "sendSalesTransaction": "/sendSalesTransaction",
344
- "selectSalesTrns": "/selectSalesTransactions",
345
- "selectInvoiceDetail": "/selectInvoiceDetail",
346
-
347
- # STOCK MANAGEMENT (NESTED PATHS - CRITICAL)
348
- "insertStockIO": "/insert/stockIO", # ← slash in path
349
- "saveStockMaster": "/save/stockMaster", # ← slash in path
350
- "selectMoveList": "/selectStockMoveLists",
289
+ 'api': {
290
+ 'sbx': {'base_url': 'https://etims-api-sbx.kra.go.ke/etims-api'}
351
291
  },
352
-
353
- http={"timeout": 30}
354
- )
292
+ 'http': {'timeout': 30},
293
+ 'oscu': {
294
+ 'tin': os.getenv('KRA_TIN'),
295
+ 'bhf_id': os.getenv('KRA_BHF_ID') or '01',
296
+ 'device_serial': os.getenv('DEVICE_SERIAL'),
297
+ 'cmc_key': '', # populated after initialization
298
+ }
299
+ }
355
300
  ```
356
301
 
357
- > 💡 **Production URL Note**:
358
- > Production base URL is `https://api.developer.go.ke/etims-oscu/api/v1` (NOT `kra.go.ke`)
302
+ > **Required environment variables**:
303
+ > `KRA_CONSUMER_KEY`, `KRA_CONSUMER_SECRET`, `KRA_TIN`, `DEVICE_SERIAL`
304
+ >
305
+ > ⚠️ **Never include trailing spaces in URLs** — they cause silent connection failures.
359
306
 
360
307
  ---
361
308
 
@@ -363,164 +310,109 @@ config = KraEtimsConfig(
363
310
 
364
311
  ### Step 1: Initialize SDK
365
312
  ```python
366
- from kra_etims_sdk import AuthClient, EtimsClient
367
- from kra_etims_sdk.exceptions import ApiException, ValidationException
313
+ from kra_etims_sdk.auth import AuthClient
314
+ from kra_etims_sdk.client import EtimsClient
368
315
 
369
316
  auth = AuthClient(config)
370
317
  etims = EtimsClient(config, auth)
371
318
  ```
372
319
 
373
- ### Step 2: Authenticate (Get Access Token)
320
+ ### Step 2: Authenticate
374
321
  ```python
375
322
  try:
376
- # Force fresh token (optional - cache used by default)
377
- token = auth.token(force_refresh=True)
378
- print(f"✅ Token acquired: {token[:20]}...")
379
- except AuthenticationException as e:
323
+ auth.forget_token() # Clear cached token
324
+ token = auth.token(force=True)
325
+ print(f"✅ Token OK: {token[:25]}...")
326
+ except Exception as e:
380
327
  print(f"❌ Authentication failed: {e}")
381
328
  exit(1)
382
329
  ```
383
330
 
384
- ### Step 3: OSCU Initialization (Critical Step)
331
+ ### Step 3: OSCU Initialization (If Needed)
332
+ > Only required for `cmcKey`-dependent operations (e.g., saving branch users or insurance).
333
+
385
334
  ```python
386
335
  try:
387
- # ⚠️ MUST use KRA-approved device serial (NOT dynamic!)
388
- # Common sandbox test value (if pre-provisioned by KRA):
389
-
390
- response = etims.select_init_info({
391
- "tin": config.oscu["tin"],
392
- "bhfId": config.oscu["bhf_id"],
393
- "dvcSrlNo": config.oscu["device_serial"],
336
+ init_resp = etims.select_init_osdc_info({
337
+ 'tin': config['oscu']['tin'],
338
+ 'bhfId': config['oscu']['bhf_id'],
339
+ 'dvcSrlNo': config['oscu']['device_serial'],
394
340
  })
395
-
396
- # Extract cmcKey (sandbox returns at root level)
397
- cmc_key = response.get("cmcKey") or response.get("data", {}).get("cmcKey")
341
+
342
+ cmc_key = init_resp.get('cmcKey')
398
343
  if not cmc_key:
399
344
  raise RuntimeError("cmcKey not found in response")
400
345
 
401
- # Update config IMMEDIATELY
402
- config.oscu["cmc_key"] = cmc_key
403
-
404
- # Recreate client with updated config (critical!)
405
- etims = EtimsClient(config, auth)
406
-
346
+ config['oscu']['cmc_key'] = cmc_key
347
+ etims = EtimsClient(config, auth) # Reinitialize to inject cmcKey
407
348
  print(f"✅ OSCU initialized. cmcKey: {cmc_key[:15]}...")
408
349
 
409
- except ApiException as e:
410
- if e.error_code == "901":
411
- print("❌ DEVICE NOT VALID (resultCd 901)")
412
- print(" → Device serial not registered with KRA")
413
- print(" → Contact timsupport@kra.go.ke for approved serial")
414
- print(" → Common sandbox test value: 'dvcv1130' (may work if pre-provisioned)")
415
- exit(1)
416
- raise
350
+ except Exception as e:
351
+ print(f"❌ OSCU Init failed: {e}")
352
+ exit(1)
417
353
  ```
418
354
 
419
- ### Step 4: Business Operations (Postman-Compliant Payload)
355
+ ### Step 4: Business Operations
420
356
  ```python
421
- from datetime import datetime, timedelta
422
-
423
- def kra_date(days_offset=0):
424
- """Generate KRA-compliant date strings"""
425
- d = datetime.now() + timedelta(days=days_offset)
426
- return d.strftime("%Y%m%d%H%M%S") # YYYYMMDDHHmmss
427
-
428
- # Fetch code list (demonstrates header injection)
429
- try:
430
- codes = etims.select_code_list({
431
- "tin": config.oscu["tin"],
432
- "bhfId": config.oscu["bhf_id"],
433
- "lastReqDt": kra_date(-7), # NOT future date
434
- })
435
- print(f"✅ Retrieved {len(codes.get('itemList', []))} codes")
436
- except Exception as e:
437
- print(f"❌ Code list fetch failed: {e}")
438
-
439
- # Send sales transaction (FULL Postman payload structure)
440
- try:
441
- response = etims.send_sales_transaction({
442
- "invcNo": 1, # INTEGER (sequential) - NOT string!
443
- "custTin": "A123456789Z",
444
- "custNm": "Test Customer",
445
- "salesTyCd": "N", # N=Normal, R=Return
446
- "rcptTyCd": "R", # R=Receipt
447
- "pmtTyCd": "01", # 01=Cash
448
- "salesSttsCd": "01", # 01=Completed
449
- "cfmDt": kra_date(), # YYYYMMDDHHmmss
450
- "salesDt": kra_date()[:8], # YYYYMMDD (NO time)
451
- "totItemCnt": 1,
452
- # TAX BREAKDOWN (ALL 15 FIELDS REQUIRED)
453
- "taxblAmtA": 0.00, "taxblAmtB": 0.00, "taxblAmtC": 81000.00,
454
- "taxblAmtD": 0.00, "taxblAmtE": 0.00,
455
- "taxRtA": 0.00, "taxRtB": 0.00, "taxRtC": 0.00,
456
- "taxRtD": 0.00, "taxRtE": 0.00,
457
- "taxAmtA": 0.00, "taxAmtB": 0.00, "taxAmtC": 0.00,
458
- "taxAmtD": 0.00, "taxAmtE": 0.00,
459
- "totTaxblAmt": 81000.00,
460
- "totTaxAmt": 0.00,
461
- "totAmt": 81000.00,
462
- "regrId": "Admin", "regrNm": "Admin",
463
- "modrId": "Admin", "modrNm": "Admin",
464
- "itemList": [{
465
- "itemSeq": 1,
466
- "itemCd": "KE2NTBA00000001", # Must exist in KRA system
467
- "itemClsCd": "1000000000",
468
- "itemNm": "Brand A",
469
- "barCd": "", # Nullable but REQUIRED field
470
- "pkgUnitCd": "NT",
471
- "pkg": 1, # Package quantity
472
- "qtyUnitCd": "BA",
473
- "qty": 90.0,
474
- "prc": 1000.00,
475
- "splyAmt": 81000.00,
476
- "dcRt": 10.0, # Discount rate %
477
- "dcAmt": 9000.00, # Discount amount
478
- "taxTyCd": "C", # C = Zero-rated/Exempt
479
- "taxblAmt": 81000.00,
480
- "taxAmt": 0.00,
481
- "totAmt": 81000.00, # splyAmt - dcAmt + taxAmt
482
- }],
483
- })
484
-
485
- print(f"✅ Sales transaction sent (resultCd: {response['resultCd']})")
486
- print(f"Receipt Signature: {response['data']['rcptSign']}")
487
-
488
- except ValidationException as e:
489
- print("❌ Validation failed:")
490
- for err in e.errors:
491
- field = err.get('loc', [])[0] if err.get('loc') else 'unknown'
492
- print(f" • {field}: {err.get('msg', 'validation error')}")
357
+ # Fetch code list
358
+ codes = etims.select_code_list({'lastReqDt': '20260101000000'})
359
+
360
+ # Save an item
361
+ item_resp = etims.save_item({
362
+ 'itemCd': 'KE1NTXU0000006',
363
+ 'itemClsCd': '5059690800',
364
+ 'itemNm': 'Test Material',
365
+ 'pkgUnitCd': 'NT',
366
+ 'qtyUnitCd': 'U',
367
+ 'taxTyCd': 'B',
368
+ 'dftPrc': 3500,
369
+ 'useYn': 'Y',
370
+ 'regrId': 'Test', 'regrNm': 'Test',
371
+ 'modrId': 'Test', 'modrNm': 'Test',
372
+ })
493
373
 
494
- except ApiException as e:
495
- print(f"❌ KRA API Error ({e.error_code}): {e}")
496
- if e.details and 'resultMsg' in e.details:
497
- print(f"KRA Message: {e.details['resultMsg']}")
374
+ # Save purchase transaction (full tax breakdown required)
375
+ purchase_resp = etims.save_purchase({
376
+ 'invcNo': 1,
377
+ 'spplrTin': 'A123456789Z',
378
+ 'pchsTyCd': 'N',
379
+ 'rcptTyCd': 'P',
380
+ 'pmtTyCd': '01',
381
+ 'pchsSttsCd': '02',
382
+ 'cfmDt': '20260206120000',
383
+ 'pchsDt': '20260206',
384
+ 'totItemCnt': 1,
385
+ 'taxblAmtA': 0, 'taxblAmtB': 10500, 'taxblAmtC': 0, 'taxblAmtD': 0, 'taxblAmtE': 0,
386
+ 'taxRtA': 0, 'taxRtB': 18, 'taxRtC': 0, 'taxRtD': 0, 'taxRtE': 0,
387
+ 'taxAmtA': 0, 'taxAmtB': 1890, 'taxAmtC': 0, 'taxAmtD': 0, 'taxAmtE': 0,
388
+ 'totTaxblAmt': 10500,
389
+ 'totTaxAmt': 1890,
390
+ 'totAmt': 10500,
391
+ 'regrId': 'Test', 'regrNm': 'Test',
392
+ 'modrId': 'Test', 'modrNm': 'Test',
393
+ 'itemList': [/* ... */],
394
+ })
498
395
  ```
499
396
 
397
+ > ✅ All payloads are validated using internal Pydantic schemas before sending.
398
+
500
399
  ---
501
400
 
502
401
  ## API Reference
503
402
 
504
- ### Functional Categories (8 Total)
403
+ ### Functional Categories & Methods
505
404
 
506
- | Category | Purpose | Endpoints |
507
- |----------|---------|-----------|
508
- | **Initialization** | Device registration & cmcKey acquisition | `select_init_info` |
509
- | **Data Management** | Retrieve standard codes & master data | `select_code_list`, `select_item_cls_list`, `select_bhf_list`, `select_taxpayer_info`, `select_customer_list`, `select_notice_list` |
510
- | **Branch Management** | Manage branch offices & users | `branch_insurance_info`, `branch_user_account`, `branch_send_customer_info` |
511
- | **Item Management** | Item master data | `save_item`, `item_info` |
512
- | **Purchase Management** | Purchase transactions | `select_purchase_trns`, `send_purchase_transaction_info` |
513
- | **Sales Management** | Sales transactions & invoices | `send_sales_transaction`, `select_sales_trns`, `select_invoice_detail` |
514
- | **Stock Management** | Inventory movements & stock levels | `insert_stock_io`, `save_stock_master`, `select_move_list` |
405
+ | Category | Methods |
406
+ |--------|--------|
407
+ | **Initialization** | `select_init_osdc_info()` |
408
+ | **Data Management** | `select_code_list()`, `select_customer()`, `select_notice_list()`, `select_branches()`, `select_item_classes()`, `select_items()` |
409
+ | **Branch Management** | `save_branch_customer()`, `save_branch_user()`, `save_branch_insurance()` |
410
+ | **Item Management** | `save_item()`, `save_item_composition()` |
411
+ | **Purchase Management** | `save_purchase()`, `select_purchases()` |
412
+ | **Sales Management** | `save_sales_transaction()` |
413
+ | **Stock Management** | `save_stock_io()`, `save_stock_master()`, `select_stock_movement()` |
515
414
 
516
- ### Core Classes
517
-
518
- | Class | Purpose |
519
- |-------|---------|
520
- | `AuthClient` | Token generation, caching (60s buffer), and refresh management |
521
- | `BaseClient` | HTTP request handling, header management, error unwrapping |
522
- | `EtimsClient` | Business endpoint methods (all 8 functional categories) |
523
- | `Validator` | Payload validation against KRA schemas (Pydantic v2) |
415
+ > 🔍 Each method maps to a KRA endpoint alias defined internally (e.g., `save_purchase` → `insertTrnsPurchase`).
524
416
 
525
417
  ---
526
418
 
@@ -554,38 +446,20 @@ except ApiException as e:
554
446
  ### Handling Pattern
555
447
  ```python
556
448
  try:
557
- response = etims.send_sales_transaction(payload)
449
+ response = etims.save_purchase(payload)
558
450
 
559
451
  except ValidationException as e:
560
452
  print("❌ Validation failed:")
561
- for error in e.errors:
562
- field = error.get('loc', [])[0] if error.get('loc') else 'unknown'
563
- print(f" • {field}: {error.get('msg', 'validation error')}")
453
+ for field, msg in e.details.items():
454
+ print(f" • {field}: {msg}")
564
455
 
565
456
  except ApiException as e:
566
457
  print(f"❌ KRA API Error ({e.error_code}): {e}")
567
-
568
- # Get full KRA response for debugging
569
458
  if e.details and 'resultMsg' in e.details:
570
459
  print(f"KRA Message: {e.details['resultMsg']}")
571
-
572
- # Handle specific error codes
573
- if e.error_code == "901":
574
- print("→ Device serial not registered with KRA")
575
- elif e.error_code == "902":
576
- print("→ cmcKey expired - reinitialize OSCU")
577
- elif e.error_code == "500":
578
- print("→ Invalid payload - check date formats/tax fields")
579
460
 
580
461
  except AuthenticationException as e:
581
462
  print(f"❌ Authentication failed: {e}")
582
-
583
- # Attempt token refresh
584
- try:
585
- auth.token(force_refresh=True)
586
- # Retry operation...
587
- except Exception as ex:
588
- print(f"Token refresh failed: {ex}")
589
463
  ```
590
464
 
591
465
  ### Comprehensive KRA Error Codes
@@ -622,18 +496,14 @@ except AuthenticationException as e:
622
496
  **Cause**: cmcKey expired or not set in config
623
497
  **Solution**:
624
498
  ```python
625
- # After initialization:
626
- config.oscu["cmc_key"] = extracted_cmc_key
499
+ config['oscu']['cmc_key'] = extracted_cmc_key
627
500
  etims = EtimsClient(config, auth) # MUST recreate client
628
501
  ```
629
502
 
630
503
  ### ❌ Trailing spaces in URLs
631
504
 
632
505
  **Cause**: Copy-paste errors from documentation
633
- **Solution**: Always use `.strip()` on URLs:
634
- ```python
635
- "token_url": "https://sbx.kra.go.ke/v1/token/generate ".strip(),
636
- ```
506
+ **Solution**: Always verify URLs have no trailing whitespace.
637
507
 
638
508
  ### ❌ Invoice number rejected
639
509
 
@@ -641,7 +511,6 @@ etims = EtimsClient(config, auth) # MUST recreate client
641
511
  **Solution**: Use sequential integers starting from 1:
642
512
  ```python
643
513
  "invcNo": 1, # ✅ Correct
644
- # NOT "INV001" ❌
645
514
  ```
646
515
 
647
516
  ---
@@ -719,7 +588,7 @@ KRA mandates successful completion of automated tests before verification:
719
588
  3. Deploy directly to production environment
720
589
  4. No SLA execution required
721
590
 
722
- > 💡 **Production URL**: `https://api.developer.go.ke/etims-oscu/api/v1`
591
+ > 💡 **Production URL**: `https://etims-api.kra.go.ke/etims-api`
723
592
  > ⚠️ **Never use sandbox credentials in production** – KRA monitors environment separation strictly
724
593
 
725
594
  ---
@@ -740,7 +609,7 @@ KRA mandates successful completion of automated tests before verification:
740
609
  - **Email**: ebartile@gmail.com (for integration guidance)
741
610
  - **Emergency Hotline**: +254757807150 (business hours only)
742
611
 
743
- > ℹ️ **Disclaimer**: This SDK is not officially endorsed by Kenya Revenue Authority. Always verify integration requirements with KRA before production deployment. KRA may update API specifications without notice – monitor [GavaConnect Portal](https://developer.go.ke) for updates.
612
+ > ℹ️ **Disclaimer**: This SDK is independently developed by Paybill Kenya and is **not affiliated with or endorsed by the Kenya Revenue Authority (KRA)**. Always verify integration requirements with KRA before production deployment. KRA may update API specifications without notice – monitor [GavaConnect Portal](https://developer.go.ke) for updates.
744
613
 
745
614
  ---
746
615
 
@@ -748,7 +617,7 @@ KRA mandates successful completion of automated tests before verification:
748
617
 
749
618
  MIT License
750
619
 
751
- Copyright © 2024-2026 Bartile Emmanuel / Paybill Kenya
620
+ Copyright © 20242026 Bartile Emmanuel / Paybill Kenya
752
621
 
753
622
  Permission is hereby granted, free of charge, to any person obtaining a copy
754
623
  of this software and associated documentation files (the "Software"), to deal
@@ -775,4 +644,3 @@ SOFTWARE.
775
644
  This SDK was developed by **Bartile Emmanuel** for Paybill Kenya to simplify KRA eTIMS OSCU integration for Kenyan businesses. Special thanks to KRA for providing comprehensive API documentation and Postman collections.
776
645
 
777
646
  > 🇰🇪 **Proudly Made in Kenya** – Supporting digital tax compliance for East Africa's largest economy.
778
- > *Tested on KRA Sandbox • Built with Python • Pydantic v2 Validation • Production Ready*