# KRA eTims SDK

A comprehensive Node.js SDK for integrating with the Kenya Revenue Authority (KRA) Electronic Tax Invoice Management System (eTims).

[![npm version](https://img.shields.io/npm/v/kra-etims-sdk.svg)](https://www.npmjs.com/package/kra-etims-sdk)
[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)

## Features

- Complete KRA eTims API integration
- Express.js server mode for easy API endpoint exposure
- CORS support with domain whitelisting
- Comprehensive validation for all API requests
- Detailed logging and error handling
- Environment-based configuration
- TypeScript support

## Author

Shadrack Matata  
Email: kisamba.debug@gmail.com  
Tel: +254722854082 / +254733854082



## Installation

```bash
npm install kra-etims-sdk
```

## Quick Start

### Basic Usage

```javascript
const KRAeTimsSDK = require('kra-etims-sdk');

// Initialize the SDK
const sdk = new KRAeTimsSDK();

// Authenticate with KRA eTims API
async function authenticate() {
  try {
    const token = await sdk.authenticate('your_username', 'your_password');
    console.log('Authentication successful:', token);
    return token;
  } catch (error) {
    console.error('Authentication failed:', error);
  }
}

// Send a sales transaction
async function sendSalesTransaction() {
  try {
    // Authenticate first
    await authenticate();
    
    // Prepare sales transaction data
    const salesData = {
      tin: 'P000000045R',
      bhfId: '00',
      invcNo: 'INV001',
      salesTrnsItems: [
        {
          itemCd: 'ITEM001',
          itemNm: 'Test Item',
          qty: 1,
          prc: 100,
          splyAmt: 100,
          dcRt: 0,
          dcAmt: 0,
          taxTyCd: 'V',
          taxAmt: 16
        }
      ]
    };
    
    // Send sales transaction
    const result = await sdk.sendSalesTransaction(salesData);
    console.log('Sales transaction sent:', result);
  } catch (error) {
    console.error('Failed to send sales transaction:', error);
  }
}
```

## Express Server Integration

### Complete Express Integration Example

Below is a comprehensive example of integrating the KRA eTims SDK with an Express application as middleware:

```javascript
const express = require('express');
const bodyParser = require('body-parser');
const morgan = require('morgan'); // Optional for logging
const KRAeTimsSDK = require('kra-etims-sdk');

// Initialize Express app
const app = express();
const port = process.env.PORT || 3000;

// Middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(morgan('dev')); // Optional for logging

// Initialize the KRA eTims SDK
const etimsSDK = new KRAeTimsSDK({
  // Optional configuration
  logLevel: 'info', // 'debug', 'info', 'warn', 'error'
  timeout: 30000, // API request timeout in ms
});

// Mount the SDK as middleware
app.use('/etims-api', etimsSDK.middleware());

// Custom routes that use the SDK directly

// Authentication example
app.post('/api/login', async (req, res) => {
  try {
    const { username, password } = req.body;
    const token = await etimsSDK.authenticate(username, password);
    res.json({ success: true, token });
  } catch (error) {
    console.error('Authentication error:', error);
    res.status(401).json({ success: false, error: error.message });
  }
});

// Sales transaction example
app.post('/api/sales', async (req, res) => {
  try {
    // Authenticate first (or use middleware to handle this)
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    // Send sales transaction
    const result = await etimsSDK.sendSalesTransaction(req.body);
    res.json({ success: true, data: result });
  } catch (error) {
    console.error('Sales transaction error:', error);
    res.status(400).json({ success: false, error: error.message });
  }
});

// Get stock information example
app.post('/api/stock/info', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    const result = await etimsSDK.getStockMoveList(req.body);
    res.json({ success: true, data: result });
  } catch (error) {
    console.error('Stock info error:', error);
    res.status(400).json({ success: false, error: error.message });
  }
});

// Error handling middleware
app.use((err, req, res, next) => {
  console.error('Application error:', err);
  res.status(500).json({ success: false, error: 'Internal server error' });
});

// Start the server
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});
```

### Authentication Middleware Example

Create a reusable authentication middleware for your Express routes:

```javascript
// auth.middleware.js
const KRAeTimsSDK = require('kra-etims-sdk');
const sdk = new KRAeTimsSDK();

const authMiddleware = async (req, res, next) => {
  try {
    // You could also check for an existing token and validate it
    await sdk.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    req.etimsSDK = sdk; // Attach the authenticated SDK to the request
    next();
  } catch (error) {
    console.error('KRA eTims authentication error:', error);
    res.status(401).json({ success: false, error: 'Authentication failed' });
  }
};

module.exports = authMiddleware;
```

Then use it in your routes:

```javascript
const express = require('express');
const authMiddleware = require('./auth.middleware');
const router = express.Router();

// Apply authentication middleware to all routes
router.use(authMiddleware);

// Now all routes have access to the authenticated SDK via req.etimsSDK
router.post('/sales/send', async (req, res) => {
  try {
    const result = await req.etimsSDK.sendSalesTransaction(req.body);
    res.json({ success: true, data: result });
  } catch (error) {
    res.status(400).json({ success: false, error: error.message });
  }
});

module.exports = router;
```

### Setting Up as a Standalone Server

For comparison, here's how to set up the SDK as a standalone server:

```javascript
const KRAeTimsSDK = require('kra-etims-sdk');

// Initialize the SDK with server option
const sdk = new KRAeTimsSDK({ server: true });

// Start the server
const port = process.env.PORT || 5000;
sdk.start(port).then(() => {
  console.log(`KRA eTims server started on port ${port}`);
}).catch(err => {
  console.error('Failed to start server:', err);
});
```

## Environment Configuration

Create a `.env` file in your project root:

```
# API Base URLs
DEV_API_BASE_URL=https://etims-api-sbx.kra.go.ke
PROD_API_BASE_URL=https://etims-api.kra.go.ke/etims-api

# Authentication
API_USERNAME=your_username
API_PASSWORD=your_password

# Environment
NODE_ENV=development
PORT=5000

# CORS Configuration
CORS_ALLOWED_ORIGINS=http://localhost:3000,https://yourdomain.com
```

### Switching Between Production and Development Endpoints

The SDK automatically selects the appropriate API base URL based on the `NODE_ENV` environment variable:

- When `NODE_ENV=development` (or not set), the SDK uses `DEV_API_BASE_URL` (sandbox environment)
- When `NODE_ENV=production`, the SDK uses `PROD_API_BASE_URL` (production environment)

#### Method 1: Using Environment Variables

```bash
# For development (sandbox)
NODE_ENV=development npm start

# For production
NODE_ENV=production npm start
```

#### Method 2: Setting NODE_ENV in Your Code

```javascript
// Before initializing the SDK
process.env.NODE_ENV = 'production'; // or 'development'

const KRAeTimsSDK = require('kra-etims-sdk');
const sdk = new KRAeTimsSDK();
```

#### Method 3: Explicitly Setting the API Base URL

You can also override the automatic selection by explicitly setting the API base URL when initializing the SDK:

```javascript
const KRAeTimsSDK = require('kra-etims-sdk');

// For development (sandbox)
const devSdk = new KRAeTimsSDK({
  apiBaseUrl: 'https://etims-api-sbx.kra.go.ke'
});

// For production
const prodSdk = new KRAeTimsSDK({
  apiBaseUrl: 'https://etims-api.kra.go.ke/etims-api'
});
```

## CORS and Security

The SDK includes built-in CORS support with domain whitelisting to secure your API endpoints.

### Configuring CORS

You can configure allowed origins in your `.env` file:

```
CORS_ALLOWED_ORIGINS=https://yourdomain.com,https://app.yourdomain.com
```

By default, the SDK allows requests from `http://localhost:3000` and `http://localhost:5000` in development.

## API Endpoints and Integration Examples

This section provides comprehensive examples of how to use each endpoint in your Express application.

### Authentication

#### `POST /api/auth/token` - Get authentication token

**Request Body:**
```json
{
  "username": "your_username",
  "password": "your_password"
}
```

**Express Integration Example:**
```javascript
const express = require('express');
const KRAeTimsSDK = require('kra-etims-sdk');
const router = express.Router();

// Initialize the SDK
const etimsSDK = new KRAeTimsSDK();

router.post('/auth', async (req, res) => {
  try {
    const { username, password } = req.body;
    
    // Call the SDK's authentication method
    const token = await etimsSDK.authenticate(username, password);
    
    // Store token in session or return to client
    req.session.etimsToken = token;
    
    res.json({
      success: true,
      message: 'Authentication successful',
      expiresAt: etimsSDK.getTokenExpiry()
    });
  } catch (error) {
    console.error('Authentication error:', error);
    res.status(401).json({
      success: false,
      message: 'Authentication failed',
      error: error.message
    });
  }
});

module.exports = router;
```

### Initialization

#### `POST /api/initialization/osdc-info` - Initialize OSDC Info

**Request Body:**
```json
{
  "tin": "P000000045R",
  "bhfId": "00",
  "dvcSrlNo": "MOVA22"
}
```

**Express Integration Example:**
```javascript
router.post('/initialize', async (req, res) => {
  try {
    // Ensure authentication
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    const initData = req.body;
    const result = await etimsSDK.initializeOsdcInfo(initData);
    
    res.json({
      success: true,
      message: 'OSDC initialization successful',
      data: result
    });
  } catch (error) {
    console.error('Initialization error:', error);
    res.status(400).json({
      success: false,
      message: 'Initialization failed',
      error: error.message
    });
  }
});
```

### Basic Data Management

#### `POST /api/basic-data/code-list` - Get code list

**Request Body:**
```json
{
  "tin": "P000000045R",
  "bhfId": "00",
  "lastReqDt": "20220101010101"
}
```

**Express Integration Example:**
```javascript
router.post('/code-list', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    const result = await etimsSDK.getCodeList(req.body);
    
    res.json({
      success: true,
      data: result
    });
  } catch (error) {
    console.error('Code list error:', error);
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
});
```

#### `POST /api/basic-data/item-cls-list` - Get item classification list

**Request Body:**
```json
{
  "tin": "P000000045R",
  "bhfId": "00",
  "lastReqDt": "20220101010101"
}
```

**Express Integration Example:**
```javascript
router.post('/item-classifications', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    const result = await etimsSDK.getItemClassificationList(req.body);
    
    // Cache the results for future use
    req.app.locals.itemClassifications = result;
    
    res.json({
      success: true,
      count: result.length,
      data: result
    });
  } catch (error) {
    console.error('Item classification error:', error);
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
});
```

#### `POST /api/basic-data/bhf-list` - Get branch list

**Request Body:**
```json
{
  "lastReqDt": "20220101010101"
}
```

**Express Integration Example:**
```javascript
router.post('/branches', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    const result = await etimsSDK.getBranchList(req.body);
    
    res.json({
      success: true,
      count: result.length,
      data: result
    });
  } catch (error) {
    console.error('Branch list error:', error);
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
});
```

#### `POST /api/basic-data/notice-list` - Get notice list

**Request Body:**
```json
{
  "tin": "P000000045R",
  "bhfId": "00",
  "lastReqDt": "20220101010101"
}
```

**Express Integration Example:**
```javascript
router.post('/notices', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    const result = await etimsSDK.getNoticeList(req.body);
    
    res.json({
      success: true,
      data: result
    });
  } catch (error) {
    console.error('Notice list error:', error);
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
});
```

#### `POST /api/basic-data/taxpayer-info` - Get taxpayer info

**Request Body:**
```json
{
  "tin": "P000000045R",
  "bhfId": "00",
  "lastReqDt": "20220101010101"
}
```

**Express Integration Example:**
```javascript
router.post('/taxpayer', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    const result = await etimsSDK.getTaxpayerInfo(req.body);
    
    res.json({
      success: true,
      data: result
    });
  } catch (error) {
    console.error('Taxpayer info error:', error);
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
});
```

#### `POST /api/basic-data/customer-list` - Get customer list

**Request Body:**
```json
{
  "tin": "P000000045R",
  "bhfId": "00",
  "lastReqDt": "20220101010101"
}
```

**Express Integration Example:**
```javascript
router.post('/customers', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    const result = await etimsSDK.getCustomerList(req.body);
    
    res.json({
      success: true,
      count: result.length,
      data: result
    });
  } catch (error) {
    console.error('Customer list error:', error);
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
});
```

### Sales Management

#### `POST /api/sales/send` - Send sales transaction

**Request Body:**
```json
{
  "tin": "P000000045R",
  "bhfId": "00",
  "invcNo": "INV001",
  "salesTrnsItems": [
    {
      "itemCd": "ITEM001",
      "itemNm": "Test Item",
      "qty": 1,
      "prc": 100,
      "splyAmt": 100,
      "dcRt": 0,
      "dcAmt": 0,
      "taxTyCd": "V",
      "taxAmt": 16
    }
  ]
}
```

**Express Integration Example:**
```javascript
router.post('/sales/create', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    // You might want to validate the sales data before sending
    const salesData = req.body;
    
    // Calculate totals if not provided
    if (!salesData.totals) {
      const items = salesData.salesTrnsItems;
      const totalAmount = items.reduce((sum, item) => sum + item.splyAmt, 0);
      const totalTax = items.reduce((sum, item) => sum + item.taxAmt, 0);
      
      salesData.totals = {
        totalAmount,
        totalTax,
        grandTotal: totalAmount + totalTax
      };
    }
    
    const result = await etimsSDK.sendSalesTransaction(salesData);
    
    // You might want to store the result in your database
    
    res.json({
      success: true,
      message: 'Sales transaction recorded successfully',
      data: result,
      receiptNo: result.receiptNo,
      timestamp: result.timestamp
    });
  } catch (error) {
    console.error('Sales transaction error:', error);
    res.status(400).json({
      success: false,
      message: 'Failed to record sales transaction',
      error: error.message
    });
  }
});
```

#### `POST /api/sales/select` - Get sales transaction

**Request Body:**
```json
{
  "tin": "P000000045R",
  "bhfId": "00",
  "lastReqDt": "20220101010101",
  "invcNo": "INV001"  // Optional
}
```

**Express Integration Example:**
```javascript
router.post('/sales/history', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    const result = await etimsSDK.getSalesTransactions(req.body);
    
    res.json({
      success: true,
      count: result.length,
      data: result
    });
  } catch (error) {
    console.error('Sales history error:', error);
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
});

// Get a specific invoice
router.get('/sales/invoice/:invoiceNo', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    const params = {
      tin: process.env.KRA_TIN,
      bhfId: process.env.KRA_BHF_ID,
      lastReqDt: '20220101010101',
      invcNo: req.params.invoiceNo
    };
    
    const result = await etimsSDK.getSalesTransactions(params);
    
    if (result.length === 0) {
      return res.status(404).json({
        success: false,
        message: 'Invoice not found'
      });
    }
    
    res.json({
      success: true,
      data: result[0]
    });
  } catch (error) {
    console.error('Invoice retrieval error:', error);
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
});
```

### Stock Management

#### `POST /api/stock/move-list` - Get move list

**Request Body:**
```json
{
  "tin": "P000000045R",
  "bhfId": "00",
  "lastReqDt": "20220101010101"
}
```

**Express Integration Example:**
```javascript
router.post('/stock/movements', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    const result = await etimsSDK.getStockMoveList(req.body);
    
    res.json({
      success: true,
      count: result.length,
      data: result
    });
  } catch (error) {
    console.error('Stock movement error:', error);
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
});
```

#### `POST /api/stock/save-master` - Save stock master

**Request Body:**
```json
{
  "tin": "P000000045R",
  "bhfId": "00",
  "itemCd": "ITEM001",
  "itemClsCd": "FOOD",
  "itemNm": "Test Item",
  "pkgUnitCd": "EA",
  "qtyUnitCd": "EA",
  "splyAmt": 100,
  "vatTyCd": "V"
}
```

**Express Integration Example:**
```javascript
router.post('/stock/create', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    const stockData = req.body;
    const result = await etimsSDK.saveStockMaster(stockData);
    
    res.json({
      success: true,
      message: 'Stock item created successfully',
      data: result
    });
  } catch (error) {
    console.error('Stock creation error:', error);
    res.status(400).json({
      success: false,
      message: 'Failed to create stock item',
      error: error.message
    });
  }
});
```

### Purchase Management

#### `POST /api/purchase/select` - Get purchase transaction

**Request Body:**
```json
{
  "tin": "P000000045R",
  "bhfId": "00",
  "lastReqDt": "20220101010101"
}
```

**Express Integration Example:**
```javascript
router.post('/purchases', async (req, res) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    
    const result = await etimsSDK.getPurchaseTransactions(req.body);
    
    res.json({
      success: true,
      count: result.length,
      data: result
    });
  } catch (error) {
    console.error('Purchase history error:', error);
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
});
```

### Complete Router Example

Here's how to organize all these endpoints into a complete Express router:

```javascript
// routes/etims.routes.js
const express = require('express');
const KRAeTimsSDK = require('kra-etims-sdk');
const router = express.Router();

// Initialize the SDK
const etimsSDK = new KRAeTimsSDK();

// Authentication middleware
const authMiddleware = async (req, res, next) => {
  try {
    await etimsSDK.authenticate(process.env.API_USERNAME, process.env.API_PASSWORD);
    req.etimsSDK = etimsSDK;
    next();
  } catch (error) {
    res.status(401).json({
      success: false,
      message: 'KRA eTims authentication failed',
      error: error.message
    });
  }
};

// Apply authentication middleware to all routes except login
router.use((req, res, next) => {
  if (req.path === '/auth') {
    return next();
  }
  authMiddleware(req, res, next);
});

// Authentication route
router.post('/auth', async (req, res) => {
  try {
    const { username, password } = req.body;
    const token = await etimsSDK.authenticate(username, password);
    res.json({ success: true, token });
  } catch (error) {
    res.status(401).json({ success: false, error: error.message });
  }
});

// Basic data routes
router.post('/code-list', async (req, res) => {
  try {
    const result = await req.etimsSDK.getCodeList(req.body);
    res.json({ success: true, data: result });
  } catch (error) {
    res.status(400).json({ success: false, error: error.message });
  }
});

// Add other routes here...

module.exports = router;

// In your main app.js:
// const etimsRoutes = require('./routes/etims.routes');
// app.use('/api/etims', etimsRoutes);
```

## Advanced Usage

### Custom Error Handling

```javascript
const { AuthenticationError, ValidationError } = require('kra-etims-sdk/errors');

try {
  // SDK operations
} catch (error) {
  if (error instanceof AuthenticationError) {
    // Handle authentication errors
    console.error('Authentication failed:', error.message);
  } else if (error instanceof ValidationError) {
    // Handle validation errors
    console.error('Validation failed:', error.details);
  } else {
    // Handle other errors
    console.error('Operation failed:', error);
  }
}
```

### Using with TypeScript

```typescript
import KRAeTimsSDK from 'kra-etims-sdk';
import { SalesTransaction, ApiResponse } from 'kra-etims-sdk/types';

const sdk = new KRAeTimsSDK();

async function processSale(sale: SalesTransaction): Promise<ApiResponse> {
  try {
    await sdk.authenticate('username', 'password');
    return await sdk.sendSalesTransaction(sale);
  } catch (error) {
    console.error('Failed to process sale:', error);
    throw error;
  }
}
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

This project is licensed under the ISC License - see the LICENSE file for details.


