File size: 4,421 Bytes
c0cb280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from datetime import datetime
import logging
from typing import Any, Dict, List, Optional
from accounting.models import (
    Bill,
    BillStatus,
    CategorizationProposal,
    FinancialClose,
    Invoice,
    InvoiceStatus,
    JournalEntry,
    Transaction,
    TransactionStatus,
)
from sqlalchemy import func
from sqlalchemy.orm import Session

logger = logging.getLogger(__name__)

class CloseChecklistAgent:
    """
    Agent responsible for monitoring readiness for the periodic financial close.
    """

    def __init__(self, db: Session):
        self.db = db

    async def run_close_check(self, workspace_id: str, period: str) -> Dict[str, Any]:
        """
        Evaluate if the workspace is ready for a financial close for the given period.
        """
        results = {
            "period": period,
            "is_ready": True,
            "checklist": [],
            "blockers": []
        }

        # 1. Check for Uncategorized Transactions
        uncategorized_count = self.db.query(Transaction).filter(
            Transaction.workspace_id == workspace_id,
            Transaction.status == TransactionStatus.PENDING
        ).count()

        if uncategorized_count > 0:
            results["is_ready"] = False
            results["blockers"].append(f"{uncategorized_count} transactions are still pending categorization.")
            results["checklist"].append({"task": "Categorize Transactions", "status": "blocked"})
        else:
            results["checklist"].append({"task": "Categorize Transactions", "status": "complete"})

        # 2. Check for Unbalanced Journal Entries
        # In our EventSourcedLedger, this shouldn't happen, but good to verify
        # SELECT transaction_id, SUM(CASE WHEN type='debit' THEN amount ELSE -amount END) as diff
        from sqlalchemy import case
        unbalanced = self.db.query(JournalEntry.transaction_id).group_by(JournalEntry.transaction_id).having(
            func.abs(func.sum(case((JournalEntry.type == 'debit', JournalEntry.amount), else_=-JournalEntry.amount))) > 0.001
        ).all()

        if unbalanced:
            results["is_ready"] = False
            results["blockers"].append(f"{len(unbalanced)} transactions are unbalanced in the ledger.")
            results["checklist"].append({"task": "Ledger Integrity Check", "status": "blocked"})
        else:
            results["checklist"].append({"task": "Ledger Integrity Check", "status": "complete"})

        # 3. Check for Open Invoices / Bills (Optional for soft close, blocker for hard close)
        open_bills = self.db.query(Bill).filter(
            Bill.workspace_id == workspace_id,
            Bill.status == BillStatus.OPEN
        ).count()
        
        if open_bills > 0:
            results["checklist"].append({"task": "Review Open Bills", "status": "warning", "note": f"{open_bills} bills are still open."})
        else:
            results["checklist"].append({"task": "Review Open Bills", "status": "complete"})

        # Update or create the Close record
        close_record = self.db.query(FinancialClose).filter(
            FinancialClose.workspace_id == workspace_id,
            FinancialClose.period == period
        ).first()

        if not close_record:
            close_record = FinancialClose(
                workspace_id=workspace_id,
                period=period,
                metadata_json=results
            )
            self.db.add(close_record)
        else:
            close_record.metadata_json = results
        
        self.db.commit()
        return results

    async def close_period(self, workspace_id: str, period: str, user_id: str) -> Dict[str, Any]:
        """
        Permanently close a period if ready.
        """
        check = await self.run_close_check(workspace_id, period)
        if not check["is_ready"]:
            return {"success": False, "message": "Cannot close period. Please resolve blockers.", "blockers": check["blockers"]}

        close_record = self.db.query(FinancialClose).filter(
            FinancialClose.workspace_id == workspace_id,
            FinancialClose.period == period
        ).first()

        close_record.is_closed = True
        close_record.closed_at = datetime.utcnow()
        close_record.closed_by = user_id
        
        self.db.commit()
        
        return {"success": True, "message": f"Period {period} has been closed successfully."}