File size: 5,872 Bytes
92c4ae6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
"""
Double-Entry Bookkeeping Validation

Ensures accounting invariants using exact Decimal arithmetic.
Per GAAP/IFRS: debits must equal credits exactly - no epsilon tolerance.
"""

from decimal import Decimal, InvalidOperation
from enum import Enum
from typing import List, Dict, Any, Optional


class EntryType(str, Enum):
    """Journal entry type"""
    DEBIT = "debit"
    CREDIT = "credit"


class DoubleEntryValidationError(Exception):
    """Raised when double-entry validation fails"""

    def __init__(self, message: str, debits: Decimal, credits: Decimal):
        super().__init__(message)
        self.debits = debits
        self.credits = credits
        self.difference = abs(debits - credits)


def validate_double_entry(entries: List[Dict[str, Any]]) -> Dict[str, Any]:
    """
    Validate that debits equal credits exactly (no epsilon tolerance).

    Args:
        entries: List of entry dicts with 'account_id', 'type' (DEBIT/CREDIT), 'amount'

    Returns:
        Dict with 'balanced' (bool), 'debits' (Decimal), 'credits' (Decimal)

    Raises:
        DoubleEntryValidationError: If debits != credits
        ValueError: If entries are invalid

    Examples:
        >>> validate_double_entry([
        ...     {"account_id": "acc_1", "type": EntryType.DEBIT, "amount": Decimal("100.00")},
        ...     {"account_id": "acc_2", "type": EntryType.CREDIT, "amount": Decimal("100.00")}
        ... ])
        {'balanced': True, 'debits': Decimal('100.00'), 'credits': Decimal('100.00')}
    """
    if not entries:
        raise DoubleEntryValidationError(
            "Transaction must have at least one entry",
            Decimal('0.00'),
            Decimal('0.00')
        )

    if len(entries) < 2:
        raise DoubleEntryValidationError(
            "Transaction must have at least two entries (double-entry)",
            Decimal('0.00'),
            Decimal('0.00')
        )

    # Sum debits and credits separately
    debits = Decimal('0.00')
    credits = Decimal('0.00')

    for entry in entries:
        # Validate entry structure
        if "amount" not in entry or "type" not in entry:
            raise ValueError(f"Invalid entry: {entry}")

        # Convert amount to Decimal
        try:
            amount = Decimal(str(entry["amount"]))
        except (InvalidOperation, ValueError):
            raise ValueError(f"Invalid amount: {entry['amount']}")

        # Reject negative amounts
        if amount < 0:
            raise DoubleEntryValidationError(
                f"Negative amounts not allowed: {amount}",
                Decimal('0.00'),
                Decimal('0.00')
            )

        # Round to 2 decimal places (cents)
        amount = amount.quantize(Decimal('0.00'))

        entry_type = entry["type"]
        if isinstance(entry_type, str):
            entry_type = EntryType(entry_type.lower())

        if entry_type == EntryType.DEBIT:
            debits += amount
        elif entry_type == EntryType.CREDIT:
            credits += amount
        else:
            raise ValueError(f"Invalid entry type: {entry_type}")

    # EXACT comparison - no epsilon tolerance per GAAP/IFRS
    if debits != credits:
        raise DoubleEntryValidationError(
            f"Debits ({debits}) do not equal credits ({credits}). "
            f"Difference: {abs(debits - credits)}",
            debits,
            credits
        )

    return {
        "balanced": True,
        "debits": debits,
        "credits": credits
    }


def check_balance_sheet(balance_sheet: Dict[str, Any]) -> Dict[str, Any]:
    """
    Validate balance sheet equation: Assets = Liabilities + Equity

    Args:
        balance_sheet: Dict with 'assets', 'liabilities', 'equity' lists

    Returns:
        Dict with 'balanced' (bool) and optional 'discrepancy'

    Examples:
        >>> check_balance_sheet({
        ...     "assets": [Decimal("100.00")],
        ...     "liabilities": [Decimal("50.00")],
        ...     "equity": [Decimal("50.00")]
        ... })
        {'balanced': True, 'discrepancy': None}
    """
    assets_list = balance_sheet.get("assets", [])
    liabilities_list = balance_sheet.get("liabilities", [])
    equity_list = balance_sheet.get("equity", [])

    # Sum each category
    def sum_amounts(amounts):
        return sum((Decimal(str(a)) for a in amounts), Decimal('0.00'))

    total_assets = sum_amounts(assets_list)
    total_liabilities = sum_amounts(liabilities_list)
    total_equity = sum_amounts(equity_list)

    expected_equity = total_assets - total_liabilities
    discrepancy = total_equity - expected_equity

    return {
        "balanced": discrepancy == 0,
        "discrepancy": abs(discrepancy) if discrepancy != 0 else None,
        "assets": total_assets,
        "liabilities": total_liabilities,
        "equity": total_equity
    }


def validate_journal_entries(entries: List[Dict[str, Any]]) -> List[str]:
    """
    Validate journal entries and return list of errors (empty if valid).

    Args:
        entries: List of journal entry dicts

    Returns:
        List of error messages (empty if all valid)
    """
    errors = []

    for i, entry in enumerate(entries):
        # Check required fields
        if "account_id" not in entry:
            errors.append(f"Entry {i}: missing account_id")
        if "type" not in entry:
            errors.append(f"Entry {i}: missing type")
        if "amount" not in entry:
            errors.append(f"Entry {i}: missing amount")

        # Validate amount
        if "amount" in entry:
            try:
                amount = Decimal(str(entry["amount"]))
                if amount < 0:
                    errors.append(f"Entry {i}: negative amount {amount}")
            except (InvalidOperation, ValueError):
                errors.append(f"Entry {i}: invalid amount {entry['amount']}")

    return errors