Metadata-Version: 2.4
Name: zimra
Version: 0.0.5
Summary: Unofficial Python wrapper for the ZIMRA FDMS API by Tarmica Chiwara
Home-page: https://github.com/lordskyzw/zimra
Author: Tarmica Chiwara
Author-email: Tarmica Chiwara <tarimicac@gmail.com>
License: MIT
Project-URL: Repository, https://github.com/lordskyzw/zimra
Keywords: fiscalisation,api,automation,zimra,opensource
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.25.0
Requires-Dist: colorama>=0.4.3
Requires-Dist: requests_toolbelt>=0.9.1
Dynamic: author
Dynamic: home-page
Dynamic: requires-python

<![CDATA[<div align="center">

# 🇿🇼 zimra

**Unofficial Python wrapper for the ZIMRA Fiscal Device Management System (FDMS) API**

[![PyPI version](https://img.shields.io/pypi/v/zimra)](https://pypi.org/project/zimra/)
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)

*Simplifying fiscal device integration for Zimbabwean businesses*

</div>

---

## What is this?

`zimra` is a Python library that handles the complexity of communicating with ZIMRA's Fiscal Device Management System. If you're building a POS system, e-commerce platform, or any application that needs to issue fiscal receipts in Zimbabwe — this library does the heavy lifting for you.

**What it handles for you:**
- 🔐 Device registration & certificate management (mTLS)
- 🧮 Automatic VAT calculation (tax-inclusive **and** tax-exclusive)
- 📝 Receipt preparation, signing (SHA256 + RSA PKCS#1 v1.5), and submission
- 📅 Fiscal day lifecycle (open → submit receipts → close)
- 📱 QR code generation for receipt verification
- ✅ Support for fiscal invoices, credit notes, and debit notes

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
  - [Step 1 — Register Your Device](#step-1--register-your-device)
  - [Step 2 — Initialize the Device](#step-2--initialize-the-device)
  - [Step 3 — Open a Fiscal Day](#step-3--open-a-fiscal-day)
  - [Step 4 — Prepare & Submit a Receipt](#step-4--prepare--submit-a-receipt)
  - [Step 5 — Close the Fiscal Day](#step-5--close-the-fiscal-day)
- [Tax-Exclusive Receipts](#tax-exclusive-receipts)
- [Credit Notes & Debit Notes](#credit-notes--debit-notes)
- [Exempt & Mixed Tax Items](#exempt--mixed-tax-items)
- [QR Code Generation](#qr-code-generation)
- [Utility Functions](#utility-functions)
- [API Reference](#api-reference)
- [Supported Tax Rates (2026)](#supported-tax-rates-2026)
- [Error Handling](#error-handling)
- [Contributing](#contributing)
- [License](#license)

---

## Installation

### From PyPI (recommended)

```bash
pip install zimra
```

### From source

```bash
git clone https://github.com/lordskyzw/zimra.git
cd zimra
pip install .
```

### Dependencies

The library requires the following packages (installed automatically):

| Package | Purpose |
|---------|---------|
| `requests` | HTTP communication with ZIMRA API |
| `pycryptodome` | RSA signing of receipts (PKCS#1 v1.5) |
| `cryptography` | Certificate generation (CSR, key pairs) |
| `colorama` | Colored logging output |
| `requests_toolbelt` | Enhanced HTTP utilities |

---

## Quick Start

This walkthrough takes you from a fresh device to submitting your first fiscal receipt. We'll use **test mode** throughout — switch to `test_mode=False` and `prod=True` when you're ready for production.

### Step 1 — Register Your Device

Before anything else, you need to register your fiscal device with ZIMRA. This generates an RSA key pair and exchanges a CSR for a signed certificate.

```python
from zimra import register_new_device

register_new_device(
    fiscal_device_serial_no="9029D38C011B",   # Your device's serial number
    device_id="10626",                         # Device ID assigned by ZIMRA
    activation_key="00398834",                 # 8-digit activation key from ZIMRA
    folder_name="certs",                       # Directory to save certificate & key
    certificate_filename="my_certificate",     # Output: certs/my_certificate.crt
    private_key_filename="my_private_key",     # Output: certs/my_private_key.key
    prod=False                                 # False = test environment
)
```

> **📁 After running this, you'll have two files:**
> - `certs/my_certificate.crt` — your device certificate (issued by ZIMRA)
> - `certs/my_private_key.key` — your RSA private key (keep this secure!)

### Step 2 — Initialize the Device

Create a `Device` instance using your credentials and certificate files:

```python
from zimra import Device

device = Device(
    device_id="10626",
    serialNo="9029D38C011B",
    activationKey="00398834",
    cert_path="certs/my_certificate.crt",
    private_key_path="certs/my_private_key.key",
    test_mode=True,                  # True = sandbox, False = production
    deviceModelName="Server",        # Must match what ZIMRA has on record
    deviceModelVersion="v1",
    company_name="MyCompany"
)
```

> ⚠️ **Important:** The `deviceModelName` must exactly match what's registered with ZIMRA. A mismatch will result in `403 Forbidden` errors.

You can verify connectivity and check your device configuration:

```python
# Check device status (fiscal day state, etc.)
status = device.getStatus()
print(status)
# {'fiscalDayNo': 1, 'fiscalDayStatus': 'FiscalDayClosed', ...}

# Get full device configuration (taxpayer info, applicable taxes, etc.)
config = device.getConfig()
print(config)
# {'taxPayerName': 'MY COMPANY', 'taxPayerTIN': '...', 'applicableTaxes': [...], ...}

# Simple connectivity test
ping = device.ping()
print(ping)
```

### Step 3 — Open a Fiscal Day

A fiscal day must be opened before you can submit any receipts. The `fiscalDayNo` is a sequential integer that must be greater than the last closed day.

```python
result = device.openDay(fiscalDayNo=1)
print(result)
# {'fiscalDayNo': 1, 'operationID': '0HN4FDK6T1CNI:00000001'}
```

> The library automatically checks that the current fiscal day is closed before opening a new one. If day 1 is already open, it will return an error message.

### Step 4 — Prepare & Submit a Receipt

This is the core workflow. You provide a simplified receipt object, and the library:
1. Validates all mandatory fields
2. Formats receipt lines to ZIMRA's specification  
3. Calculates taxes automatically (VAT extraction from inclusive prices)
4. Signs the receipt with your private key
5. Returns a fully prepared payload ready for submission

#### Build the receipt data:

```python
from datetime import datetime

receipt_data = {
    "receiptType": "FISCALINVOICE",               # FISCALINVOICE | CREDITNOTE | DEBITNOTE
    "receiptCurrency": "USD",                       # USD | ZWG
    "receiptCounter": 1,                            # Sequential counter per receipt type
    "receiptGlobalNo": 1,                           # Global sequential number
    "invoiceNo": "INV-001",                         # Your unique invoice number
    "receiptDate": datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
    "receiptLines": [
        {
            "item_name": "Widget A",
            "tax_percent": 15.5,                     # Standard VAT rate (2026)
            "quantity": 2,
            "unit_price": 25.00
        },
        {
            "item_name": "Widget B (Zero-rated)",
            "tax_percent": 0,                        # Zero-rated item
            "quantity": 1,
            "unit_price": 10.00
        }
    ],
    "receiptPayments": [{
        "moneyTypeCode": 0,                          # 0 = Cash, 1 = Card
        "paymentAmount": 60.00                       # Total payment amount
    }]
}
```

#### Prepare and submit:

```python
# prepareReceipt handles tax calculation, formatting, and signing
prepared = device.prepareReceipt(receipt_data)

# Submit to ZIMRA
response = device.submitReceipt(prepared)
print(response)
```

> 💡 **Tip:** The `prepareReceipt` method assumes prices are **tax-inclusive** (prices already include VAT). For tax-exclusive pricing, see [Tax-Exclusive Receipts](#tax-exclusive-receipts).

### Step 5 — Close the Fiscal Day

At the end of the business day, close the fiscal day with a summary of the day's counters:

```python
result = device.closeDay(
    fiscalDayNo=1,
    fiscalDayDate="2026-02-22",
    lastReceiptCounterValue=5,        # Total receipts submitted today
    fiscalDayCounters=[               # Summary of the day's transactions
        {
            "fiscalCounterType": "SaleByTax",
            "fiscalCounterCurrency": "USD",
            "fiscalCounterTaxPercent": 15.5,
            "fiscalCounterTaxID": 515,
            "fiscalCounterValue": 500.00
        },
        {
            "fiscalCounterType": "BalanceByMoneyType",
            "fiscalCounterCurrency": "USD",
            "fiscalCounterMoneyType": 0,
            "fiscalCounterValue": 500.00
        }
    ]
)
print(result)
```

> To close an empty day (no receipts), pass `lastReceiptCounterValue=None` and `fiscalDayCounters=[]`.

---

## Tax-Exclusive Receipts

If your prices **do not** include VAT, use `prepareReceiptTaxExclusive` instead. The library will calculate VAT on top of the base prices and adjust the receipt total accordingly.

```python
receipt_data = {
    "receiptType": "FISCALINVOICE",
    "receiptCurrency": "USD",
    "receiptCounter": 1,
    "receiptGlobalNo": 2,
    "invoiceNo": "INV-002",
    "receiptDate": datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
    "receiptLines": [
        {
            "item_name": "Service A",
            "tax_percent": 15.5,        # VAT will be calculated ON TOP of this price
            "quantity": 1,
            "unit_price": 100.00         # Pre-tax price
        }
    ],
    "receiptPayments": [{
        "moneyTypeCode": 0,
        "paymentAmount": 115.50          # base + VAT
    }]
}

prepared = device.prepareReceiptTaxExclusive(receipt_data)
response = device.submitReceipt(prepared)
```

**Key difference:**
| Method | `unit_price` means | VAT calculation |
|--------|-------------------|-----------------|
| `prepareReceipt` | Price **including** VAT | VAT extracted: `total × rate / (1 + rate)` |
| `prepareReceiptTaxExclusive` | Price **excluding** VAT | VAT added: `total × rate` |

---

## Credit Notes & Debit Notes

To issue a credit note (refund) or debit note, set `receiptType` accordingly and include the required `creditDebitNote` and `receiptNotes` fields:

```python
credit_note = {
    "receiptType": "CREDITNOTE",
    "receiptCurrency": "USD",
    "receiptCounter": 1,
    "receiptGlobalNo": 3,
    "invoiceNo": "CN-001",
    "receiptDate": datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
    "receiptNotes": "Refund for damaged goods",
    "creditDebitNote": {
        "receiptID": 12345,                    # Original receipt ID from ZIMRA
        "deviceID": 10626,
        "receiptGlobalNo": 1,                  # Original receipt's global number
        "fiscalDayNo": 1
    },
    "receiptLines": [
        {
            "item_name": "Widget A (Returned)",
            "tax_percent": 15.5,
            "quantity": 1,
            "unit_price": 25.00
        }
    ],
    "receiptPayments": [{
        "moneyTypeCode": 0,
        "paymentAmount": 25.00
    }]
}

prepared = device.prepareReceipt(credit_note)
response = device.submitReceipt(prepared)
```

---

## Exempt & Mixed Tax Items

You can mix different tax rates in a single receipt, including exempt items:

```python
receipt_data = {
    # ... standard fields ...
    "receiptLines": [
        {
            "item_name": "Standard item",
            "tax_percent": 15.5,             # Standard 15.5% VAT
            "quantity": 1,
            "unit_price": 50.00
        },
        {
            "item_name": "Reduced rate item",
            "tax_percent": 5,                # 5% withholding tax
            "quantity": 1,
            "unit_price": 30.00
        },
        {
            "item_name": "Zero-rated item",
            "tax_percent": 0,                # Zero-rated
            "quantity": 1,
            "unit_price": 20.00
        },
        {
            "item_name": "Exempt item",
            "tax_percent": "exempt",         # Use string "exempt" or "E"
            "quantity": 1,
            "unit_price": 15.00
        }
    ],
    # ...
}
```

> The library automatically groups items by tax rate, calculates consolidated tax amounts per group, and assigns the correct ZIMRA `taxID` for each.

---

## QR Code Generation

After submitting a receipt, generate a QR code URL for receipt verification:

```python
# After a successful submitReceipt call:
qr_url = device.generate_qr_code(
    signature=prepared["receiptDeviceSignature"]["signature"],
    receipt_global_no=1,
    receipt_date=datetime.now().date()
)
print(qr_url)
# https://fdmstest.zimra.co.zw/0000010626220220260000000001<md5hash>
```

This URL can be encoded into a QR code on printed receipts for customers to verify the receipt with ZIMRA.

---

## Utility Functions

### Standalone Tax Calculator

Calculate VAT without initializing a `Device`:

```python
from zimra import tax_calculator

# Extract VAT from a tax-inclusive price
# Formula: VAT = total - (total / (1 + rate/100))
vat = tax_calculator(sale_amount=115.50, tax_rate=15.5)
print(vat)  # 15.5
```

### Receipt Preprocessing

If you need to sanitize receipt data before preparing it:

```python
from zimra import preprocess_receipt, preprocess_tax_exclusivereceipt

# Ensures all numeric fields are properly formatted strings
# Calculates line_total for each receipt line
cleaned = preprocess_receipt(raw_receipt_data)
prepared = device.prepareReceipt(cleaned)
```

---

## API Reference

### `register_new_device()`

Registers a new device with ZIMRA and saves the certificate + private key locally.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `fiscal_device_serial_no` | `str` | *required* | Device serial number |
| `device_id` | `str` | *required* | ZIMRA-assigned device ID |
| `activation_key` | `str` | *required* | 8-digit activation key |
| `model_name` | `str` | `'Server'` | Device model name |
| `folder_name` | `str` | `'prod'` | Output directory for cert/key files |
| `certificate_filename` | `str` | `'certificate'` | Certificate filename (without extension) |
| `private_key_filename` | `str` | `'decrypted_key'` | Private key filename (without extension) |
| `prod` | `bool` | `False` | `True` = production, `False` = test |

### `Device` Class

#### Constructor

```python
Device(
    device_id, serialNo, activationKey,
    cert_path, private_key_path,
    test_mode=True, deviceModelName='Server',
    deviceModelVersion='v1', company_name='NexusClient'
)
```

#### Methods

| Method | Description | Returns |
|--------|-------------|---------|
| `getConfig()` | Fetch device configuration (taxpayer info, applicable taxes) | `dict` |
| `getStatus()` | Get current fiscal day status | `dict` |
| `ping()` | Connectivity test to ZIMRA server | `dict` |
| `openDay(fiscalDayNo)` | Open a new fiscal day | `dict` |
| `prepareReceipt(receiptData, ...)` | Prepare a tax-**inclusive** receipt for submission | `OrderedDict` |
| `prepareReceiptTaxExclusive(receiptData, ...)` | Prepare a tax-**exclusive** receipt for submission | `OrderedDict` |
| `submitReceipt(receiptData)` | Submit a prepared receipt to ZIMRA | `dict` |
| `closeDay(fiscalDayNo, fiscalDayDate, lastReceiptCounterValue, fiscalDayCounters)` | Close the current fiscal day | `dict` |
| `generate_qr_code(signature, receipt_global_no, receipt_date)` | Generate a QR code verification URL | `str` |
| `renewCertificate()` | Renew the device certificate | `str` |

---

## Supported Tax Rates (2026)

| Tax Rate | Tax ID | Description |
|----------|--------|-------------|
| `0` | `2` | Zero-rated (0%) |
| `"exempt"` or `"E"` | `3` | Exempt |
| `5` | `514` | Non-VAT Withholding Tax (5%) |
| `15.5` | `515` | Standard VAT (15.5%) — *current rate* |
| `15` | Legacy | Supported for backward compatibility |

---

## Error Handling

The library raises clear errors for common issues:

```python
from zimra import ZimraServerError

try:
    prepared = device.prepareReceipt(receipt_data)
    response = device.submitReceipt(prepared)
except ValueError as e:
    # Missing mandatory fields, invalid date format, invalid tax_percent, etc.
    print(f"Validation error: {e}")
except ZimraServerError as e:
    # ZIMRA server returned an error
    print(f"Server error {e.status_code}: {e}")
except Exception as e:
    print(f"Unexpected error: {e}")
```

**Common gotchas:**
- `deviceModelName` must match exactly what ZIMRA has registered — otherwise you get `403`
- Fiscal day numbers must be sequential and the previous day must be closed
- `receiptGlobalNo` must be globally unique and sequential
- Certificate files must be accessible and valid

---

## Contributing

Contributions are welcome! This project is actively maintained and there are many ways to help:

- 🐛 **Bug reports** — Open an issue with steps to reproduce
- 💡 **Feature requests** — Suggest improvements or new functionality
- 🔧 **Pull requests** — Submit code changes

```bash
git clone https://github.com/lordskyzw/zimra.git
cd zimra
pip install -e .   # Install in editable mode
```

---

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

---

<div align="center">

**Built with ❤️ by [Tarmica Chiwara](https://github.com/lordskyzw)**

*Alleviating complexity from Zimbabwean businesses*

</div>
]]>
