from datetime import date, datetime from decimal import Decimal from typing import Any from sqlalchemy import func, select from app.core.http.pagination import bounded_limit from app.core.utils.time import utc_now from app.modules.reports.constants import ReportStatus def _money(value: Decimal | int | float | None) -> str: """Format a numeric value as a two-decimal money string.""" amount = Decimal(value or 0) return f"{amount:,.2f}" def _json_safe(value: Any) -> Any: """Convert nested report payloads into JSON-storable values.""" if isinstance(value, Decimal): return float(value) if isinstance(value, (datetime, date)): return value.isoformat() if isinstance(value, list): return [_json_safe(item) for item in value] if isinstance(value, dict): return {key: _json_safe(item) for key, item in value.items()} return value def _next_code(prefix: str) -> str: """Build a compact unique code for generated report records.""" return f"{prefix}-{utc_now():%Y%m%d%H%M%S%f}" def _rate(numerator: int | Decimal, denominator: int | Decimal) -> float: """Return a rounded percentage rate, using zero when the denominator is empty.""" if not denominator: return 0.0 return round(float(numerator) / float(denominator) * 100, 2) def _as_decimal(value: Decimal | int | float | None) -> Decimal: return Decimal(str(value or 0)) class ReportQueryMixin: def _count(self, model: type, *conditions: Any) -> int: stmt = select(func.count()).select_from(model) for condition in conditions: stmt = stmt.where(condition) return int(self.db.execute(stmt).scalar() or 0) def _sum(self, column: Any, *conditions: Any) -> Decimal: stmt = select(func.sum(column)) for condition in conditions: stmt = stmt.where(condition) return _as_decimal(self.db.execute(stmt).scalar()) def _avg(self, column: Any, *conditions: Any) -> float: stmt = select(func.avg(column)) for condition in conditions: stmt = stmt.where(condition) value = self.db.execute(stmt).scalar() return round(float(value or 0), 2) def _group_counts(self, model: type, column: Any, *conditions: Any) -> dict[str, int]: stmt = select(column, func.count()).select_from(model) for condition in conditions: stmt = stmt.where(condition) stmt = stmt.group_by(column) return { str(key or ReportStatus.UNKNOWN): int(count) for key, count in self.db.execute(stmt).all() } def _records( self, model: type, *conditions: Any, limit: int = 10, order_by: Any | None = None, ) -> list[Any]: stmt = select(model) for condition in conditions: stmt = stmt.where(condition) if order_by is not None: stmt = stmt.order_by(order_by) else: stmt = stmt.order_by(model.id.desc()) return list(self.db.execute(stmt.limit(bounded_limit(limit))).scalars())