usawa

Signed, immutable accounting.
Log | Files | Refs | Submodules | LICENSE

commit abb4c99154580960c74d64b9466c4595bc09fa7b
parent e100f8f13782dc29e65390ee56c7c550597d724b
Author: Carlosokumu <carlosokumu254@gmail.com>
Date:   Tue, 24 Mar 2026 20:42:53 +0300

chore: move all core classes into gui

Diffstat:
Adummy/usawa/gui/core/__init__.py | 0
Adummy/usawa/gui/core/entry_service.py | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adummy/usawa/gui/core/models.py | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adummy/usawa/gui/core/state_manager.py | 28++++++++++++++++++++++++++++
4 files changed, 193 insertions(+), 0 deletions(-)

diff --git a/dummy/usawa/gui/core/__init__.py b/dummy/usawa/gui/core/__init__.py diff --git a/dummy/usawa/gui/core/entry_service.py b/dummy/usawa/gui/core/entry_service.py @@ -0,0 +1,70 @@ +import logging +from datetime import datetime +import uuid + +from .models import LedgerEntry +from usawa.storage.ledger_repository import LedgerRepository + +logg = logging.getLogger("core.entry_service") + + +class EntryService: + """Business logic for ledger entries""" + + def __init__(self, repository: LedgerRepository): + self.repository = repository + + def save_entry(self, entry: LedgerEntry) -> tuple[bool, str]: + try: + entry.tx_date = datetime.now() + entry.date_registered = datetime.now() + entry.transaction_ref = self._generate_transaction_ref() + + is_valid, error_msg = entry.validate() + if not is_valid: + logg.error(f"Entry validation failed: {error_msg}") + return False, error_msg + + self.repository.save(entry) + + logg.info(f"Entry saved successfully") + return True, "" + + except FileExistsError as e: + error_msg = ( + "Some file information for this entry is already recorded in the ledger" + ) + return False, error_msg + + except ValueError as e: + error_msg = f"Invalid entry data: {str(e)}" + logg.error(f"Validation error: {e}") + return False, error_msg + + except IOError as e: + error_msg = f"File error: {str(e)}" + logg.error(f"File operation failed: {e}") + return False, error_msg + + except Exception as e: + error_msg = f"Failed to save entry: {str(e)}" + logg.error(f"Unexpected error: {e}", exc_info=True) + return False, error_msg + + def save_wallet(self, wallet, passphrase): + return self.repository.save_wallet(wallet=wallet, passphrase=passphrase) + + def get_all_entries(self): + return self.repository.get_all_entries() + + def _generate_transaction_ref(self) -> str: + return str(uuid.uuid4()) + + def get_asset_bytes(self, digest: bytes) -> bytes: + return self.repository.get_asset_bytes(digest=digest) + + def export_all_entries_to_xml(self, output_path: str) -> tuple[bool, str]: + return self.repository.export_all_entries_to_xml(output_path=output_path) + + def export_entry_to_xml(self, serial: int, output_path: str) -> tuple[bool, str]: + return self.repository.export_entry_to_xml(serial, output_path) diff --git a/dummy/usawa/gui/core/models.py b/dummy/usawa/gui/core/models.py @@ -0,0 +1,95 @@ +from dataclasses import dataclass,field +from typing import Optional,List, Union +from datetime import datetime +from pathlib import Path + +@dataclass +class LedgerEntry: + """Ledger entry data model""" + + # Basic details + external_reference: Optional[str] = None + description: Optional[str] = None + + # Transaction details + amount: float = 0.0 + source_unit: str = "" + source_type: str = "" + source_path: str = "general" + dest_unit: str = "" + dest_type: str = "" + dest_path: str = "general" + + # Attachments + attachments: List[str] = field(default_factory=list) + + # Signers (public keys) + signer_pubkeys: List[str] = field(default_factory=list) + + serial: Optional[int] = None + tx_date: Optional[datetime] = None + tx_reference: Optional[str] = None + date_registered: Optional[datetime] = None + parent_digest: Optional[str] = None + unit_index: Optional[int] = None + + def validate(self) -> tuple[bool, str]: + """Validate entry data""" + if self.amount <= 0: + return False, "Amount must be greater than 0" + + if not self.source_unit or not self.dest_unit: + return False, "Unit/Currency is required for both source and destination" + + if not self.source_type or not self.dest_type: + return False, "Account type is required for both source and destination" + + if not self.source_path or not self.dest_path: + return False, "Account path is required for both source and destination" + + # Validate attachments + if self.attachments: + for filepath in self.attachments: + if not Path(filepath).exists(): + return False, f"Attachment file not found: {filepath}" + + return True, "" + + def __repr__(self): + return ( + f"LedgerEntry(" + f"external_reference={self.external_reference!r}, " + f"description={self.description!r}, " + f"serial={self.serial!r}, " + f"parent_digest={self.parent_digest}, " + f"amount={self.amount}, " + f"source_unit={self.source_unit}, " + f"tx_ref={self.tx_reference}, " + f"source_type={self.source_type}, " + f"dest_unit={self.dest_unit}, " + f"dest_type={self.dest_type})" + f"attachments={self.attachments!r})" + ) + + def add_attachment(self, filepath: Union[str, List[str]]): + """ + Add one or more attachment file paths + """ + if isinstance(filepath, str): + if filepath not in self.attachments: + self.attachments.append(filepath) + elif isinstance(filepath, list): + for path in filepath: + if path not in self.attachments: + self.attachments.append(path) + else: + raise TypeError(f"filepath must be str or List[str], got {type(filepath)}") + + def remove_attachment(self, filepath: str): + """Remove an attachment file path""" + if filepath in self.attachments: + self.attachments.remove(filepath) + + def get_attachment_count(self) -> int: + """Get number of attachments""" + return len(self.attachments) diff --git a/dummy/usawa/gui/core/state_manager.py b/dummy/usawa/gui/core/state_manager.py @@ -0,0 +1,28 @@ +import json +from pathlib import Path + +STATE_FILE = Path.home() / ".local" / "share" / "usawa" / "state.json" + + +class StateManager: + + @staticmethod + def get_state() -> dict: + if STATE_FILE.exists(): + return json.loads(STATE_FILE.read_text()) + return {} + + @staticmethod + def save_state(data: dict): + STATE_FILE.parent.mkdir(parents=True, exist_ok=True) + STATE_FILE.write_text(json.dumps(data, indent=2)) + + @staticmethod + def get(key: str, default=None): + return StateManager.get_state().get(key, default) + + @staticmethod + def set(key: str, value): + state = StateManager.get_state() + state[key] = value + StateManager.save_state(state)