Initial commit: Robot ökoszisztéma v2.0 - Stabilizált jármű és szerviz robotok

This commit is contained in:
Kincses
2026-03-04 02:03:03 +01:00
commit 250f4f4b8f
7942 changed files with 449625 additions and 0 deletions

View 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."}

View 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()

View 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."}

View 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"]}

View 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
]

View 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

View 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."}

View 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"}

View 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()
]

View 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}

View 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]

View 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)

View 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()]

View 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()]}

View 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

View 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)

View 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