tracker / app /services /record_service.py
MatrixIA's picture
Upload 24 files
fade1d6 verified
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from fastapi import HTTPException, status
from bson import ObjectId
from ..database.connection import get_collection
from ..models.monthly_record import (
MonthlyRecord, MonthlyRecordCreate, MonthlyRecordUpdate,
ExpenseCategory, month_key_from, normalize_month_name
)
class RecordService:
def __init__(self):
self.col = get_collection()
async def ensure_indexes(self) -> None:
# Unique index on month_key for quick lookup
await self.col.create_index("month_key", unique=True)
def _calc_totals(self, salary: float, expenses: List[ExpenseCategory]) -> Dict[str, float]:
total_expenses = round(sum(e.amount for e in expenses), 2)
remaining = round(salary - total_expenses, 2)
return {"total_expenses": total_expenses, "remaining": remaining}
def _serialize(self, doc: Dict[str, Any]) -> Dict[str, Any]:
if not doc:
return doc
doc["_id"] = str(doc["_id"])
return doc
async def get_by_month_key(self, month_key: str) -> Dict[str, Any]:
doc = await self.col.find_one({"month_key": month_key})
if not doc:
raise HTTPException(status_code=404, detail="No record found for this month")
return self._serialize(doc)
async def list(self, limit: int = 12, skip: int = 0) -> List[Dict[str, Any]]:
cursor = self.col.find({}).sort("month_key", 1).skip(skip).limit(limit)
return [self._serialize(d) async for d in cursor]
async def create(self, payload: MonthlyRecordCreate) -> Dict[str, Any]:
month = normalize_month_name(payload.month)
month_key = month_key_from(month, payload.year)
now = datetime.now(timezone.utc)
totals = self._calc_totals(payload.salary, payload.expenses)
doc = {
"month": month,
"year": payload.year,
"month_key": month_key,
"salary": float(payload.salary),
"expenses": [e.model_dump() for e in payload.expenses],
"total_expenses": totals["total_expenses"],
"remaining": totals["remaining"],
"created_at": now,
"updated_at": now,
}
try:
result = await self.col.insert_one(doc)
except Exception as e:
# likely duplicate month_key
raise HTTPException(status_code=400, detail=str(e))
created = await self.col.find_one({"_id": result.inserted_id})
return self._serialize(created)
async def update(self, month_key: str, payload: MonthlyRecordUpdate) -> Dict[str, Any]:
existing = await self.col.find_one({"month_key": month_key})
if not existing:
raise HTTPException(status_code=404, detail="No record found for this month")
salary = payload.salary if payload.salary is not None else existing["salary"]
expenses = payload.expenses if payload.expenses is not None else existing["expenses"]
# If expenses came as pydantic models, convert to dicts
expenses_list = [e.model_dump() if hasattr(e, "model_dump") else e for e in expenses]
totals = self._calc_totals(salary, [ExpenseCategory(**e) for e in expenses_list])
update_doc = {
"$set": {
"salary": float(salary),
"expenses": expenses_list,
"total_expenses": totals["total_expenses"],
"remaining": totals["remaining"],
"updated_at": datetime.now(timezone.utc),
}
}
await self.col.update_one({"month_key": month_key}, update_doc)
updated = await self.col.find_one({"month_key": month_key})
return self._serialize(updated)
async def delete(self, month_key: str) -> None:
result = await self.col.delete_one({"month_key": month_key})
if result.deleted_count == 0:
raise HTTPException(status_code=404, detail="No record found for this month")