fiscguy 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fiscguy/__init__.py +35 -0
- fiscguy/admin.py +19 -0
- fiscguy/api.py +235 -0
- fiscguy/apps.py +5 -0
- fiscguy/management/__init__.py +0 -0
- fiscguy/management/commands/__init__.py +0 -0
- fiscguy/management/commands/init_device.py +354 -0
- fiscguy/migrations/0001_initial.py +269 -0
- fiscguy/migrations/__init__.py +0 -0
- fiscguy/models.py +236 -0
- fiscguy/serializers.py +176 -0
- fiscguy/services/closing_day_service.py +282 -0
- fiscguy/services/configuration_service.py +60 -0
- fiscguy/services/receipt_service.py +74 -0
- fiscguy/tests/__init__.py +3 -0
- fiscguy/tests/__initt__.py +0 -0
- fiscguy/tests/test_api.py +547 -0
- fiscguy/tests.py +3 -0
- fiscguy/urls.py +21 -0
- fiscguy/utils/cert_temp_manager.py +38 -0
- fiscguy/utils/datetime_now.py +15 -0
- fiscguy/views.py +139 -0
- fiscguy/zimra_base.py +182 -0
- fiscguy/zimra_crypto.py +313 -0
- fiscguy/zimra_receipt_handler.py +500 -0
- fiscguy-0.1.2.dist-info/METADATA +407 -0
- fiscguy-0.1.2.dist-info/RECORD +30 -0
- fiscguy-0.1.2.dist-info/WHEEL +5 -0
- fiscguy-0.1.2.dist-info/licenses/LICENSE +21 -0
- fiscguy-0.1.2.dist-info/top_level.txt +1 -0
fiscguy/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ZIMRA Fiscal Device Library
|
|
3
|
+
|
|
4
|
+
Public API for fiscal device operations. Import functions directly:
|
|
5
|
+
|
|
6
|
+
from fiscguy import open_day, close_day, submit_receipt, get_status, get_taxes, get_configuration
|
|
7
|
+
|
|
8
|
+
Each function handles initialization and error handling transparently.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
default_app_config = "fiscguy.apps.FiscguyConfig"
|
|
12
|
+
|
|
13
|
+
def __getattr__(name):
|
|
14
|
+
"""Lazy-load API functions on first access."""
|
|
15
|
+
if name in (
|
|
16
|
+
"open_day",
|
|
17
|
+
"close_day",
|
|
18
|
+
"get_status",
|
|
19
|
+
"submit_receipt",
|
|
20
|
+
"get_configuration",
|
|
21
|
+
"get_taxes"
|
|
22
|
+
):
|
|
23
|
+
from fiscguy import api
|
|
24
|
+
return getattr(api, name)
|
|
25
|
+
raise AttributeError(f"module 'fiscguy' has no attribute '{name}'")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"open_day",
|
|
30
|
+
"close_day",
|
|
31
|
+
"get_status",
|
|
32
|
+
"submit_receipt",
|
|
33
|
+
"get_configuration",
|
|
34
|
+
"get_taxes",
|
|
35
|
+
]
|
fiscguy/admin.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
|
|
3
|
+
from fiscguy.models import (
|
|
4
|
+
Certs,
|
|
5
|
+
Configuration,
|
|
6
|
+
Device,
|
|
7
|
+
FiscalCounter,
|
|
8
|
+
FiscalDay,
|
|
9
|
+
Receipt,
|
|
10
|
+
Taxes,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
admin.site.register(Certs)
|
|
14
|
+
admin.site.register(Configuration)
|
|
15
|
+
admin.site.register(Taxes)
|
|
16
|
+
admin.site.register(Device)
|
|
17
|
+
admin.site.register(FiscalDay)
|
|
18
|
+
admin.site.register(Receipt)
|
|
19
|
+
admin.site.register(FiscalCounter)
|
fiscguy/api.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ZIMRA Fiscal Device Public API.
|
|
3
|
+
|
|
4
|
+
Provides high-level functions for fiscal operations:
|
|
5
|
+
- open_day: Open a new fiscal day
|
|
6
|
+
- close_day: Close the open fiscal day
|
|
7
|
+
- get_status: Get device and fiscal status
|
|
8
|
+
- submit_receipt: Create and submit a receipt to ZIMRA
|
|
9
|
+
- get_configuration: Fetch device configuration
|
|
10
|
+
- get_taxes: Fetch available tax types
|
|
11
|
+
|
|
12
|
+
This module encapsulates business logic previously in views,
|
|
13
|
+
providing a clean library interface for both API and programmatic use.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Dict, Any
|
|
17
|
+
from loguru import logger
|
|
18
|
+
|
|
19
|
+
from fiscguy.models import Device, FiscalDay, Taxes
|
|
20
|
+
from fiscguy.services.closing_day_service import ClosingDayService
|
|
21
|
+
from fiscguy.services.receipt_service import ReceiptService
|
|
22
|
+
from fiscguy.zimra_base import ZIMRAClient
|
|
23
|
+
from fiscguy.zimra_receipt_handler import ZIMRAReceiptHandler
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Module-level instances
|
|
27
|
+
_device = None
|
|
28
|
+
_client = None
|
|
29
|
+
_receipt_handler = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_device() -> Device:
|
|
33
|
+
"""Get or cache the first device. Raises if none exists."""
|
|
34
|
+
global _device
|
|
35
|
+
if _device is None:
|
|
36
|
+
_device = Device.objects.first()
|
|
37
|
+
if not _device:
|
|
38
|
+
raise RuntimeError("No Device found. Please run init_device management command.")
|
|
39
|
+
return _device
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_client() -> ZIMRAClient:
|
|
43
|
+
"""Get or cache the ZIMRA client. Lazy initialization."""
|
|
44
|
+
global _client
|
|
45
|
+
if _client is None:
|
|
46
|
+
device = _get_device()
|
|
47
|
+
logger.info(f"Initializing ZIMRA client for device {device}")
|
|
48
|
+
_client = ZIMRAClient(device)
|
|
49
|
+
return _client
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _get_receipt_handler() -> ZIMRAReceiptHandler:
|
|
53
|
+
"""Get or cache the receipt handler. Lazy initialization."""
|
|
54
|
+
global _receipt_handler
|
|
55
|
+
if _receipt_handler is None:
|
|
56
|
+
_receipt_handler = ZIMRAReceiptHandler()
|
|
57
|
+
return _receipt_handler
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_status() -> Dict[str, Any]:
|
|
61
|
+
"""
|
|
62
|
+
Get the current device and fiscal day status.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
dict: Status response from ZIMRA FDMS.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
RuntimeError: If device not found or FDMS request fails.
|
|
69
|
+
"""
|
|
70
|
+
logger.info("Fetching device status")
|
|
71
|
+
client = _get_client()
|
|
72
|
+
return client.get_status()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def open_day() -> Dict[str, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Open a new fiscal day.
|
|
78
|
+
|
|
79
|
+
Creates a FiscalDay record and calls ZIMRA to open the day.
|
|
80
|
+
Returns early if a day is already open.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
dict: Response from ZIMRA FDMS or local message if already open.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
RuntimeError: If device not found or FDMS request fails.
|
|
87
|
+
"""
|
|
88
|
+
logger.info("Opening fiscal day")
|
|
89
|
+
client = _get_client()
|
|
90
|
+
return client.open_day()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def close_day() -> Dict[str, Any]:
|
|
94
|
+
"""
|
|
95
|
+
Close the open fiscal day.
|
|
96
|
+
|
|
97
|
+
Collects fiscal counters, builds closing string, signs it, and submits
|
|
98
|
+
to ZIMRA. Updates the fiscal day status and returns the status payload.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
dict: Final device/fiscal status from ZIMRA FDMS.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
RuntimeError: If no open fiscal day or device not found.
|
|
105
|
+
Exception: If FDMS request fails.
|
|
106
|
+
"""
|
|
107
|
+
logger.info("Closing fiscal day")
|
|
108
|
+
device = _get_device()
|
|
109
|
+
receipt_handler = _get_receipt_handler()
|
|
110
|
+
client = _get_client()
|
|
111
|
+
|
|
112
|
+
# Get open fiscal day
|
|
113
|
+
fiscal_day = FiscalDay.objects.filter(is_open=True).first()
|
|
114
|
+
if not fiscal_day:
|
|
115
|
+
return {"error": "No open fiscal day to close"}
|
|
116
|
+
|
|
117
|
+
# Collect counters and build closing payload
|
|
118
|
+
fiscal_counters = fiscal_day.counters.all()
|
|
119
|
+
tax_map = {t.tax_id: t.name for t in Taxes.objects.all()}
|
|
120
|
+
|
|
121
|
+
logger.info(f"Fiscal counters for day {fiscal_day.day_no}: {list(fiscal_counters)}")
|
|
122
|
+
|
|
123
|
+
service = ClosingDayService(
|
|
124
|
+
device=device,
|
|
125
|
+
fiscal_day=fiscal_day,
|
|
126
|
+
fiscal_counters=fiscal_counters,
|
|
127
|
+
tax_map=tax_map,
|
|
128
|
+
receipt_handler=receipt_handler,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
closing_string, payload = service.close_day()
|
|
132
|
+
|
|
133
|
+
logger.info(f"Closing fiscal day string: {closing_string}")
|
|
134
|
+
logger.info(f"Closing payload: {payload}")
|
|
135
|
+
|
|
136
|
+
# Submit to ZIMRA and fetch final status
|
|
137
|
+
client.close_day(payload)
|
|
138
|
+
status_payload = client.get_status()
|
|
139
|
+
|
|
140
|
+
return status_payload
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def submit_receipt(receipt_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
144
|
+
"""
|
|
145
|
+
Create and submit a receipt to ZIMRA.
|
|
146
|
+
|
|
147
|
+
Parses the receipt payload, creates Receipt and ReceiptLine records,
|
|
148
|
+
generates receipt data (signature/hash), and submits to ZIMRA.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
receipt_data (dict): Receipt payload with structure:
|
|
152
|
+
{
|
|
153
|
+
"receipt_type": str,
|
|
154
|
+
"currency": str,
|
|
155
|
+
"total_amount": float,
|
|
156
|
+
"lines": [
|
|
157
|
+
{
|
|
158
|
+
"product": str,
|
|
159
|
+
"quantity": float,
|
|
160
|
+
"unit_price": float,
|
|
161
|
+
"line_total": float,
|
|
162
|
+
"tax_amount": float, # optional, can be calculated from tax percent if tax_name provided
|
|
163
|
+
"tax_name": str,
|
|
164
|
+
},
|
|
165
|
+
...
|
|
166
|
+
],
|
|
167
|
+
...
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
dict: Serialized receipt with all fields including ID and lines.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ValidationError: If receipt_data is invalid (missing required fields, invalid tax).
|
|
175
|
+
Exception: If FDMS submission fails.
|
|
176
|
+
"""
|
|
177
|
+
logger.info(f"Submitting receipt: {receipt_data}")
|
|
178
|
+
receipt_handler = _get_receipt_handler()
|
|
179
|
+
|
|
180
|
+
service = ReceiptService(receipt_handler=receipt_handler)
|
|
181
|
+
receipt, submission_res = service.create_and_submit_receipt(receipt_data)
|
|
182
|
+
|
|
183
|
+
logger.info(f"Receipt submitted to ZIMRA: {submission_res}")
|
|
184
|
+
return receipt
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_configuration() -> Dict[str, Any]:
|
|
188
|
+
"""
|
|
189
|
+
Get the stored device configuration.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
dict: Configuration fields (tax_payer_name, tin_number, vat_number, etc.)
|
|
193
|
+
or empty dict if no configuration exists.
|
|
194
|
+
"""
|
|
195
|
+
from fiscguy.models import Configuration
|
|
196
|
+
from fiscguy.serializers import ConfigurationSerializer
|
|
197
|
+
|
|
198
|
+
logger.info("Fetching device configuration")
|
|
199
|
+
config = Configuration.objects.first()
|
|
200
|
+
if not config:
|
|
201
|
+
logger.warning("No configuration found")
|
|
202
|
+
return {}
|
|
203
|
+
return ConfigurationSerializer(config).data
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_taxes() -> list:
|
|
207
|
+
"""
|
|
208
|
+
Get all available tax types.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
list: Array of tax objects with fields:
|
|
212
|
+
{
|
|
213
|
+
"id": int,
|
|
214
|
+
"code": str,
|
|
215
|
+
"name": str,
|
|
216
|
+
"tax_id": int,
|
|
217
|
+
"percent": float
|
|
218
|
+
}
|
|
219
|
+
"""
|
|
220
|
+
from fiscguy.serializers import TaxSerializer
|
|
221
|
+
|
|
222
|
+
logger.info("Fetching taxes")
|
|
223
|
+
taxes = Taxes.objects.all()
|
|
224
|
+
return TaxSerializer(taxes, many=True).data
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# module-level shortcuts
|
|
228
|
+
__all__ = [
|
|
229
|
+
"open_day",
|
|
230
|
+
"close_day",
|
|
231
|
+
"get_status",
|
|
232
|
+
"submit_receipt",
|
|
233
|
+
"get_configuration",
|
|
234
|
+
"get_taxes",
|
|
235
|
+
]
|
fiscguy/apps.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shutil
|
|
3
|
+
import tempfile
|
|
4
|
+
import threading
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from django.core.management.base import BaseCommand
|
|
9
|
+
from django.db import transaction
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from fiscguy.models import Certs, Configuration, Device, Taxes
|
|
13
|
+
from fiscguy.zimra_crypto import ZIMRACrypto
|
|
14
|
+
from fiscguy.services.configuration_service import create_or_update_config
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
Management command to register a ZIMRA fiscal device and fetch its
|
|
18
|
+
configuration from the ZIMRA FDMS API.
|
|
19
|
+
|
|
20
|
+
Primary responsibilities:
|
|
21
|
+
- Interactively collect device registration details from the user.
|
|
22
|
+
- Generate CSR and register the device to obtain a signed certificate.
|
|
23
|
+
- Fetch device configuration using the signed certificate and persist
|
|
24
|
+
the taxpayer configuration and applicable taxes into local models.
|
|
25
|
+
|
|
26
|
+
This command intentionally writes/updates a single `Configuration` row
|
|
27
|
+
and replaces the `Taxes` table contents with the `applicableTaxes`
|
|
28
|
+
returned from ZIMRA. Database writes are wrapped in a transaction to
|
|
29
|
+
avoid partial updates.
|
|
30
|
+
|
|
31
|
+
Note: network calls to ZIMRA use client certificates stored in the
|
|
32
|
+
`Certs` model. Temporary files are created for the requests library
|
|
33
|
+
and removed afterwards.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
crypto = ZIMRACrypto()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Command(BaseCommand):
|
|
41
|
+
"""Django management command for device registration.
|
|
42
|
+
|
|
43
|
+
Usage (interactive): run `python manage.py init_device` and follow
|
|
44
|
+
the prompts. The command will:
|
|
45
|
+
- create/update a `Device` record
|
|
46
|
+
- generate a CSR and register the device to obtain a signed cert
|
|
47
|
+
- fetch and persist configuration and taxes from ZIMRA's FDMS
|
|
48
|
+
|
|
49
|
+
The class keeps operations small and logs failures rather than
|
|
50
|
+
raising unhandled exceptions so it can be used in ad-hoc admin
|
|
51
|
+
workflows.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
help = "Interactive registration of a new ZIMRA device"
|
|
55
|
+
|
|
56
|
+
def handle(self, *args, **options):
|
|
57
|
+
print("\n" + "*" * 75)
|
|
58
|
+
print("*" + " " * 73 + "*")
|
|
59
|
+
print("* **** ** **** **** **** ** ** ** ** ")
|
|
60
|
+
print("* ** ** ** ** ** ** ** ** ** ")
|
|
61
|
+
print("* **** ** **** ** ** ** ** ** ***** ")
|
|
62
|
+
print("* ** ** ** ** ** ** ** ** ** ")
|
|
63
|
+
print("* ** ** **** **** **** **** ** ")
|
|
64
|
+
print("*" + " " * 73 + "*")
|
|
65
|
+
print("*" * 75)
|
|
66
|
+
print("\nDeveloped by Casper Moyo")
|
|
67
|
+
print("Version 1.0.0\n")
|
|
68
|
+
print(
|
|
69
|
+
"Welcome to device registration please input the following provided information as proveded by ZIMRA\n"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
environment = input(
|
|
73
|
+
"Enter yes for production environment and no for test enviroment: "
|
|
74
|
+
).strip()
|
|
75
|
+
org = input("Enter your organisation name: ").strip()
|
|
76
|
+
device_id = input("Enter your device ID: ").strip()
|
|
77
|
+
activation_key = input("Enter activation Key: ").strip()
|
|
78
|
+
model_version = input("Enter device model version eg v1: ").strip()
|
|
79
|
+
model_name = input("Enter device model name eg Server: ").strip()
|
|
80
|
+
device_sn = input("Enter device serial number: ").strip()
|
|
81
|
+
|
|
82
|
+
if not environment.lower() in ["yes", "no"]:
|
|
83
|
+
self.stdout.write(
|
|
84
|
+
self.style.ERROR("Please input environment between yes or no")
|
|
85
|
+
)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
not device_id
|
|
90
|
+
or not model_version
|
|
91
|
+
or not model_name
|
|
92
|
+
or not environment
|
|
93
|
+
or not device_sn
|
|
94
|
+
or not org
|
|
95
|
+
or not activation_key
|
|
96
|
+
):
|
|
97
|
+
self.stdout.write(self.style.ERROR("All fields are required."))
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
device = Device.objects.first()
|
|
101
|
+
env = True if environment.lower() == "yes" else False
|
|
102
|
+
|
|
103
|
+
if not device:
|
|
104
|
+
device = Device.objects.create(
|
|
105
|
+
org_name=org,
|
|
106
|
+
activation_key=activation_key,
|
|
107
|
+
device_id=device_id,
|
|
108
|
+
device_model_name=model_name,
|
|
109
|
+
device_serial_number=device_sn,
|
|
110
|
+
device_model_version=model_version,
|
|
111
|
+
production=env,
|
|
112
|
+
)
|
|
113
|
+
print(
|
|
114
|
+
f"Device {device.device_id} created for {'production' if env else 'test'} environment."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
else:
|
|
118
|
+
if device.production != env:
|
|
119
|
+
print("\n" + "!" * 75)
|
|
120
|
+
print("!" + " " * 73 + "!")
|
|
121
|
+
print("! WARNING: ENVIRONMENT SWITCH DETECTED" + " " * 33 + "!")
|
|
122
|
+
print("!" + " " * 73 + "!")
|
|
123
|
+
print("!" + " " * 73 + "!")
|
|
124
|
+
print("! Switching from", "PRODUCTION" if device.production else "TEST", "to", "PRODUCTION" if env else "TEST" + " " * (73 - 54) + "!")
|
|
125
|
+
print("!" + " " * 73 + "!")
|
|
126
|
+
print("! ALL TEST DATA WILL BE PERMANENTLY DELETED:" + " " * 28 + "!")
|
|
127
|
+
print("! - Fiscal Days" + " " * 57 + "!")
|
|
128
|
+
print("! - Fiscal Counters" + " " * 52 + "!")
|
|
129
|
+
print("! - Receipts & Receipt Lines" + " " * 40 + "!")
|
|
130
|
+
print("! - Device Configuration" + " " * 47 + "!")
|
|
131
|
+
print("! - Certificates" + " " * 55 + "!")
|
|
132
|
+
print("! - Device Record" + " " * 54 + "!")
|
|
133
|
+
print("!" + " " * 73 + "!")
|
|
134
|
+
print("!" * 75)
|
|
135
|
+
|
|
136
|
+
confirm = input(
|
|
137
|
+
"\nType 'YES' to confirm data deletion and switch environment, or press Enter to cancel: "
|
|
138
|
+
).strip()
|
|
139
|
+
|
|
140
|
+
if confirm.upper() != "YES":
|
|
141
|
+
print("Environment switch cancelled. No data was deleted.")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
print("\nDeleting all test data...")
|
|
145
|
+
self.delete_all_test_data()
|
|
146
|
+
print("✓ All test data has been deleted.\n")
|
|
147
|
+
|
|
148
|
+
device.org_name = org
|
|
149
|
+
device.activation_key = activation_key
|
|
150
|
+
device.device_id = device_id
|
|
151
|
+
device.device_model_name = model_name
|
|
152
|
+
device.device_serial_number = device_sn
|
|
153
|
+
device.device_model_version = model_version
|
|
154
|
+
device.production = env
|
|
155
|
+
device.save()
|
|
156
|
+
print(
|
|
157
|
+
f"Device {device.device_id} updated to {'production' if env else 'test'} environment."
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
device.org_name = org
|
|
161
|
+
device.activation_key = activation_key
|
|
162
|
+
device.device_id = device_id
|
|
163
|
+
device.save()
|
|
164
|
+
print(f"Device {device.device_id} updated for current environment.")
|
|
165
|
+
|
|
166
|
+
cert_key, csr = crypto.generate_key_and_csr(device_sn, device_id, env)
|
|
167
|
+
|
|
168
|
+
# register the device and get signed certificate from ZIMRA
|
|
169
|
+
self.register_device(
|
|
170
|
+
device_id, activation_key, model_name, model_version, env, csr, device_sn
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# get zimra configurations for the provided device
|
|
174
|
+
zimra_config = self.get_config(
|
|
175
|
+
device_id, model_name, model_version, device.production
|
|
176
|
+
)
|
|
177
|
+
print(zimra_config)
|
|
178
|
+
|
|
179
|
+
def delete_all_test_data(self) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Delete all test data when switching environments.
|
|
182
|
+
|
|
183
|
+
Deletes in order of dependencies:
|
|
184
|
+
- Fiscal Days
|
|
185
|
+
- Fiscal Counters
|
|
186
|
+
- Receipts & Receipt Lines
|
|
187
|
+
- Configuration
|
|
188
|
+
- Certificates
|
|
189
|
+
- Device
|
|
190
|
+
- Taxes
|
|
191
|
+
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
from fiscguy.models import (
|
|
195
|
+
FiscalDay,
|
|
196
|
+
FiscalCounter,
|
|
197
|
+
Receipt,
|
|
198
|
+
ReceiptLine,
|
|
199
|
+
Configuration,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
with transaction.atomic():
|
|
203
|
+
# Delete in order of dependencies (child tables first)
|
|
204
|
+
logger.info("Deleting receipt lines...")
|
|
205
|
+
count = ReceiptLine.objects.all().delete()[0]
|
|
206
|
+
self.stdout.write(self.style.SUCCESS(f" Deleted {count} receipt lines"))
|
|
207
|
+
|
|
208
|
+
logger.info("Deleting receipts...")
|
|
209
|
+
count = Receipt.objects.all().delete()[0]
|
|
210
|
+
self.stdout.write(self.style.SUCCESS(f" Deleted {count} receipts"))
|
|
211
|
+
|
|
212
|
+
logger.info("Deleting fiscal counters...")
|
|
213
|
+
count = FiscalCounter.objects.all().delete()[0]
|
|
214
|
+
self.stdout.write(self.style.SUCCESS(f" Deleted {count} fiscal counters"))
|
|
215
|
+
|
|
216
|
+
logger.info("Deleting fiscal days...")
|
|
217
|
+
count = FiscalDay.objects.all().delete()[0]
|
|
218
|
+
self.stdout.write(self.style.SUCCESS(f" Deleted {count} fiscal days"))
|
|
219
|
+
|
|
220
|
+
logger.info("Deleting configuration...")
|
|
221
|
+
count = Configuration.objects.all().delete()[0]
|
|
222
|
+
self.stdout.write(self.style.SUCCESS(f" Deleted {count} configuration records"))
|
|
223
|
+
|
|
224
|
+
logger.info("Deleting certificates...")
|
|
225
|
+
count = Certs.objects.all().delete()[0]
|
|
226
|
+
self.stdout.write(self.style.SUCCESS(f" Deleted {count} certificates"))
|
|
227
|
+
|
|
228
|
+
logger.info("Deleting taxes...")
|
|
229
|
+
count = Taxes.objects.all().delete()[0]
|
|
230
|
+
self.stdout.write(self.style.SUCCESS(f" Deleted {count} tax records"))
|
|
231
|
+
|
|
232
|
+
logger.info("Deleting device...")
|
|
233
|
+
count = Device.objects.all().delete()[0]
|
|
234
|
+
self.stdout.write(self.style.SUCCESS(f" Deleted {count} devices"))
|
|
235
|
+
|
|
236
|
+
logger.info("All test data successfully deleted")
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.exception(f"Error deleting test data: {e}")
|
|
239
|
+
self.stdout.write(self.style.ERROR(f"ERROR: Failed to delete test data: {e}"))
|
|
240
|
+
raise
|
|
241
|
+
|
|
242
|
+
def get_config(
|
|
243
|
+
self, device_id: str, model_name: str, model_version: str, env: bool
|
|
244
|
+
) -> dict:
|
|
245
|
+
"""Fetch device configuration from ZIMRA and persist locally.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
device_id (str): Device identifier supplied by ZIMRA.
|
|
249
|
+
model_name (str): Device model name header required by FDMS.
|
|
250
|
+
model_version (str): Device model version header.
|
|
251
|
+
env (bool): True for production FDMS endpoint, False for test.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
dict | None: Parsed JSON response from FDMS on success, or
|
|
255
|
+
None when the HTTP request fails.
|
|
256
|
+
|
|
257
|
+
Side effects:
|
|
258
|
+
- Creates/updates a single `Configuration` row.
|
|
259
|
+
- Replaces all rows in `Taxes` with the `applicableTaxes`
|
|
260
|
+
returned by the FDMS response.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
logger.info(f"Fetching device: {device_id} configurations")
|
|
264
|
+
|
|
265
|
+
url = (
|
|
266
|
+
f"https://fdmsapi.zimra.co.zw/Device/v1/{device_id}"
|
|
267
|
+
if env
|
|
268
|
+
else f"https://fdmsapitest.zimra.co.zw/Device/v1/{device_id}"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
headers = {
|
|
272
|
+
"Content-Type": "application/json",
|
|
273
|
+
"deviceModelName": model_name,
|
|
274
|
+
"deviceModelVersion": model_version,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
temp_dir = Path(tempfile.mkdtemp(prefix="zimra_fdms_"))
|
|
278
|
+
cert_path = temp_dir / "client_cert.pem"
|
|
279
|
+
key_path = temp_dir / "client_key.pem"
|
|
280
|
+
|
|
281
|
+
from fiscguy.models import Certs
|
|
282
|
+
|
|
283
|
+
cert = Certs.objects.first()
|
|
284
|
+
cert_path.write_text(cert.certificate)
|
|
285
|
+
key_path.write_text(cert.certificate_key)
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
response = requests.get(
|
|
289
|
+
f"{url}/getConfig",
|
|
290
|
+
headers=headers,
|
|
291
|
+
cert=(str(cert_path), str(key_path)),
|
|
292
|
+
timeout=30,
|
|
293
|
+
)
|
|
294
|
+
response.raise_for_status()
|
|
295
|
+
res = response.json()
|
|
296
|
+
|
|
297
|
+
create_or_update_config(res)
|
|
298
|
+
logger.info(f"Configuration for device {device_id} updated successfully.")
|
|
299
|
+
return res
|
|
300
|
+
except requests.RequestException as e:
|
|
301
|
+
logger.error(f"Error fetching config: {e}")
|
|
302
|
+
return None
|
|
303
|
+
finally:
|
|
304
|
+
cert_path.unlink(missing_ok=True)
|
|
305
|
+
key_path.unlink(missing_ok=True)
|
|
306
|
+
temp_dir.rmdir()
|
|
307
|
+
|
|
308
|
+
def register_device(
|
|
309
|
+
self, device_id, activation_key, model_name, model_version, env, csr, device_sn
|
|
310
|
+
):
|
|
311
|
+
logger.info(f"Registering device: {device_id}")
|
|
312
|
+
url = (
|
|
313
|
+
f"https://fdmsapi.zimra.co.zw/Public/v1/{device_id}"
|
|
314
|
+
if env
|
|
315
|
+
else f"https://fdmsapitest.zimra.co.zw/Public/v1/{device_id}"
|
|
316
|
+
)
|
|
317
|
+
print(csr)
|
|
318
|
+
csr = csr.replace("\n", "")
|
|
319
|
+
print(csr)
|
|
320
|
+
|
|
321
|
+
payload = {
|
|
322
|
+
"activationKey": activation_key,
|
|
323
|
+
"deviceSerial": device_sn,
|
|
324
|
+
"certificateRequest": csr,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
headers = {
|
|
328
|
+
"Content-Type": "application/json",
|
|
329
|
+
"deviceModelName": model_name,
|
|
330
|
+
"deviceModelVersion": model_version,
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
response = requests.post(
|
|
335
|
+
f"{url}/RegisterDevice", json=payload, headers=headers
|
|
336
|
+
)
|
|
337
|
+
response.raise_for_status()
|
|
338
|
+
|
|
339
|
+
logger.info(response.json())
|
|
340
|
+
|
|
341
|
+
signed_certificate = response.json().get("certificate")
|
|
342
|
+
|
|
343
|
+
if signed_certificate:
|
|
344
|
+
from fiscguy.models import Certs
|
|
345
|
+
|
|
346
|
+
cert = Certs.objects.first()
|
|
347
|
+
cert.certificate = signed_certificate
|
|
348
|
+
cert.save()
|
|
349
|
+
|
|
350
|
+
logger.info(f"Device {device_id} registered successfully.")
|
|
351
|
+
return signed_certificate
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.error(f"Device registration failed: {e}")
|
|
354
|
+
return
|