usawa

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

commit b49271fd252daca2424701499990e422886d8e13
parent 0abe35d8af9ee6a73fa49a2cbeba6f522fcf58bb
Author: Carlosokumu <carlosokumu254@gmail.com>
Date:   Thu, 19 Feb 2026 19:39:44 +0300

initialize core submodule

Diffstat:
Adummy/usawa/core/__init__.py | 0
Adummy/usawa/core/entry_service.py | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adummy/usawa/core/models.py | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 151 insertions(+), 0 deletions(-)

diff --git a/dummy/usawa/core/__init__.py b/dummy/usawa/core/__init__.py diff --git a/dummy/usawa/core/entry_service.py b/dummy/usawa/core/entry_service.py @@ -0,0 +1,60 @@ +import logging +from datetime import datetime +from typing import Optional +import uuid + +from usawa.service import UnixClient +from usawa.storage.entry_mapper import EntryMapper +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,unixClient: UnixClient): + self.repository = repository + self.unix_client = unixClient + + def save_entry(self, entry: LedgerEntry) -> bool: + """ + Save entry with business logic + + :param entry: Entry to save + :type entry: LedgerEntry + :return: True if saved successfully + :rtype: bool + """ + + entry.tx_date = datetime.now() + entry.date_registered = datetime.now() + entry.parent_digest = self._get_parent_digest() + entry.unit_index = self._get_unit_index() + entry.transaction_ref = self._generate_transaction_ref() + entry.serial = self.repository.get_next_serial() + + is_valid, error_msg = entry.validate() + if not is_valid: + logg.error(f"Entry validation failed: {error_msg}") + return False + + return self.repository.save(entry) + + + def _get_next_serial(self) -> int: + """Get next serial number""" + return self.repository.get_max_serial() + 1 + + def _get_parent_digest(self) -> str: + """Get digest of previous ledger state""" + return self.repository._get_parent_digest() + + def _get_unit_index(self) -> int: + """Get Unix timestamp for unit validation""" + return int(datetime.now().timestamp()) + + def _generate_transaction_ref(self) -> str: + """Generate UUID for transaction""" + return str(uuid.uuid4()) +\ No newline at end of file diff --git a/dummy/usawa/core/models.py b/dummy/usawa/core/models.py @@ -0,0 +1,90 @@ +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" + + # Attachmentments + attachments: List[str] = field(default_factory=list) + + # Metadata (auto-generated) + serial: Optional[int] = None + tx_date: Optional[datetime] = 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"amount={self.amount}, " + f"source_unit={self.source_unit}, " + 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)