diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 5f6a571..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,859 +0,0 @@ -# FiscGuy Architecture & Engineering Documentation - -This document provides comprehensive technical documentation for FiscGuy developers and maintainers. - -## Table of Contents - -1. [Project Overview](#project-overview) -2. [Architecture](#architecture) -3. [Data Models](#data-models) -4. [Service Layer](#service-layer) -5. [Cryptography & Security](#cryptography--security) -6. [ZIMRA Integration](#zimra-integration) -7. [Receipt Processing Pipeline](#receipt-processing-pipeline) -8. [Fiscal Day Management](#fiscal-day-management) -9. [Error Handling](#error-handling) -10. [Database Design](#database-design) -11. [Development Guidelines](#development-guidelines) - -## Project Overview - -FiscGuy is a Django-based library for integrating with ZIMRA (Zimbabwe Revenue Authority) fiscal devices. It manages: - -- **Device Registration & Management** - Certificate-based authentication with ZIMRA FDMS -- **Receipt Generation & Submission** - Full receipt lifecycle with cryptographic signing -- **Fiscal Day Operations** - Opening/closing fiscal days with counter management -- **Configuration Management** - Device and taxpayer configuration persistence -- **Tax Management** - Support for multiple tax types and rates - -**Technology Stack:** -- Django 4.2+ -- Django REST Framework -- Cryptography (RSA, SHA-256, MD5) -- QRCode Generation -- ZIMRA FDMS API (HTTPS with certificate-based auth) - -## Architecture - -FiscGuy follows a **layered architecture**: - -``` -┌─────────────────────────────────────────────────────┐ -│ REST API Layer (views.py) │ -│ ReceiptView, OpenDayView, CloseDayView, etc. │ -└──────────────────┬──────────────────────────────────┘ - │ -┌──────────────────┴──────────────────────────────────┐ -│ Service Layer (services/) │ -│ ReceiptService, OpenDayService, ClosingDayService │ -└──────────────────┬──────────────────────────────────┘ - │ -┌──────────────────┴──────────────────────────────────┐ -│ Data Persistence Layer (models.py) │ -│ Device, Receipt, FiscalDay, Configuration, etc. │ -└──────────────────┬──────────────────────────────────┘ - │ -┌──────────────────┴──────────────────────────────────┐ -│ ZIMRA Integration Layer (zimra_*.py) │ -│ ZIMRAClient, ZIMRACrypto, ZIMRAReceiptHandler │ -└──────────────────┬──────────────────────────────────┘ - │ - ↓ - ZIMRA FDMS REST API -``` - -### Layer Responsibilities - -#### 1. REST API Layer (`views.py`) -- **ReceiptView** - List/paginate receipts (GET), create & submit receipts (POST) -- **OpenDayView** - Open fiscal day operations -- **CloseDayView** - Close fiscal day and calculate counters -- **StatusView** - Query device status from ZIMRA -- **ConfigurationView** - Fetch device configuration -- **TaxView** - List available taxes -- **BuyerViewset** - CRUD operations for buyers - -#### 2. Service Layer (`services/`) - -**ReceiptService** - Orchestrates receipt creation and submission: -- Validates receipt payload via serializers -- Persists receipt to database -- Delegates processing to ZIMRAReceiptHandler -- Returns atomic operation result (all-or-nothing) - -**OpenDayService** - Opens fiscal day on ZIMRA: -- Queries ZIMRA for next day number -- Creates FiscalDay record -- Auto-called if no open day exists when submitting receipt - -**ClosingDayService** - Closes fiscal day: -- Queries ZIMRA for fiscal counters -- Builds counters hashstring per spec -- Sends closing request to ZIMRA -- Updates local FiscalDay record - -**ConfigurationService** - Synchronizes configuration: -- Fetches device config from ZIMRA -- Syncs taxes from ZIMRA -- Manages tax updates - -**StatusService** - Queries ZIMRA status -**PingService** - Device connectivity check -**CertsService** - Certificate management - -#### 3. Data Persistence Layer (`models.py`) - -See [Data Models](#data-models) section below. - -#### 4. ZIMRA Integration Layer - -**ZIMRAClient** (`zimra_base.py`): -- HTTP/HTTPS requests to ZIMRA FDMS -- Certificate-based authentication -- Request/response handling -- Timeout management (30s default) - -**ZIMRACrypto** (`zimra_crypto.py`): -- RSA signing with SHA-256 -- SHA-256 hashing for integrity -- MD5 for verification code generation -- QR code verification code from signature - -**ZIMRAReceiptHandler** (`zimra_receipt_handler.py`): -- Complete receipt processing pipeline -- Receipt data building per ZIMRA spec -- Hash and signature generation -- QR code creation -- Fiscal counter updates -- FDMS submission - -## Data Models - -### Device -```python -Fields: -- org_name: str (max 255) -- activation_key: str (max 255) -- device_id: str (unique, max 100) -- device_model_name: str (optional) -- device_serial_number: str (optional) -- device_model_version: str (optional) -- production: bool (test/production environment flag) -- created_at: datetime (auto_now_add) - -Relationships: -- configuration (OneToOne) → Configuration -- certificate (OneToOne) → Certs -- fiscal_days (OneToMany) → FiscalDay -- fiscal_counters (OneToMany) → FiscalCounter -- receipts (OneToMany) → Receipt -``` - -**Purpose:** Represents a physical/logical ZIMRA fiscal device. Multiple devices can be registered (e.g., different POS terminals). - -### Configuration -```python -Fields: -- device: OneToOneField → Device -- tax_payer_name: str (max 255) -- tin_number: str (max 20) -- vat_number: str (max 20) -- address: str (max 255) -- phone_number: str (max 20) -- email: EmailField -- url: URLField (test/production ZIMRA endpoint) -- tax_inclusive: bool (tax calculation mode) -- created_at/updated_at: datetime - -Relationships: -- device (OneToOne) → Device -``` - -**Purpose:** Stores taxpayer configuration synced from ZIMRA. One config per device. - -### Certs -```python -Fields: -- device: OneToOneField → Device -- csr: TextField (Certificate Signing Request) -- certificate: TextField (X.509 certificate) -- certificate_key: TextField (RSA private key) -- production: bool (test/production cert) -- created_at/updated_at: datetime - -Relationships: -- device (OneToOne) → Device -``` - -**Purpose:** Stores device certificates and private keys for ZIMRA authentication. - -### FiscalDay -```python -Fields: -- device: ForeignKey → Device -- day_no: int (ZIMRA fiscal day number) -- receipt_counter: int (receipts issued today, default 0) -- is_open: bool (day open/closed) -- created_at/updated_at: datetime - -Constraints: -- unique_together: (device, day_no) - -Indexes: -- (device, is_open) - for fast open day queries -``` - -**Purpose:** Represents a fiscal day (accounting period). Each device has one open fiscal day at a time. - -### FiscalCounter -```python -Fields: -- device: ForeignKey → Device -- fiscal_day: ForeignKey → FiscalDay -- fiscal_counter_type: CharField - - SaleByTax, SaleTaxByTax - - CreditNoteByTax, CreditNoteTaxByTax - - DebitNoteByTax, DebitNoteTaxByTax - - BalanceByMoneyType, Other -- fiscal_counter_currency: CharField (USD, ZWG) -- fiscal_counter_tax_percent: Decimal (optional) -- fiscal_counter_tax_id: int (optional) -- fiscal_counter_money_type: CharField (Cash, Card, BankTransfer, MobileMoney) -- fiscal_counter_value: Decimal (accumulated counter value) -- created_at/updated_at: datetime - -Constraints: -- Indexed: (device, fiscal_day) - -Relationships: -- device (ForeignKey) → Device -- fiscal_day (ForeignKey) → FiscalDay -``` - -**Purpose:** Accumulates receipt values by type, currency, and tax. Updated on receipt submission. Used to close fiscal day. - -### Receipt -```python -Fields: -- device: ForeignKey → Device (FIXED: was missing, added in v0.1.6) -- receipt_number: str (unique, auto-generated as R-{global_number:08d}) -- receipt_type: str - - fiscalinvoice (normal receipt) - - creditnote (debit customer) - - debitnote (credit customer, not mandatory) -- total_amount: Decimal (12 digits, 2 decimals) -- currency: str (USD or ZWG) -- qr_code: ImageField (PNG, uploaded to Zimra_qr_codes/) -- code: str (verification code, extracted from signature) -- global_number: int (ZIMRA global receipt number) -- hash_value: str (SHA-256 hash) -- signature: TextField (RSA signature, base64) -- zimra_inv_id: str (ZIMRA internal receipt ID) -- buyer: ForeignKey → Buyer (optional) -- payment_terms: str (Cash, Card, BankTransfer, MobileWallet, Coupon, Credit, Other) -- submitted: bool (whether sent to ZIMRA) -- is_credit_note: bool -- credit_note_reason: str (optional) -- credit_note_reference: str (receipt_number of original receipt) -- created_at/updated_at: datetime - -Constraints: -- receipt_number: unique - -Relationships: -- device (ForeignKey) → Device -- buyer (ForeignKey) → Buyer (optional) -- lines (OneToMany) → ReceiptLine -``` - -**Purpose:** Core receipt entity. Stores receipt data, cryptographic material, and ZIMRA metadata. - -### ReceiptLine -```python -Fields: -- receipt: ForeignKey → Receipt -- product: str (max 255) -- quantity: Decimal (10 digits, 2 decimals) -- unit_price: Decimal (12 digits, 2 decimals) -- line_total: Decimal (quantity × unit_price, 12 digits, 2 decimals) -- tax_amount: Decimal (12 digits, 2 decimals) -- tax_type: ForeignKey → Taxes (optional) -- created_at/updated_at: datetime - -Constraints: -- Indexed: (receipt) - -Relationships: -- receipt (ForeignKey) → Receipt -- tax_type (ForeignKey) → Taxes (optional) -``` - -**Purpose:** Line items on a receipt (products/services). - -### Taxes -```python -Fields: -- code: str (tax code, max 10) -- name: str (human-readable tax name, max 100) -- tax_id: int (ZIMRA tax identifier) -- percent: Decimal (tax rate, 5 digits, 2 decimals) -- created_at: datetime - -Constraints: -- Indexed: (tax_id) -- Ordered by: tax_id - -Example rows: -- Standard Rated 15.5%, tax_id=1, percent=15.50 -- Zero Rated 0%, tax_id=4, percent=0.00 -- Exempt 0%, tax_id=5, percent=0.00 -``` - -**Purpose:** Tax type definitions. Auto-synced from ZIMRA on configuration init and day opening. - -### Buyer -```python -Fields: -- name: str (max 255, registered business name) -- address: str (max 255, optional) -- tin_number: str (max 255, unique within buyer records) -- trade_name: str (max 100, optional, e.g., branch name) -- email: EmailField (optional) -- phonenumber: str (max 20, optional) -- created_at/updated_at: datetime - -Constraints: -- Indexed: (tin_number) - -Relationships: -- receipts (OneToMany) → Receipt -``` - -**Purpose:** Customer/buyer information. Optional on receipts (can be null for cash sales). Uses `get_or_create` to avoid duplicates by TIN. - -## Service Layer - -### ReceiptService - -**Location:** `fiscguy/services/receipt_service.py` - -**Purpose:** Validates, persists, and submits receipts to ZIMRA. - -**Key Method:** `create_and_submit_receipt(data: dict) → tuple[Receipt, dict]` - -```python -Flow: -1. Adds device ID to request data -2. Validates via ReceiptCreateSerializer -3. Persists receipt to DB (with buyer creation/linking) -4. Fetches fully hydrated receipt (with lines, buyer) -5. Delegates to ZIMRAReceiptHandler.process_and_submit() -6. Returns (Receipt, submission_result) - -Atomicity: -- Wrapped in @transaction.atomic -- If submission fails: entire operation rolled back, receipt NOT saved -- If submission succeeds: receipt marked as submitted=True - -Raises: -- serializer.ValidationError: invalid payload -- ReceiptSubmissionError: processing/FDMS submission failed -``` - -### OpenDayService - -**Location:** `fiscguy/services/open_day_service.py` - -**Purpose:** Opens a new fiscal day with ZIMRA and syncs taxes. - -**Key Method:** `open_day() → dict` - -```python -Flow: -1. Queries ZIMRA status for next day_no (lastFiscalDayNo + 1) -2. Syncs latest taxes from ZIMRA -3. Creates FiscalDay record (is_open=True) -4. Returns ZIMRA response - -Auto-call: -- Triggered automatically if no open day exists when submitting first receipt -- Adds 5-second delay to allow ZIMRA processing -``` - -### ClosingDayService - -**Location:** `fiscguy/services/closing_day_service.py` - -**Purpose:** Closes fiscal day and sends closing hash to ZIMRA. - -**Key Method:** `close_day() → dict` - -```python -Flow: -1. Fetches open FiscalDay -2. Queries ZIMRA for fiscal counters (SaleByTax, SaleTaxByTax, etc.) -3. Fetches local receipts for the day -4. Builds counters per ZIMRA spec (see below) -5. Creates closing hashstring with counters -6. Signs hashstring with RSA -7. Sends closing request to ZIMRA -8. Marks FiscalDay as is_open=False -9. Saves fiscal counters to DB - -Counter Ordering (per spec 13.3.1): -- Sorted by (currency ASC, taxID ASC) -- Zero-value counters EXCLUDED -- Format: "counter1|counter2|..." -``` - -### ConfigurationService - -**Location:** `fiscguy/services/configuration_service.py` - -**Purpose:** Syncs taxpayer config and taxes from ZIMRA. - -**Key Methods:** -- `get_configuration()` - Fetches config from ZIMRA -- `sync_taxes()` - Fetches and updates tax records -- `sync_all()` - Full sync - -### StatusService & PingService - -- **StatusService** - Queries device status from ZIMRA -- **PingService** - Tests device connectivity - -## Cryptography & Security - -### ZIMRACrypto - -**Location:** `fiscguy/zimra_crypto.py` - -**Algorithms:** -- **Signing:** RSA-2048 with PKCS#1 v1.5 padding, SHA-256 -- **Hashing:** SHA-256 -- **Verification Code:** MD5 (from signature bytes) -- **Encoding:** Base64 - -**Key Methods:** - -#### `generate_receipt_hash_and_signature(signature_string: str) → dict` -```python -signature_string = "receipt|data|string|built|per|spec" -hash_value = SHA256(signature_string) # base64 encoded -signature = RSA_SIGN(signature_string) # base64 encoded - -Returns: {"hash": hash_value, "signature": signature} -``` - -**Critical:** The signature_string format is specified by ZIMRA spec (see `ZIMRAReceiptHandler._build_receipt_data()`). - -#### `sign_data(data: str) → str` -- Signs arbitrary data with RSA private key -- Returns base64-encoded signature - -#### `get_hash(data: str) → str` -- SHA-256 hash, base64-encoded - -#### `generate_verification_code(base64_signature: str) → str` -- Extracts 16-character code from signature for QR -- Used in QR code data - -#### `load_private_key() → RSAPrivateKey` -- Loads from stored certificate PEM -- Caches result - -### Certificate Management - -**Location:** `fiscguy/utils/cert_temp_manager.py` - -**Purpose:** Manages temporary PEM files for ZIMRA HTTPS authentication. - -**Usage:** -- ZIMRAClient creates temporary PEM file from certificate + key -- Session uses cert for mutual TLS authentication -- Cleanup on object destruction - -## ZIMRA Integration - -### ZIMRAClient - -**Location:** `fiscguy/zimra_base.py` - -**Purpose:** HTTP client for ZIMRA FDMS API. - -**Endpoints:** -- **Device API** (requires cert): `https://fdmsapi[test].zimra.co.zw/Device/v1/{device_id}/...` -- **Public API** (no cert): `https://fdmsapi[test].zimra.co.zw/Public/v1/{device_id}/...` - -**Environment Detection:** -- If `Certs.production=True`: uses production URL -- Else: uses test URL - -**Key Methods:** -- `register_device(payload)` - Register device (public endpoint, no cert) -- `get_status()` - Query device status (device endpoint) -- `submit_receipt(payload)` - Submit receipt to ZIMRA -- `open_fiscal_day(payload)` - Open fiscal day -- `close_fiscal_day(payload)` - Close fiscal day - -**Session Management:** -- Persistent `requests.Session` with cert authentication -- Headers include device model name/version -- Timeout: 30 seconds - -### ZIMRA API Payloads - -#### Receipt Submission - -```json -{ - "receiptNumber": "R-00000001", - "receiptType": "F", // F=invoice, C=credit note, D=debit note - "receiptTotal": 100.00, - "receiptCurrency": "USD", - "receiptGlobalNo": 1, - "receiptDateTime": "2026-04-01T10:30:00Z", - "receiptDescription": "...", - "buyerTIN": "1234567890", // optional - "paymentMethod": "Cash", - "receiptLineItems": [ - { - "itemNumber": 1, - "itemDescription": "Product", - "itemQuantity": 1.00, - "itemUnitPrice": 100.00, - "itemTaxType": "Standard Rated", - "itemTaxAmount": 15.50 - } - ], - "hash": "base64-encoded-sha256", - "signature": "base64-encoded-rsa-signature" -} -``` - -#### Fiscal Day Close - -```json -{ - "hash": "base64-encoded-hashstring", - "signature": "base64-encoded-rsa-signature", - "counters": [ - { - "counterType": "SaleByTax", - "counterCurrency": "USD", - "counterTaxType": "Standard Rated", - "counterTaxId": 1, - "counterValue": 1000.00 - }, - ... - ] -} -``` - -## Receipt Processing Pipeline - -### Complete Flow - -``` -POST /api/receipts/ - ↓ -ReceiptView.post() - ↓ -ReceiptService.create_and_submit_receipt() - ├─ ReceiptCreateSerializer.validate() - │ ├─ Validate credit note (if applicable) - │ └─ Validate TIN (if buyer provided) - ├─ ReceiptCreateSerializer.create() - │ ├─ Get or create Buyer (from buyer_data) - │ ├─ Create Receipt (device + lines) - │ └─ Create ReceiptLine items - │ - ├─ ZIMRAReceiptHandler.process_and_submit() - │ ├─ _ensure_fiscal_day_open() - │ │ ├─ Check if FiscalDay open - │ │ └─ If not: auto-call OpenDayService.open_day() - │ │ - │ ├─ _build_receipt_data() - │ │ └─ Construct signature_string per ZIMRA spec - │ │ - │ ├─ ZIMRACrypto.generate_receipt_hash_and_signature() - │ │ ├─ SHA256 hash - │ │ └─ RSA sign - │ │ - │ ├─ _generate_qr_code() - │ │ ├─ Extract verification code from signature - │ │ ├─ Create QR PNG image - │ │ └─ Save to Receipt.qr_code - │ │ - │ ├─ _update_fiscal_counters() - │ │ └─ Increment FiscalCounter values - │ │ - │ └─ _submit_to_fdms() - │ ├─ POST receipt to ZIMRA - │ ├─ Parse response - │ └─ Return submission_result - │ - └─ Update Receipt (hash, signature, global_no, zimra_inv_id, submitted=True) - └─ Save to DB - -Returns: Response(ReceiptSerializer(receipt), 201) -``` - -### Atomic Transaction - -The entire flow (ReceiptService.create_and_submit_receipt) is wrapped in `@transaction.atomic`: - -```python -@transaction.atomic -def create_and_submit_receipt(self, data: dict): - # All or nothing - # If step N fails → all changes rolled back -``` - -### Automatic Fiscal Day Opening - -If no open fiscal day exists: -1. OpenDayService auto-opens one -2. 5-second delay for ZIMRA processing -3. Continues with receipt submission - -## Fiscal Day Management - -### Fiscal Day Lifecycle - -``` -State: is_open=False - ↓ [POST /open-day/] -State: is_open=True, receipts can be submitted - ↓ [receipts submitted, counters accumulated] - ↓ [POST /close-day/] -State: is_open=False, counters reset -``` - -### Fiscal Counter Update - -On receipt submission: - -```python -for each line_item in receipt.lines: - for each tax_type on line: - counter_type = f"SaleByTax" or "SaleTaxByTax" or "CreditNoteByTax" etc. - counter = FiscalCounter.objects.filter( - fiscal_day=fiscal_day, - fiscal_counter_type=counter_type, - fiscal_counter_currency=receipt.currency, - fiscal_counter_tax_id=line.tax_type.tax_id - ) - counter.fiscal_counter_value += line.amount_with_tax - counter.save() -``` - -**Raw-level DB Lock:** -To prevent race conditions, counter updates use F() for row-level locking: - -```python -FiscalCounter.objects.filter(...).update( - fiscal_counter_value=F('fiscal_counter_value') + amount -) -``` - -## Error Handling - -### Exception Hierarchy - -``` -FiscalisationError (base) -├── CertNotFoundError -├── CryptoError -├── DeviceNotFoundError -├── ConfigurationError -├── TaxError -├── FiscalDayError -├── ReceiptSubmissionError -├── StatusError -├── ZIMRAAPIError -├── DeviceRegistrationError -└── ... others -``` - -### Receipt Submission Error Handling - -**Flow:** - -```python -try: - receipt, submission_res = ReceiptService(device).create_and_submit_receipt(data) - return Response(serializer.data, 201) -except ReceiptSubmissionError as exc: - # Entire transaction rolled back - return Response({"error": str(exc)}, 422) -except Exception: - return Response({"error": "Unexpected error"}, 500) -``` - -**Key:** If ReceiptSubmissionError is raised, @transaction.atomic ensures the receipt is NOT saved. - -### Validation Errors - -**ReceiptCreateSerializer.validate():** -- Credit note validation -- TIN validation (10 digits) -- Receipt reference validation -- Amount sign validation (credit notes must be negative) - -## Database Design - -### Indexes - -```python -Device: - - device_id (UNIQUE) - -FiscalDay: - - (device, day_no) UNIQUE - - (device, is_open) - -FiscalCounter: - - (device, fiscal_day) - -Receipt: - - receipt_number (UNIQUE) - - (device, -created_at) - -ReceiptLine: - - (receipt) - -Taxes: - - tax_id - -Buyer: - - tin_number -``` - -### Relationships - -``` -Device (1) - ├─ Configuration (0..1) - ├─ Certs (0..1) - ├─ FiscalDay (0..*) - ├─ FiscalCounter (0..*) - └─ Receipt (0..*) - └─ ReceiptLine (1..*) - └─ Taxes (0..1) - └─ Buyer (0..1) -``` - -## Development Guidelines - -### Adding New Features - -1. **Model Changes** - - Update `models.py` - - Create migration: `python manage.py makemigrations fiscguy` - - Document in ARCHITECTURE.md - -2. **API Endpoints** - - Create view in `views.py` - - Add to `urls.py` - - Create serializer in `serializers.py` - - Add tests in `tests/` - -3. **Business Logic** - - Implement in `services/` - - Keep views thin (just HTTP handling) - - Use serializers for validation - -4. **ZIMRA Integration** - - Extend `ZIMRAClient` for new endpoints - - Handle API responses in services - - Add error handling - -### Testing - -**Run all tests:** -```bash -pytest -``` - -**Coverage:** -```bash -pytest --cov=fiscguy -``` - -**Specific test:** -```bash -pytest fiscguy/tests/test_receipt_service.py -``` - -### Code Quality - -**Linting:** -```bash -flake8 fiscguy/ -``` - -**Type checking:** -```bash -mypy fiscguy/ -``` - -**Code formatting:** -```bash -black fiscguy/ -``` - -### Atomic Transactions - -**Always wrap** state-changing operations (receipt creation, day opening/closing) in `@transaction.atomic`: - -```python -@transaction.atomic -def my_state_changing_operation(self): - # All-or-nothing - pass -``` - -### Logging - -Use `loguru` for structured logging: - -```python -from loguru import logger - -logger.info(f"Receipt {receipt.id} submitted") -logger.warning(f"FDMS offline, using provisional number") -logger.error(f"Failed to sign receipt: {e}") -logger.exception(f"Unexpected error") # includes traceback -``` - -### Private Methods - -Prefix with `_` (e.g., `_build_receipt_data()`, `_ensure_fiscal_day_open()`). Public methods (called from views/tests) have no prefix. - -### Model Meta Options - -- Always define `ordering` (for consistent query results) -- Use `indexes` for frequently-filtered fields -- Use `unique_together` for composite unique constraints -- Document in docstring - -### Serializer Best Practices - -- Separate read and write serializers (ReceiptSerializer vs ReceiptCreateSerializer) -- Mark read-only fields: `read_only_fields = [...]` -- Implement `validate()` for cross-field validation -- Use `transaction.atomic` in `create()` for complex nested creates - -### Configuration Management - -- Store ZIMRA environment URLs in `Configuration.url` -- Certificate environment (test vs production) in `Certs.production` -- Sync config on device init and day opening -- Use `get_or_create` to avoid duplicates - ---- - -**Last Updated:** April 2026 -**Version:** 0.1.6 -**Maintainers:** Casper Moyo (@cassymyo) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4a9a0fa..b7241f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,7 @@ Be respectful, inclusive, and professional in all interactions. ```bash # Clone the repository -git clone https://github.com/cassymyo-spec/zimra.git +git clone https://github.com/digtaltouchcode/fisc.git cd zimra /. to change to fiscguy # Create virtual environment diff --git a/DOCS_INDEX.md b/DOCS_INDEX.md deleted file mode 100644 index 3bd5f28..0000000 --- a/DOCS_INDEX.md +++ /dev/null @@ -1,184 +0,0 @@ -# FiscGuy Documentation Index - -Welcome to FiscGuy documentation! This guide helps you navigate the different documentation files based on your role and needs. - -## 📚 Documentation Files Overview - -### For New Users & Integration - -**Start here if you're:** -- Installing FiscGuy for the first time -- Integrating FiscGuy into your Django project -- Building a client application -- Looking for API examples - -**Read:** -1. **[USER_GUIDE.md](USER_GUIDE.md)** (15K) - Complete user guide with: - - Installation steps - - Quick start (5-minute setup) - - Full API endpoint reference - - 4 practical usage examples - - Concepts & terminology - - 30+ FAQs and troubleshooting - -2. **[README.md](README.md)** (9K) - Project overview with: - - Feature highlights - - Installation options - - Environment switching guide - - Basic setup instructions - ---- - -### For Developers & Maintainers - -**Read if you're:** -- Contributing to FiscGuy -- Understanding internal architecture -- Adding new features -- Debugging issues -- Designing integrations - -**Read:** -1. **[ARCHITECTURE.md](ARCHITECTURE.md)** (24K) - Technical deep dive covering: - - Layered architecture (REST → Services → Models → ZIMRA) - - Complete data model documentation - - Service layer details - - Cryptographic operations - - ZIMRA FDMS integration - - Receipt processing pipeline - - Fiscal day management - - Database design & indexes - - Development guidelines - -2. **[CONTRIBUTING.md](CONTRIBUTING.md)** (6K) - Contribution guidelines with: - - Code style requirements (Black, isort, flake8) - - Test requirements - - PR process - - Issue reporting - ---- - -### For Integration & DevOps - -**Read if you're:** -- Deploying FiscGuy to production -- Setting up ZIMRA environment -- Managing certificates -- Configuring Django settings - -**Read:** -1. **[INSTALL.md](INSTALL.md)** (6K) - Detailed installation guide -2. **[USER_GUIDE.md](USER_GUIDE.md)** - Quick Start section (Step 1-5) -3. **[ARCHITECTURE.md](ARCHITECTURE.md)** - Deployment considerations section - ---- - -### API Reference - -**For API endpoint details, request/response examples, and error codes:** - -**Read:** -1. **[USER_GUIDE.md](USER_GUIDE.md#api-endpoints)** - Quick API reference with curl examples -2. **[endpoints.md](endpoints.md)** - Detailed API specification -3. **[ARCHITECTURE.md](ARCHITECTURE.md#zimra-integration)** - ZIMRA payload specifications - ---- - -## Quick Navigation - -| Need | Document | Section | -|------|----------|---------| -| Install FiscGuy | [USER_GUIDE.md](USER_GUIDE.md#installation) | Installation | -| Setup project | [USER_GUIDE.md](USER_GUIDE.md#quick-start) | Quick Start | -| API examples | [USER_GUIDE.md](USER_GUIDE.md#usage-examples) | Usage Examples | -| Troubleshoot | [USER_GUIDE.md](USER_GUIDE.md#troubleshooting) | Troubleshooting | -| Answer a question | [USER_GUIDE.md](USER_GUIDE.md#faq) | FAQ | -| Understand architecture | [ARCHITECTURE.md](ARCHITECTURE.md#architecture) | Architecture | -| Data models | [ARCHITECTURE.md](ARCHITECTURE.md#data-models) | Data Models | -| Services | [ARCHITECTURE.md](ARCHITECTURE.md#service-layer) | Service Layer | -| Cryptography | [ARCHITECTURE.md](ARCHITECTURE.md#cryptography--security) | Cryptography | -| ZIMRA API | [ARCHITECTURE.md](ARCHITECTURE.md#zimra-integration) | ZIMRA Integration | -| Receipt flow | [ARCHITECTURE.md](ARCHITECTURE.md#receipt-processing-pipeline) | Receipt Pipeline | -| Dev guidelines | [ARCHITECTURE.md](ARCHITECTURE.md#development-guidelines) | Dev Guidelines | -| Contribute | [CONTRIBUTING.md](CONTRIBUTING.md) | All | - ---- - -## Documentation Philosophy - -**FiscGuy documentation is organized by audience:** - -1. **[USER_GUIDE.md](USER_GUIDE.md)** - Practical, example-driven, task-focused - - How do I...? - - Why does this happen? - - What does this mean? - -2. **[ARCHITECTURE.md](ARCHITECTURE.md)** - Technical, comprehensive, reference-style - - How does this work? - - What are the relationships? - - What are the constraints? - -3. **[CONTRIBUTING.md](CONTRIBUTING.md)** - Process-focused, standards-based - - How do I contribute? - - What are the standards? - -4. **[endpoints.md](endpoints.md)** - Specification-style - - What are all the endpoints? - - What are request/response formats? - ---- - -## Version Information - -- **Current Version:** 0.1.6 (unreleased) -- **Python:** 3.11, 3.12, 3.13 -- **Django:** 4.2+ -- **DRF:** 3.14+ -- **Last Updated:** April 1, 2026 - ---- - -## Getting Help - -| Question Type | Where to Look | -|---------------|---------------| -| "How do I...?" | [USER_GUIDE.md](USER_GUIDE.md) | -| "Why isn't it working?" | [USER_GUIDE.md#troubleshooting](USER_GUIDE.md#troubleshooting) | -| "I have a question" | [USER_GUIDE.md#faq](USER_GUIDE.md#faq) | -| "How does it work?" | [ARCHITECTURE.md](ARCHITECTURE.md) | -| "I want to contribute" | [CONTRIBUTING.md](CONTRIBUTING.md) | -| "I need API details" | [endpoints.md](endpoints.md) | -| "Issues/bugs" | https://github.com/digitaltouchcode/fisc/issues | -| "Email support" | cassymyo@gmail.com | - ---- - -## Documentation Standards - -All FiscGuy documentation: -- ✅ Uses Markdown with proper formatting -- ✅ Includes table of contents for long documents -- ✅ Provides practical examples -- ✅ Follows clear structure (concept → details → examples) -- ✅ Includes appropriate diagrams/flowcharts -- ✅ Is kept in sync with code changes -- ✅ Is reviewed in pull requests - ---- - -## Recent Documentation Updates - -**Version 0.1.6:** -- Added ARCHITECTURE.md (comprehensive internal documentation) -- Added USER_GUIDE.md (comprehensive user documentation) -- Updated CHANGELOG.md with device field fix -- Added device field to ReceiptCreateSerializer - -See [CHANGELOG.md](CHANGELOG.md) for full version history. - ---- - -**Happy coding! 🚀** - -For quick help, start with [USER_GUIDE.md](USER_GUIDE.md). -For technical depth, see [ARCHITECTURE.md](ARCHITECTURE.md). diff --git a/DOCUMENTATION_SUMMARY.md b/DOCUMENTATION_SUMMARY.md deleted file mode 100644 index d25e8c9..0000000 --- a/DOCUMENTATION_SUMMARY.md +++ /dev/null @@ -1,359 +0,0 @@ -# Documentation Project Summary - -## Overview - -Comprehensive documentation for FiscGuy has been created to serve both general users and internal engineering teams. This includes **1,768 lines** of new documentation across 3 files. - ---- - -## Files Created - -### 1. ARCHITECTURE.md (859 lines) -**Internal Engineering Documentation** - -**Purpose:** Technical reference for developers and maintainers - -**Contents:** -- Project overview and technology stack -- Layered architecture with diagrams and layer responsibilities -- Complete data model documentation: - - Device, Configuration, Certs models - - FiscalDay, FiscalCounter tracking - - Receipt, ReceiptLine, Taxes, Buyer models - - Relationships, constraints, and indexes -- Service layer details: - - ReceiptService (validation, persistence, submission) - - OpenDayService (fiscal day opening) - - ClosingDayService (counter aggregation, day closure) - - ConfigurationService, StatusService, PingService -- Cryptography & security: - - RSA-2048 signing with SHA-256 - - Hash generation and verification codes - - Certificate management -- ZIMRA integration: - - ZIMRAClient HTTP layer - - API endpoints (device vs. public) - - Example API payloads (receipt, fiscal day close) -- Receipt processing pipeline: - - Step-by-step flow with atomic transactions - - Automatic fiscal day opening -- Fiscal day management: - - Lifecycle and state transitions - - Counter updates with F() locking -- Database design: - - Index strategy - - Relationship diagram -- Development guidelines: - - Feature addition checklist - - Testing guidelines - - Code quality standards - - Atomic transaction patterns - - Logging best practices - -**Best for:** Developers adding features, understanding internals, code reviews - ---- - -### 2. USER_GUIDE.md (725 lines) -**General User & Integration Guide** - -**Purpose:** Practical documentation for users and integrators - -**Contents:** -- Feature overview (8 key features) -- Installation (PyPI, from source, requirements) -- Quick start guide (5 steps to working system): - - Add to Django settings - - Run migrations - - Include URLs - - Register device - - Make first request -- API endpoints reference: - - Receipt management (create, list, detail) - - Fiscal day management (open, close) - - Device management (status, config, sync) - - Taxes listing - - Buyer CRUD - - Full curl examples for each -- Usage examples: - - Simple cash receipt - - Receipt with buyer details - - Credit note (refund) - - Django code integration -- Concepts & terminology: - - Fiscal devices - - Fiscal days - - Receipt types (invoice, credit note, debit note) - - Receipt counters - - Payment methods - - Tax types -- Troubleshooting guide: - - 10+ common issues with solutions - - ZIMRA offline handling - - Missing configuration - - Invalid TIN format - - Timeout issues - - Device registration -- FAQ section: - - 15+ frequently asked questions - - Fiscal day automation - - Multiple devices - - ZIMRA offline behavior - - Credit note creation - - Multi-currency - - QR code storage - - Transaction IDs - - And more... - -**Best for:** Users installing FiscGuy, integrating into projects, API consumers, troubleshooting - ---- - -### 3. DOCS_INDEX.md (184 lines) -**Documentation Navigation & Index** - -**Purpose:** Guide users to correct documentation - -**Contents:** -- Documentation overview by audience: - - New users & integration - - Developers & maintainers - - Integration & DevOps - - API reference -- Quick navigation table -- Documentation philosophy -- Getting help guide -- Version information -- Recent updates - -**Best for:** First-time visitors, finding right documentation, reference - ---- - -## Updated Files - -### CHANGELOG.md -Updated to document: -1. New documentation files (ARCHITECTURE.md, USER_GUIDE.md) -2. Device field fix in ReceiptCreateSerializer -3. Summary of documentation content - ---- - -## Documentation Statistics - -| Metric | Value | -|--------|-------| -| Total lines | 1,768 | -| Files created | 3 | -| Files updated | 1 (CHANGELOG.md) | -| Diagrams/flowcharts | 3 | -| Tables | 10+ | -| Code examples | 20+ | -| API endpoint examples | 8 | -| FAQ entries | 15+ | -| Troubleshooting entries | 10+ | - ---- - -## Documentation Organization - -``` -FiscGuy Documentation Structure: - -DOCS_INDEX.md (START HERE) - ├─ For Users → USER_GUIDE.md - │ ├─ Installation - │ ├─ Quick Start - │ ├─ API Reference - │ ├─ Usage Examples - │ ├─ Troubleshooting - │ └─ FAQ - │ - ├─ For Developers → ARCHITECTURE.md - │ ├─ Architecture - │ ├─ Data Models - │ ├─ Services - │ ├─ Cryptography - │ ├─ ZIMRA Integration - │ ├─ Pipelines - │ └─ Dev Guidelines - │ - ├─ For Contributors → CONTRIBUTING.md - │ ├─ Code Standards - │ ├─ Testing - │ └─ PR Process - │ - └─ For API Details → endpoints.md - ├─ All endpoints - ├─ Request/response - └─ Error codes -``` - ---- - -## Key Highlights - -### ARCHITECTURE.md Highlights -- ✅ Complete data model reference (9 models, all relationships documented) -- ✅ Service layer architecture with method signatures -- ✅ Cryptographic operations explained (RSA, SHA-256, MD5) -- ✅ Receipt processing pipeline with flow diagram -- ✅ Fiscal counter management and ordering (per spec 13.3.1) -- ✅ Atomic transaction patterns for consistency -- ✅ Development checklist for new features -- ✅ 20+ code examples and snippets - -### USER_GUIDE.md Highlights -- ✅ 5-minute quick start guide -- ✅ 8 complete API endpoint examples with curl -- ✅ 4 real-world usage examples (cash, buyer, credit note, Django) -- ✅ Comprehensive troubleshooting (10+ issues with solutions) -- ✅ 15+ FAQ entries covering common questions -- ✅ Clear concept explanations for new users -- ✅ Links to detailed technical docs when needed - -### DOCS_INDEX.md Highlights -- ✅ Audience-based navigation -- ✅ Quick reference table -- ✅ Getting help guide -- ✅ Documentation philosophy -- ✅ Single source of truth for doc locations - ---- - -## Content Quality - -**All documentation:** -- ✅ Uses clear, professional language -- ✅ Includes practical examples -- ✅ Follows Markdown best practices -- ✅ Has proper structure (TOC, sections, subsections) -- ✅ Contains relevant diagrams/tables -- ✅ Cross-references related documents -- ✅ Accurate to codebase (reflects v0.1.6 state) -- ✅ Formatted for easy reading -- ✅ Optimized for search and discoverability - ---- - -## How Users Should Navigate - -### First Time User -1. Read DOCS_INDEX.md (2 min) -2. Read USER_GUIDE.md#quick-start (5 min) -3. Run `python manage.py init_device` (2-3 min) -4. Try API endpoint example (2 min) -5. Reference [USER_GUIDE.md](USER_GUIDE.md) as needed - -### Integrating into Existing Project -1. Read DOCS_INDEX.md (2 min) -2. Read USER_GUIDE.md#installation (3 min) -3. Read USER_GUIDE.md#api-endpoints (5 min) -4. Pick usage example matching your needs -5. Reference endpoints as needed - -### Contributing to FiscGuy -1. Read DOCS_INDEX.md (2 min) -2. Read CONTRIBUTING.md (5 min) -3. Read ARCHITECTURE.md (20 min) -4. Find relevant section and reference -5. Implement changes following guidelines - -### Debugging Issues -1. Check USER_GUIDE.md#troubleshooting (5 min) -2. Check USER_GUIDE.md#faq (5 min) -3. Check ARCHITECTURE.md for internals (10-30 min) -4. Check GitHub issues -5. Contact cassymyo@gmail.com - ---- - -## Related Existing Documentation - -These files were already present and complement the new documentation: - -- **README.md** - Project overview (kept as is) -- **INSTALL.md** - Installation details -- **CONTRIBUTING.md** - Contribution guidelines -- **endpoints.md** - Detailed API specification -- **CHANGELOG.md** - Version history (updated) - ---- - -## Coverage Analysis - -| Topic | Coverage | Document | -|-------|----------|----------| -| Installation | Complete | USER_GUIDE.md, INSTALL.md | -| API Reference | Complete | endpoints.md, USER_GUIDE.md | -| Architecture | Complete | ARCHITECTURE.md | -| Data Models | Complete | ARCHITECTURE.md | -| Services | Complete | ARCHITECTURE.md | -| Cryptography | Complete | ARCHITECTURE.md | -| ZIMRA Integration | Complete | ARCHITECTURE.md | -| Examples | Complete | USER_GUIDE.md | -| Troubleshooting | Complete | USER_GUIDE.md | -| FAQ | Complete | USER_GUIDE.md | -| Contributing | Complete | CONTRIBUTING.md | -| Development | Complete | ARCHITECTURE.md | - ---- - -## Maintenance & Updates - -**Documentation should be updated when:** -- New models are added (update ARCHITECTURE.md) -- New API endpoints are created (update endpoints.md, USER_GUIDE.md) -- Service logic changes (update ARCHITECTURE.md) -- New features are added (update CHANGELOG.md, relevant docs) -- Common issues emerge (update USER_GUIDE.md#troubleshooting) -- FAQ questions are received (update USER_GUIDE.md#faq) - -**Review process:** -- PR author updates documentation -- Reviewers check accuracy -- Merge only after doc review passes - ---- - -## Success Metrics - -✅ **User Onboarding:** 5-minute quick start available -✅ **Developer Guidance:** Complete architecture reference exists -✅ **API Clarity:** All endpoints documented with examples -✅ **Problem Solving:** Troubleshooting covers 10+ scenarios -✅ **Knowledge Base:** FAQ answers 15+ questions -✅ **Navigation:** Single index for all documentation -✅ **Maintenance:** Clear update guidelines -✅ **Quality:** Professional, well-structured content - ---- - -## Files Summary - -| File | Lines | Purpose | Audience | -|------|-------|---------|----------| -| ARCHITECTURE.md | 859 | Technical reference | Developers | -| USER_GUIDE.md | 725 | User guide & examples | Users/Integrators | -| DOCS_INDEX.md | 184 | Navigation & index | Everyone | -| **Total** | **1,768** | Complete documentation | All | - ---- - -## Conclusion - -FiscGuy now has **comprehensive, professional documentation** serving all audiences: - -- **Users** can quickly get started with clear examples and troubleshooting -- **Developers** have detailed architecture and implementation reference -- **Contributors** understand guidelines and patterns -- **Everyone** can easily find relevant information - -The documentation is **maintainable, cross-referenced, and aligned** with current code (v0.1.6). - ---- - -**Documentation Created:** April 1, 2026 -**Version:** 0.1.6 -**Status:** Ready for use ✅ diff --git a/INSTALL.md b/INSTALL.md deleted file mode 100644 index 26a8565..0000000 --- a/INSTALL.md +++ /dev/null @@ -1,300 +0,0 @@ -# Fiscguy Package Installation & Setup Guide - -## About Fiscguy - -Fiscguy is a Python library for integrating ZIMRA fiscal devices with your Django applications. This guide helps you get started quickly. - -## Installation - -### Via pip (from PyPI) - -```bash -pip install fiscguy -``` - -### From Source (Development) - -```bash -git clone https://github.com/cassymyo-spec/zimra.git -cd zimra -pip install -e . -``` - -### With Development Dependencies - -```bash -pip install -e ".[dev]" # Includes testing, linting, type checking tools -``` - -## Quick Setup (5 minutes) - -### 1. Add to Django Settings - -```python -# settings.py -INSTALLED_APPS = [ - # ... - 'fiscguy', - 'rest_framework', -] - -# Optional: Configure fiscal operations -FISCAL_SETTINGS = { - 'ENVIRONMENT': 'test', # or 'production' - 'TIMEZONE': 'Africa/Harare', -} -``` - -### 2. Run Migrations - -```bash -python manage.py migrate fiscguy -``` - -### 3. Initialize Device - -```bash -python manage.py init_device -``` - -This interactive command will: -- Prompt for device information -- Generate certificate signing request (CSR) -- Register device with ZIMRA -- Fetch configuration and taxes - -### 4. Use the Library - -```python -from fiscguy import open_day, submit_receipt, close_day - -# Open fiscal day -open_day() - -# Submit a receipt -receipt = submit_receipt({ - 'receipt_type': 'fiscalinvoice', - 'currency': 'USD', - 'total_amount': '100.00', - 'payment_terms': 'cash', - 'lines': [ - { - 'product': 'Service', - 'quantity': '1', - 'unit_price': '100.00', - 'line_total': '100.00', - 'tax_amount': '15.50', - 'tax_name': 'standard rated 15.5%', - } - ], - 'buyer': 1, # Buyer ID -}) - -# Close fiscal day -close_day() -``` - -## API Functions - -### Six Core Functions - -1. **`open_day()`** - Open a fiscal day -2. **`close_day()`** - Close the current fiscal day -3. **`submit_receipt(receipt_data)`** - Submit a receipt -4. **`get_status()`** - Get device status -5. **`get_configuration()`** - Get device configuration -6. **`get_taxes()`** - Get available tax types - -## REST Endpoints (if using Django views) - -Fiscguy also provides REST API endpoints: - -``` -GET /fiscguy/open-day/ - Open fiscal day -GET /fiscguy/close-day/ - Close fiscal day -GET /fiscguy/get-status/ - Get status -POST /fiscguy/receipts/ - Submit receipt -GET /fiscguy/receipts/{id}/ - Get receipt -GET /fiscguy/configuration/ - Get configuration -GET /fiscguy/taxes/ - Get taxes -``` - -## Database Models - -Fiscguy includes these Django models: - -- **Device** - Fiscal device info -- **FiscalDay** - Fiscal day records -- **Receipt** - Receipt records -- **ReceiptLine** - Receipt line items -- **Taxes** - Tax types -- **Configuration** - Device configuration -- **Certs** - Device certificates -- **Buyer** - Customer info -- **FiscalCounter** - Receipt counters - -Access them: - -```python -from fiscguy.models import Device, Receipt, Taxes - -device = Device.objects.first() -receipts = Receipt.objects.all() -taxes = Taxes.objects.all() -``` - -## Configuration - -### Environment Variables - -## Testing - -### Run Unit Tests - -```bash -# All tests -pytest - -# Specific test -pytest fiscguy/tests/test_api.py::OpenDayTest - -# With coverage -pytest --cov=fiscguy --cov-report=html -``` - -### Mock External Services - -Tests automatically mock ZIMRA API calls and crypto operations, so they run fast without network access. - -## Error Handling - -All API functions raise exceptions on error: - -```python -from rest_framework.exceptions import ValidationError -from fiscguy import submit_receipt - -try: - receipt = submit_receipt(data) -except ValidationError as e: - print(f"Validation error: {e.detail}") -except RuntimeError as e: - print(f"Runtime error: {e}") -``` - -## Troubleshooting - -### "No Device found" - -``` -RuntimeError: No Device found. Please run init_device management command. -``` - -**Solution:** Run device initialization: -```bash -python manage.py init_device -``` - -### "Tax with name 'X' not found" - -``` -ValidationError: Tax with name 'X' not found -``` - -**Solution:** Check available taxes and use exact name: -```python -from fiscguy.models import Taxes -taxes = Taxes.objects.all() -for tax in taxes: - print(f"{tax.name} - {tax.percent}%") -``` - -### Certificate Errors - -``` -MalformedFraming: Unable to load PEM file -``` - -**Solution:** Re-register device: -```bash -python manage.py init_device -``` - -### "No open fiscal day" - -``` -RuntimeError: No open fiscal day -``` - -**Solution:** Open a fiscal day first: -```python -from fiscguy import open_day -open_day() -``` - -## Development - -### Setting Up Development Environment - -```bash -# Clone repo -git clone https://github.com/cassymyo-spec/zimra.git -cd zimra - -# Create virtual environment -python -m venv venv -source venv/bin/activate - -# Install in editable mode with dev tools -pip install -e ".[dev]" - -# Install pre-commit hooks (optional) -pre-commit install -``` - -### Code Quality Checks - -```bash -# Format code -black fiscguy -isort fiscguy - -# Lint -flake8 fiscguy -pylint fiscguy - -# Type checking -mypy fiscguy - -# Tests -pytest -``` - -## Documentation - -- **README.md** - Project overview and quick start -- **CONTRIBUTING.md** - Contributing guidelines -- **CHANGELOG.md** - Version history -- Inline docstrings - Function documentation -- `fiscguy/api.py` - Public API module - -## Next Steps - -1. Install fiscguy -2. Run migrations -3. Initialize device -4. Submit your first receipt! -5. Read [API Reference](README.md#api-reference) -6. Check [Contributing Guide](CONTRIBUTING.md) - -## Support - -- Email: cassymyo@gmail.com -- Report Issues: [GitHub Issues](https://github.com/cassymyo-spec/zimra/issues) -- Discussions: [GitHub Discussions](https://github.com/cassymyo-spec/zimra/discussions) - -## License - -MIT License - See [LICENSE](LICENSE) - ---- diff --git a/LICENSE b/LICENSE index fba3e68..dd2af41 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Casper Moyo +Copyright (c) 2026 Fiscguy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3eb417d..72d6e7e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# FiscGuy -
+# FiscGuy + [![Tests](https://github.com/digitaltouchcode/fisc/actions/workflows/tests.yml/badge.svg?branch=release)](https://github.com/digitaltouchcode/fisc/actions/workflows/tests.yml?query=branch%3Arelease) [![PyPI version](https://img.shields.io/pypi/v/fiscguy.svg?v=1)](https://pypi.org/project/fiscguy/) [![Downloads](https://static.pepy.tech/badge/fiscguy)](https://pepy.tech/project/fiscguy) @@ -9,495 +9,410 @@ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) ---- - **The Modern Python Library for ZIMRA Fiscal Device Integration** -Production-ready library for integrating with ZIMRA (Zimbabwe Revenue Authority) fiscal devices. Built with Django and Django REST Framework, FiscGuy provides a simple, Pythonic API for managing fiscal operations with enterprise-grade security and reliability. +FiscGuy gives Django applications a simple, Pythonic interface for every fiscal operation required by the Zimbabwe Revenue Authority — receipt submission, fiscal day management, certificate handling, and more. Built on Django REST Framework, it drops into any Django project in minutes. -[Documentation](https://github.com/digitaltouchcode/fisc#documentation) • [API Reference](#api-endpoints) • [Contributing](#contributing) - -
+[Installation](#installation) • [Quick Start](#quick-start) • [API Reference](#api-reference) • [REST Endpoints](#rest-endpoints) • [Docs](#documentation) • [Contributing](#contributing) --- -## ✨ Features - -🔐 **Secure Device Integration** — Certificate-based mutual TLS authentication with ZIMRA FDMS - -📝 **Receipt Management** — Create, sign, and submit receipts with automatic validation and cryptographic signing - -🗓️ **Fiscal Day Operations** — Automatic fiscal day management with intelligent counter tracking and state management - -⚙️ **Device Configuration** — Sync taxpayer information and tax rates directly from ZIMRA + -💳 **Credit & Debit Notes** — Issue refunds and adjustments per ZIMRA specifications +## Features -💱 **Multi-Currency Support** — Handle USD and ZWG transactions seamlessly +- **Six core API functions** — `open_day`, `close_day`, `submit_receipt`, `get_status`, `get_configuration`, `get_taxes` +- **Full fiscal day lifecycle** — open, manage counters, close with ZIMRA-compliant hash and signature +- **Receipt types** — Fiscal Invoice, Credit Note, Debit Note with correct counter tracking +- **Certificate management** — CSR generation, device registration, certificate renewal via `init_device` +- **Multi-currency** — USD and ZWG support with per-currency counter tracking +- **Multiple payment methods** — Cash, Card, Mobile Wallet, Bank Transfer, Coupon, Credit, Other +- **Buyer management** — optional buyer TIN and registration data on receipts +- **Cursor pagination** — efficient receipt listing for large datasets +- **Typed exceptions** — every error condition has its own exception class +- **90%+ test coverage** — mocked ZIMRA and crypto, fast CI -📊 **QR Code Generation** — Auto-generate verification codes for receipt validation +--- -✅ **Fully Tested** — 90%+ code coverage with 22+ comprehensive test cases +## Requirements -🚀 **Production Ready** — Battle-tested in live ZIMRA deployments +- Python 3.11, 3.12, or 3.13 +- Django 4.2+ +- Django REST Framework 3.14+ -## 🚀 Installation +--- -### PyPI +## Installation ```bash pip install fiscguy ``` -### From Source +### From source ```bash git clone https://github.com/digitaltouchcode/fisc.git cd fisc -pip install -e . +pip install -e ".[dev]" ``` -### Requirements - -- Python 3.11+ (tested on 3.11, 3.12, 3.13) -- Django 4.2+ -- Django REST Framework 3.14+ - --- -## ⚡ 5-Minute Quick Start +## Quick Start -### 1️⃣ Add to Django Settings +### 1. Add to Django settings ```python # settings.py INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'rest_framework', - 'fiscguy', # ← Add this + # ... + "fiscguy", + "rest_framework", ] ``` -### 2️⃣ Run Migrations +### 2. Run migrations ```bash -python manage.py migrate fiscguy +python manage.py migrate ``` -### 3️⃣ Register Your Fiscal Device +### 3. Initialise your device ```bash python manage.py init_device ``` -This interactive command will guide you through: -- Device information entry -- Certificate generation & registration with ZIMRA -- Configuration and tax synchronization - -> ⚠️ **Note:** Environment switching (test ↔ production) will delete all existing data in that environment and require confirmation with `YES`. +This interactive command collects your device credentials, generates a CSR, registers the device with ZIMRA, and fetches taxes and configuration automatically. -### 4️⃣ Include URLs +### 4. Include URLs ```python # urls.py from django.urls import path, include urlpatterns = [ - path('api/', include('fiscguy.urls')), + path("fiscguy/", include("fiscguy.urls")), ] ``` -### 5️⃣ Submit Your First Receipt +### 5. Submit your first receipt -```bash -curl -X POST http://localhost:8000/api/receipts/ \ - -H "Content-Type: application/json" \ - -d '{ +```python +from fiscguy import open_day, submit_receipt, close_day + +open_day() + +receipt = submit_receipt({ "receipt_type": "fiscalinvoice", - "total_amount": "100.00", "currency": "USD", - "payment_terms": "cash", - "lines": [{ - "product": "Test Item", - "quantity": 1, - "unit_price": "100.00", - "line_total": "100.00", - "tax_name": "standard rated 15.5%" - }] - }' -``` + "total_amount": "115.00", + "payment_terms": "Cash", + "lines": [ + { + "product": "Consulting Service", + "quantity": "1", + "unit_price": "115.00", + "line_total": "115.00", + "tax_amount": "15.00", + "tax_name": "standard rated 15%", + } + ], +}) -> 💡 **Pro Tip:** First receipt automatically opens a fiscal day! No need to call `/open_day/` manually. +close_day() +``` --- -## 🎯 Key Concepts - -### Automatic Fiscal Day Opening +## API Reference -When you submit your first receipt without an open fiscal day, FiscGuy **automatically opens a new fiscal day** in the background: +FiscGuy exposes six top-level functions. Import them directly from `fiscguy`: -``` -Submit Receipt #1 → Auto-open Fiscal Day → Process Receipt → Automatic 5s ZIMRA delay -Submit Receipt #2 → Use open Fiscal Day → Process Receipt -... -Call close_day() → Close Fiscal Day for the day +```python +from fiscguy import open_day, close_day, submit_receipt, get_status, get_configuration, get_taxes ``` -No manual management needed! Just submit receipts and FiscGuy handles the rest. +### `open_day()` -### Environment Switching +Opens a new fiscal day. Syncs the `fiscalDayNo` from FDMS and fetches the latest configuration and taxes. -When switching between test and production environments: +```python +from fiscguy import open_day + +result = open_day() +# {"fiscalDayNo": 42, "fiscalDayOpened": "2026-03-30T08:00:00"} +``` -| Scenario | Safe? | Action | -|----------|-------|--------| -| **Test → Production** | ✅ Yes | Confirm deletion of test data | -| **Production → Test** | ⚠️ No | Only if you're certain about losing production data | +**Raises:** `FiscalDayError` if a day is already open or FDMS rejects the request. --- -## 📚 Usage Examples +### `submit_receipt(receipt_data)` -### Example 1: Simple Receipt +Validates, signs, and submits a receipt to ZIMRA FDMS. Increments fiscal counters. If FDMS is offline, the receipt is saved locally and queued for automatic sync. ```python -from fiscguy.models import Device -from rest_framework.test import APIClient - -device = Device.objects.first() -client = APIClient() - -response = client.post('/api/receipts/', { - 'receipt_type': 'fiscalinvoice', - 'total_amount': '150.00', - 'currency': 'USD', - 'payment_terms': 'Cash', - 'lines': [ +from fiscguy import submit_receipt + +receipt = submit_receipt({ + "receipt_type": "fiscalinvoice", # fiscalinvoice | creditnote | debitnote + "currency": "USD", # USD | ZWG + "total_amount": "115.00", + "payment_terms": "Cash", # Cash | Card | MobileWallet | BankTransfer | Coupon | Credit | Other + "lines": [ { - 'product': 'Bread', - 'quantity': 2, - 'unit_price': '50.00', - 'line_total': '100.00', - 'tax_name': 'standard rated 15.5%' + "product": "Item name", + "quantity": "2", + "unit_price": "57.50", + "line_total": "115.00", + "tax_amount": "15.00", + "tax_name": "standard rated 15%", } - ] + ], + # Optional + "buyer": 1, # Buyer model ID + "credit_note_reason": "...", # Required for creditnote + "credit_note_reference": "R-...", # Required for creditnote — original receipt number }) - -print(response.data['receipt_number']) # R-00000001 -print(response.data['zimra_inv_id']) # ZIM-123456 ``` -### Example 2: Credit Note (Refund) +**Returns:** Serialized receipt data including `receipt_number`, `qr_code`, `hash_value`, and `zimra_inv_id`. -```python -response = client.post('/api/receipts/', { - 'receipt_type': 'creditnote', - 'credit_note_reference': 'R-00000001', # Original receipt - 'credit_note_reason': 'customer_return', - 'total_amount': '-50.00', - 'currency': 'USD', - 'payment_terms': 'Cash', - 'lines': [ - { - 'product': 'Bread (Returned)', - 'quantity': 1, - 'unit_price': '-50.00', - 'line_total': '-50.00', - 'tax_name': 'standard rated 15.5%' - } - ] -}) -``` +**Raises:** `ReceiptSubmissionError` on any processing or FDMS failure. -### Example 3: Receipt with Buyer Information +--- -```python -response = client.post('/api/receipts/', { - 'receipt_type': 'fiscalinvoice', - 'total_amount': '500.00', - 'currency': 'USD', - 'payment_terms': 'BankTransfer', - 'buyer': { - 'name': 'Tech Solutions Ltd', - 'tin_number': '1234567890', - 'email': 'tech@example.com', - 'address': '123 Tech Park' - }, - 'lines': [ - { - 'product': 'Software License', - 'quantity': 1, - 'unit_price': '500.00', - 'line_total': '500.00', - 'tax_name': 'standard rated 15.5%' - } - ] -}) -``` +### `close_day()` ---- +Builds the fiscal day closing string, signs it with the device private key, and submits it to ZIMRA. Marks the fiscal day as closed in the database. -## 📡 API Endpoints +```python +from fiscguy import close_day -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/api/receipts/` | `POST` | Create and submit a receipt | -| `/api/receipts/` | `GET` | List all receipts (paginated) | -| `/api/receipts/{id}/` | `GET` | Get receipt details | -| `/api/open-day/` | `POST` | Open a fiscal day | -| `/api/close-day/` | `POST` | Close the current fiscal day | -| `/api/status/` | `GET` | Get device and fiscal status | -| `/api/configuration/` | `GET` | Get device configuration | -| `/api/taxes/` | `GET` | List available taxes | -| `/api/buyer/` | `GET` | List all buyers | -| `/api/buyer/` | `POST` | Create a buyer | +result = close_day() +# {"fiscalDayStatus": "FiscalDayClosed", ...} +``` -For detailed API documentation, see [USER_GUIDE.md](USER_GUIDE.md#api-endpoints) or [endpoints.md](endpoints.md). +**Raises:** `CloseDayError` if FDMS rejects the request (e.g. `CountersMismatch`, `BadCertificateSignature`). --- -## 📊 Database Models +### `get_status()` -FiscGuy provides comprehensive Django ORM models: +Fetches the current device and fiscal day status from FDMS. -- **Device** — Fiscal device information and status -- **FiscalDay** — Fiscal day records with open/close tracking -- **FiscalCounter** — Receipt counters aggregated by type and currency -- **Receipt** — Receipt records with automatic signing and ZIMRA tracking -- **ReceiptLine** — Line items within receipts -- **Taxes** — Tax type definitions synced from ZIMRA -- **Configuration** — Device configuration and taxpayer information -- **Certs** — Device certificates and cryptographic keys -- **Buyer** — Buyer/customer information for receipts +```python +from fiscguy import get_status -All models are fully documented in [ARCHITECTURE.md](ARCHITECTURE.md#data-models). +status = get_status() +# {"fiscalDayStatus": "FiscalDayOpened", "lastReceiptGlobalNo": 142, ...} +``` --- -## ⚙️ Architecture +### `get_configuration()` -FiscGuy follows a clean layered architecture: - -``` -┌─────────────────────────────────────────────┐ -│ REST API Layer (views.py) │ -├─────────────────────────────────────────────┤ -│ Service Layer (services/) │ -├─────────────────────────────────────────────┤ -│ Data Layer (models.py, serializers.py) │ -├─────────────────────────────────────────────┤ -│ ZIMRA Integration (zimra_*.py) │ -└──────────────┬──────────────────────────────┘ - │ - ↓ - ZIMRA FDMS REST API -``` +Returns the stored taxpayer configuration. -**Key Design Principles:** -- 🏗️ **Separation of Concerns** — Clear boundaries between layers -- 🔒 **Atomic Operations** — Database transactions ensure data consistency -- 🔐 **Cryptographic Security** — RSA-2048 signing with SHA-256 hashing -- 📋 **ZIMRA Compliance** — Fully compliant with ZIMRA FDMS specifications -- ✅ **Comprehensive Testing** — 90%+ code coverage with 22+ test cases +```python +from fiscguy import get_configuration -For complete architecture details, see [ARCHITECTURE.md](ARCHITECTURE.md). +config = get_configuration() +# {"tax_payer_name": "ACME Ltd", "tin_number": "...", ...} +``` --- -## 🧪 Testing - -```bash -# Run all tests -pytest +### `get_taxes()` -# Run with coverage report -pytest --cov=fiscguy --cov-report=html +Returns all configured tax types. -# Run specific test -pytest fiscguy/tests/test_api.py::SubmitReceiptTest +```python +from fiscguy import get_taxes -# Run with verbose output -pytest -v +taxes = get_taxes() +# [{"tax_id": 1, "name": "Exempt", "percent": "0.00"}, ...] ``` -All tests mock external ZIMRA API calls, so they run fast without network dependencies. - --- -## 💻 Development +## REST Endpoints -### Setup Development Environment +When URLs are included, FiscGuy exposes the following endpoints: -```bash -# Clone repository -git clone https://github.com/digitaltouchcode/fisc.git -cd fisc +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/fiscguy/open-day/` | Open a new fiscal day | +| `POST` | `/fiscguy/close-day/` | Close the current fiscal day | +| `GET` | `/fiscguy/get-status/` | Get device and fiscal day status | +| `POST` | `/fiscguy/get-ping/` | Ping FDMS to report device is online | +| `GET` | `/fiscguy/receipts/` | List receipts (cursor paginated) | +| `POST` | `/fiscguy/receipts/` | Submit a new receipt | +| `GET` | `/fiscguy/receipts/{id}/` | Retrieve a receipt by ID | +| `GET` | `/fiscguy/configuration/` | Get taxpayer configuration | +| `POST` | `/fiscguy/sync-config/` | Manually sync configuration from FDMS | +| `GET` | `/fiscguy/taxes/` | List all tax types | +| `POST` | `/fiscguy/issue-certificate/` | Renew device certificate | +| `*` | `/fiscguy/buyer/` | Buyer CRUD (ModelViewSet) | -# Create virtual environment -python -m venv venv -source venv/bin/activate # On Windows: venv\Scripts\activate +### Pagination -# Install in development mode -pip install -e ".[dev]" +Receipt listing supports cursor-based pagination: + +``` +GET /fiscguy/receipts/?page_size=20 ``` -### Code Quality +Default page size: `10`. Maximum: `100`. -```bash -# Format with Black -black fiscguy +--- -# Sort imports with isort -isort fiscguy +## Error Handling -# Lint with flake8 -flake8 fiscguy +All operations raise typed exceptions. Import them from `fiscguy.exceptions`: -# Type checking with mypy -mypy fiscguy +```python +from fiscguy.exceptions import ( + ReceiptSubmissionError, + CloseDayError, + FiscalDayError, + ConfigurationError, + CertificateError, + DevicePingError, + StatusError, +) -# All checks at once -black fiscguy && isort fiscguy && flake8 fiscguy && mypy fiscguy +try: + close_day() +except CloseDayError as e: + print(f"Close day failed: {e}") ``` -### Project Structure - -``` -fiscguy/ -├── models.py # Django ORM models -├── serializers.py # DRF serializers for validation -├── views.py # REST API endpoints -├── zimra_base.py # ZIMRA FDMS HTTP client -├── zimra_crypto.py # Cryptographic operations -├── zimra_receipt_handler.py # Receipt formatting & signing -├── services/ # Business logic layer -│ ├── receipt_service.py -│ ├── closing_day_service.py -│ ├── configuration_service.py -│ └── status_service.py -├── management/commands/ # Django management commands -│ └── init_device.py -└── tests/ # Unit tests (22+ test cases) -``` +| Exception | Raised when | +|-----------|-------------| +| `ReceiptSubmissionError` | Receipt processing or FDMS submission fails | +| `CloseDayError` | FDMS rejects the close day request | +| `FiscalDayError` | Fiscal day cannot be opened or is already open | +| `ConfigurationError` | Configuration is missing or sync fails | +| `CertificateError` | Certificate issuance or renewal fails | +| `DevicePingError` | Ping to FDMS fails | +| `StatusError` | Status fetch from FDMS fails | +| `DeviceRegistrationError` | Device registration with ZIMRA fails | +| `CryptoError` | RSA signing or hashing fails | --- -## 📚 Documentation +## Models -FiscGuy has comprehensive documentation for all audiences: +FiscGuy adds the following tables to your database: -| Document | For | Content | -|----------|-----|---------| -| **[USER_GUIDE.md](USER_GUIDE.md)** | Users & Integrators | Installation, API reference, examples, troubleshooting, FAQ | -| **[ARCHITECTURE.md](ARCHITECTURE.md)** | Developers | Technical details, data models, service layer, cryptography | -| **[INSTALL.md](INSTALL.md)** | DevOps & Setup | Detailed installation and configuration | -| **[CONTRIBUTING.md](CONTRIBUTING.md)** | Contributors | Development specifications and ERP integration | -| **[DOCS_INDEX.md](DOCS_INDEX.md)** | Everyone | Documentation navigation and quick reference | +| Model | Description | +|-------|-------------| +| `Device` | Fiscal device registration details | +| `Configuration` | Taxpayer configuration synced from FDMS | +| `Certs` | Device certificate and private key | +| `Taxes` | Tax types synced from FDMS on day open | +| `FiscalDay` | Daily fiscal period with receipt counter | +| `FiscalCounter` | Running totals per tax / payment method | +| `Receipt` | Submitted receipts with hash, signature, QR code | +| `ReceiptLine` | Individual line items on a receipt | +| `Buyer` | Optional buyer registration data | -**Start here:** [DOCS_INDEX.md](DOCS_INDEX.md) for guided navigation. - ---- - -## 🐛 Error Handling +Access them directly: ```python -from rest_framework.exceptions import ValidationError -from fiscguy.services.receipt_service import ReceiptService +from fiscguy.models import Device, Receipt, FiscalDay, Taxes -try: - service = ReceiptService() - receipt = service.create_receipt(data) -except ValidationError as e: - print(f"Validation Error: {e.detail}") -except RuntimeError as e: - print(f"Runtime Error: {e}") +device = Device.objects.first() +open_days = FiscalDay.objects.filter(is_open=True) +receipts = Receipt.objects.select_related("buyer").prefetch_related("lines") ``` -Common exceptions: - -- `ValidationError` — Invalid input data -- `RuntimeError` — No device registered or fiscal day issues -- `ZIMRAException` — ZIMRA API communication errors - --- -## 🤝 Contributing - -We welcome contributions! Here's how to get started: +## Management Commands -1. **Fork** the repository -2. **Create** a feature branch: `git checkout -b feature/amazing-feature` -3. **Write** tests for new features -4. **Run** code quality checks: `black . && isort . && flake8 . && mypy .` -5. **Commit** with descriptive messages: `git commit -m "feat: add amazing feature"` -6. **Push** to your fork and **open a PR** +### `init_device` -For detailed guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md). +Interactive device setup — run once per device: -### Code Standards +```bash +python manage.py init_device +``` -- **Style Guide:** [PEP 8](https://pep8.org/) with [Black](https://github.com/psf/black) -- **Imports:** Sorted with [isort](https://pycqa.github.io/isort/) -- **Linting:** [flake8](https://flake8.pycqa.org/) and [pylint](https://pylint.readthedocs.io/) -- **Type Checking:** [mypy](https://www.mypy-lang.org/) -- **Test Coverage:** 90%+ required -- **Testing Framework:** [pytest](https://pytest.org/) +The command will: +1. Prompt for `org_name`, `activation_key`, `device_id`, `device_model_name`, `device_model_version`, `device_serial_number` +2. Ask whether to use production or testing FDMS +3. Generate an RSA key pair and CSR +4. Register the device with ZIMRA to obtain a signed certificate +5. Fetch and persist configuration and taxes --- -## 📄 License +## Testing -FiscGuy is licensed under the **MIT License**. See [LICENSE](LICENSE) for details. +```bash +# Run all tests +pytest ---- +# With coverage report +pytest --cov=fiscguy --cov-report=html -## 🤔 FAQ +# Run a specific test file +pytest fiscguy/tests/test_views.py -**Q: Do I need to open a fiscal day manually?** -A: No! FiscGuy automatically opens a fiscal day when you submit your first receipt of the day. +# Run a specific test +pytest fiscguy/tests/test_closing_day_service.py::TestBuildSaleByTax +``` -**Q: Can I use FiscGuy without Django?** -A: FiscGuy is built for Django. If you need a standalone library, check our API layer at `fiscguy/zimra_base.py`. +All tests mock ZIMRA API calls and crypto operations — no network access required. -**Q: What's the difference between receipts, credit notes, and debit notes?** -A: -- **Receipt** — Normal sale (positive amount) -- **Credit Note** — Refund/return (negative amount) -- **Debit Note** — Not mandatory; rarely used +--- -**Q: How do I handle ZIMRA being offline?** -A: Receipts are cached locally and automatically submitted when ZIMRA comes back online. +## Documentation -**Q: Can I switch from test to production?** -A: Yes! Run `python manage.py init_device` and confirm the environment switch. All test data will be deleted. +Full documentation lives in the `docs/` folder: -More FAQs in [USER_GUIDE.md](USER_GUIDE.md#faq). +| Document | Description | +|----------|-------------| +| [`docs/installation.md`](docs/installation.md) | Detailed installation and setup guide | +| [`docs/receipt-types.md`](docs/receipt-types.md) | Fiscal Invoice, Credit Note, Debit Note rules | +| [`docs/fiscal-counters.md`](docs/fiscal-counters.md) | How counters work and how they are calculated | +| [`docs/closing-day.md`](docs/closing-day.md) | Closing day hash string and signature spec | +| [`docs/certificate-management.md`](docs/certificate-management.md) | Certificate lifecycle and renewal | +| [`docs/error-reference.md`](docs/error-reference.md) | All exceptions and what causes them | +| [`CHANGELOG.md`](CHANGELOG.md) | Version history | +| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Contributing guidelines | --- -## 💬 Support & Community +## Contributing -- 📧 **Email:** cassymyo@gmail.com -- 🐛 **Issues:** [GitHub Issues](https://github.com/digitaltouchcode/fisc/issues) -- 💬 **Discussions:** [GitHub Discussions](https://github.com/digitaltouchcode/fisc/discussions) -- 📚 **Documentation:** [DOCS_INDEX.md](DOCS_INDEX.md) +Contributions are welcome. Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) first. ---- - -## 🙏 Acknowledgments +```bash +# Set up dev environment +git clone https://github.com/digitaltouchcode/fisc.git +cd fisc +pip install -e ".[dev]" +pre-commit install -FiscGuy is built on the excellent Django and Django REST Framework ecosystems. Special thanks to the ZIMRA Authority for the FDMS API specifications. +# Before submitting a PR +black fiscguy +isort fiscguy +flake8 fiscguy +pytest +``` --- -
+## License -**Made with ❤️ by Casper Moyo** +MIT — see [LICENSE](LICENSE). -[⭐ Star us on GitHub](https://github.com/digitaltouchcode/fisc) +--- +
+Built for Zimbabwe 🇿🇼 by Digital Touch Code
diff --git a/USER_GUIDE.md b/USER_GUIDE.md deleted file mode 100644 index 949f524..0000000 --- a/USER_GUIDE.md +++ /dev/null @@ -1,725 +0,0 @@ -# FiscGuy User & Integration Guide - -**FiscGuy** is a production-ready Python library for integrating with ZIMRA (Zimbabwe Revenue Authority) fiscal devices. It simplifies fiscal operations through a clean REST API and robust business logic. - -**Table of Contents:** -- [Features](#features) -- [Installation](#installation) -- [Quick Start](#quick-start) -- [API Endpoints](#api-endpoints) -- [Usage Examples](#usage-examples) -- [Concepts](#concepts) -- [Troubleshooting](#troubleshooting) -- [FAQ](#faq) - ---- - -## Features - -✅ **Secure Device Integration** - Certificate-based mutual TLS with ZIMRA FDMS - -✅ **Receipt Management** - Create, sign, and submit receipts with multiple tax types - -✅ **Fiscal Day Operations** - Automatic fiscal day management with counter tracking - -✅ **Device Configuration** - Sync taxpayer info and tax rates from ZIMRA - -✅ **Credit/Debit Notes** - Issue refunds and adjustments per ZIMRA spec - -✅ **Multi-Currency Support** - Handle USD and ZWG transactions - -✅ **QR Code Generation** - Auto-generate receipt verification QR codes - -✅ **Fully Tested** - 90%+ code coverage, 22+ test cases - -✅ **Production Ready** - Used in live ZIMRA deployments - ---- - -## Installation - -### Via PyPI - -```bash -pip install fiscguy -``` - -### From Source - -```bash -git clone https://github.com/digitaltouchcode/fisc.git -cd fisc -pip install -e . -``` - -### Requirements - -- Python 3.11+ (tested on 3.11, 3.12, 3.13) -- Django 4.2+ -- Django REST Framework 3.14+ - ---- - -## Quick Start - -### Step 1: Add to Django Settings - -```python -# settings.py -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'rest_framework', - 'fiscguy', # Add this -] -``` - -### Step 2: Run Migrations - -```bash -python manage.py makemigrations fiscguy -python manage.py migrate fiscguy -``` - -### Step 3: Include URLs - -```python -# urls.py -from django.urls import path, include - -urlpatterns = [ - path('api/', include('fiscguy.urls')), -] -``` - -### Step 4: Register Your Device - -```bash -python manage.py init_device -``` - -This interactive command will: -- Collect device information (org name, device ID, model) -- Generate and register certificates with ZIMRA -- Sync device configuration and tax rates -- Confirm successful registration - -**⚠️ Environment Switching:** -If switching from test to production (or vice versa), the command warns you and requires confirmation to delete all test/old production data. - -### Step 5: Make Your First Request - -```bash -curl -X GET http://localhost:8000/api/configuration/ \ - -H "Content-Type: application/json" -``` - -You should receive your device configuration. - ---- - -## API Endpoints - -### Receipt Management - -#### Create & Submit Receipt (Auto-opens day if needed) -``` -POST /api/receipts/ -Content-Type: application/json - -{ - "receipt_type": "fiscalinvoice", - "total_amount": "100.00", - "currency": "USD", - "payment_terms": "Cash", - "lines": [ - { - "product": "Product Name", - "quantity": "1", - "unit_price": "100.00", - "line_total": "100.00", - "tax_name": "standard rated 15.5%" - } - ] -} - -Returns: 201 Created -{ - "id": 1, - "device": 1, - "receipt_number": "R-00000001", - "receipt_type": "fiscalinvoice", - "total_amount": "100.00", - "qr_code": "https://...", - "code": "ABC1234567890", - "hash_value": "base64...", - "signature": "base64...", - "zimra_inv_id": "ZIM-123456", - "submitted": true, - "created_at": "2026-04-01T10:30:00Z" -} -``` - -#### List Receipts (Paginated) -``` -GET /api/receipts/?page_size=20 - -Returns: 200 OK -{ - "next": "https://api/receipts/?cursor=...", - "previous": null, - "results": [...] -} -``` - -#### Get Receipt Details -``` -GET /api/receipts/{id}/ - -Returns: 200 OK -{ - "id": 1, - "receipt_number": "R-00000001", - "lines": [ - { - "id": 1, - "product": "Product", - "quantity": "1", - "unit_price": "100.00", - "line_total": "100.00", - "tax_amount": "15.50", - "tax_type": "standard rated 15.5%" - } - ], - "buyer": null, - ... -} -``` - -### Fiscal Day Management - -#### Open Fiscal Day -``` -POST /api/open-day/ - -Returns: 200 OK -{ - "fiscal_day_number": 1, - "is_open": true, - "message": "Fiscal day opened" -} -``` - -**Note:** Automatically called when submitting the first receipt of the day. - -#### Close Fiscal Day -``` -POST /api/close-day/ - -Returns: 200 OK -{ - "fiscal_day_number": 1, - "is_open": false, - "receipt_count": 15, - "message": "Fiscal day closed" -} -``` - -**What happens:** -- Sums all receipt counters (by type, currency, tax) -- Sends closing hash to ZIMRA -- Resets fiscal day for next day's receipts - -### Device Management - -#### Get Device Status -``` -GET /api/get-status/ - -Returns: 200 OK -{ - "device_id": "ABC123", - "org_name": "My Business", - "is_online": true, - "open_fiscal_day": 1, - "last_receipt_no": "R-00000042", - "last_receipt_global_no": 42 -} -``` - -#### Get Device Configuration -``` -GET /api/configuration/ - -Returns: 200 OK -{ - "id": 1, - "device": 1, - "tax_payer_name": "My Business Ltd", - "tin_number": "1234567890", - "vat_number": "VAT123", - "address": "123 Main St", - "phone_number": "+263123456789", - "email": "info@mybusiness.com" -} -``` - -#### Sync Configuration & Taxes -``` -POST /api/sync-config/ - -Returns: 200 OK -{ - "config_synced": true, - "taxes_synced": true, - "tax_count": 5, - "message": "Configuration synchronized" -} -``` - -### Taxes - -#### List Available Taxes -``` -GET /api/taxes/ - -Returns: 200 OK -[ - { - "id": 1, - "code": "STD", - "name": "standard rated 15.5%", - "tax_id": 1, - "percent": "15.50" - }, - { - "id": 2, - "code": "ZRO", - "name": "zero rated 0%", - "tax_id": 4, - "percent": "0.00" - }, - { - "id": 3, - "code": "EXM", - "name": "exempt 0%", - "tax_id": 5, - "percent": "0.00" - } -] -``` - -### Buyers (Optional) - -#### List Buyers -``` -GET /api/buyer/ - -Returns: 200 OK -[ - { - "id": 1, - "name": "John's Retail", - "tin_number": "1234567890", - "trade_name": "John's Store", - "email": "john@retail.com", - "address": "456 Commerce Ave", - "phonenumber": "+263987654321" - } -] -``` - -#### Create Buyer -``` -POST /api/buyer/ -Content-Type: application/json - -{ - "name": "Jane's Shop", - "tin_number": "0987654321", - "trade_name": "Jane's Retail", - "email": "jane@shop.com", - "address": "789 Business St", - "phonenumber": "+263111111111" -} - -Returns: 201 Created -``` - -#### Update Buyer -``` -PATCH /api/buyer/{id}/ - -Returns: 200 OK -``` - -#### Delete Buyer -``` -DELETE /api/buyer/{id}/ - -Returns: 204 No Content -``` - ---- - -## Usage Examples - -### Example 1: Simple Cash Receipt - -```bash -curl -X POST http://localhost:8000/api/receipts/ \ - -H "Content-Type: application/json" \ - -d '{ - "receipt_type": "fiscalinvoice", - "total_amount": "150.00", - "currency": "USD", - "payment_terms": "Cash", - "lines": [ - { - "product": "Bread", - "quantity": "2", - "unit_price": "50.00", - "line_total": "100.00", - "tax_name": "standard rated 15.5%" - }, - { - "product": "Milk", - "quantity": "1", - "unit_price": "43.48", - "line_total": "50.00", - "tax_name": "exempt 0%" - } - ] - }' -``` - -**Response:** -```json -{ - "id": 5, - "receipt_number": "R-00000005", - "receipt_type": "fiscalinvoice", - "total_amount": "150.00", - "submitted": true, - "zimra_inv_id": "ZIM-789012" -} -``` - -### Example 2: Receipt with Buyer - -```bash -curl -X POST http://localhost:8000/api/receipts/ \ - -H "Content-Type: application/json" \ - -d '{ - "receipt_type": "fiscalinvoice", - "total_amount": "500.00", - "currency": "USD", - "payment_terms": "BankTransfer", - "buyer": { - "name": "Tech Solutions Ltd", - "tin_number": "1234567890", - "trade_name": "Tech Shop", - "email": "tech@example.com", - "address": "123 Tech Park", - "phonenumber": "+263123456789" - }, - "lines": [ - { - "product": "Laptop", - "quantity": "1", - "unit_price": "400.00", - "line_total": "400.00", - "tax_name": "standard rated 15.5%" - }, - { - "product": "Warranty", - "quantity": "1", - "unit_price": "100.00", - "line_total": "100.00", - "tax_name": "standard rated 15.5%" - } - ] - }' -``` - -### Example 3: Credit Note (Refund) - -```bash -# First, get the receipt number to refund -curl -X GET http://localhost:8000/api/receipts/ - -# Then issue a credit note -curl -X POST http://localhost:8000/api/receipts/ \ - -H "Content-Type: application/json" \ - -d '{ - "receipt_type": "creditnote", - "credit_note_reference": "R-00000005", - "credit_note_reason": "Customer returned item", - "total_amount": "-100.00", - "currency": "USD", - "payment_terms": "Cash", - "lines": [ - { - "product": "Bread (Returned)", - "quantity": "2", - "unit_price": "-50.00", - "line_total": "-100.00", - "tax_name": "standard rated 15.5%" - } - ] - }' -``` - -**Key differences:** -- `receipt_type`: "creditnote" -- `total_amount`: negative -- `line_total` and `unit_price`: negative -- `credit_note_reference`: original receipt number (must exist) - -### Example 4: Integration with Django Code - -```python -from fiscguy.models import Receipt, ReceiptLine, Buyer -from fiscguy.services.receipt_service import ReceiptService -from fiscguy.models import Device - -# Get the device -device = Device.objects.first() - -# Create receipt data -receipt_data = { - "device": device.id, - "receipt_type": "fiscalinvoice", - "total_amount": "100.00", - "currency": "USD", - "payment_terms": "Cash", - "lines": [ - { - "product": "Service", - "quantity": "1", - "unit_price": "100.00", - "line_total": "100.00", - "tax_name": "standard rated 15.5%" - } - ] -} - -# Create and submit receipt -service = ReceiptService(device) -receipt, submission_result = service.create_and_submit_receipt(receipt_data) - -print(f"Receipt created: {receipt.receipt_number}") -print(f"Submitted to ZIMRA: {receipt.submitted}") -print(f"ZIMRA ID: {receipt.zimra_inv_id}") -``` - ---- - -## Concepts - -### Fiscal Device - -A physical or logical device registered with ZIMRA. Each device has: -- **Unique device ID** - Assigned during registration -- **Certificates** - For ZIMRA authentication (test and/or production) -- **Configuration** - Taxpayer info (TIN, name, address, VAT number) -- **Fiscal Days** - Daily accounting periods -- **Receipts** - All issued receipts - -### Fiscal Day - -An accounting period (usually daily) during which: -1. Receipts are issued and signed with cryptographic material -2. Receipt counters accumulate (by type, currency, tax) -3. Day is closed with a closing hash sent to ZIMRA -4. Cannot reopen a closed fiscal day - -**Important:** First receipt automatically opens the day if needed. - -### Receipt Types - -| Type | Description | Receiver | Amount Sign | -|------|-------------|----------|-------------| -| **Fiscal Invoice** | Normal sale | Customer | Positive (+) | -| **Credit Note** | Refund/discount | Customer | Negative (-) | -| **Debit Note** | Surcharge/adjustment | Customer | Positive (+) | - -### Receipt Counters - -FiscGuy tracks counters by: -- **Type**: SaleByTax, SaleTaxByTax, CreditNoteByTax, etc. -- **Currency**: USD or ZWG -- **Tax Rate**: Standard, Zero-Rated, Exempt, Withholding - -Counters are summed at day-close and sent to ZIMRA. - -### Payment Methods - -- Cash -- Card -- Bank Transfer -- Mobile Wallet -- Coupon -- Credit -- Other - -### Tax Types (Synced from ZIMRA) - -- **Standard Rated** (typically 15.5%) -- **Zero Rated** (0%, e.g., exports) -- **Exempt** (0%, e.g., education) -- **Withholding** (applied by buyer) - ---- - -## Troubleshooting - -### Issue: "No open fiscal day and FDMS is unreachable" - -**Cause:** Network error or ZIMRA is offline during first receipt submission. - -**Solution:** -1. Check internet connectivity -2. Verify ZIMRA API availability -3. Ensure device certificates are valid -4. Manually open day: `POST /api/open-day/` - -### Issue: "ZIMRA configuration missing" - -**Cause:** Device configuration not synced. - -**Solution:** -```bash -python manage.py init_device -# Or: -curl -X POST http://localhost:8000/api/sync-config/ -``` - -### Issue: "TIN number is incorrect, must be ten digit" - -**Cause:** Buyer TIN is not exactly 10 digits. - -**Solution:** -- Format TIN as 10 digits (e.g., `0123456789`) -- Pad with leading zeros if needed - -### Issue: "Tax with name 'X' not found" - -**Cause:** Requested tax doesn't exist in database. - -**Solution:** -1. Check available taxes: `GET /api/taxes/` -2. Use exact tax name from list -3. Sync taxes: `POST /api/sync-config/` - -### Issue: "Referenced receipt does not exist" (Credit Note) - -**Cause:** Trying to create credit note for receipt that doesn't exist locally. - -**Solution:** -- Verify original receipt number is correct -- Original receipt must be submitted to ZIMRA before creating credit note - -### Issue: Timeout or "FDMS error" in logs - -**Cause:** ZIMRA API timeout (>30 seconds). - -**Solution:** -- Check network latency to ZIMRA servers -- Retry the request -- Monitor ZIMRA status page - -### Issue: "Device is not registered" - -**Cause:** Device table is empty. - -**Solution:** -```bash -python manage.py init_device -``` - -### Issue: Receipts not marked as `submitted=True` - -**Cause:** ZIMRA API call failed or device is offline. - -**Solution:** -- Check ZIMRA connectivity -- Review server logs for error details -- Re-submit receipt (transaction ensures atomicity) - ---- - -## FAQ - -### Q: Do I need to manually open fiscal days? - -**A:** No, the first receipt of the day automatically opens it. You only manually open if needed. - -### Q: Can I use multiple devices? - -**A:** Yes, FiscGuy supports multiple devices. Each device has its own config and receipts. Note: The API uses `Device.objects.first()`, so you may want to extend views for device selection. - -### Q: What happens if ZIMRA is offline? - -**A:** Receipts fail submission with `ReceiptSubmissionError`. The receipt is rolled back (not saved). Retry when ZIMRA is back online. - -### Q: Can I issue credit notes for receipts from another system? - -**A:** No, the original receipt must exist in FiscGuy's database and be submitted to ZIMRA. - -### Q: What's the difference between zero-rated and exempt taxes? - -**A:** Both are 0%, but: -- **Zero-Rated**: Used for exports, VAT recovery allowed -- **Exempt**: Used for education/health, VAT recovery NOT allowed -- Functionally, FiscGuy treats both as 0% tax - -### Q: How do I handle multi-currency transactions? - -**A:** Set `currency` field per receipt (USD or ZWG). Counters are tracked separately by currency. - -### Q: Can I edit receipts after submission? - -**A:** No, issued receipts are immutable per ZIMRA spec. Issue a credit note to refund/adjust. - -### Q: Where are QR codes stored? - -**A:** In the `media/Zimra_qr_codes/` directory (configurable via Django settings). Also accessible via API in `receipt.qr_code`. - -### Q: What's the transaction ID (zimra_inv_id)? - -**A:** The ID assigned by ZIMRA during submission. Use this to match receipts in ZIMRA reports. - -### Q: How do I check remaining API rate limits? - -**A:** FiscGuy doesn't enforce limits, but ZIMRA may. Check ZIMRA documentation or contact support. - -### Q: Is there a webhook for receipt updates? - -**A:** No, poll the API: `GET /api/receipts/` or `GET /api/receipts/{id}/` - -### Q: Can I use FiscGuy with asyncio/celery? - -**A:** Yes, but ensure database transactions are atomic. See ARCHITECTURE.md for transaction patterns. - ---- - -## Getting Help - -- **Documentation:** See ARCHITECTURE.md for technical details -- **Issues:** https://github.com/digitaltouchcode/fisc/issues -- **Email:** cassymyo@gmail.com -- **Examples:** See `fiscguy/tests/` for test cases - ---- - -## License - -MIT License - See LICENSE file for details - ---- - -**Last Updated:** April 2026 -**Version:** 0.1.6 -**Maintainers:** Casper Moyo (@cassymyo) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..b255534 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,921 @@ +# FiscGuy — Engineering Architecture + +> Internal engineering reference for contributors and maintainers. +> Version 0.1.6 · Last updated April 2026 · Maintainer: Casper Moyo + +--- + +## Table of Contents + +1. [System Overview](#1-system-overview) +2. [Layer Architecture](#2-layer-architecture) +3. [Component Map](#3-component-map) +4. [Data Models](#4-data-models) +5. [Receipt Processing Pipeline](#5-receipt-processing-pipeline) +6. [Fiscal Day Lifecycle](#6-fiscal-day-lifecycle) +7. [Closing Day & Signature Spec](#7-closing-day--signature-spec) +8. [Cryptography](#8-cryptography) +9. [ZIMRA Client](#9-zimra-client) +10. [Fiscal Counters](#10-fiscal-counters) +11. [Error Handling](#11-error-handling) +12. [Database Design](#12-database-design) +13. [Development Guidelines](#13-development-guidelines) + +--- + +## 1. System Overview + +FiscGuy is a Django library that wraps the full ZIMRA Fiscal Device Management System (FDMS) API. It handles every phase of fiscal device integration: device registration, certificate management, fiscal day management, receipt signing and submission, and fiscal counter tracking. + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ Host Django Application │ +│ │ +│ from fiscguy import open_day, submit_receipt, close_day │ +│ urlpatterns += [path("fiscguy/", include("fiscguy.urls"))] │ +└────────────────────────────────┬─────────────────────────────────────────┘ + │ + ┌────────────▼────────────┐ + │ FiscGuy Library │ + │ │ + │ REST API → Services │ + │ Services → ZIMRA │ + │ Services → DB │ + └────────────┬────────────┘ + │ HTTPS + mTLS + ┌────────────▼────────────┐ + │ ZIMRA FDMS │ + │ │ + │ fdmsapitest.zimra.co.zw │ + │ fdmsapi.zimra.co.zw │ + └──────────────────────────┘ +``` + +--- + +## 2. Layer Architecture + +FiscGuy follows a strict four-layer architecture. Each layer has a single responsibility and communicates only with the layer directly below it. + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ │ +│ REST API LAYER · views.py │ +│ │ +│ HTTP in → validate device exists → delegate to service │ +│ Handle typed exceptions → return DRF Response │ +│ Never contains business logic │ +│ │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ SERVICE LAYER · services/ │ +│ │ +│ ReceiptService OpenDayService ClosingDayService │ +│ ConfigurationService StatusService PingService │ +│ CertificateService │ +│ │ +│ All business logic lives here. Atomic transactions. │ +│ Raises typed FiscalisationError subclasses on failure. │ +│ │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ZIMRA INTEGRATION LAYER · zimra_*.py │ +│ │ +│ ZIMRAClient HTTP to FDMS, mTLS, sessions │ +│ ZIMRAReceiptHandler Full receipt pipeline │ +│ ZIMRACrypto RSA signing, SHA-256, MD5, QR │ +│ │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ DATA LAYER · models.py │ +│ │ +│ Device Configuration Certs FiscalDay FiscalCounter │ +│ Receipt ReceiptLine Taxes Buyer │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ + SQLite / PostgreSQL / MySQL +``` + +--- + +## 3. Component Map + +``` +fiscguy/ +│ +├── views.py REST endpoints (thin HTTP layer) +├── urls.py URL routing +├── models.py All Django models +├── serializers.py DRF serializers (validation + create) +├── exceptions.py Typed exception hierarchy +├── apps.py Django app config +│ +├── services/ +│ ├── receipt_service.py Receipt create + submit orchestration +│ ├── open_day_service.py Fiscal day opening +│ ├── closing_day_service.py Fiscal day closing + counter hash +│ ├── configuration_service.py Tax + config sync from FDMS +│ ├── status_service.py FDMS status queries +│ ├── ping_service.py FDMS connectivity check +│ └── certs_service.py Certificate renewal +│ +├── zimra_base.py ZIMRAClient — HTTP to FDMS +├── zimra_crypto.py ZIMRACrypto — RSA/SHA256/MD5 +├── zimra_receipt_handler.py ZIMRAReceiptHandler — full pipeline +│ +├── utils/ +│ ├── cert_temp_manager.py Temp PEM file lifecycle +│ └── datetime_now.py Timestamp helpers +│ +├── management/ +│ └── commands/ +│ └── init_device.py Interactive device registration +│ +└── tests/ + ├── conftest.py + ├── test_views.py + ├── test_services.py + ├── test_closing_day_service.py + └── test_zimra_base.py +``` + +--- + +## 4. Data Models + +### Entity Relationship + +``` + ┌──────────────┐ + │ Device │ + │──────────────│ + │ org_name │ + │ device_id ◄──┼── unique + │ activation_key│ + │ production │ + └──────┬───────┘ + │ + ┌─────────────────┼──────────────────────┐ + │ │ │ + ┌─────────▼──────┐ ┌───────▼──────┐ ┌──────────▼──────┐ + │ Configuration │ │ Certs │ │ FiscalDay │ + │────────────────│ │──────────────│ │─────────────────│ + │ tax_payer_name │ │ csr │ │ day_no │ + │ tin_number │ │ certificate │ │ receipt_counter │ + │ vat_number │ │ cert_key │ │ is_open │ + │ address │ │ production │ └────────┬────────┘ + │ url │ └──────────────┘ │ + └────────────────┘ ┌─────────▼──────────┐ + │ FiscalCounter │ + ┌───────────────────────────│────────────────────│ + │ │ counter_type │ + │ │ currency │ + │ │ tax_id │ + │ │ tax_percent │ + │ │ money_type │ + │ │ value │ + │ └────────────────────┘ + ┌─────────▼──────┐ + │ Receipt │ + │────────────────│ ┌──────────────┐ + │ receipt_number │◄──────│ ReceiptLine │ + │ receipt_type │ 1..* │──────────────│ + │ total_amount │ │ product │ + │ currency │ │ quantity │ + │ global_number │ │ unit_price │ + │ hash_value │ │ line_total │ + │ signature │ │ tax_amount │ + │ qr_code │ │ tax_type ────┼──► Taxes + │ submitted │ └──────────────┘ + │ payment_terms │ + │ buyer ─────────┼──► Buyer + └────────────────┘ +``` + +### Model Reference + +#### `Device` +The root entity. Every other model links back to it. + +| Field | Type | Notes | +|-------|------|-------| +| `org_name` | CharField | Organisation name | +| `device_id` | CharField | Unique. Assigned by ZIMRA | +| `activation_key` | CharField | Used during registration | +| `device_model_name` | CharField | Sent as HTTP header to FDMS | +| `device_model_version` | CharField | Sent as HTTP header to FDMS | +| `device_serial_number` | CharField | | +| `production` | BooleanField | Switches FDMS URL test ↔ production | + +#### `Certs` +Stores the device's X.509 certificate and RSA private key. OneToOne with Device. + +| Field | Type | Notes | +|-------|------|-------| +| `csr` | TextField | PEM-encoded Certificate Signing Request | +| `certificate` | TextField | PEM-encoded X.509 certificate from ZIMRA | +| `certificate_key` | TextField | PEM-encoded RSA private key — **never expose** | +| `production` | BooleanField | Whether this is a production cert | + +> ⚠️ **Security:** `certificate_key` is stored plaintext. Encryption at rest is planned for v0.1.7. Do not expose via API or logs. + +#### `FiscalDay` +One row per trading day. Only one can be `is_open=True` per device at a time. + +| Field | Type | Notes | +|-------|------|-------| +| `day_no` | IntegerField | Sourced from FDMS (`lastFiscalDayNo + 1`) | +| `receipt_counter` | IntegerField | Increments on each submitted receipt | +| `is_open` | BooleanField | Exactly one open day per device | + +Constraint: `unique_together = (device, day_no)`. +Index: `(device, is_open)` — used on every receipt submission. + +#### `FiscalCounter` +Accumulates running totals per tax group or payment method within a fiscal day. + +| Field | Type | Notes | +|-------|------|-------| +| `fiscal_counter_type` | CharField | See counter type enum below | +| `fiscal_counter_currency` | CharField | `USD` or `ZWG` | +| `fiscal_counter_tax_id` | IntegerField | Null for BalanceByMoneyType | +| `fiscal_counter_tax_percent` | DecimalField | Null for exempt and BalanceByMoneyType | +| `fiscal_counter_money_type` | CharField | Only for BalanceByMoneyType | +| `fiscal_counter_value` | DecimalField | Running total, can be negative | + +Counter type enum (ZIMRA spec section 5.4.4): + +| Value | Enum order | Tracks | +|-------|-----------|--------| +| `SaleByTax` | 0 | Sales amount per tax | +| `SaleTaxByTax` | 1 | Tax amount from sales | +| `CreditNoteByTax` | 2 | Credit note amounts (negative) | +| `CreditNoteTaxByTax` | 3 | Tax from credit notes (negative) | +| `DebitNoteByTax` | 4 | Debit note amounts | +| `DebitNoteTaxByTax` | 5 | Tax from debit notes | +| `BalanceByMoneyType` | 6 | Total by payment method | + +#### `Receipt` + +| Field | Type | Notes | +|-------|------|-------| +| `receipt_number` | CharField | `R-{global_number:08d}`. Unique | +| `receipt_type` | CharField | `fiscalinvoice` / `creditnote` / `debitnote` | +| `total_amount` | DecimalField | Negative for credit notes | +| `global_number` | IntegerField | From FDMS `lastReceiptGlobalNo + 1` | +| `hash_value` | CharField | SHA-256 of signature string, base64 | +| `signature` | TextField | RSA signature, base64 | +| `qr_code` | ImageField | PNG saved to `Zimra_qr_codes/` | +| `code` | CharField | 16-char verification code from signature | +| `zimra_inv_id` | CharField | FDMS-assigned receipt ID | +| `submitted` | BooleanField | False if queued offline | + +#### `Taxes` +Synced from FDMS on every `open_day()` and `init_device`. Do not edit manually. + +--- + +## 5. Receipt Processing Pipeline + +``` +POST /receipts/ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ ReceiptView.post() │ +│ Get device → call ReceiptService │ +└──────────────────────────┬──────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ ReceiptService.create_and_submit_receipt() @transaction.atomic│ +│ │ +│ 1. Inject device ID into payload │ +│ 2. ReceiptCreateSerializer.is_valid() │ +│ ├─ Validate receipt type, currency, amounts │ +│ ├─ Credit note: check reference exists, amount sign │ +│ └─ Buyer TIN: must be 10 digits if provided │ +│ 3. serializer.save() → Receipt + ReceiptLine rows created │ +│ 4. Re-fetch with select_related + prefetch_related │ +│ 5. ZIMRAReceiptHandler.process_and_submit() │ +│ │ +│ ┌ If ReceiptSubmissionError raised ──────────────────────────┐ │ +│ │ @transaction.atomic rolls back Receipt + ReceiptLine rows │ │ +│ └────────────────────────────────────────────────────────────┘ │ +└──────────────────────────┬──────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ ZIMRAReceiptHandler.process_and_submit() │ +│ │ +│ ┌─ _ensure_fiscal_day_open() ──────────────────────────────┐ │ +│ │ Query FiscalDay.is_open=True │ │ +│ │ If none: OpenDayService.open_day() → auto-open │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ _get_next_global_number() ──────────────────────────────┐ │ +│ │ GET /getStatus → lastReceiptGlobalNo │ │ +│ │ Compare with local last → log warning if mismatch │ │ +│ │ Return fdms_last + 1 │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ _build_receipt_data() ──────────────────────────────────┐ │ +│ │ Build receiptLines list │ │ +│ │ Calculate tax groups (salesAmountWithTax, taxAmount) │ │ +│ │ Build receiptTaxes list │ │ +│ │ Resolve previousReceiptHash (chain) │ │ +│ │ generate_receipt_signature_string() → signature_string │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ ZIMRACrypto.generate_receipt_hash_and_signature() ──────┐ │ +│ │ hash = base64(SHA256(signature_string)) │ │ +│ │ sig = base64(RSA_SIGN(signature_string, private_key)) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ _generate_qr_code() ────────────────────────────────────┐ │ +│ │ verification_code = MD5(signature_bytes)[:16] │ │ +│ │ qr_url = {fdms_base}/{device_id}{date}{global_no}{code} │ │ +│ │ Save PNG to receipt.qr_code │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ _update_fiscal_counters() ──────────────────────────────┐ │ +│ │ For each tax group in receiptTaxes: │ │ +│ │ FiscalCounter get_or_create → F() increment │ │ +│ │ BalanceByMoneyType += paymentAmount │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ _submit_to_fdms() ──────────────────────────────────────┐ │ +│ │ POST /SubmitReceipt │ │ +│ │ On success: FiscalDay.receipt_counter += 1 │ │ +│ │ Return FDMS response (receiptID, serverSignature, etc.) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└──────────────────────────┬──────────────────────────────────────┘ + │ + ▼ + receipt.submitted = True + receipt.zimra_inv_id = ... + receipt.save() + Return 201 +``` + +--- + +## 6. Fiscal Day Lifecycle + +``` + ┌──────────────────────────────┐ + │ No open fiscal day (start) │ + └──────────────┬───────────────┘ + │ + POST /open-day/ or auto-open + │ + ┌──────────────▼───────────────┐ + │ OpenDayService.open_day() │ + │ │ + │ GET /getStatus │ + │ → lastFiscalDayNo │ + │ next_day_no = last + 1 │ + │ │ + │ POST /openDay { │ + │ fiscalDayNo: N, │ + │ fiscalDayOpened: datetime │ + │ } │ + │ │ + │ FiscalDay.objects.create( │ + │ day_no=N, is_open=True │ + │ ) │ + └──────────────┬───────────────┘ + │ + ┌──────────────▼───────────────┐ + │ Fiscal Day OPEN │ + │ is_open = True │ + │ receipt_counter = 0 │ + │ │ + │ ← receipts submitted │ + │ ← counters accumulate │ + │ ← receipt_counter++ │ + └──────────────┬───────────────┘ + │ + POST /close-day/ + │ + ┌──────────────▼───────────────┐ + │ ClosingDayService.close_day()│ + │ │ + │ Build counter string │ + │ Assemble closing string │ + │ SHA-256 + RSA sign │ + │ │ + │ POST /CloseDay { payload } │ + │ │ + │ sleep(10) │ + │ GET /getStatus │ + │ → fiscalDayStatus │ + └──────────────┬───────────────┘ + │ + ┌───────────────────┼──────────────────────┐ + │ │ │ + FiscalDayClosed FiscalDayCloseFailed Unexpected + │ │ │ + is_open=False raise CloseDayError raise CloseDayError + return status (day stays open, + retry allowed) +``` + +### Day No Resolution + +`OpenDayService` always defers to FDMS for the authoritative day number: + +``` +GET /getStatus → lastFiscalDayNo = N + +Local last day_no == N ? + Yes → proceed, next = N + 1 + No → log WARNING "Local/FDMS day_no mismatch", still use N + 1 +``` + +--- + +## 7. Closing Day & Signature Spec + +Per ZIMRA API spec section 13.3.1. + +### Closing String Assembly + +``` +closing_string = ( + str(device.device_id) + + str(fiscal_day.day_no) + + fiscal_day.created_at.strftime("%Y-%m-%d") ← day OPEN date, not today + + sale_by_tax_string + + sale_tax_by_tax_string + + credit_note_by_tax_string + + credit_note_tax_by_tax_string + + balance_by_money_type_string +).upper() +``` + +> ⚠️ **Critical:** `fiscalDayDate` must be the date the day was **opened**, not today's date. Using today's date causes `CountersMismatch` if the day spans midnight. + +### Counter String Format + +Each counter line: `TYPE + CURRENCY + [TAX_PERCENT or MONEY_TYPE] + VALUE_IN_CENTS` + +``` +SALEBYTAXUSD15.00115000 +SALEBYTAXUSD0.005000 +SALEBYTAXUSDEXEMPT_NOTREALFIELD ← exempt: empty tax part +SALEBYTAXUSD67475 ← exempt example (empty between USD and value) +BALANCEBYMONEYTYPEUSDLCASH69975 ← note the L between currency and money type +BALANCEBYMONEYTYPEZWGLCARD69975 +``` + +**Rules:** + +| Rule | Detail | +|------|--------| +| All uppercase | `.upper()` applied to entire string | +| Amounts in cents | `int(round(value * 100))` — preserves sign | +| Tax percent format | Always two decimal places: `15` → `15.00`, `0` → `0.00`, `14.5` → `14.50` | +| Exempt tax percent | Empty string — nothing between currency and value | +| BalanceByMoneyType | Literal `L` between currency and money type (`USDLCASH`, `ZWGLCARD`) | +| Zero-value counters | Excluded entirely (spec section 4.11) | +| Sort order | Type enum ASC → currency alpha ASC → taxID/moneyType ASC | + +### Sort Order (spec section 13.3.1) + +``` +FiscalCounterType enum order: + SaleByTax(0) → SaleTaxByTax(1) → CreditNoteByTax(2) → + CreditNoteTaxByTax(3) → DebitNoteByTax(4) → + DebitNoteTaxByTax(5) → BalanceByMoneyType(6) + +Within each type: + currency ASC (USD before ZWG) + taxID ASC (for byTax) / moneyType ASC alpha (for BalanceByMoneyType) +``` + +### Signature Generation + +``` +hash = base64( SHA256( closing_string.encode("utf-8") ) ) +signature = base64( RSA_PKCS1v15_SIGN( closing_string, private_key ) ) + +payload = { + "fiscalDayDeviceSignature": { + "hash": hash, + "signature": signature + } +} +``` + +### Common Close Day Errors + +| Error | Root cause | +|-------|-----------| +| `CountersMismatch` | Wrong date, missing `L`, wrong tax percent format, unsorted counters, zero counters included | +| `BadCertificateSignature` | Certificate expired / wrong private key used | +| `FiscalDayCloseFailed` | FDMS validation failed — check `fiscalDayClosingErrorCode` | + +--- + +## 8. Cryptography + +### ZIMRACrypto + +**Location:** `zimra_crypto.py` + +**Library:** `cryptography` (replaces deprecated `pyOpenSSL`) + +``` +┌──────────────────────────────────────────────────────┐ +│ ZIMRACrypto │ +│ │ +│ private_key_path ──► CertTempManager │ +│ (temp file from Certs model) │ +│ │ +│ load_private_key() ──► RSAPrivateKey (cached) │ +│ │ +│ get_hash(data) ──► SHA256(data) → base64 │ +│ │ +│ sign_data(data) ──► RSA PKCS1v15 → base64 │ +│ │ +│ generate_receipt_hash_and_signature(string) │ +│ → { hash: str, signature: str } │ +│ │ +│ generate_verification_code(signature_b64) │ +│ → MD5(sig_bytes).hexdigest()[:16].upper() │ +│ → formatted as XXXX-XXXX-XXXX-XXXX │ +│ │ +│ generate_key_and_csr(device) │ +│ → RSA 2048 key pair │ +│ → CSR with CN=ZIMRA-{serial}-{device_id} │ +└──────────────────────────────────────────────────────┘ +``` + +### Receipt Signature String + +Per ZIMRA spec section 13.2.1: + +``` +{deviceID} +{receiptType} ← UPPERCASE e.g. FISCALINVOICE +{receiptCurrency} ← UPPERCASE e.g. USD +{receiptGlobalNo} +{receiptDate} ← YYYY-MM-DDTHH:mm:ss +{receiptTotal_in_cents} ← negative for credit notes +{receiptTaxes} ← concatenated, ordered by taxID ASC +{previousReceiptHash} ← omitted if first receipt of day +``` + +Tax line format: `taxCode + taxPercent + taxAmount_cents + salesAmountWithTax_cents` + +### Private Key Lifecycle + +``` +init_device + │ + ▼ +ZIMRACrypto.generate_key_and_csr() + │ RSA 2048 key pair generated in memory + │ CSR built and signed + │ + ▼ +ZIMRAClient.register_device() + │ CSR sent to FDMS + │ Signed certificate returned + │ + ▼ +Certs.objects.create( + csr=csr_pem, + certificate=cert_pem, + certificate_key=private_key_pem +) + │ + ▼ +ZIMRACrypto (at runtime) + │ + ▼ +CertTempManager + │ Writes cert + key to tempfile.mkdtemp() + │ Returns path for load_private_key() + │ + ▼ +ZIMRAClient.session.cert = pem_path ← mTLS + │ + ▼ +ZIMRAClient.close() / __del__ + │ shutil.rmtree(temp_dir) ← cleanup +``` + +--- + +## 9. ZIMRA Client + +### ZIMRAClient + +**Location:** `zimra_base.py` + +``` +┌───────────────────────────────────────────────────────┐ +│ ZIMRAClient │ +│ │ +│ __init__(device) │ +│ ├─ Load Configuration (cached @property) │ +│ ├─ Load Certs (cached @property) │ +│ ├─ Set base_url / public_url based on production │ +│ ├─ Write temp PEM from Certs │ +│ └─ Create requests.Session with cert + headers │ +│ │ +│ Endpoints: │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ register_device(payload) → public, no cert │ │ +│ │ get_status() → GET /getStatus │ │ +│ │ get_config() → GET /getConfig │ │ +│ │ ping() → POST /ping │ │ +│ │ open_day(payload) → POST /openDay │ │ +│ │ close_day(payload) → POST /CloseDay │ │ +│ │ submit_receipt(payload) → POST /SubmitReceipt │ │ +│ │ issue_certificate(payload)→ POST /issueCert │ │ +│ └─────────────────────────────────────────────────┘ │ +│ │ +│ _request(method, endpoint) │ +│ ├─ Build full URL │ +│ ├─ session.request(timeout=30) │ +│ ├─ response.raise_for_status() │ +│ └─ On error: log + re-raise requests.RequestException│ +│ │ +│ Lifecycle: │ +│ close() → session.close() + rmtree(temp_dir) │ +│ __enter__ / __exit__ → context manager │ +│ __del__ → close() on GC │ +└───────────────────────────────────────────────────────┘ +``` + +### URLs + +| Environment | Device API | Public API | +|-------------|-----------|-----------| +| Testing | `https://fdmsapitest.zimra.co.zw/Device/v1/{device_id}` | `https://fdmsapitest.zimra.co.zw/Public/v1/{device_id}` | +| Production | `https://fdmsapi.zimra.co.zw/Device/v1/{device_id}` | `https://fdmsapi.zimra.co.zw/Public/v1/{device_id}` | + +All Device API requests use mutual TLS. The Public API (`RegisterDevice`) uses plain HTTPS with no client cert. + +--- + +## 10. Fiscal Counters + +### Update Flow (per receipt) + +``` +ZIMRAReceiptHandler._update_fiscal_counters_inner() + +For each tax group in receiptTaxes: +│ +├─ receipt_type == "fiscalinvoice" +│ ├─ SaleByTax += salesAmountWithTax (per tax group) +│ └─ SaleTaxByTax += taxAmount (non-exempt, non-zero only) +│ +├─ receipt_type == "creditnote" +│ ├─ CreditNoteByTax += -salesAmountWithTax (negative) +│ └─ CreditNoteTaxByTax += -taxAmount (negative, non-exempt, non-zero) +│ +└─ receipt_type == "debitnote" + ├─ DebitNoteByTax += salesAmountWithTax + └─ DebitNoteTaxByTax += taxAmount + +Always (all types): + BalanceByMoneyType += paymentAmount (negative for credit notes) +``` + +### Race Condition Prevention + +Counter updates use Django `F()` expressions for atomic DB-level increments, preventing lost updates under concurrent receipt submission: + +```python +# Instead of: +counter.fiscal_counter_value += amount # ← race condition +counter.save() + +# FiscGuy uses: +FiscalCounter.objects.filter(...).update( + fiscal_counter_value=F("fiscal_counter_value") + amount +) +``` + +### get_or_create Key + +Each unique combination gets its own row: + +```python +FiscalCounter.objects.get_or_create( + fiscal_counter_type=counter_type, + fiscal_counter_currency=currency, + fiscal_counter_tax_id=tax_id, + fiscal_counter_tax_percent=tax_percent, + fiscal_counter_money_type=money_type, + fiscal_day=fiscal_day, + defaults={"fiscal_counter_value": amount}, +) +``` + +--- + +## 11. Error Handling + +### Exception Hierarchy + +``` +FiscalisationError +├── ReceiptSubmissionError Receipt can't be processed or submitted +├── CloseDayError Day close rejected by FDMS +├── FiscalDayError Day open failed +├── ConfigurationError Config missing or sync failed +├── CertificateError Cert issuance/renewal failed +├── DevicePingError Ping to FDMS failed +├── StatusError Status fetch failed +├── DeviceRegistrationError Registration with ZIMRA failed +├── CryptoError RSA/hash operation failed +├── CertNotFoundError No cert found in DB +├── PersistenceError DB write failed +├── ZIMRAAPIError Generic FDMS API error +├── ValidationError Data validation failed +├── AuthenticationError mTLS auth failed +├── TaxError Tax CRUD failed +├── DeviceNotFoundError Device not in DB +├── ZIMRAClientError Client-level failure +└── TenantNotFoundError Multi-tenant lookup failed +``` + +### View Error Mapping + +``` +ReceiptSubmissionError → 422 Unprocessable Entity +CloseDayError → 422 Unprocessable Entity +FiscalDayError → 400 Bad Request +ConfigurationError → 500 Internal Server Error +CertificateError → 422 Unprocessable Entity +DevicePingError → 500 Internal Server Error +StatusError → 500 Internal Server Error +No device found → 404 Not Found +No open fiscal day → 400 Bad Request +Exception (catch-all) → 500 Internal Server Error +``` + +--- + +## 12. Database Design + +### Indexes + +``` +Device: + device_id UNIQUE + +FiscalDay: + (device_id, day_no) UNIQUE + (device_id, is_open) INDEX ← every receipt submission queries this + +FiscalCounter: + (device_id, fiscal_day_id) INDEX + +Receipt: + receipt_number UNIQUE + (device_id, -created_at) INDEX ← paginated receipt listing + +ReceiptLine: + (receipt_id) INDEX + +Taxes: + (tax_id) INDEX ← looked up on every receipt line + +Buyer: + (tin_number) INDEX +``` + +### Query Patterns + +| Operation | Query | +|-----------|-------| +| Get open fiscal day | `FiscalDay.objects.filter(device=d, is_open=True).first()` | +| Get open day (with lock) | `select_for_update().filter(device=d, is_open=True).first()` | +| Get receipt with lines | `select_related("buyer").prefetch_related("lines")` | +| Build tax map | `{t.tax_id: t.name for t in Taxes.objects.all()}` | +| Upsert counter | `get_or_create(...)` then `F()` update | + +--- + +## 13. Development Guidelines + +### Adding a New Service + +1. Create `fiscguy/services/my_service.py` +2. Accept `device: Device` in `__init__` +3. Raise typed `FiscalisationError` subclasses — never raw `Exception` +4. Wrap DB writes in `transaction.atomic` +5. Add a view in `views.py` and route in `urls.py` +6. Add tests in `fiscguy/tests/` + +```python +class MyService: + def __init__(self, device: Device): + self.device = device + self.client = ZIMRAClient(device) + + @transaction.atomic + def do_thing(self) -> dict: + try: + result = self.client.some_endpoint() + except requests.RequestException as exc: + raise MyError("FDMS call failed") from exc + + MyModel.objects.create(device=self.device, ...) + return result +``` + +### Adding a New Endpoint + +```python +# views.py +class MyView(APIView): + def post(self, request): + device = Device.objects.first() + if not device: + return Response({"error": "No device registered"}, status=404) + try: + result = MyService(device).do_thing() + return Response(result, status=200) + except MyError as exc: + logger.error(f"My thing failed: {exc}") + return Response({"error": str(exc)}, status=422) + except Exception: + logger.exception("Unexpected error") + return Response({"error": "Internal server error"}, status=500) + +# urls.py +path("my-endpoint/", MyView.as_view(), name="my-endpoint"), +``` + +### Logging + +Use `loguru`. Follow these levels: + +| Level | When | +|-------|------| +| `logger.info()` | Normal operations — receipt submitted, day opened | +| `logger.warning()` | Recoverable issues — FDMS/local mismatch, offline queue | +| `logger.error()` | Handled failures — receipt rejected, close failed | +| `logger.exception()` | Unexpected errors — always in `except` blocks, includes traceback | + +Never log private keys, raw certificates, or full receipt payloads at INFO level. + +### Migrations + +```bash +# After model changes +python manage.py makemigrations fiscguy + +# Apply +python manage.py migrate + +# Never edit existing migrations +# Always create a new migration for changes +``` + +### Testing + +```bash +pytest # all tests +pytest --cov=fiscguy --cov-report=html # with coverage +pytest fiscguy/tests/test_closing_day_service.py # single file +pytest -k "test_build_sale_by_tax" # single test +``` + +Mock external calls at the boundary — patch `ZIMRAClient`, `ZIMRACrypto`, and `requests`. Never make real FDMS calls in tests. + +```python +@patch("fiscguy.services.open_day_service.ZIMRAClient") +def test_open_day_success(self, MockClient): + MockClient.return_value.get_status.return_value = {"lastFiscalDayNo": 5} + MockClient.return_value.open_day.return_value = {"fiscalDayNo": 6} + ... +``` + +### Code Style + +- Line length: 100 (Black) +- Imports: isort with Black profile +- Private methods: prefix `_` +- Type hints on all public method signatures +- Docstrings on all public classes and methods + +```bash +black fiscguy && isort fiscguy && flake8 fiscguy && mypy fiscguy +``` + +--- + +> **Internal use only.** Do not publish this document. +> Maintainer: Casper Moyo · cassymyo@gmail.com +> Version: 0.1.6 · April 2026 diff --git a/docs/certificate-management.md b/docs/certificate-management.md new file mode 100644 index 0000000..be1dc2a --- /dev/null +++ b/docs/certificate-management.md @@ -0,0 +1,101 @@ +# Certificate Management + +FiscGuy uses mutual TLS authentication with ZIMRA FDMS. The device must hold a valid certificate issued by ZIMRA to submit any signed request. + +--- + +## How Certificates Work + +1. `init_device` generates an RSA key pair and a Certificate Signing Request (CSR) +2. The CSR is sent to ZIMRA FDMS `RegisterDevice` endpoint +3. ZIMRA returns a signed certificate +4. The certificate and private key are stored in the `Certs` model +5. Every request to FDMS uses the certificate for mutual TLS authentication + +--- + +## Certificate Storage + +Certificates are stored in the `Certs` model: + +```python +from fiscguy.models import Certs + +cert = Certs.objects.first() +print(cert.certificate) # PEM-encoded certificate +print(cert.certificate_key) # PEM-encoded private key +print(cert.csr) # Original CSR +print(cert.production) # True = production, False = testing +``` + +At runtime, `ZIMRAClient` writes the certificate and key to a temporary PEM file used by the `requests` session. The temp file is cleaned up when the client is closed. + +--- + +## Certificate Renewal + +Certificates expire. When they do, all signed requests will fail with `BadCertificateSignature` or an authentication error. + +### Via REST endpoint + +``` +POST /fiscguy/issue-certificate/ +``` + +Response on success: +```json +{"message": "Certificate issued successfully"} +``` + +### Via Python + +```python +from fiscguy.services.certs_service import CertificateService +from fiscguy.models import Device + +device = Device.objects.first() +CertificateService(device).issue_certificate() +``` + +### Raises + +| Exception | Cause | +|-----------|-------| +| `CertificateError` | FDMS rejected the renewal request | +| `Exception` | Unexpected error during renewal | + +--- + +## Key Generation + +FiscGuy supports two key algorithms as per ZIMRA spec section 12: + +| Algorithm | Spec reference | +|-----------|----------------| +| RSA 2048 | Section 12.1.2 | +| ECC ECDSA secp256r1 (P-256) | Section 12.1.1 | + +The `cryptography` library is used for all key generation and signing. `pyOpenSSL` is no longer used. + +--- + +## Security Notes + +- The private key never leaves the device. Only the CSR is sent to ZIMRA. +- Do not commit `Certs` data to version control. +- In production, consider encrypting `Certs.certificate` and `Certs.certificate_key` at rest using a library like `cryptography.fernet`. See the project roadmap for planned support. +- The temporary PEM file is written to a `tempfile.mkdtemp()` directory and deleted when `ZIMRAClient.close()` is called. + +--- + +## Checking Certificate Status + +```python +from fiscguy import get_status + +status = get_status() +# Check for certificate-related errors in the response +print(status) +``` + +If the certificate is expired or invalid, `get_status()` will raise `StatusError` with an authentication error from FDMS. diff --git a/docs/closing-day.md b/docs/closing-day.md new file mode 100644 index 0000000..4899535 --- /dev/null +++ b/docs/closing-day.md @@ -0,0 +1,156 @@ +# Closing a Fiscal Day + +At the end of each trading day, the fiscal day must be closed by submitting a signed summary of all fiscal counters to ZIMRA FDMS. + +--- + +## Quick Close + +```python +from fiscguy import close_day + +result = close_day() +# {"fiscalDayStatus": "FiscalDayClosed", ...} +``` + +Or via REST: + +``` +POST /fiscguy/close-day/ +``` + +--- + +## What Happens During Close + +`ClosingDayService.close_day()` performs these steps in order: + +1. **Build counter strings** — each counter type is serialised into the ZIMRA closing string format +2. **Assemble the closing string** — `deviceID + fiscalDayNo + fiscalDayDate + counters` +3. **Hash and sign** — SHA-256 hash of the string, signed with the device RSA private key +4. **Build payload** — closing string, signature, fiscal day counters, receipt counter +5. **Submit to FDMS** — `POST /CloseDay` +6. **Poll for status** — waits 10 seconds then calls `GET /getStatus` +7. **Update database** — marks `FiscalDay.is_open = False` on success + +--- + +## Closing String Specification + +From ZIMRA API spec section 13.3.1. Fields concatenated in this exact order: + +| Order | Field | Format | +|-------|-------|--------| +| 1 | `deviceID` | Integer as-is | +| 2 | `fiscalDayNo` | Integer as-is | +| 3 | `fiscalDayDate` | `YYYY-MM-DD` — **date the fiscal day was opened**, not today | +| 4 | `fiscalDayCounters` | Concatenated counter string (see below) | + +All text **uppercase**. No separators between fields. + +### Counter String + +Each counter line: `TYPE || CURRENCY || [TAX_PERCENT or MONEY_TYPE] || VALUE_IN_CENTS` + +**Sort order:** +1. Counter type — ascending by enum value (`SaleByTax=0` → `BalanceByMoneyType=6`) +2. Currency — alphabetical ascending +3. TaxID — ascending (for byTax types) / MoneyType — ascending (for BalanceByMoneyType) + +**Tax percent formatting:** +- Integer percent: always two decimals — `15` → `15.00`, `0` → `0.00` +- Decimal percent: `14.5` → `14.50` +- Exempt (no percent): empty string — nothing between currency and value + +**BalanceByMoneyType:** has a literal `L` between currency and money type: + +``` +BALANCEBYMONEYTYPEUSDLCASH3700 +BALANCEBYMONEYTYPEZWGLCARD1500000 +BALANCEBYMONEYTYPEZWGLCASH2000000 +``` + +**Zero-value counters:** excluded entirely (per spec section 4.11). + +**Amounts:** in cents, preserving sign. `-699.75` → `-69975`. + +### Full Example + +From ZIMRA spec section 13.3.1: + +``` +321842019-09-23 +SALEBYTAXZWL2300000 +SALEBYTAXZWL0.001200000 +SALEBYTAXUSD14.502500 +SALEBYTAXZWL15.001200 +SALETAXBYTAXUSD15.00250 +SALETAXBYTAXZWL15.00230000 +BALANCEBYMONEYTYPEUSDLCASH3700 +BALANCEBYMONEYTYPEZWLCASH2000000 +BALANCEBYMONEYTYPEZWLCARD1500000 +``` + +Hash (SHA-256, base64): `OdT8lLI0JXhXl1XQgr64Zb1ltFDksFXThVxqM6O8xZE=` + +--- + +## Common Close Day Errors + +### `CountersMismatch` + +FDMS computed different counter values from what was submitted. + +**Causes:** +- `fiscalDayDate` in the closing string uses today's date instead of the fiscal day open date +- Tax percent not formatted as two decimal places (`15` instead of `15.00`) +- `BalanceByMoneyType` missing the `L` separator +- Counters not sorted in the correct order +- Credit note counter not negated, or using `receiptTotal` instead of per-tax `salesAmountWithTax` +- Zero-value counters included in the payload + +### `BadCertificateSignature` + +FDMS cannot verify the device signature. + +**Causes:** +- Wrong private key used for signing (key doesn't match the registered certificate) +- Certificate has expired — run `POST /fiscguy/issue-certificate/` +- Certificate has been revoked + +### `FiscalDayCloseFailed` + +FDMS accepted the request but validation failed. The day remains open and can be retried. + +Check `fiscalDayClosingErrorCode` in the response for the specific reason. + +--- + +## Fiscal Day Status Values + +| Status | Meaning | +|--------|---------| +| `FiscalDayOpened` | Day is open, receipts can be submitted | +| `FiscalDayCloseInitiated` | Close request submitted, processing | +| `FiscalDayClosed` | Day closed successfully | +| `FiscalDayCloseFailed` | Close attempt failed — day remains open, retry allowed | + +--- + +## Retrying a Failed Close + +If `close_day()` raises `CloseDayError` with `FiscalDayCloseFailed`, the day remains open and you can correct the issue and retry: + +```python +from fiscguy import close_day +from fiscguy.exceptions import CloseDayError + +try: + close_day() +except CloseDayError as e: + print(f"Close failed: {e}") + # Investigate, fix, then retry: + close_day() +``` + +FDMS allows close retries when the fiscal day status is `FiscalDayOpened` or `FiscalDayCloseFailed`. diff --git a/docs/error-reference.md b/docs/error-reference.md new file mode 100644 index 0000000..e4f5519 --- /dev/null +++ b/docs/error-reference.md @@ -0,0 +1,176 @@ +# Error Reference + +All FiscGuy exceptions inherit from `FiscalisationError`. Import them from `fiscguy.exceptions`. + +--- + +## Exception Hierarchy + +``` +FiscalisationError +├── ReceiptSubmissionError +├── CloseDayError +├── FiscalDayError +├── ConfigurationError +├── CertificateError +├── DevicePingError +├── StatusError +├── DeviceRegistrationError +├── CryptoError +├── CertNotFoundError +├── PersistenceError +├── ZIMRAAPIError +├── ValidationError +├── AuthenticationError +├── TaxError +├── DeviceNotFoundError +├── TenantNotFoundError +└── ZIMRAClientError +``` + +--- + +## Common Exceptions + +### `ReceiptSubmissionError` + +Raised when a receipt cannot be processed or submitted. + +```python +from fiscguy.exceptions import ReceiptSubmissionError + +try: + submit_receipt(data) +except ReceiptSubmissionError as e: + print(e) +``` + +Common causes: +- No open fiscal day — call `open_day()` first +- Invalid receipt data (missing required fields, wrong types) +- FDMS rejected the receipt (validation errors) +- FDMS unreachable and auto-queue failed +- Credit note references a non-existent original receipt +- Credit note amount exceeds original receipt amount + +--- + +### `CloseDayError` + +Raised when the fiscal day cannot be closed. + +```python +from fiscguy.exceptions import CloseDayError + +try: + close_day() +except CloseDayError as e: + print(e) +``` + +Common causes and fixes: + +| Error code | Cause | Fix | +|------------|-------|-----| +| `CountersMismatch` | Closing string counters don't match FDMS records | Check closing string format — date, tax percent format, L separator | +| `BadCertificateSignature` | Device signature cannot be verified | Certificate expired or wrong key — renew certificate | +| `FiscalDayCloseFailed` | FDMS validation failed | Check `fiscalDayClosingErrorCode` in logs | +| Empty response | FDMS returned nothing | Retry after a delay | + +--- + +### `FiscalDayError` + +Raised when a fiscal day cannot be opened. + +Common causes: +- A fiscal day is already open +- FDMS rejected the open request +- Previous fiscal day was not closed + +--- + +### `ConfigurationError` + +Raised when configuration is missing or sync fails. + +Common causes: +- `init_device` was not run +- FDMS unreachable during configuration sync +- Configuration sync after `open_day()` failed (day opened, config not updated) + +--- + +### `CertificateError` + +Raised when certificate issuance or renewal fails. + +Common causes: +- FDMS rejected the certificate request +- Device not registered with ZIMRA +- Network failure during certificate request + +--- + +### `DevicePingError` + +Raised when the device ping to FDMS fails. + +--- + +### `StatusError` + +Raised when the status fetch from FDMS fails. + +--- + +### `DeviceRegistrationError` + +Raised during `init_device` if ZIMRA rejects the registration request. + +Common causes: +- Invalid activation key +- Device ID already registered +- Network failure + +--- + +### `CryptoError` + +Raised when RSA signing, hashing, or key generation fails. + +--- + +## HTTP Status Codes (REST API) + +| HTTP Status | Meaning | +|-------------|---------| +| `200 OK` | Success | +| `201 Created` | Receipt submitted successfully | +| `400 Bad Request` | Invalid request — fiscal day already open, no open day to close | +| `404 Not Found` | No device registered | +| `405 Method Not Allowed` | Wrong HTTP method | +| `422 Unprocessable Entity` | FDMS rejected the request | +| `500 Internal Server Error` | Unexpected server error | + +--- + +## Logging + +FiscGuy uses `loguru` for structured logging. All service operations log at appropriate levels: + +```python +# In your Django project — configure loguru sink +from loguru import logger + +logger.add("fiscguy.log", level="INFO", rotation="1 day") +``` + +Key log events: + +| Level | Event | +|-------|-------| +| `INFO` | Receipt submitted, day opened/closed, client initialised | +| `WARNING` | FDMS offline (receipt queued), global number mismatch | +| `ERROR` | Receipt submission failed, close day failed | +| `EXCEPTION` | Unexpected errors with full traceback | diff --git a/docs/fiscal-counters.md b/docs/fiscal-counters.md new file mode 100644 index 0000000..36e4e95 --- /dev/null +++ b/docs/fiscal-counters.md @@ -0,0 +1,127 @@ +# Fiscal Counters + +Fiscal counters are running totals that accumulate throughout a fiscal day. At close of day they are submitted to ZIMRA as part of the closing payload and used to verify the hash signature. + +--- + +## Counter Types + +| Counter | Tracks | By Tax | By Currency | By Payment | +|---------|--------|--------|-------------|------------| +| `SaleByTax` | Total sales amount including tax | ✓ | ✓ | | +| `SaleTaxByTax` | Tax portion of sales | ✓ | ✓ | | +| `CreditNoteByTax` | Total credit note amounts | ✓ | ✓ | | +| `CreditNoteTaxByTax` | Tax portion of credit notes | ✓ | ✓ | | +| `DebitNoteByTax` | Total debit note amounts | ✓ | ✓ | | +| `DebitNoteTaxByTax` | Tax portion of debit notes | ✓ | ✓ | | +| `BalanceByMoneyType` | Total collected by payment method | | ✓ | ✓ | + +--- + +## How Counters Are Updated + +Every time a receipt is submitted, `_update_fiscal_counters_inner` runs automatically. You never need to update counters manually. + +### Fiscal Invoice + +``` +SaleByTax += salesAmountWithTax (per tax group) +SaleTaxByTax += taxAmount (per tax group, non-exempt/non-zero only) +BalanceByMoneyType += paymentAmount (per payment method) +``` + +### Credit Note + +Credit note values are **negative** — each counter decreases: + +``` +CreditNoteByTax += salesAmountWithTax (negative, per tax group) +CreditNoteTaxByTax += taxAmount (negative, non-exempt/non-zero only) +BalanceByMoneyType += paymentAmount (negative) +``` + +### Debit Note + +``` +DebitNoteByTax += salesAmountWithTax (per tax group) +DebitNoteTaxByTax += taxAmount (non-exempt/non-zero only) +BalanceByMoneyType += paymentAmount +``` + +--- + +## Counter Rows in the Database + +Each unique combination of `(counter_type, currency, tax_id, tax_percent, money_type, fiscal_day)` gets its own `FiscalCounter` row. On first encounter it is created; on subsequent receipts it is incremented. + +```python +from fiscguy.models import FiscalCounter, FiscalDay + +day = FiscalDay.objects.filter(is_open=True).first() +counters = day.counters.all() + +for c in counters: + print(c.fiscal_counter_type, c.fiscal_counter_currency, c.fiscal_counter_value) +``` + +--- + +## Zero-Value Counters + +Per ZIMRA spec (section 4.11): **zero-value counters must not be submitted** to FDMS. FiscGuy automatically excludes them from the closing payload and closing string. + +--- + +## Closing String Format + +At `close_day()`, all counters are concatenated into a single string for signing. The format per ZIMRA spec section 13.3.1: + +``` +{deviceID}{fiscalDayNo}{fiscalDayDate}{counters...} +``` + +Each counter line is: + +``` +{TYPE}{CURRENCY}[L]{TAX_PERCENT_OR_MONEY_TYPE}{VALUE_IN_CENTS} +``` + +Rules: +- All text **uppercase** +- Amounts in **cents** (multiply by 100, integer, negative for credit notes) +- Tax percent always **two decimal places** (`15.00`, `0.00`, `14.50`) +- Exempt entries use **empty string** for tax percent (nothing between currency and value) +- `BalanceByMoneyType` has a literal **`L`** between currency and money type (e.g. `BALANCEBYMONEYTYPEUSDLCASH3700`) +- Ordered by: counter type ascending → currency ascending → taxID/moneyType ascending + +Example: + +``` +23265842026-03-30 +SALEBYTAXZWG0.005000 +SALEBYTAXZWG15.50134950 +SALETAXBYTAXZWG15.5018110 +BALANCEBYMONEYTYPEZWGLCARD69975 +BALANCEBYMONEYTYPEZWGLCASH69975 +``` + +(joined as one string, no newlines) + +--- + +## Resetting Counters + +Counters reset automatically when a fiscal day is closed. The next `open_day()` starts fresh from zero. + +If you need to inspect counters mid-day: + +```python +from fiscguy.models import FiscalDay, FiscalCounter + +fiscal_day = FiscalDay.objects.filter(is_open=True).first() +print(fiscal_day.counters.all().values( + "fiscal_counter_type", + "fiscal_counter_currency", + "fiscal_counter_value", +)) +``` diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..aa790d6 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,201 @@ +# Installation & Setup + +## Requirements + +- Python 3.11, 3.12, or 3.13 +- Django 4.2+ +- Django REST Framework 3.14+ + +--- + +## Install + +### From PyPI + +```bash +pip install fiscguy +``` + +### From source + +```bash +git clone https://github.com/digitaltouchcode/fisc.git +cd fisc +pip install -e ".[dev]" +``` + +--- + +## Django Setup + +### 1. Add to `INSTALLED_APPS` + +```python +# settings.py +INSTALLED_APPS = [ + "django.contrib.contenttypes", + "django.contrib.auth", + "rest_framework", + "fiscguy", + # ... your apps +] +``` + +### 2. Run migrations + +```bash +python manage.py migrate +``` + +### 3. Include URLs + +```python +# your project urls.py +from django.urls import path, include + +urlpatterns = [ + path("fiscguy/", include("fiscguy.urls")), +] +``` + +### 4. Media files (for QR codes) + +FiscGuy saves receipt QR codes to `MEDIA_ROOT`. Configure it in settings: + +```python +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" +``` + +And serve media in development: + +```python +# urls.py +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + ... +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +``` + +--- + +## Device Initialisation + +Run once per device. This is the most important setup step: + +```bash +python manage.py init_device +``` + +You will be prompted for: + +| Prompt | Example | Description | +|--------|---------|-------------| +| Organisation name | `ACME Ltd` | Your company name | +| Activation key | `ABC-123-XYZ` | Provided by ZIMRA | +| Device ID | `23265` | Provided by ZIMRA | +| Device model name | `FiscGuy-v1` | Your device model | +| Device model version | `1.0.0` | Your device version | +| Device serial number | `SN0001` | Your device serial | +| Production? | `y/n` | Use production or test FDMS | + +The command then: +1. Creates the `Device` record in your database +2. Generates an RSA key pair and CSR +3. Registers the device with ZIMRA FDMS +4. Obtains a signed certificate and stores it in `Certs` +5. Fetches and persists taxpayer configuration and taxes + +--- + +## Verify Setup + +```python +from fiscguy.models import Device, Configuration, Taxes + +# Device should exist +device = Device.objects.first() +print(device) # "ACME Ltd - 23265" + +# Config should be populated +config = Configuration.objects.first() +print(config.tax_payer_name) + +# Taxes should be populated +print(Taxes.objects.all()) +``` + +--- + +## Environment: Test vs Production + +`init_device` asks whether to use the production or testing FDMS. This sets `Device.production` and `Certs.production`, which determines which FDMS URL is used: + +| Environment | URL | +|-------------|-----| +| Testing | `https://fdmsapitest.zimra.co.zw` | +| Production | `https://fdmsapi.zimra.co.zw` | + +To switch environments, re-run `init_device`. + +--- + +## Development Dependencies + +```bash +pip install -e ".[dev]" +``` + +Includes: `pytest`, `pytest-django`, `pytest-cov`, `black`, `isort`, `flake8`, `pylint`, `mypy`, `django-stubs`. + +### Pre-commit hooks + +```bash +pre-commit install +``` + +Runs `black`, `isort`, and `flake8` on every commit. + +--- + +## Troubleshooting + +### `RuntimeError: No Device found` + +Run `python manage.py init_device`. + +### `RuntimeError: ZIMRA configuration missing` + +The `Configuration` record is missing. Either `init_device` didn't complete, or run: + +```python +from fiscguy import get_configuration +get_configuration() +``` + +### `MalformedFraming: Unable to load PEM file` + +The certificate stored in `Certs` is corrupted or missing. Re-run `init_device`. + +### `No open fiscal day` + +Open a fiscal day before submitting receipts: + +```python +from fiscguy import open_day +open_day() +``` + +### Certificate expired + +```bash +# via REST endpoint +POST /fiscguy/issue-certificate/ + +# or via Python +from fiscguy.services.certs_service import CertificateService +from fiscguy.models import Device +CertificateService(Device.objects.first()).issue_certificate() +``` diff --git a/docs/receipt-types.md b/docs/receipt-types.md new file mode 100644 index 0000000..70626e3 --- /dev/null +++ b/docs/receipt-types.md @@ -0,0 +1,186 @@ +# Receipt Types + +FiscGuy supports three receipt types as defined by the ZIMRA Fiscal Device Gateway API. + +--- + +## Fiscal Invoice (`fiscalinvoice`) + +A standard sale receipt. The most common receipt type. + +```python +from fiscguy import submit_receipt + +receipt = submit_receipt({ + "receipt_type": "fiscalinvoice", + "currency": "USD", + "total_amount": "115.00", + "payment_terms": "Cash", + "lines": [ + { + "product": "Product Name", + "quantity": "1", + "unit_price": "115.00", + "line_total": "115.00", + "tax_amount": "15.00", + "tax_name": "standard rated 15%", + } + ], +}) +``` + +**Counter impact:** + +| Counter | Value | +|---------|-------| +| `SaleByTax` | `+salesAmountWithTax` per tax group | +| `SaleTaxByTax` | `+taxAmount` per tax group (non-exempt, non-zero only) | +| `BalanceByMoneyType` | `+paymentAmount` | + +**Rules:** +- `receiptTotal` must be `>= 0` +- `receiptLinePrice` must be `> 0` for Sale lines +- `paymentAmount` must be `>= 0` + +--- + +## Credit Note (`creditnote`) + +A return or reversal against a previously issued fiscal invoice. + +```python +receipt = submit_receipt({ + "receipt_type": "creditnote", + "currency": "USD", + "total_amount": "-115.00", # Must be <= 0 + "payment_terms": "Cash", + "credit_note_reason": "Customer returned goods — defective", + "credit_note_reference": "R-00000142", # Original receipt number + "lines": [ + { + "product": "Product Name", + "quantity": "1", + "unit_price": "-115.00", # Must be < 0 for Sale lines + "line_total": "-115.00", + "tax_amount": "-15.00", + "tax_name": "standard rated 15%", + } + ], +}) +``` + +**Counter impact:** Credit note amounts are **negative**, so each counter decreases: + +| Counter | Value | +|---------|-------| +| `CreditNoteByTax` | `+salesAmountWithTax` (negative) per tax group | +| `CreditNoteTaxByTax` | `+taxAmount` (negative) per tax group (non-exempt, non-zero only) | +| `BalanceByMoneyType` | `+paymentAmount` (negative) | + +**Rules (ZIMRA spec):** +- `receiptTotal` must be `<= 0` +- `receiptNotes` (credit_note_reason) is **mandatory** +- `creditDebitNote` reference to original invoice is **mandatory** +- Original receipt must exist in FDMS (RCPT032) +- Original receipt must have been issued within the last 12 months (RCPT033) +- Total credit amount must not exceed the original receipt amount net of prior credits (RCPT035) +- Tax types must be a subset of those on the original invoice — you cannot introduce new tax types (RCPT036) +- Currency must match the original invoice (RCPT043) +- `receiptLinePrice` must be `< 0` for Sale lines +- `paymentAmount` must be `<= 0` + +--- + +## Debit Note (`debitnote`) + +An upward adjustment against a previously issued fiscal invoice (e.g. additional charges). + +```python +receipt = submit_receipt({ + "receipt_type": "debitnote", + "currency": "USD", + "total_amount": "23.00", + "payment_terms": "Card", + "credit_note_reason": "Additional delivery charge", + "credit_note_reference": "R-00000142", + "lines": [ + { + "product": "Delivery fee", + "quantity": "1", + "unit_price": "23.00", + "line_total": "23.00", + "tax_amount": "3.00", + "tax_name": "standard rated 15%", + } + ], +}) +``` + +**Counter impact:** + +| Counter | Value | +|---------|-------| +| `DebitNoteByTax` | `+salesAmountWithTax` per tax group | +| `DebitNoteTaxByTax` | `+taxAmount` per tax group | +| `BalanceByMoneyType` | `+paymentAmount` | + +--- + +## Payment Methods + +| Value | Description | +|-------|-------------| +| `Cash` | Physical cash | +| `Card` | Credit or debit card | +| `MobileWallet` | Mobile money (EcoCash, etc.) | +| `BankTransfer` | Direct bank transfer | +| `Coupon` | Voucher or coupon | +| `Credit` | Credit account | +| `Other` | Any other method | + +--- + +## Tax Types + +Taxes are fetched from FDMS on every `open_day()` and stored in the `Taxes` model. + +```python +from fiscguy.models import Taxes + +for tax in Taxes.objects.all(): + print(f"{tax.tax_id}: {tax.name} @ {tax.percent}%") +``` + +Typical ZIMRA tax types: + +| Tax ID | Name | Percent | +|--------|------|---------| +| 1 | Exempt | 0% | +| 2 | Zero Rated 0% | 0% | +| 3+ | Standard Rated | 15% or 15.5% | + +When building a receipt line, pass the `tax_name` exactly as it appears in `Taxes.name` so the correct `tax_id` and `tax_percent` are resolved. + +--- + +## Buyer Data (Optional) + +Attach buyer registration data to any receipt type: + +```python +# Create a buyer first +from fiscguy.models import Buyer +buyer = Buyer.objects.create( + name="ACME Corp", + tin_number="1234567890", + email="accounts@acme.co.zw", +) + +# Pass buyer ID in receipt payload +receipt = submit_receipt({ + ... + "buyer": buyer.id, +}) +``` + +ZIMRA requires both `buyerRegisterName` and `buyerTIN` if buyer data is included (RCPT043). diff --git a/fiscguy/exceptions.py b/fiscguy/exceptions.py index f42d40a..10216c3 100644 --- a/fiscguy/exceptions.py +++ b/fiscguy/exceptions.py @@ -1,114 +1,118 @@ class FiscalisationError(Exception): - """Base exception for all fiscalisation service errors.""" + """Base exception for all fiscalisation-related errors.""" pass class CertNotFoundError(FiscalisationError): - """Raised when a certificate is not found.""" + """Raised when a required certificate cannot be found.""" pass class CryptoError(FiscalisationError): - """Raised when cryptographic operations fail.""" + """Raised when a cryptographic operation fails.""" pass class PersistenceError(FiscalisationError): - """Raised when database persistence operations fail.""" + """Raised when a database persistence operation fails.""" pass class RegistrationError(FiscalisationError): - """Raised when device registration fails.""" + """Raised when a general registration process fails.""" pass class DeviceNotFoundError(FiscalisationError): - """Raised when a device is not found.""" + """Raised when a requested device cannot be found.""" pass class TenantNotFoundError(FiscalisationError): - """Raised when a tenant is not found.""" + """Raised when a tenant cannot be found.""" pass class ZIMRAAPIError(FiscalisationError): - """Raised when ZIMRA API calls fail.""" + """Raised when a ZIMRA API request fails or returns an error.""" pass class ValidationError(FiscalisationError): - """Raised when data validation fails.""" + """Raised when input data fails validation checks.""" pass class AuthenticationError(FiscalisationError): - """Raised when authentication fails.""" + """Raised when authentication fails or credentials are invalid.""" pass class ConfigurationError(FiscalisationError): - """Raised when configuration is invalid or missing.""" + """Raised when required configuration is missing or invalid.""" pass class TaxError(FiscalisationError): - """Raised when tax crud operations fail.""" + """Raised when tax-related operations fail.""" pass class FiscalDayError(FiscalisationError): - """Raised when fiscal day opening fails""" + """Raised when opening a fiscal day fails.""" pass class ReceiptSubmissionError(FiscalisationError): - """Rasied when a receipt submission fails""" + """Raised when submission of a receipt fails.""" pass class DeviceRegistrationError(FiscalisationError): - """Raised when device registration fails""" + """Raised when device registration fails.""" pass class CertificateError(FiscalisationError): - """Raised when they is a cerificate error""" + """Raised when there is a certificate-related error.""" pass class StatusError(FiscalisationError): + """Raised when an invalid or unexpected status is encountered.""" + pass class DevicePingError(FiscalisationError): + """Raised when a device ping or connectivity check fails.""" + pass class ZIMRAClientError(FiscalisationError): + """Raised when the ZIMRA client encounters an internal error.""" + pass class CloseDayError(FiscalisationError): - pass + """Raised when closing a fiscal day fails.""" - -class CertificateError(FiscalisationError): pass