Files
service-finder/backend/app/services/cost_service.py

144 lines
5.9 KiB
Python
Executable File

# /opt/docker/dev/service_finder/backend/app/services/cost_service.py
import uuid
import logging
from decimal import Decimal
from typing import Any, Dict
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc, func
from app.models.asset import AssetCost, AssetTelemetry, ExchangeRate
from app.services.gamification_service import GamificationService
from app.services.config_service import config
from app.schemas.asset_cost import AssetCostCreate
from datetime import datetime
logger = logging.getLogger(__name__)
class CostService:
"""
Industrial Cost & Telemetry Service.
Összeköti a pénzügyi kiadásokat, az OCR bizonylatokat és a jármű állapotát.
"""
async def record_cost(self, db: AsyncSession, cost_in: AssetCostCreate, user_id: int):
""" Teljes körű költségrögzítés: Konverzió + Telemetria + OCR + XP. """
try:
# 1. Dinamikus konfiguráció lekérése
base_currency = await config.get_setting(db, "finance_base_currency", default="EUR")
base_xp = await config.get_setting(db, "xp_per_cost_log", default=50)
ocr_multiplier = await config.get_setting(db, "xp_multiplier_ocr_cost", default=1.5)
# 2. Intelligens Árfolyamkezelés
exchange_rate = Decimal("1.0")
if cost_in.currency_local != base_currency:
rate_stmt = select(ExchangeRate).where(
ExchangeRate.target_currency == cost_in.currency_local
).order_by(desc(ExchangeRate.updated_at)).limit(1)
rate_res = await db.execute(rate_stmt)
rate_obj = rate_res.scalar_one_or_none()
exchange_rate = rate_obj.rate if rate_obj else Decimal("1.0")
amt_base = Decimal(str(cost_in.amount_local)) / exchange_rate if exchange_rate > 0 else Decimal("0")
# 3. Költség rekord rögzítése (Kapcsolva a Robot 1 OCR dokumentumához)
new_cost = AssetCost(
asset_id=cost_in.asset_id,
organization_id=cost_in.organization_id,
driver_id=user_id,
cost_type=cost_in.cost_type,
amount_local=cost_in.amount_local,
currency_local=cost_in.currency_local,
amount_eur=amt_base,
net_amount_local=cost_in.net_amount_local,
vat_rate=cost_in.vat_rate,
exchange_rate_used=exchange_rate,
mileage_at_cost=cost_in.mileage_at_cost,
date=cost_in.date or datetime.now(),
# OCR Kapcsolat
document_id=cost_in.document_id,
is_ai_generated=cost_in.document_id is not None,
data=cost_in.data or {}
)
db.add(new_cost)
# 4. Automatikus Telemetria (Kilométeróra frissítés)
if cost_in.mileage_at_cost:
await self._sync_telemetry(db, cost_in.asset_id, cost_in.mileage_at_cost)
# 5. Gamification (Értékesebb az adat, ha van róla fotó/OCR)
final_xp = base_xp
if new_cost.is_ai_generated:
final_xp = int(base_xp * float(ocr_multiplier))
await GamificationService.award_points(
db, user_id=user_id, amount=final_xp, reason=f"EXPENSE_LOG_{cost_in.cost_type}"
)
await db.commit()
await db.refresh(new_cost)
return new_cost
except Exception as e:
await db.rollback()
logger.error(f"CostService Error: {e}")
raise e
async def _sync_telemetry(self, db: AsyncSession, asset_id: int, mileage: int):
""" Segédfüggvény: Biztonságos óraállás frissítés. """
stmt = select(AssetTelemetry).where(AssetTelemetry.asset_id == asset_id)
res = await db.execute(stmt)
telemetry = res.scalar_one_or_none()
if telemetry:
# Csak akkor frissítünk, ha az új érték nagyobb (nincs visszatekerés)
if mileage > (telemetry.current_mileage or 0):
telemetry.current_mileage = mileage
telemetry.last_updated = datetime.now()
else:
db.add(AssetTelemetry(asset_id=asset_id, current_mileage=mileage))
async def get_asset_financial_summary(self, db: AsyncSession, asset_id: uuid.UUID) -> Dict[str, Any]:
"""
Dinamikus pénzügyi összesítő SQL szintű aggregációval.
MB 2.0: Nem loopolunk Pythonban, a DB számol!
"""
# 1. Lekérjük az összesített adatokat kategóriánként (Local és EUR)
stmt = (
select(
AssetCost.cost_type,
func.sum(AssetCost.amount_local).label("total_local"),
func.sum(AssetCost.amount_eur).label("total_eur"),
func.count(AssetCost.id).label("transaction_count")
)
.where(AssetCost.asset_id == asset_id)
.group_by(AssetCost.cost_type)
)
res = await db.execute(stmt)
rows = res.all()
summary = {
"by_category": {},
"grand_total_local": Decimal("0.0"),
"grand_total_eur": Decimal("0.0"),
"total_transactions": 0
}
for row in rows:
cat = row.cost_type or "OTHER"
summary["by_category"][cat] = {
"local": float(row.total_local),
"eur": float(row.total_eur),
"count": row.transaction_count
}
summary["grand_total_local"] += row.total_local
summary["grand_total_eur"] += row.total_eur
summary["total_transactions"] += row.transaction_count
# Decimal konverzió a JSON-höz
summary["grand_total_local"] = float(summary["grand_total_local"])
summary["grand_total_eur"] = float(summary["grand_total_eur"])
return summary
cost_service = CostService()