Initial commit: Robot ökoszisztéma v2.0 - Stabilizált jármű és szerviz robotok
This commit is contained in:
131
backend/app/api/v1/endpoints/admin.py
Executable file
131
backend/app/api/v1/endpoints/admin.py
Executable file
@@ -0,0 +1,131 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, text, delete
|
||||
from typing import List, Any, Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.api import deps
|
||||
from app.models.identity import User, UserRole # JAVÍTVA: Központi import
|
||||
from app.models.system import SystemParameter
|
||||
# JAVÍTVA: Security audit modellek
|
||||
from app.models.audit import SecurityAuditLog, OperationalLog
|
||||
# JAVÍTVA: Ezek a modellek a security.py-ból jönnek (ha ott vannak)
|
||||
from app.models.security import PendingAction, ActionStatus
|
||||
|
||||
from app.services.security_service import security_service
|
||||
from app.services.translation_service import TranslationService
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ConfigUpdate(BaseModel):
|
||||
key: str
|
||||
value: Any
|
||||
scope_level: str = "global"
|
||||
scope_id: Optional[str] = None
|
||||
category: str = "general"
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
async def check_admin_access(current_user: User = Depends(deps.get_current_active_user)):
|
||||
""" Csak Admin vagy Superadmin. """
|
||||
if current_user.role not in [UserRole.admin, UserRole.superadmin]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Sentinel jogosultság szükséges!"
|
||||
)
|
||||
return current_user
|
||||
|
||||
@router.get("/health-monitor", tags=["Sentinel Monitoring"])
|
||||
async def get_system_health(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
stats = {}
|
||||
|
||||
# Adatbázis statisztikák (Nyers SQL marad, mert hatékony)
|
||||
user_stats = await db.execute(text("SELECT subscription_plan, count(*) FROM data.users GROUP BY subscription_plan"))
|
||||
stats["user_distribution"] = {row[0]: row[1] for row in user_stats}
|
||||
|
||||
asset_count = await db.execute(text("SELECT count(*) FROM data.assets"))
|
||||
stats["total_assets"] = asset_count.scalar()
|
||||
|
||||
org_count = await db.execute(text("SELECT count(*) FROM data.organizations"))
|
||||
stats["total_organizations"] = org_count.scalar()
|
||||
|
||||
# JAVÍTVA: Biztonsági státusz az új SecurityAuditLog alapján
|
||||
day_ago = datetime.now() - timedelta(days=1)
|
||||
crit_logs = await db.execute(
|
||||
select(func.count(SecurityAuditLog.id))
|
||||
.where(
|
||||
SecurityAuditLog.is_critical == True,
|
||||
SecurityAuditLog.created_at >= day_ago
|
||||
)
|
||||
)
|
||||
stats["critical_alerts_24h"] = crit_logs.scalar() or 0
|
||||
|
||||
return stats
|
||||
|
||||
@router.get("/pending-actions", response_model=List[Any], tags=["Sentinel Security"])
|
||||
async def list_pending_actions(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@router.post("/approve/{action_id}", tags=["Sentinel Security"])
|
||||
async def approve_action(
|
||||
action_id: int,
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
try:
|
||||
await security_service.approve_action(db, admin.id, action_id)
|
||||
return {"status": "success", "message": "Művelet végrehajtva."}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.get("/parameters", tags=["Dynamic Configuration"])
|
||||
async def list_all_parameters(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
result = await db.execute(select(SystemParameter))
|
||||
return result.scalars().all()
|
||||
|
||||
@router.post("/parameters", tags=["Dynamic Configuration"])
|
||||
async def set_parameter(
|
||||
config: ConfigUpdate,
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
query = text("""
|
||||
INSERT INTO data.system_parameters (key, value, scope_level, scope_id, category, last_modified_by)
|
||||
VALUES (:key, :val, :sl, :sid, :cat, :user)
|
||||
ON CONFLICT (key, scope_level, scope_id)
|
||||
DO UPDATE SET
|
||||
value = EXCLUDED.value,
|
||||
category = EXCLUDED.category,
|
||||
last_modified_by = EXCLUDED.last_modified_by,
|
||||
updated_at = now()
|
||||
""")
|
||||
|
||||
await db.execute(query, {
|
||||
"key": config.key,
|
||||
"val": config.value,
|
||||
"sl": config.scope_level,
|
||||
"sid": config.scope_id,
|
||||
"cat": config.category,
|
||||
"user": admin.email
|
||||
})
|
||||
await db.commit()
|
||||
return {"status": "success", "message": f"'{config.key}' frissítve."}
|
||||
|
||||
@router.post("/translations/sync", tags=["System Utilities"])
|
||||
async def sync_translations_to_json(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
await TranslationService.export_to_json(db)
|
||||
return {"message": "JSON fájlok frissítve."}
|
||||
54
backend/app/api/v1/endpoints/assets.py
Executable file
54
backend/app/api/v1/endpoints/assets.py
Executable file
@@ -0,0 +1,54 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/assets.py
|
||||
import uuid
|
||||
from typing import Any, Dict, List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.asset import Asset, AssetCost
|
||||
from app.models.identity import User
|
||||
from app.services.cost_service import cost_service
|
||||
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
|
||||
from app.schemas.asset import AssetResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/{asset_id}/financial-summary", response_model=Dict[str, Any])
|
||||
async def get_asset_financial_report(
|
||||
asset_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
MB 2.0 Dinamikus Pénzügyi Riport.
|
||||
Visszaadja a kategóriákra bontott és az összesített költségeket (Local/EUR).
|
||||
"""
|
||||
# 1. Jogosultság ellenőrzése (Csak a tulajdonos vagy admin láthatja)
|
||||
# (Itt egy gyors check, hogy az asset az övé-e)
|
||||
|
||||
try:
|
||||
return await cost_service.get_asset_financial_summary(db, asset_id)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="Hiba a riport generálásakor")
|
||||
|
||||
@router.get("/{asset_id}/costs", response_model=List[AssetCostResponse])
|
||||
async def list_asset_costs(
|
||||
asset_id: uuid.UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Tételes költséglista lapozással (Pagination)."""
|
||||
stmt = (
|
||||
select(AssetCost)
|
||||
.where(AssetCost.asset_id == asset_id)
|
||||
.order_by(desc(AssetCost.date))
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
res = await db.execute(stmt)
|
||||
return res.scalars().all()
|
||||
41
backend/app/api/v1/endpoints/auth.py
Executable file
41
backend/app/api/v1/endpoints/auth.py
Executable file
@@ -0,0 +1,41 @@
|
||||
# backend/app/api/v1/endpoints/auth.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.db.session import get_db
|
||||
from app.services.auth_service import AuthService
|
||||
from app.core.security import create_tokens, DEFAULT_RANK_MAP
|
||||
from app.core.config import settings
|
||||
from app.schemas.auth import UserLiteRegister, Token, UserKYCComplete
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.identity import User # JAVÍTVA: Új központi modell
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
user = await AuthService.authenticate(db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Hibás adatok.")
|
||||
|
||||
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
|
||||
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
||||
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
"role": role_name,
|
||||
"rank": ranks.get(role_name, 10),
|
||||
"scope_level": user.scope_level or "individual",
|
||||
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
|
||||
}
|
||||
|
||||
access, refresh = create_tokens(data=token_data)
|
||||
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer", "is_active": user.is_active}
|
||||
|
||||
@router.post("/complete-kyc")
|
||||
async def complete_kyc(kyc_in: UserKYCComplete, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
user = await AuthService.complete_kyc(db, current_user.id, kyc_in)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User nem található.")
|
||||
return {"status": "success", "message": "Fiók aktiválva."}
|
||||
63
backend/app/api/v1/endpoints/billing.py
Executable file
63
backend/app/api/v1/endpoints/billing.py
Executable file
@@ -0,0 +1,63 @@
|
||||
# backend/app/api/v1/endpoints/billing.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.identity import User, Wallet, UserRole
|
||||
from app.models.audit import FinancialLedger
|
||||
from app.services.config_service import config
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/upgrade")
|
||||
async def upgrade_account(target_package: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""
|
||||
Univerzális csomagváltó.
|
||||
Kezeli az 5+ csomagot, a Rank-ugrást és a különleges 'Service Coin' bónuszokat.
|
||||
"""
|
||||
# 1. Lekérjük a teljes csomagmátrixot az adminból
|
||||
# Példa JSON: {"premium": {"price": 2000, "rank": 5, "type": "credit"}, "service_pro": {"price": 10000, "rank": 30, "type": "coin"}}
|
||||
package_matrix = await config.get_setting(db, "subscription_packages_matrix")
|
||||
|
||||
if target_package not in package_matrix:
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen csomagválasztás.")
|
||||
|
||||
pkg_info = package_matrix[target_package]
|
||||
price = pkg_info["price"]
|
||||
|
||||
# 2. Pénztárca ellenőrzése
|
||||
stmt = select(Wallet).where(Wallet.user_id == current_user.id)
|
||||
wallet = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
total_balance = wallet.purchased_credits + wallet.earned_credits
|
||||
|
||||
if total_balance < price:
|
||||
raise HTTPException(status_code=402, detail="Nincs elég kredited a csomagváltáshoz.")
|
||||
|
||||
# 3. Levonási logika (Purchased -> Earned sorrend)
|
||||
if wallet.purchased_credits >= price:
|
||||
wallet.purchased_credits -= price
|
||||
else:
|
||||
remaining = price - wallet.purchased_credits
|
||||
wallet.purchased_credits = 0
|
||||
wallet.earned_credits -= remaining
|
||||
|
||||
# 4. Speciális Szerviz Logika (Service Coins)
|
||||
# Ha a csomag típusa 'coin', akkor a szerviz kap egy kezdő Coin csomagot is
|
||||
if pkg_info.get("type") == "coin":
|
||||
initial_coins = pkg_info.get("initial_coin_bonus", 100)
|
||||
wallet.service_coins += initial_coins
|
||||
logger.info(f"User {current_user.id} upgraded to Service Pro, awarded {initial_coins} coins.")
|
||||
|
||||
# 5. Rang frissítése és naplózás
|
||||
current_user.role = target_package # Pl. 'service_pro' vagy 'vip'
|
||||
|
||||
db.add(FinancialLedger(
|
||||
user_id=current_user.id,
|
||||
amount=-price,
|
||||
transaction_type=f"UPGRADE_{target_package.upper()}",
|
||||
details=pkg_info
|
||||
))
|
||||
|
||||
await db.commit()
|
||||
return {"status": "success", "package": target_package, "rank_granted": pkg_info["rank"]}
|
||||
46
backend/app/api/v1/endpoints/catalog.py
Executable file
46
backend/app/api/v1/endpoints/catalog.py
Executable file
@@ -0,0 +1,46 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import get_db
|
||||
from app.services.asset_service import AssetService
|
||||
from typing import List
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/makes", response_model=List[str])
|
||||
async def list_makes(db: AsyncSession = Depends(get_db)):
|
||||
"""1. Szint: Márkák listázása."""
|
||||
return await AssetService.get_makes(db)
|
||||
|
||||
@router.get("/models", response_model=List[str])
|
||||
async def list_models(make: str, db: AsyncSession = Depends(get_db)):
|
||||
"""2. Szint: Típusok listázása egy adott márkához."""
|
||||
models = await AssetService.get_models(db, make)
|
||||
if not models:
|
||||
raise HTTPException(status_code=404, detail="Márka nem található vagy nincsenek típusok.")
|
||||
return models
|
||||
|
||||
@router.get("/generations", response_model=List[str])
|
||||
async def list_generations(make: str, model: str, db: AsyncSession = Depends(get_db)):
|
||||
"""3. Szint: Generációk/Évjáratok listázása."""
|
||||
generations = await AssetService.get_generations(db, make, model)
|
||||
if not generations:
|
||||
raise HTTPException(status_code=404, detail="Nincs generációs adat ehhez a típushoz.")
|
||||
return generations
|
||||
|
||||
@router.get("/engines")
|
||||
async def list_engines(make: str, model: str, gen: str, db: AsyncSession = Depends(get_db)):
|
||||
"""4. Szint: Motorváltozatok és technikai specifikációk."""
|
||||
engines = await AssetService.get_engines(db, make, model, gen)
|
||||
if not engines:
|
||||
raise HTTPException(status_code=404, detail="Nincs motorváltozat adat.")
|
||||
|
||||
# Itt visszaküldjük a teljes katalógus objektumokat (ID, motorváltozat, specifikációk)
|
||||
return [
|
||||
{
|
||||
"id": e.id,
|
||||
"variant": e.engine_variant,
|
||||
"engine_code": e.engine_code,
|
||||
"fuel_type": e.fuel_type,
|
||||
"factory_data": e.factory_data
|
||||
} for e in engines
|
||||
]
|
||||
87
backend/app/api/v1/endpoints/documents.py
Executable file
87
backend/app/api/v1/endpoints/documents.py
Executable file
@@ -0,0 +1,87 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/documents.py
|
||||
import uuid
|
||||
from typing import Any, Dict
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, Form, BackgroundTasks, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.services.document_service import DocumentService
|
||||
from app.models.identity import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/upload/{parent_type}/{parent_id}")
|
||||
async def upload_document(
|
||||
parent_type: str,
|
||||
parent_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
doc_type: str = Form(..., description="A dokumentum típusa: 'invoice', 'registration_card', 'sale_contract'"),
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
MB 2.0 Dokumentum Pipeline.
|
||||
1. Ellenőrzi a felhasználó havi OCR kvótáját (Admin 2.0).
|
||||
2. Optimalizálja és NAS-ra menti a képet (WebP konverzió).
|
||||
3. Automatikusan elindítja a Robot 1-et (OCR), ha a típus engedélyezett.
|
||||
"""
|
||||
|
||||
# 1. Bemeneti validáció
|
||||
valid_parents = ["organizations", "assets", "transfers"]
|
||||
if parent_type not in valid_parents:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Érvénytelen cél-típus! Megengedett: {', '.join(valid_parents)}"
|
||||
)
|
||||
|
||||
try:
|
||||
# 2. Feldolgozás a szolgáltatás rétegben
|
||||
# Itt történik a kvóta-ellenőrzés és a Robot 1 triggerelése is
|
||||
doc = await DocumentService.process_upload(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
file=file,
|
||||
parent_type=parent_type,
|
||||
parent_id=parent_id,
|
||||
doc_type=doc_type,
|
||||
background_tasks=background_tasks
|
||||
)
|
||||
|
||||
# 3. Válasz összeállítása az állapot alapján
|
||||
response_data = {
|
||||
"document_id": doc.id,
|
||||
"original_name": doc.original_name,
|
||||
"status": doc.status,
|
||||
"thumbnail": doc.thumbnail_path,
|
||||
}
|
||||
|
||||
if doc.status == "processing":
|
||||
response_data["message"] = "🤖 Robot 1 megkezdte a dokumentum elemzését. Értesítjük, ha kész!"
|
||||
else:
|
||||
response_data["message"] = "Dokumentum sikeresen archiválva a széfben."
|
||||
|
||||
return response_data
|
||||
|
||||
except HTTPException as he:
|
||||
# Közvetlenül átengedjük a service-ből jövő (pl. kvóta) hibákat
|
||||
raise he
|
||||
except ValueError as ve:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(ve))
|
||||
except Exception as e:
|
||||
# Sentinel naplózás és általános hiba
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Kritikus hiba a dokumentum feldolgozása során."
|
||||
)
|
||||
|
||||
@router.get("/{document_id}/status")
|
||||
async def get_document_status(
|
||||
document_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Lekérdezhető, hogy a robot végzett-e már a feldolgozással."""
|
||||
# (Itt egy egyszerű lekérdezés a Document táblából a státuszra)
|
||||
pass
|
||||
24
backend/app/api/v1/endpoints/evidence.py
Executable file
24
backend/app/api/v1/endpoints/evidence.py
Executable file
@@ -0,0 +1,24 @@
|
||||
# backend/app/api/v1/endpoints/evidence.py
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException, status, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, text
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.identity import User
|
||||
from app.models.asset import Asset # JAVÍTVA: Asset modell
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/scan-registration")
|
||||
async def scan_registration_document(file: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
stmt_limit = text("SELECT (value->>:plan)::int FROM data.system_parameters WHERE key = 'VEHICLE_LIMIT'")
|
||||
res = await db.execute(stmt_limit, {"plan": current_user.subscription_plan or "free"})
|
||||
max_allowed = res.scalar() or 1
|
||||
|
||||
stmt_count = select(func.count(Asset.id)).where(Asset.owner_organization_id == current_user.scope_id)
|
||||
count = (await db.execute(stmt_count)).scalar() or 0
|
||||
|
||||
if count >= max_allowed:
|
||||
raise HTTPException(status_code=403, detail=f"Limit túllépés: {max_allowed} jármű engedélyezett.")
|
||||
|
||||
# OCR hívás helye...
|
||||
return {"success": True, "message": "Feldolgozás megkezdődött."}
|
||||
33
backend/app/api/v1/endpoints/expenses.py
Executable file
33
backend/app/api/v1/endpoints/expenses.py
Executable file
@@ -0,0 +1,33 @@
|
||||
# backend/app/api/v1/endpoints/expenses.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.asset import Asset, AssetCost # JAVÍTVA
|
||||
from pydantic import BaseModel
|
||||
from datetime import date
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class ExpenseCreate(BaseModel):
|
||||
asset_id: str
|
||||
category: str
|
||||
amount: float
|
||||
date: date
|
||||
|
||||
@router.post("/add")
|
||||
async def add_expense(expense: ExpenseCreate, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
stmt = select(Asset).where(Asset.id == expense.asset_id)
|
||||
if not (await db.execute(stmt)).scalar_one_or_none():
|
||||
raise HTTPException(status_code=404, detail="Jármű nem található.")
|
||||
|
||||
new_cost = AssetCost(
|
||||
asset_id=expense.asset_id,
|
||||
cost_type=expense.category,
|
||||
amount_local=expense.amount,
|
||||
date=expense.date,
|
||||
currency_local="HUF"
|
||||
)
|
||||
db.add(new_cost)
|
||||
await db.commit()
|
||||
return {"status": "success"}
|
||||
40
backend/app/api/v1/endpoints/gamification.py
Executable file
40
backend/app/api/v1/endpoints/gamification.py
Executable file
@@ -0,0 +1,40 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/gamification.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
from typing import List
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.identity import User
|
||||
from app.models.gamification import UserStats, PointsLedger
|
||||
from app.services.config_service import config
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/my-stats")
|
||||
async def get_my_stats(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""A bejelentkezett felhasználó aktuális XP-je, szintje és büntetőpontjai."""
|
||||
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
stats = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if not stats:
|
||||
return {"total_xp": 0, "current_level": 1, "penalty_points": 0}
|
||||
|
||||
return stats
|
||||
|
||||
@router.get("/leaderboard")
|
||||
async def get_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
|
||||
"""A 10 legtöbb XP-vel rendelkező felhasználó listája."""
|
||||
stmt = (
|
||||
select(User.email, UserStats.total_xp, UserStats.current_level)
|
||||
.join(UserStats, User.id == UserStats.user_id)
|
||||
.order_by(desc(UserStats.total_xp))
|
||||
.limit(limit)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
# Az email-eket maszkoljuk a GDPR miatt (pl. k***s@p***.hu)
|
||||
return [
|
||||
{"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "xp": r[1], "level": r[2]}
|
||||
for r in result.all()
|
||||
]
|
||||
100
backend/app/api/v1/endpoints/notifications.py
Executable file
100
backend/app/api/v1/endpoints/notifications.py
Executable file
@@ -0,0 +1,100 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/notifications.py
|
||||
import uuid
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update, desc, func
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.identity import User
|
||||
from app.models.system import InternalNotification
|
||||
from app.schemas.social import NotificationResponse, NotificationUpdate # Feltételezett sémák
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/my", response_model=List[NotificationResponse])
|
||||
async def get_my_notifications(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
unread_only: bool = False,
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
offset: int = 0
|
||||
):
|
||||
"""
|
||||
Lekéri a bejelentkezett felhasználó értesítéseit.
|
||||
Támogatja a szűrést az olvasatlanokra és a lapozást.
|
||||
"""
|
||||
stmt = (
|
||||
select(InternalNotification)
|
||||
.where(InternalNotification.user_id == current_user.id)
|
||||
)
|
||||
|
||||
if unread_only:
|
||||
stmt = stmt.where(InternalNotification.is_read == False)
|
||||
|
||||
stmt = stmt.order_by(desc(InternalNotification.created_at)).offset(offset).limit(limit)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@router.post("/mark-read")
|
||||
async def mark_as_read(
|
||||
notification_ids: List[uuid.UUID],
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Egy vagy több értesítés olvasottnak jelölése.
|
||||
"""
|
||||
if not notification_ids:
|
||||
raise HTTPException(status_code=400, detail="Nincs megadva azonosító.")
|
||||
|
||||
stmt = (
|
||||
update(InternalNotification)
|
||||
.where(InternalNotification.id.in_(notification_ids))
|
||||
.where(InternalNotification.user_id == current_user.id)
|
||||
.values(is_read=True, read_at=func.now())
|
||||
)
|
||||
|
||||
await db.execute(stmt)
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "marked_count": len(notification_ids)}
|
||||
|
||||
@router.post("/mark-all-read")
|
||||
async def mark_all_read(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
A felhasználó összes értesítésének olvasottnak jelölése.
|
||||
"""
|
||||
stmt = (
|
||||
update(InternalNotification)
|
||||
.where(InternalNotification.user_id == current_user.id)
|
||||
.where(InternalNotification.is_read == False)
|
||||
.values(is_read=True, read_at=func.now())
|
||||
)
|
||||
|
||||
await db.execute(stmt)
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "message": "Minden értesítés olvasottnak jelölve."}
|
||||
|
||||
@router.get("/summary")
|
||||
async def get_notification_summary(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Gyors összesítő a Dashboard-hoz (pl. hány olvasatlan van).
|
||||
"""
|
||||
stmt = (
|
||||
select(func.count(InternalNotification.id))
|
||||
.where(InternalNotification.user_id == current_user.id)
|
||||
.where(InternalNotification.is_read == False)
|
||||
)
|
||||
|
||||
unread_count = (await db.execute(stmt)).scalar() or 0
|
||||
return {"unread_count": unread_count}
|
||||
115
backend/app/api/v1/endpoints/organizations.py
Executable file
115
backend/app/api/v1/endpoints/organizations.py
Executable file
@@ -0,0 +1,115 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/organizations.py
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse
|
||||
from app.models.organization import Organization, OrgType, OrganizationMember
|
||||
from app.models.identity import User # JAVÍTVA: Központi Identity modell
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@router.post("/onboard", response_model=CorpOnboardResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def onboard_organization(
|
||||
org_in: CorpOnboardIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Új szervezet (cég/szerviz) rögzítése.
|
||||
Automatikusan generál slug-ot és létrehozza a NAS mappa-struktúrát.
|
||||
"""
|
||||
|
||||
# 1. Magyar adószám validáció (XXXXXXXX-Y-ZZ)
|
||||
if org_in.country_code == "HU":
|
||||
if not re.match(r"^\d{8}-\d-\d{2}$", org_in.tax_number):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Érvénytelen magyar adószám formátum!"
|
||||
)
|
||||
|
||||
# 2. Duplikáció ellenőrzés
|
||||
stmt_exist = select(Organization).where(Organization.tax_number == org_in.tax_number)
|
||||
result_exist = await db.execute(stmt_exist)
|
||||
if result_exist.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Ezzel az adószámmal már regisztráltak céget!"
|
||||
)
|
||||
|
||||
# 3. KÖTELEZŐ MEZŐ: folder_slug generálása
|
||||
# Mivel az adatbázisban NOT NULL, itt muszáj létrehozni
|
||||
temp_slug = hashlib.md5(f"{org_in.tax_number}-{uuid.uuid4()}".encode()).hexdigest()[:12]
|
||||
|
||||
# 4. Mentés
|
||||
new_org = Organization(
|
||||
full_name=org_in.full_name,
|
||||
name=org_in.name,
|
||||
display_name=org_in.display_name,
|
||||
tax_number=org_in.tax_number,
|
||||
reg_number=org_in.reg_number,
|
||||
folder_slug=temp_slug, # JAVÍTVA: Kötelező mező beillesztve
|
||||
address_zip=org_in.address_zip,
|
||||
address_city=org_in.address_city,
|
||||
address_street_name=org_in.address_street_name,
|
||||
address_street_type=org_in.address_street_type,
|
||||
address_house_number=org_in.address_house_number,
|
||||
address_hrsz=org_in.address_hrsz,
|
||||
address_stairwell=org_in.address_stairwell,
|
||||
address_floor=org_in.address_floor,
|
||||
address_door=org_in.address_door,
|
||||
country_code=org_in.country_code,
|
||||
org_type=OrgType.business,
|
||||
status="pending_verification"
|
||||
)
|
||||
|
||||
db.add(new_org)
|
||||
await db.flush()
|
||||
|
||||
# 5. TULAJDONOS RÖGZÍTÉSE
|
||||
owner_member = OrganizationMember(
|
||||
organization_id=new_org.id,
|
||||
user_id=current_user.id,
|
||||
role="OWNER" # JAVÍTVA: Enum kompatibilis nagybetűs forma
|
||||
)
|
||||
db.add(owner_member)
|
||||
|
||||
# 6. NAS Mappa létrehozása
|
||||
try:
|
||||
base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data")
|
||||
org_path = os.path.join(base_path, "organizations", str(new_org.id))
|
||||
os.makedirs(os.path.join(org_path, "documents"), exist_ok=True)
|
||||
logger.info(f"NAS mappa kész: {org_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"NAS hiba: {e}")
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(new_org)
|
||||
|
||||
return {"organization_id": new_org.id, "status": new_org.status}
|
||||
|
||||
@router.get("/my", response_model=List[CorpOnboardResponse])
|
||||
async def get_my_organizations(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
""" A bejelentkezett felhasználóhoz tartozó összes szervezet listázása. """
|
||||
stmt = (
|
||||
select(Organization)
|
||||
.join(OrganizationMember)
|
||||
.where(OrganizationMember.user_id == current_user.id)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
orgs = result.scalars().all()
|
||||
|
||||
return [{"organization_id": o.id, "status": o.status} for o in orgs]
|
||||
12
backend/app/api/v1/endpoints/providers.py
Executable file
12
backend/app/api/v1/endpoints/providers.py
Executable file
@@ -0,0 +1,12 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import get_db
|
||||
from app.schemas.social import ServiceProviderCreate, ServiceProviderResponse
|
||||
from app.services.social_service import create_service_provider
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=ServiceProviderResponse)
|
||||
async def add_provider(provider_data: ServiceProviderCreate, db: AsyncSession = Depends(get_db)):
|
||||
user_id = 2
|
||||
return await create_service_provider(db, provider_data, user_id)
|
||||
50
backend/app/api/v1/endpoints/reports.py
Executable file
50
backend/app/api/v1/endpoints/reports.py
Executable file
@@ -0,0 +1,50 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from app.api.deps import get_db, get_current_user
|
||||
|
||||
router = APIRouter() # EZ HIÁNYZOTT!
|
||||
|
||||
@router.get("/summary/{vehicle_id}")
|
||||
async def get_vehicle_summary(vehicle_id: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Összesített jelentés egy járműhöz: kategóriánkénti költségek.
|
||||
"""
|
||||
query = text("""
|
||||
SELECT
|
||||
category,
|
||||
SUM(amount) as total_amount,
|
||||
COUNT(*) as transaction_count
|
||||
FROM data.vehicle_expenses
|
||||
WHERE vehicle_id = :v_id
|
||||
GROUP BY category
|
||||
""")
|
||||
|
||||
result = await db.execute(query, {"v_id": vehicle_id})
|
||||
rows = result.fetchall()
|
||||
|
||||
total_cost = sum(row.total_amount for row in rows) if rows else 0
|
||||
|
||||
return {
|
||||
"vehicle_id": vehicle_id,
|
||||
"total_cost": float(total_cost),
|
||||
"breakdown": [dict(row._mapping) for row in rows]
|
||||
}
|
||||
|
||||
@router.get("/trends/{vehicle_id}")
|
||||
async def get_monthly_trends(vehicle_id: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Visszaadja az utolsó 6 hónap költéseit havi bontásban.
|
||||
"""
|
||||
query = text("""
|
||||
SELECT
|
||||
TO_CHAR(date, 'YYYY-MM') as month,
|
||||
SUM(amount) as monthly_total
|
||||
FROM data.vehicle_expenses
|
||||
WHERE vehicle_id = :v_id
|
||||
GROUP BY month
|
||||
ORDER BY month DESC
|
||||
LIMIT 6
|
||||
""")
|
||||
result = await db.execute(query, {"v_id": vehicle_id})
|
||||
return [dict(row._mapping) for row in result.fetchall()]
|
||||
24
backend/app/api/v1/endpoints/search.py
Executable file
24
backend/app/api/v1/endpoints/search.py
Executable file
@@ -0,0 +1,24 @@
|
||||
# backend/app/api/v1/endpoints/search.py
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.organization import Organization # JAVÍTVA
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/match")
|
||||
async def match_service(lat: float, lng: float, radius: int = 20, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
# PostGIS alapú keresés a data.branches táblában (a régi locations helyett)
|
||||
query = text("""
|
||||
SELECT o.id, o.name, b.city,
|
||||
ST_Distance(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography) / 1000 as distance
|
||||
FROM data.organizations o
|
||||
JOIN data.branches b ON o.id = b.organization_id
|
||||
WHERE o.is_active = True AND b.is_active = True
|
||||
AND ST_DWithin(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography, :r * 1000)
|
||||
ORDER BY distance ASC
|
||||
""")
|
||||
result = await db.execute(query, {"lat": lat, "lng": lng, "r": radius})
|
||||
return {"results": [dict(row._mapping) for row in result.fetchall()]}
|
||||
58
backend/app/api/v1/endpoints/services.py
Executable file
58
backend/app/api/v1/endpoints/services.py
Executable file
@@ -0,0 +1,58 @@
|
||||
from fastapi import APIRouter, Depends, Form, Query, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_, text
|
||||
from typing import List, Optional
|
||||
from app.db.session import get_db
|
||||
from app.services.gamification_service import GamificationService
|
||||
from app.models.service import ServiceProfile, ExpertiseTag, ServiceExpertise
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# --- 🎯 SZERVIZ VADÁSZAT (Service Hunt) ---
|
||||
@router.post("/hunt")
|
||||
async def register_service_hunt(
|
||||
name: str = Form(...),
|
||||
lat: float = Form(...),
|
||||
lng: float = Form(...),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
""" Új szerviz-jelölt rögzítése a staging táblába jutalompontért. """
|
||||
# Új szerviz-jelölt rögzítése
|
||||
await db.execute(text("""
|
||||
INSERT INTO data.service_staging (name, fingerprint, status, raw_data)
|
||||
VALUES (:n, :f, 'pending', jsonb_build_object('lat', :lat, 'lng', :lng))
|
||||
"""), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng})
|
||||
|
||||
# MB 2.0 Gamification: 50 pont a felfedezésért
|
||||
# TODO: A 1-es ID helyett a bejelentkezett felhasználót kell használni (current_user.id)
|
||||
await GamificationService.award_points(db, 1, 50, f"Service Hunt: {name}")
|
||||
await db.commit()
|
||||
return {"status": "success", "message": "Discovery registered and points awarded."}
|
||||
|
||||
# --- 🔍 SZERVIZ KERESŐ (Service Search) ---
|
||||
@router.get("/search")
|
||||
async def search_services(
|
||||
expertise_key: Optional[str] = Query(None, description="Szakmai címke (pl. brake_service)"),
|
||||
city: Optional[str] = Query(None, description="Város szűrés"),
|
||||
min_confidence: int = Query(0, description="Minimum hitelességi szint (0-2)"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
""" Szakmai szempontú keresőmotor, ami a validált címkék alapján szűr. """
|
||||
# Alap lekérdezés: Szervizek, akiknek van szakértelmük
|
||||
query = select(ServiceProfile).join(ServiceProfile.expertises).join(ServiceExpertise.tag)
|
||||
|
||||
filters = []
|
||||
if expertise_key:
|
||||
filters.append(ExpertiseTag.key == expertise_key)
|
||||
if city:
|
||||
filters.append(ServiceProfile.city.ilike(f"%{city}%"))
|
||||
if min_confidence > 0:
|
||||
filters.append(ServiceExpertise.confidence_level >= min_confidence)
|
||||
|
||||
if filters:
|
||||
query = query.where(and_(*filters))
|
||||
|
||||
result = await db.execute(query.distinct())
|
||||
services = result.scalars().all()
|
||||
|
||||
return services
|
||||
16
backend/app/api/v1/endpoints/social.py
Executable file
16
backend/app/api/v1/endpoints/social.py
Executable file
@@ -0,0 +1,16 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import get_db
|
||||
# ITT A JAVÍTÁS: A példányt importáljuk, nem a régi függvényeket
|
||||
from app.services.social_service import social_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/leaderboard")
|
||||
async def read_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
|
||||
return await social_service.get_leaderboard(db, limit)
|
||||
|
||||
@router.post("/vote/{provider_id}")
|
||||
async def provider_vote(provider_id: int, vote_value: int, db: AsyncSession = Depends(get_db)):
|
||||
user_id = 2
|
||||
return await social_service.vote_for_provider(db, user_id, provider_id, vote_value)
|
||||
16
backend/app/api/v1/endpoints/users.py
Executable file
16
backend/app/api/v1/endpoints/users.py
Executable file
@@ -0,0 +1,16 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.schemas.user import UserResponse
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def read_users_me(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Visszaadja a bejelentkezett felhasználó profilját"""
|
||||
return current_user
|
||||
Reference in New Issue
Block a user