kra-etims-sdk 0.1.2__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.2 → kra_etims_sdk-0.1.3}/PKG-INFO +117 -250
  2. {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/README.md +116 -249
  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.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/PKG-INFO +117 -250
  8. {kra_etims_sdk-0.1.2 → 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.2/kra_etims_sdk/base_client.py +0 -67
  11. kra_etims_sdk-0.1.2/kra_etims_sdk/client.py +0 -106
  12. kra_etims_sdk-0.1.2/kra_etims_sdk/schemas.py +0 -212
  13. kra_etims_sdk-0.1.2/kra_etims_sdk/validator.py +0 -47
  14. kra_etims_sdk-0.1.2/tests/test_etims.py +0 -222
  15. {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/LICENSE +0 -0
  16. {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk/__init__.py +0 -0
  17. {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk/auth.py +0 -0
  18. {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk/exceptions.py +0 -0
  19. {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/SOURCES.txt +0 -0
  20. {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/dependency_links.txt +0 -0
  21. {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/requires.txt +0 -0
  22. {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/top_level.txt +0 -0
  23. {kra_etims_sdk-0.1.2 → 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.2
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:
@@ -200,24 +189,23 @@ Before integration, you **MUST** complete these prerequisites:
200
189
  ```python
201
190
  # 1. Initialize FIRST (returns cmcKey)
202
191
  response = etims.select_init_osdc_info({
203
- "tin": config.oscu["tin"],
204
- "bhfId": config.oscu["bhf_id"],
205
- "dvcSrlNo": config.oscu["device_serial"], # KRA-approved serial
192
+ "tin": config["oscu"]["tin"],
193
+ "bhfId": config["oscu"]["bhf_id"],
194
+ "dvcSrlNo": config["oscu"]["device_serial"], # KRA-approved serial
206
195
  })
207
196
 
208
- # 2. Extract cmcKey (sandbox returns at root level)
209
- cmc_key = response.get("cmcKey") or response.get("data", {}).get("cmcKey")
197
+ # 2. Extract cmcKey
198
+ cmc_key = response.get("cmcKey")
210
199
 
211
200
  # 3. Update config IMMEDIATELY
212
- config.oscu["cmc_key"] = cmc_key
201
+ config["oscu"]["cmc_key"] = cmc_key
213
202
 
214
203
  # 4. Recreate client with updated config (critical!)
215
204
  etims = EtimsClient(config, auth)
216
-
217
- # 5. ALL subsequent requests require cmcKey in headers
218
- etims.select_code_list(...)
219
205
  ```
220
206
 
207
+ > 🔔 Note: `cmcKey` is only required for certain write operations (branch/user/insurance), not all endpoints.
208
+
221
209
  ### 3. Invoice Numbering Rules
222
210
  - **MUST be sequential integers** (1, 2, 3...) – **NOT strings** (`INV001`)
223
211
  - Must be unique per branch office (`bhfId`)
@@ -269,94 +257,52 @@ etims.select_code_list(...)
269
257
 
270
258
  ```bash
271
259
  pip install kra-etims-sdk
272
- # OR with dev dependencies
273
- pip install "kra-etims-sdk[dev]"
274
260
  ```
275
261
 
276
262
  ### Requirements
277
263
  - Python 3.9+
278
264
  - `requests` (≥2.31)
279
265
  - `pydantic` (≥2.0)
280
- - `typing-extensions` (for Python <3.10)
266
+
267
+ > 💡 The SDK uses plain dictionaries for configuration — no custom config class required.
281
268
 
282
269
  ---
283
270
 
284
271
  ## Configuration
285
272
 
273
+ Define your config as a **plain Python dictionary**:
274
+
286
275
  ```python
287
- from kra_etims_sdk import KraEtimsConfig
288
276
  import os
289
-
290
- config = KraEtimsConfig(
291
- env="sandbox", # "sandbox" | "production"
292
- cache_file="./.kra_token.json",
293
-
294
- auth={
295
- "sandbox": {
296
- "token_url": "https://sbx.kra.go.ke/v1/token/generate".strip(),
297
- "consumer_key": os.environ["KRA_CONSUMER_KEY"],
298
- "consumer_secret": os.environ["KRA_CONSUMER_SECRET"],
299
- },
300
- "production": {
301
- "token_url": "https://kra.go.ke/v1/token/generate".strip(),
302
- "consumer_key": os.environ["KRA_PROD_CONSUMER_KEY"],
303
- "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'),
304
287
  }
305
288
  },
306
-
307
- api={
308
- "sandbox": {"base_url": "https://etims-api-sbx.kra.go.ke/etims-api".strip()},
309
- "production": {"base_url": "https://etims-api.kra.go.ke/etims-api".strip()}
310
- },
311
-
312
- oscu={
313
- "tin": os.environ["KRA_TIN"],
314
- "bhf_id": os.environ["KRA_BHF_ID"],
315
- "cmc_key": os.environ["CMC_KEY"] # Set AFTER initialization
316
- },
317
-
318
- endpoints={
319
- # INITIALIZATION (ONLY endpoint without tin/bhfId/cmcKey headers)
320
- "selectInitOsdcInfo": "/selectInitOsdcInfo",
321
-
322
- # DATA MANAGEMENT
323
- "selectCodeList": "/selectCodeList",
324
- "selectItemClsList": "/selectItemClass",
325
- "selectBhfList": "/branchList",
326
- "selectTaxpayerInfo": "/selectTaxpayerInfo",
327
- "selectCustomerList": "/selectCustomerList",
328
- "selectNoticeList": "/selectNoticeList",
329
-
330
- # BRANCH MANAGEMENT
331
- "branchInsuranceInfo": "/branchInsuranceInfo",
332
- "branchUserAccount": "/branchUserAccount",
333
- "branchSendCustomerInfo": "/branchSendCustomerInfo",
334
-
335
- # ITEM MANAGEMENT
336
- "saveItem": "/saveItem",
337
- "itemInfo": "/itemInfo",
338
-
339
- # PURCHASE MANAGEMENT
340
- "selectPurchaseTrns": "/getPurchaseTransactionInfo",
341
- "sendPurchaseTransactionInfo": "/sendPurchaseTransactionInfo",
342
-
343
- # SALES MANAGEMENT
344
- "sendSalesTransaction": "/sendSalesTransaction",
345
- "selectSalesTrns": "/selectSalesTransactions",
346
- "selectInvoiceDetail": "/selectInvoiceDetail",
347
-
348
- # STOCK MANAGEMENT (NESTED PATHS - CRITICAL)
349
- "insertStockIO": "/insert/stockIO", # ← slash in path
350
- "saveStockMaster": "/save/stockMaster", # ← slash in path
351
- "selectMoveList": "/selectStockMoveLists",
289
+ 'api': {
290
+ 'sbx': {'base_url': 'https://etims-api-sbx.kra.go.ke/etims-api'}
352
291
  },
353
-
354
- http={"timeout": 30}
355
- )
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
+ }
356
300
  ```
357
301
 
358
- > 💡 **Production URL Note**:
359
- > Production base URL is `https://etims-api.kra.go.ke/etims-api` (NOT `https://etims-api-sbx.kra.go.ke/etims-api`)
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.
360
306
 
361
307
  ---
362
308
 
@@ -364,164 +310,109 @@ config = KraEtimsConfig(
364
310
 
365
311
  ### Step 1: Initialize SDK
366
312
  ```python
367
- from kra_etims_sdk import AuthClient, EtimsClient
368
- from kra_etims_sdk.exceptions import ApiException, ValidationException
313
+ from kra_etims_sdk.auth import AuthClient
314
+ from kra_etims_sdk.client import EtimsClient
369
315
 
370
316
  auth = AuthClient(config)
371
317
  etims = EtimsClient(config, auth)
372
318
  ```
373
319
 
374
- ### Step 2: Authenticate (Get Access Token)
320
+ ### Step 2: Authenticate
375
321
  ```python
376
322
  try:
377
- # Force fresh token (optional - cache used by default)
378
- token = auth.token(force_refresh=True)
379
- print(f"✅ Token acquired: {token[:20]}...")
380
- 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:
381
327
  print(f"❌ Authentication failed: {e}")
382
328
  exit(1)
383
329
  ```
384
330
 
385
- ### 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
+
386
334
  ```python
387
335
  try:
388
- # ⚠️ MUST use KRA-approved device serial (NOT dynamic!)
389
- # Common sandbox test value (if pre-provisioned by KRA):
390
-
391
- response = etims.select_init_info({
392
- "tin": config.oscu["tin"],
393
- "bhfId": config.oscu["bhf_id"],
394
- "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'],
395
340
  })
396
-
397
- # Extract cmcKey (sandbox returns at root level)
398
- cmc_key = response.get("cmcKey") or response.get("data", {}).get("cmcKey")
341
+
342
+ cmc_key = init_resp.get('cmcKey')
399
343
  if not cmc_key:
400
344
  raise RuntimeError("cmcKey not found in response")
401
345
 
402
- # Update config IMMEDIATELY
403
- config.oscu["cmc_key"] = cmc_key
404
-
405
- # Recreate client with updated config (critical!)
406
- etims = EtimsClient(config, auth)
407
-
346
+ config['oscu']['cmc_key'] = cmc_key
347
+ etims = EtimsClient(config, auth) # Reinitialize to inject cmcKey
408
348
  print(f"✅ OSCU initialized. cmcKey: {cmc_key[:15]}...")
409
349
 
410
- except ApiException as e:
411
- if e.error_code == "901":
412
- print("❌ DEVICE NOT VALID (resultCd 901)")
413
- print(" → Device serial not registered with KRA")
414
- print(" → Contact timsupport@kra.go.ke for approved serial")
415
- print(" → Common sandbox test value: 'dvcv1130' (may work if pre-provisioned)")
416
- exit(1)
417
- raise
350
+ except Exception as e:
351
+ print(f"❌ OSCU Init failed: {e}")
352
+ exit(1)
418
353
  ```
419
354
 
420
- ### Step 4: Business Operations (Postman-Compliant Payload)
355
+ ### Step 4: Business Operations
421
356
  ```python
422
- from datetime import datetime, timedelta
423
-
424
- def kra_date(days_offset=0):
425
- """Generate KRA-compliant date strings"""
426
- d = datetime.now() + timedelta(days=days_offset)
427
- return d.strftime("%Y%m%d%H%M%S") # YYYYMMDDHHmmss
428
-
429
- # Fetch code list (demonstrates header injection)
430
- try:
431
- codes = etims.select_code_list({
432
- "tin": config.oscu["tin"],
433
- "bhfId": config.oscu["bhf_id"],
434
- "lastReqDt": kra_date(-7), # NOT future date
435
- })
436
- print(f"✅ Retrieved {len(codes.get('itemList', []))} codes")
437
- except Exception as e:
438
- print(f"❌ Code list fetch failed: {e}")
439
-
440
- # Send sales transaction (FULL Postman payload structure)
441
- try:
442
- response = etims.send_sales_transaction({
443
- "invcNo": 1, # INTEGER (sequential) - NOT string!
444
- "custTin": "A123456789Z",
445
- "custNm": "Test Customer",
446
- "salesTyCd": "N", # N=Normal, R=Return
447
- "rcptTyCd": "R", # R=Receipt
448
- "pmtTyCd": "01", # 01=Cash
449
- "salesSttsCd": "01", # 01=Completed
450
- "cfmDt": kra_date(), # YYYYMMDDHHmmss
451
- "salesDt": kra_date()[:8], # YYYYMMDD (NO time)
452
- "totItemCnt": 1,
453
- # TAX BREAKDOWN (ALL 15 FIELDS REQUIRED)
454
- "taxblAmtA": 0.00, "taxblAmtB": 0.00, "taxblAmtC": 81000.00,
455
- "taxblAmtD": 0.00, "taxblAmtE": 0.00,
456
- "taxRtA": 0.00, "taxRtB": 0.00, "taxRtC": 0.00,
457
- "taxRtD": 0.00, "taxRtE": 0.00,
458
- "taxAmtA": 0.00, "taxAmtB": 0.00, "taxAmtC": 0.00,
459
- "taxAmtD": 0.00, "taxAmtE": 0.00,
460
- "totTaxblAmt": 81000.00,
461
- "totTaxAmt": 0.00,
462
- "totAmt": 81000.00,
463
- "regrId": "Admin", "regrNm": "Admin",
464
- "modrId": "Admin", "modrNm": "Admin",
465
- "itemList": [{
466
- "itemSeq": 1,
467
- "itemCd": "KE2NTBA00000001", # Must exist in KRA system
468
- "itemClsCd": "1000000000",
469
- "itemNm": "Brand A",
470
- "barCd": "", # Nullable but REQUIRED field
471
- "pkgUnitCd": "NT",
472
- "pkg": 1, # Package quantity
473
- "qtyUnitCd": "BA",
474
- "qty": 90.0,
475
- "prc": 1000.00,
476
- "splyAmt": 81000.00,
477
- "dcRt": 10.0, # Discount rate %
478
- "dcAmt": 9000.00, # Discount amount
479
- "taxTyCd": "C", # C = Zero-rated/Exempt
480
- "taxblAmt": 81000.00,
481
- "taxAmt": 0.00,
482
- "totAmt": 81000.00, # splyAmt - dcAmt + taxAmt
483
- }],
484
- })
485
-
486
- print(f"✅ Sales transaction sent (resultCd: {response['resultCd']})")
487
- print(f"Receipt Signature: {response['data']['rcptSign']}")
488
-
489
- except ValidationException as e:
490
- print("❌ Validation failed:")
491
- for err in e.errors:
492
- field = err.get('loc', [])[0] if err.get('loc') else 'unknown'
493
- 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
+ })
494
373
 
495
- except ApiException as e:
496
- print(f"❌ KRA API Error ({e.error_code}): {e}")
497
- if e.details and 'resultMsg' in e.details:
498
- 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
+ })
499
395
  ```
500
396
 
397
+ > ✅ All payloads are validated using internal Pydantic schemas before sending.
398
+
501
399
  ---
502
400
 
503
401
  ## API Reference
504
402
 
505
- ### Functional Categories (8 Total)
403
+ ### Functional Categories & Methods
506
404
 
507
- | Category | Purpose | Endpoints |
508
- |----------|---------|-----------|
509
- | **Initialization** | Device registration & cmcKey acquisition | `select_init_info` |
510
- | **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` |
511
- | **Branch Management** | Manage branch offices & users | `branch_insurance_info`, `branch_user_account`, `branch_send_customer_info` |
512
- | **Item Management** | Item master data | `save_item`, `item_info` |
513
- | **Purchase Management** | Purchase transactions | `select_purchase_trns`, `send_purchase_transaction_info` |
514
- | **Sales Management** | Sales transactions & invoices | `send_sales_transaction`, `select_sales_trns`, `select_invoice_detail` |
515
- | **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()` |
516
414
 
517
- ### Core Classes
518
-
519
- | Class | Purpose |
520
- |-------|---------|
521
- | `AuthClient` | Token generation, caching (60s buffer), and refresh management |
522
- | `BaseClient` | HTTP request handling, header management, error unwrapping |
523
- | `EtimsClient` | Business endpoint methods (all 8 functional categories) |
524
- | `Validator` | Payload validation against KRA schemas (Pydantic v2) |
415
+ > 🔍 Each method maps to a KRA endpoint alias defined internally (e.g., `save_purchase` → `insertTrnsPurchase`).
525
416
 
526
417
  ---
527
418
 
@@ -555,38 +446,20 @@ except ApiException as e:
555
446
  ### Handling Pattern
556
447
  ```python
557
448
  try:
558
- response = etims.send_sales_transaction(payload)
449
+ response = etims.save_purchase(payload)
559
450
 
560
451
  except ValidationException as e:
561
452
  print("❌ Validation failed:")
562
- for error in e.errors:
563
- field = error.get('loc', [])[0] if error.get('loc') else 'unknown'
564
- print(f" • {field}: {error.get('msg', 'validation error')}")
453
+ for field, msg in e.details.items():
454
+ print(f" • {field}: {msg}")
565
455
 
566
456
  except ApiException as e:
567
457
  print(f"❌ KRA API Error ({e.error_code}): {e}")
568
-
569
- # Get full KRA response for debugging
570
458
  if e.details and 'resultMsg' in e.details:
571
459
  print(f"KRA Message: {e.details['resultMsg']}")
572
-
573
- # Handle specific error codes
574
- if e.error_code == "901":
575
- print("→ Device serial not registered with KRA")
576
- elif e.error_code == "902":
577
- print("→ cmcKey expired - reinitialize OSCU")
578
- elif e.error_code == "500":
579
- print("→ Invalid payload - check date formats/tax fields")
580
460
 
581
461
  except AuthenticationException as e:
582
462
  print(f"❌ Authentication failed: {e}")
583
-
584
- # Attempt token refresh
585
- try:
586
- auth.token(force_refresh=True)
587
- # Retry operation...
588
- except Exception as ex:
589
- print(f"Token refresh failed: {ex}")
590
463
  ```
591
464
 
592
465
  ### Comprehensive KRA Error Codes
@@ -623,18 +496,14 @@ except AuthenticationException as e:
623
496
  **Cause**: cmcKey expired or not set in config
624
497
  **Solution**:
625
498
  ```python
626
- # After initialization:
627
- config.oscu["cmc_key"] = extracted_cmc_key
499
+ config['oscu']['cmc_key'] = extracted_cmc_key
628
500
  etims = EtimsClient(config, auth) # MUST recreate client
629
501
  ```
630
502
 
631
503
  ### ❌ Trailing spaces in URLs
632
504
 
633
505
  **Cause**: Copy-paste errors from documentation
634
- **Solution**: Always use `.strip()` on URLs:
635
- ```python
636
- "token_url": "https://sbx.kra.go.ke/v1/token/generate ".strip(),
637
- ```
506
+ **Solution**: Always verify URLs have no trailing whitespace.
638
507
 
639
508
  ### ❌ Invoice number rejected
640
509
 
@@ -642,7 +511,6 @@ etims = EtimsClient(config, auth) # MUST recreate client
642
511
  **Solution**: Use sequential integers starting from 1:
643
512
  ```python
644
513
  "invcNo": 1, # ✅ Correct
645
- # NOT "INV001" ❌
646
514
  ```
647
515
 
648
516
  ---
@@ -741,7 +609,7 @@ KRA mandates successful completion of automated tests before verification:
741
609
  - **Email**: ebartile@gmail.com (for integration guidance)
742
610
  - **Emergency Hotline**: +254757807150 (business hours only)
743
611
 
744
- > ℹ️ **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.
745
613
 
746
614
  ---
747
615
 
@@ -749,7 +617,7 @@ KRA mandates successful completion of automated tests before verification:
749
617
 
750
618
  MIT License
751
619
 
752
- Copyright © 2024-2026 Bartile Emmanuel / Paybill Kenya
620
+ Copyright © 20242026 Bartile Emmanuel / Paybill Kenya
753
621
 
754
622
  Permission is hereby granted, free of charge, to any person obtaining a copy
755
623
  of this software and associated documentation files (the "Software"), to deal
@@ -776,4 +644,3 @@ SOFTWARE.
776
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.
777
645
 
778
646
  > 🇰🇪 **Proudly Made in Kenya** – Supporting digital tax compliance for East Africa's largest economy.
779
- > *Tested on KRA Sandbox • Built with Python • Pydantic v2 Validation • Production Ready*