import json import logging from datetime import datetime from langchain_core.tools import tool from langgraph.types import interrupt from src.db.connection import get_connection logger = logging.getLogger("cashy.tools") @tool def create_transaction( transaction_type: str, transaction_description: str, amount: float, account_name: str, category_name: str = "", date: str = "", notes: str = "", ) -> str: """Create a new financial transaction (expense, income, or transfer). transaction_type must be 'expense', 'income', or 'transfer'. amount must be a positive number. date format: YYYY-MM-DD (optional, defaults to today). The user will be asked to confirm before the transaction is created.""" logger.info("[create_transaction] type=%s amount=%.2f account=%s category=%s", transaction_type, amount, account_name, category_name or "none") # --- Validation --- if amount <= 0: return json.dumps({"success": False, "error": "Amount must be positive"}) if transaction_type not in ("expense", "income", "transfer"): return json.dumps( {"success": False, "error": "transaction_type must be 'expense', 'income', or 'transfer'"} ) if date and date.strip(): try: transaction_date = datetime.strptime(date.strip(), "%Y-%m-%d").date() except ValueError: return json.dumps({"success": False, "error": "Invalid date format. Use YYYY-MM-DD"}) else: transaction_date = datetime.now().date() # --- Resolve names to IDs --- try: with get_connection() as conn: with conn.cursor() as cur: cur.execute( "SELECT id, name FROM accounts WHERE name ILIKE %s AND is_active = true", (f"%{account_name}%",), ) account = cur.fetchone() if not account: return json.dumps( {"success": False, "error": f"Account '{account_name}' not found"} ) account_id, account_full_name = account category_id = None category_full_name = "Uncategorized" if category_name and category_name.strip(): cur.execute( "SELECT id, name FROM categories WHERE name ILIKE %s AND is_active = true", (f"%{category_name}%",), ) category = cur.fetchone() if category: category_id, category_full_name = category except Exception as e: logger.error("[create_transaction] Lookup error: %s", e) return json.dumps({"success": False, "error": str(e)}) entry_type = "credit" if transaction_type == "income" else "debit" desc = transaction_description.strip() note = notes.strip() if notes and notes.strip() else None # --- Confirmation gate --- confirmation = { "action": "create_transaction", "message": f"Create {transaction_type} of ${amount:.2f} on {account_full_name}?", "details": { "type": transaction_type, "amount": float(amount), "account": account_full_name, "category": category_full_name, "date": str(transaction_date), "description": desc, }, } response = interrupt(confirmation) if not response.get("approved"): logger.info("[create_transaction] Cancelled by user") return json.dumps({"success": False, "message": "Transaction cancelled by user"}) # --- Execute DB write --- try: with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ INSERT INTO transactions (transaction_date, description, transaction_type, total_amount, notes) VALUES (%s, %s, %s, %s, %s) RETURNING id """, (transaction_date, desc, transaction_type, amount, note), ) transaction_id = cur.fetchone()[0] cur.execute( """ INSERT INTO transaction_entries (transaction_id, account_id, category_id, amount, entry_type, description) VALUES (%s, %s, %s, %s, %s, %s) RETURNING id """, (transaction_id, account_id, category_id, amount, entry_type, desc), ) entry_id = cur.fetchone()[0] logger.info("[create_transaction] Created txn_id=%d entry_id=%d", transaction_id, entry_id) return json.dumps( { "success": True, "transaction_id": transaction_id, "entry_id": entry_id, "message": f"{transaction_type.title()} of ${amount:.2f} recorded on {transaction_date}", "details": { "type": transaction_type, "amount": float(amount), "account": account_full_name, "category": category_full_name, "date": str(transaction_date), "description": desc, }, }, default=str, ) except Exception as e: logger.error("[create_transaction] Error: %s", e) return json.dumps({"success": False, "error": str(e)})