""" Financial/Ledger API endpoints """ from typing import List, Optional, Dict, Any, Union from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session, joinedload from sqlalchemy import or_, func, and_, desc, asc, text from datetime import date, datetime, timedelta, timezone from app.database.base import get_db from app.models.ledger import Ledger from app.models.files import File from app.models.rolodex import Rolodex from app.models.lookups import Employee, TransactionType, TransactionCode from app.models.user import User from app.auth.security import get_current_user from app.services.cache import invalidate_search_cache from app.services.query_utils import apply_sorting, paginate_with_total from app.models.additional import Deposit, Payment router = APIRouter() # Pydantic schemas from pydantic import BaseModel, ConfigDict class LedgerBase(BaseModel): file_no: str date: date t_code: str t_type: str t_type_l: Optional[str] = None empl_num: str quantity: float = 0.0 rate: float = 0.0 amount: float billed: str = "N" note: Optional[str] = None class LedgerCreate(LedgerBase): pass class LedgerUpdate(BaseModel): date: Optional[date] = None t_code: Optional[str] = None t_type: Optional[str] = None t_type_l: Optional[str] = None empl_num: Optional[str] = None quantity: Optional[float] = None rate: Optional[float] = None amount: Optional[float] = None billed: Optional[str] = None note: Optional[str] = None class LedgerResponse(LedgerBase): id: int item_no: int model_config = ConfigDict(from_attributes=True) class FinancialSummary(BaseModel): """Financial summary for a file""" file_no: str total_hours: float total_hourly_fees: float total_flat_fees: float total_disbursements: float total_credits: float total_charges: float amount_owing: float unbilled_amount: float billed_amount: float class PaginatedLedgerResponse(BaseModel): items: List[LedgerResponse] total: int class DepositResponse(BaseModel): deposit_date: date total: float notes: Optional[str] = None payments: Optional[List[Dict]] = None # Optional, depending on include_payments class PaymentCreate(BaseModel): file_no: Optional[str] = None client_id: Optional[str] = None regarding: Optional[str] = None amount: float note: Optional[str] = None payment_method: str = "CHECK" reference: Optional[str] = None apply_to_trust: bool = False @router.get("/ledger/{file_no}", response_model=Union[List[LedgerResponse], PaginatedLedgerResponse]) async def get_file_ledger( file_no: str, skip: int = Query(0, ge=0, description="Offset for pagination"), limit: int = Query(100, ge=1, le=500, description="Page size"), billed_only: Optional[bool] = Query(None, description="Filter billed vs unbilled entries"), sort_by: Optional[str] = Query("date", description="Sort by: date, item_no, amount, billed"), sort_dir: Optional[str] = Query("desc", description="Sort direction: asc or desc"), include_total: bool = Query(False, description="When true, returns {items, total} instead of a plain list"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get ledger entries for specific file""" query = db.query(Ledger).filter(Ledger.file_no == file_no) if billed_only is not None: billed_filter = "Y" if billed_only else "N" query = query.filter(Ledger.billed == billed_filter) # Sorting (whitelisted) query = apply_sorting( query, sort_by, sort_dir, allowed={ "date": [Ledger.date, Ledger.item_no], "item_no": [Ledger.item_no], "amount": [Ledger.amount], "billed": [Ledger.billed, Ledger.date], }, ) entries, total = paginate_with_total(query, skip, limit, include_total) if include_total: return {"items": entries, "total": total or 0} return entries @router.post("/ledger/", response_model=LedgerResponse) async def create_ledger_entry( entry_data: LedgerCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Create new ledger entry""" # Verify file exists file_obj = db.query(File).filter(File.file_no == entry_data.file_no).first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) # Get next item number for this file max_item = db.query(func.max(Ledger.item_no)).filter( Ledger.file_no == entry_data.file_no ).scalar() or 0 entry = Ledger( **entry_data.model_dump(), item_no=max_item + 1 ) db.add(entry) db.commit() db.refresh(entry) # Update file balances (simplified version) await _update_file_balances(file_obj, db) try: await invalidate_search_cache() except Exception: pass return entry @router.put("/ledger/{entry_id}", response_model=LedgerResponse) async def update_ledger_entry( entry_id: int, entry_data: LedgerUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Update ledger entry""" entry = db.query(Ledger).filter(Ledger.id == entry_id).first() if not entry: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Ledger entry not found" ) # Update fields for field, value in entry_data.model_dump(exclude_unset=True).items(): setattr(entry, field, value) db.commit() db.refresh(entry) # Update file balances file_obj = db.query(File).filter(File.file_no == entry.file_no).first() if file_obj: await _update_file_balances(file_obj, db) try: await invalidate_search_cache() except Exception: pass return entry @router.delete("/ledger/{entry_id}") async def delete_ledger_entry( entry_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Delete ledger entry""" entry = db.query(Ledger).filter(Ledger.id == entry_id).first() if not entry: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Ledger entry not found" ) file_no = entry.file_no db.delete(entry) db.commit() # Update file balances file_obj = db.query(File).filter(File.file_no == file_no).first() if file_obj: await _update_file_balances(file_obj, db) try: await invalidate_search_cache() except Exception: pass return {"message": "Ledger entry deleted successfully"} @router.get("/reports/{file_no}", response_model=FinancialSummary) async def get_financial_report( file_no: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get financial summary report for file""" file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) # Calculate totals from ledger entries ledger_entries = db.query(Ledger).filter(Ledger.file_no == file_no).all() total_hours = 0.0 total_hourly_fees = 0.0 total_flat_fees = 0.0 total_disbursements = 0.0 total_credits = 0.0 unbilled_amount = 0.0 billed_amount = 0.0 for entry in ledger_entries: if entry.t_type == "2": # Hourly fees total_hours += entry.quantity total_hourly_fees += entry.amount elif entry.t_type == "3": # Flat fees total_flat_fees += entry.amount elif entry.t_type == "4": # Disbursements total_disbursements += entry.amount elif entry.t_type == "5": # Credits total_credits += entry.amount if entry.billed == "Y": billed_amount += entry.amount else: unbilled_amount += entry.amount total_charges = total_hourly_fees + total_flat_fees + total_disbursements amount_owing = total_charges - total_credits return FinancialSummary( file_no=file_no, total_hours=total_hours, total_hourly_fees=total_hourly_fees, total_flat_fees=total_flat_fees, total_disbursements=total_disbursements, total_credits=total_credits, total_charges=total_charges, amount_owing=amount_owing, unbilled_amount=unbilled_amount, billed_amount=billed_amount ) async def _update_file_balances(file_obj: File, db: Session): """Update file balance totals (simplified version of Tally_Ledger)""" ledger_entries = db.query(Ledger).filter(Ledger.file_no == file_obj.file_no).all() # Reset balances file_obj.trust_bal = 0.0 file_obj.hours = 0.0 file_obj.hourly_fees = 0.0 file_obj.flat_fees = 0.0 file_obj.disbursements = 0.0 file_obj.credit_bal = 0.0 # Calculate totals for entry in ledger_entries: if entry.t_type == "1": # Trust file_obj.trust_bal += entry.amount elif entry.t_type == "2": # Hourly fees file_obj.hours += entry.quantity file_obj.hourly_fees += entry.amount elif entry.t_type == "3": # Flat fees file_obj.flat_fees += entry.amount elif entry.t_type == "4": # Disbursements file_obj.disbursements += entry.amount elif entry.t_type == "5": # Credits file_obj.credit_bal += entry.amount file_obj.total_charges = file_obj.hourly_fees + file_obj.flat_fees + file_obj.disbursements file_obj.amount_owing = file_obj.total_charges - file_obj.credit_bal # Calculate transferable amount if file_obj.amount_owing > 0 and file_obj.trust_bal > 0: if file_obj.trust_bal >= file_obj.amount_owing: file_obj.transferable = file_obj.amount_owing else: file_obj.transferable = file_obj.trust_bal else: file_obj.transferable = 0.0 db.commit() async def _create_ledger_payment( file_no: str, amount: float, payment_date: date, payment_method: str, reference: Optional[str], notes: Optional[str], apply_to_trust: bool, empl_num: str, db: Session ) -> Ledger: # Get next item number max_item = db.query(func.max(Ledger.item_no)).filter( Ledger.file_no == file_no ).scalar() or 0 # Determine transaction type and code if apply_to_trust: t_type = "1" # Trust t_code = "TRUST" description = f"Trust deposit - {payment_method}" else: t_type = "5" # Credit/Payment t_code = "PMT" description = f"Payment received - {payment_method}" if reference: description += f" - Ref: {reference}" if notes: description += f" - {notes}" # Create ledger entry entry = Ledger( file_no=file_no, item_no=max_item + 1, date=payment_date, t_code=t_code, t_type=t_type, t_type_l="C", # Credit empl_num=empl_num, quantity=0.0, rate=0.0, amount=amount, billed="Y", # Payments are automatically considered "billed" note=description ) db.add(entry) db.flush() # To get ID return entry # Additional Financial Management Endpoints @router.get("/time-entries/recent") async def get_recent_time_entries( days: int = Query(7, ge=1, le=30), employee: Optional[str] = Query(None), status: Optional[str] = Query(None, description="billed|unbilled"), q: Optional[str] = Query(None, description="text search across description, file, employee, matter, client name"), page: int = Query(1, ge=1), limit: int = Query(50, ge=1, le=200), sort_by: str = Query("date"), sort_dir: str = Query("desc"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get recent time entries across all files with server-side sorting and pagination""" cutoff_date = date.today() - timedelta(days=days) # Base query with joins for sorting/searching by client/matter base_query = db.query(Ledger) \ .join(File, Ledger.file_no == File.file_no) \ .outerjoin(Rolodex, File.id == Rolodex.id) \ .options(joinedload(Ledger.file).joinedload(File.owner)) \ .filter(and_( Ledger.date >= cutoff_date, Ledger.t_type == "2" )) if employee: base_query = base_query.filter(Ledger.empl_num == employee) # Status/billed filtering if status: status_l = str(status).strip().lower() if status_l in ("billed", "unbilled"): billed_value = "Y" if status_l == "billed" else "N" base_query = base_query.filter(Ledger.billed == billed_value) # Text search across multiple fields if q: query_text = f"%{q.strip()}%" base_query = base_query.filter( or_( Ledger.note.ilike(query_text), Ledger.file_no.ilike(query_text), Ledger.empl_num.ilike(query_text), File.regarding.ilike(query_text), Rolodex.first.ilike(query_text), Rolodex.last.ilike(query_text) ) ) # Sorting mapping (supported columns) sort_map = { "date": Ledger.date, "file_no": Ledger.file_no, "client_name": Rolodex.last, # best-effort: sort by client last name "empl_num": Ledger.empl_num, "quantity": Ledger.quantity, "hours": Ledger.quantity, # alias "rate": Ledger.rate, "amount": Ledger.amount, "billed": Ledger.billed, "description": Ledger.note, } sort_column = sort_map.get(sort_by.lower(), Ledger.date) direction = desc if str(sort_dir).lower() == "desc" else asc # Total count for pagination (distinct on Ledger.id to avoid join-induced dupes) total_count = base_query.with_entities(func.count(func.distinct(Ledger.id))).scalar() # Apply sorting and pagination offset = (page - 1) * limit page_query = base_query.order_by(direction(sort_column)).offset(offset).limit(limit) entries = page_query.all() # Format results with file and client information results = [] for entry in entries: file_obj = entry.file client = file_obj.owner if file_obj else None results.append({ "id": entry.id, "date": entry.date.isoformat(), "file_no": entry.file_no, "client_name": f"{client.first or ''} {client.last}".strip() if client else "Unknown", "matter": file_obj.regarding if file_obj else "", "employee": entry.empl_num, "hours": entry.quantity, "rate": entry.rate, "amount": entry.amount, "description": entry.note, "billed": entry.billed == "Y" }) return { "entries": results, "total_count": total_count, "page": page, "limit": limit, "sort_by": sort_by, "sort_dir": sort_dir, } @router.post("/time-entry/quick") async def create_quick_time_entry( file_no: str, hours: float, description: str, entry_date: Optional[date] = None, employee: Optional[str] = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Quick time entry creation""" # Verify file exists and get default rate file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException(status_code=404, detail="File not found") # Use file's default rate and employee if not provided rate = file_obj.rate_per_hour empl_num = employee or file_obj.empl_num entry_date = entry_date or date.today() # Get next item number max_item = db.query(func.max(Ledger.item_no)).filter( Ledger.file_no == file_no ).scalar() or 0 # Create time entry entry = Ledger( file_no=file_no, item_no=max_item + 1, date=entry_date, t_code="TIME", t_type="2", t_type_l="D", empl_num=empl_num, quantity=hours, rate=rate, amount=hours * rate, billed="N", note=description ) db.add(entry) db.commit() db.refresh(entry) # Update file balances await _update_file_balances(file_obj, db) return { "id": entry.id, "message": f"Time entry created: {hours} hours @ ${rate}/hr = ${entry.amount}", "entry": { "date": entry.date.isoformat(), "hours": hours, "rate": rate, "amount": entry.amount, "description": description } } @router.get("/unbilled-entries") async def get_unbilled_entries( file_no: Optional[str] = Query(None), employee: Optional[str] = Query(None), start_date: Optional[date] = Query(None), end_date: Optional[date] = Query(None), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get all unbilled entries for billing preparation""" query = db.query(Ledger)\ .options(joinedload(Ledger.file).joinedload(File.owner))\ .filter(Ledger.billed == "N")\ .order_by(Ledger.file_no, Ledger.date) if file_no: query = query.filter(Ledger.file_no == file_no) if employee: query = query.filter(Ledger.empl_num == employee) if start_date: query = query.filter(Ledger.date >= start_date) if end_date: query = query.filter(Ledger.date <= end_date) entries = query.all() # Group by file for easier billing files_data = {} total_unbilled = 0.0 for entry in entries: file_no = entry.file_no if file_no not in files_data: file_obj = entry.file client = file_obj.owner if file_obj else None files_data[file_no] = { "file_no": file_no, "client_name": f"{client.first or ''} {client.last}".strip() if client else "Unknown", "client_id": file_obj.id if file_obj else "", "matter": file_obj.regarding if file_obj else "", "entries": [], "total_amount": 0.0, "total_hours": 0.0 } entry_data = { "id": entry.id, "date": entry.date.isoformat(), "type": entry.t_code, "employee": entry.empl_num, "description": entry.note, "quantity": entry.quantity, "rate": entry.rate, "amount": entry.amount } files_data[file_no]["entries"].append(entry_data) files_data[file_no]["total_amount"] += entry.amount if entry.t_type == "2": # Time entries files_data[file_no]["total_hours"] += entry.quantity total_unbilled += entry.amount return { "files": list(files_data.values()), "total_unbilled_amount": total_unbilled, "total_files": len(files_data) } @router.post("/bill-entries") async def mark_entries_as_billed( entry_ids: List[int], bill_date: Optional[date] = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Mark multiple entries as billed""" bill_date = bill_date or date.today() # Get entries to bill entries = db.query(Ledger).filter(Ledger.id.in_(entry_ids)).all() if not entries: raise HTTPException(status_code=404, detail="No entries found") # Update entries to billed status billed_amount = 0.0 affected_files = set() for entry in entries: entry.billed = "Y" billed_amount += entry.amount affected_files.add(entry.file_no) db.commit() # Update file balances for affected files for file_no in affected_files: file_obj = db.query(File).filter(File.file_no == file_no).first() if file_obj: await _update_file_balances(file_obj, db) return { "message": f"Marked {len(entries)} entries as billed", "billed_amount": billed_amount, "bill_date": bill_date.isoformat(), "affected_files": list(affected_files) } @router.get("/financial-dashboard") async def get_financial_dashboard( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get financial dashboard summary""" # Total financial metrics total_charges = db.query(func.sum(File.total_charges)).scalar() or 0 total_owing = db.query(func.sum(File.amount_owing)).scalar() or 0 total_trust = db.query(func.sum(File.trust_bal)).scalar() or 0 total_hours = db.query(func.sum(File.hours)).scalar() or 0 # Unbilled amounts unbilled_total = db.query(func.sum(Ledger.amount))\ .filter(Ledger.billed == "N").scalar() or 0 # Recent activity (last 30 days) thirty_days_ago = date.today() - timedelta(days=30) recent_entries = db.query(func.count(Ledger.id))\ .filter(Ledger.date >= thirty_days_ago).scalar() or 0 recent_amount = db.query(func.sum(Ledger.amount))\ .filter(Ledger.date >= thirty_days_ago).scalar() or 0 # Top files by balance top_files = db.query(File.file_no, File.amount_owing, File.total_charges)\ .filter(File.amount_owing > 0)\ .order_by(desc(File.amount_owing))\ .limit(10).all() # Employee activity employee_stats = db.query( Ledger.empl_num, func.sum(Ledger.quantity).label('total_hours'), func.sum(Ledger.amount).label('total_amount'), func.count(Ledger.id).label('entry_count') ).filter( and_( Ledger.date >= thirty_days_ago, Ledger.t_type == "2" # Time entries only ) ).group_by(Ledger.empl_num).all() return { "summary": { "total_charges": float(total_charges), "total_owing": float(total_owing), "total_trust": float(total_trust), "total_hours": float(total_hours), "unbilled_amount": float(unbilled_total) }, "recent_activity": { "entries_count": recent_entries, "total_amount": float(recent_amount), "period_days": 30 }, "top_files": [ { "file_no": f[0], "amount_owing": float(f[1]), "total_charges": float(f[2]) } for f in top_files ], "employee_stats": [ { "employee": stat[0], "hours": float(stat[1] or 0), "amount": float(stat[2] or 0), "entries": stat[3] } for stat in employee_stats ] } @router.get("/lookups/transaction-codes") async def get_transaction_codes( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get available transaction codes""" codes = db.query(TransactionCode).filter(TransactionCode.active == True).all() return [ { "code": c.t_code, "description": c.description, "type": c.t_type, "default_rate": c.default_rate } for c in codes ] @router.get("/lookups/transaction-types") async def get_transaction_types( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get available transaction types""" types = db.query(TransactionType).filter(TransactionType.active == True).all() return [ { "type": t.t_type, "description": t.description, "debit_credit": t.debit_credit } for t in types ] @router.get("/reports/time-summary") async def get_time_summary_report( start_date: date = Query(...), end_date: date = Query(...), employee: Optional[str] = Query(None), file_no: Optional[str] = Query(None), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Generate time summary report""" query = db.query(Ledger)\ .options(joinedload(Ledger.file).joinedload(File.owner))\ .filter(and_( Ledger.date >= start_date, Ledger.date <= end_date, Ledger.t_type == "2" # Time entries only ))\ .order_by(Ledger.date) if employee: query = query.filter(Ledger.empl_num == employee) if file_no: query = query.filter(Ledger.file_no == file_no) entries = query.all() # Summarize by employee and file summary = {} total_hours = 0.0 total_amount = 0.0 for entry in entries: emp = entry.empl_num file_no = entry.file_no if emp not in summary: summary[emp] = { "employee": emp, "files": {}, "total_hours": 0.0, "total_amount": 0.0 } if file_no not in summary[emp]["files"]: file_obj = entry.file client = file_obj.owner if file_obj else None summary[emp]["files"][file_no] = { "file_no": file_no, "client_name": f"{client.first or ''} {client.last}".strip() if client else "Unknown", "matter": file_obj.regarding if file_obj else "", "hours": 0.0, "amount": 0.0, "entries": [] } # Add entry details summary[emp]["files"][file_no]["entries"].append({ "date": entry.date.isoformat(), "hours": entry.quantity, "rate": entry.rate, "amount": entry.amount, "description": entry.note, "billed": entry.billed == "Y" }) # Update totals summary[emp]["files"][file_no]["hours"] += entry.quantity summary[emp]["files"][file_no]["amount"] += entry.amount summary[emp]["total_hours"] += entry.quantity summary[emp]["total_amount"] += entry.amount total_hours += entry.quantity total_amount += entry.amount # Convert to list format report_data = [] for emp_data in summary.values(): emp_data["files"] = list(emp_data["files"].values()) report_data.append(emp_data) return { "report_period": { "start_date": start_date.isoformat(), "end_date": end_date.isoformat() }, "summary": { "total_hours": total_hours, "total_amount": total_amount, "total_entries": len(entries) }, "employees": report_data } @router.post("/payments/") async def record_payment( file_no: str, amount: float, payment_date: Optional[date] = None, payment_method: str = "CHECK", reference: Optional[str] = None, notes: Optional[str] = None, apply_to_trust: bool = False, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException(status_code=404, detail="File not found") payment_date = payment_date or date.today() entry = await _create_ledger_payment( file_no=file_no, amount=amount, payment_date=payment_date, payment_method=payment_method, reference=reference, notes=notes, apply_to_trust=apply_to_trust, empl_num=file_obj.empl_num, db=db ) db.commit() db.refresh(entry) await _update_file_balances(file_obj, db) return { "id": entry.id, "message": f"Payment of ${amount} recorded successfully", "payment": { "amount": amount, "date": payment_date.isoformat(), "method": payment_method, "applied_to": "trust" if apply_to_trust else "balance", "reference": reference }, "new_balance": { "amount_owing": file_obj.amount_owing, "trust_balance": file_obj.trust_bal } } @router.post("/expenses/") async def record_expense( file_no: str, amount: float, description: str, expense_date: Optional[date] = None, category: str = "MISC", employee: Optional[str] = None, receipts: bool = False, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Record an expense/disbursement against a file""" # Verify file exists file_obj = db.query(File).filter(File.file_no == file_no).first() if not file_obj: raise HTTPException(status_code=404, detail="File not found") expense_date = expense_date or date.today() empl_num = employee or file_obj.empl_num # Get next item number max_item = db.query(func.max(Ledger.item_no)).filter( Ledger.file_no == file_no ).scalar() or 0 # Add receipt info to description full_description = description if receipts: full_description += " (Receipts on file)" # Create expense entry entry = Ledger( file_no=file_no, item_no=max_item + 1, date=expense_date, t_code=category, t_type="4", # Disbursements t_type_l="D", # Debit empl_num=empl_num, quantity=0.0, rate=0.0, amount=amount, billed="N", note=full_description ) db.add(entry) db.commit() db.refresh(entry) # Update file balances await _update_file_balances(file_obj, db) return { "id": entry.id, "message": f"Expense of ${amount} recorded successfully", "expense": { "amount": amount, "date": expense_date.isoformat(), "category": category, "description": description, "employee": empl_num } } @router.post("/deposits/") async def create_deposit( deposit_date: date, notes: Optional[str] = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): existing = db.query(Deposit).filter(Deposit.deposit_date == deposit_date).first() if existing: raise HTTPException(status_code=400, detail="Deposit for this date already exists") deposit = Deposit( deposit_date=deposit_date, total=0.0, notes=notes ) db.add(deposit) db.commit() db.refresh(deposit) return deposit @router.post("/deposits/{deposit_date}/payments/") async def add_payment_to_deposit( deposit_date: date, payment_data: PaymentCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): deposit = db.query(Deposit).filter(Deposit.deposit_date == deposit_date).first() if not deposit: raise HTTPException(status_code=404, detail="Deposit not found") if not payment_data.file_no: raise HTTPException(status_code=400, detail="file_no is required for payments") file_obj = db.query(File).filter(File.file_no == payment_data.file_no).first() if not file_obj: raise HTTPException(status_code=404, detail="File not found") # Create ledger entry first ledger_entry = await _create_ledger_payment( file_no=payment_data.file_no, amount=payment_data.amount, payment_date=deposit_date, payment_method=payment_data.payment_method, reference=payment_data.reference, notes=payment_data.note, apply_to_trust=payment_data.apply_to_trust, empl_num=file_obj.empl_num, db=db ) # Create payment record payment = Payment( deposit_date=deposit_date, file_no=payment_data.file_no, client_id=payment_data.client_id, regarding=payment_data.regarding, amount=payment_data.amount, note=payment_data.note ) db.add(payment) # Update deposit total deposit.total += payment_data.amount db.commit() db.refresh(payment) await _update_file_balances(file_obj, db) return payment @router.get("/deposits/", response_model=List[DepositResponse]) async def list_deposits( start_date: Optional[date] = None, end_date: Optional[date] = None, include_payments: bool = False, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): query = db.query(Deposit) if start_date: query = query.filter(Deposit.deposit_date >= start_date) if end_date: query = query.filter(Deposit.deposit_date <= end_date) query = query.order_by(Deposit.deposit_date.desc()) deposits = query.all() results = [] for dep in deposits: dep_data = { "deposit_date": dep.deposit_date, "total": dep.total, "notes": dep.notes } if include_payments: payments = db.query(Payment).filter(Payment.deposit_date == dep.deposit_date).all() dep_data["payments"] = [p.__dict__ for p in payments] results.append(dep_data) return results @router.get("/deposits/{deposit_date}", response_model=DepositResponse) async def get_deposit( deposit_date: date, include_payments: bool = True, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): deposit = db.query(Deposit).filter(Deposit.deposit_date == deposit_date).first() if not deposit: raise HTTPException(status_code=404, detail="Deposit not found") dep_data = { "deposit_date": deposit.deposit_date, "total": deposit.total, "notes": deposit.notes } if include_payments: payments = db.query(Payment).filter(Payment.deposit_date == deposit_date).all() dep_data["payments"] = [p.__dict__ for p in payments] return dep_data @router.get("/reports/deposits") async def get_deposit_report( start_date: date, end_date: date, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): deposits = db.query(Deposit).filter( Deposit.deposit_date >= start_date, Deposit.deposit_date <= end_date ).order_by(Deposit.deposit_date).all() total_deposits = sum(d.total for d in deposits) report = { "period": { "start": start_date.isoformat(), "end": end_date.isoformat() }, "total_deposits": total_deposits, "deposit_count": len(deposits), "deposits": [ { "date": d.deposit_date.isoformat(), "total": d.total, "notes": d.notes, "payment_count": db.query(Payment).filter(Payment.deposit_date == d.deposit_date).count() } for d in deposits ] } return report