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:
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)