refaktorálás javításai
This commit is contained in:
@@ -7,20 +7,23 @@ 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
|
||||
from app.models.system import SystemParameter, ParameterScope
|
||||
from app.services.system_service import system_service
|
||||
# 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
|
||||
from app.services.translation_service import TranslationService
|
||||
from app.services.odometer_service import OdometerService
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional as Opt
|
||||
|
||||
class ConfigUpdate(BaseModel):
|
||||
key: str
|
||||
value: Any
|
||||
scope_level: str = "global"
|
||||
scope_level: ParameterScope = ParameterScope.GLOBAL
|
||||
scope_id: Optional[str] = None
|
||||
category: str = "general"
|
||||
|
||||
@@ -43,13 +46,13 @@ async def get_system_health(
|
||||
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"))
|
||||
user_stats = await db.execute(text("SELECT subscription_plan, count(*) FROM identity.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"))
|
||||
asset_count = await db.execute(text("SELECT count(*) FROM vehicle.assets"))
|
||||
stats["total_assets"] = asset_count.scalar()
|
||||
|
||||
org_count = await db.execute(text("SELECT count(*) FROM data.organizations"))
|
||||
org_count = await db.execute(text("SELECT count(*) FROM fleet.organizations"))
|
||||
stats["total_organizations"] = org_count.scalar()
|
||||
|
||||
# JAVÍTVA: Biztonsági státusz az új SecurityAuditLog alapján
|
||||
@@ -101,7 +104,7 @@ async def set_parameter(
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
query = text("""
|
||||
INSERT INTO data.system_parameters (key, value, scope_level, scope_id, category, last_modified_by)
|
||||
INSERT INTO system.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
|
||||
@@ -122,10 +125,114 @@ async def set_parameter(
|
||||
await db.commit()
|
||||
return {"status": "success", "message": f"'{config.key}' frissítve."}
|
||||
|
||||
@router.get("/parameters/scoped", tags=["Dynamic Configuration"])
|
||||
async def get_scoped_parameter(
|
||||
key: str,
|
||||
user_id: Optional[str] = None,
|
||||
region_id: Optional[str] = None,
|
||||
country_code: Optional[str] = None,
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
"""
|
||||
Hierarchikus paraméterlekérdezés a következő prioritással:
|
||||
User > Region > Country > Global.
|
||||
"""
|
||||
value = await system_service.get_scoped_parameter(
|
||||
db, key, user_id, region_id, country_code, default=None
|
||||
)
|
||||
if value is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Paraméter '{key}' nem található a megadott scope-okban."
|
||||
)
|
||||
return {"key": key, "value": value}
|
||||
|
||||
@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."}
|
||||
return {"message": "JSON fájlok frissítve."}
|
||||
|
||||
|
||||
# ==================== SMART ODOMETER ADMIN API ====================
|
||||
|
||||
class OdometerStatsResponse(BaseModel):
|
||||
vehicle_id: int
|
||||
last_recorded_odometer: int
|
||||
last_recorded_date: datetime
|
||||
daily_avg_distance: float
|
||||
estimated_current_odometer: float
|
||||
confidence_score: float
|
||||
manual_override_avg: Opt[float]
|
||||
is_confidence_high: bool = Field(..., description="True ha confidence_score >= threshold")
|
||||
|
||||
class ManualOverrideRequest(BaseModel):
|
||||
daily_avg: Opt[float] = Field(None, description="Napi átlagos kilométer (km/nap). Ha null, törli a manuális beállítást.")
|
||||
|
||||
@router.get("/odometer/{vehicle_id}", tags=["Smart Odometer"])
|
||||
async def get_odometer_stats(
|
||||
vehicle_id: int,
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
"""
|
||||
Jármű kilométeróra statisztikáinak lekérése.
|
||||
|
||||
A rendszer automatikusan frissíti a statisztikákat, ha szükséges.
|
||||
"""
|
||||
# Frissítjük a statisztikákat
|
||||
odometer_state = await OdometerService.update_vehicle_stats(db, vehicle_id)
|
||||
if not odometer_state:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Jármű nem található ID: {vehicle_id}"
|
||||
)
|
||||
|
||||
# Confidence threshold lekérése
|
||||
confidence_threshold = await OdometerService.get_system_param(
|
||||
db, 'ODOMETER_CONFIDENCE_THRESHOLD', 0.5
|
||||
)
|
||||
|
||||
return OdometerStatsResponse(
|
||||
vehicle_id=odometer_state.vehicle_id,
|
||||
last_recorded_odometer=odometer_state.last_recorded_odometer,
|
||||
last_recorded_date=odometer_state.last_recorded_date,
|
||||
daily_avg_distance=float(odometer_state.daily_avg_distance),
|
||||
estimated_current_odometer=float(odometer_state.estimated_current_odometer),
|
||||
confidence_score=odometer_state.confidence_score,
|
||||
manual_override_avg=float(odometer_state.manual_override_avg) if odometer_state.manual_override_avg else None,
|
||||
is_confidence_high=odometer_state.confidence_score >= confidence_threshold
|
||||
)
|
||||
|
||||
@router.patch("/odometer/{vehicle_id}", tags=["Smart Odometer"])
|
||||
async def set_odometer_manual_override(
|
||||
vehicle_id: int,
|
||||
request: ManualOverrideRequest,
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_admin_access)
|
||||
):
|
||||
"""
|
||||
Adminisztrátori manuális átlag beállítása a kilométeróra becsléshez.
|
||||
|
||||
Ha a user csal vagy hibás az adat, az admin ezzel felülírhatja az automatikus számítást.
|
||||
"""
|
||||
odometer_state = await OdometerService.set_manual_override(
|
||||
db, vehicle_id, request.daily_avg
|
||||
)
|
||||
|
||||
if not odometer_state:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Jármű nem található ID: {vehicle_id}"
|
||||
)
|
||||
|
||||
action = "beállítva" if request.daily_avg is not None else "törölve"
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Manuális átlag {action}: {request.daily_avg} km/nap",
|
||||
"vehicle_id": vehicle_id,
|
||||
"manual_override_avg": odometer_state.manual_override_avg
|
||||
}
|
||||
196
backend/app/api/v1/endpoints/analytics.py
Normal file
196
backend/app/api/v1/endpoints/analytics.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""
|
||||
Analytics API endpoints for TCO (Total Cost of Ownership) dashboard.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api import deps
|
||||
from app.schemas.analytics import TCOSummaryResponse, TCOErrorResponse
|
||||
from app.services.analytics_service import TCOAnalytics
|
||||
from app.models import Vehicle
|
||||
from app.models.organization import OrganizationMember
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def verify_vehicle_access(
|
||||
vehicle_id: uuid.UUID,
|
||||
db: AsyncSession,
|
||||
current_user
|
||||
) -> Vehicle:
|
||||
"""
|
||||
Verify that the current user has access to the vehicle (either as owner or via organization).
|
||||
Raises HTTP 404 if vehicle not found, 403 if access denied.
|
||||
"""
|
||||
# 1. Check if vehicle exists
|
||||
vehicle = await db.get(Vehicle, vehicle_id)
|
||||
if not vehicle:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Vehicle with ID {vehicle_id} not found."
|
||||
)
|
||||
|
||||
# 2. Check if user is superadmin (global access)
|
||||
if current_user.role == "superadmin":
|
||||
return vehicle
|
||||
|
||||
# 3. Check if user is member of the vehicle's organization
|
||||
# (Vehicle.organization_id matches user's organization membership)
|
||||
# First, get user's organization memberships
|
||||
from sqlalchemy import select
|
||||
stmt = select(OrganizationMember).where(
|
||||
OrganizationMember.user_id == current_user.id,
|
||||
OrganizationMember.organization_id == vehicle.organization_id
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
membership = result.scalar_one_or_none()
|
||||
|
||||
if membership:
|
||||
return vehicle
|
||||
|
||||
# 4. If user is not a member, check if they have fleet manager role with cross-org access
|
||||
# (This could be extended based on RBAC)
|
||||
# For now, deny access
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You do not have permission to access this vehicle's analytics."
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{vehicle_id}/summary",
|
||||
response_model=TCOSummaryResponse,
|
||||
responses={
|
||||
404: {"model": TCOErrorResponse, "description": "Vehicle not found"},
|
||||
403: {"model": TCOErrorResponse, "description": "Access denied"},
|
||||
500: {"model": TCOErrorResponse, "description": "Internal server error"},
|
||||
},
|
||||
summary="Get TCO summary for a vehicle",
|
||||
description="Returns Total Cost of Ownership analytics for a specific vehicle, "
|
||||
"including user-specific costs, lifetime costs, and benchmark comparisons."
|
||||
)
|
||||
async def get_tco_summary(
|
||||
vehicle_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
current_user = Depends(deps.get_current_active_user),
|
||||
):
|
||||
"""
|
||||
Retrieve TCO analytics for a vehicle.
|
||||
|
||||
Steps:
|
||||
1. Verify user has access to the vehicle.
|
||||
2. Use TCOAnalytics service to compute user TCO, lifetime TCO, and benchmark.
|
||||
3. Transform results into the response schema.
|
||||
"""
|
||||
try:
|
||||
# Access verification
|
||||
vehicle = await verify_vehicle_access(vehicle_id, db, current_user)
|
||||
|
||||
analytics = TCOAnalytics()
|
||||
|
||||
# 1. User TCO (current user's organization)
|
||||
user_tco_result = await analytics.get_user_tco(
|
||||
db=db,
|
||||
organization_id=current_user.organization_id or vehicle.organization_id,
|
||||
currency_target="HUF",
|
||||
include_categories=None, # all categories
|
||||
)
|
||||
|
||||
# 2. Lifetime TCO (across all owners, anonymized)
|
||||
lifetime_tco_result = await analytics.get_vehicle_lifetime_tco(
|
||||
db=db,
|
||||
vehicle_model_id=vehicle.vehicle_model_id,
|
||||
currency_target="HUF",
|
||||
anonymize=True,
|
||||
)
|
||||
|
||||
# 3. Benchmark TCO (global benchmark for similar vehicles)
|
||||
benchmark_result = await analytics.get_global_benchmark(
|
||||
db=db,
|
||||
vehicle_model_id=vehicle.vehicle_model_id,
|
||||
currency_target="HUF",
|
||||
)
|
||||
|
||||
# Transform results into schema objects
|
||||
# Note: This is a simplified transformation; you may need to adapt based on actual service output.
|
||||
user_tco_list = []
|
||||
if "by_category" in user_tco_result:
|
||||
for cat_code, cat_data in user_tco_result["by_category"].items():
|
||||
# Calculate percentage
|
||||
total = user_tco_result.get("total_amount", 0)
|
||||
percentage = (cat_data["total"] / total * 100) if total > 0 else 0
|
||||
user_tco_list.append({
|
||||
"category_id": 0, # TODO: map from category code to ID
|
||||
"category_code": cat_code,
|
||||
"category_name": cat_data.get("name", cat_code),
|
||||
"amount": cat_data["total"],
|
||||
"currency": user_tco_result.get("currency", "HUF"),
|
||||
"amount_huf": cat_data["total"], # already in HUF
|
||||
"percentage": round(percentage, 2),
|
||||
})
|
||||
|
||||
lifetime_tco_list = []
|
||||
if "by_category" in lifetime_tco_result:
|
||||
for cat_code, cat_data in lifetime_tco_result["by_category"].items():
|
||||
total = lifetime_tco_result.get("total_lifetime_cost", 0)
|
||||
percentage = (cat_data["total"] / total * 100) if total > 0 else 0
|
||||
lifetime_tco_list.append({
|
||||
"category_id": 0,
|
||||
"category_code": cat_code,
|
||||
"category_name": cat_data.get("name", cat_code),
|
||||
"amount": cat_data["total"],
|
||||
"currency": lifetime_tco_result.get("currency", "HUF"),
|
||||
"amount_huf": cat_data["total"],
|
||||
"percentage": round(percentage, 2),
|
||||
})
|
||||
|
||||
benchmark_tco_list = []
|
||||
if "by_category" in benchmark_result:
|
||||
for cat_code, cat_data in benchmark_result["by_category"].items():
|
||||
total = benchmark_result.get("total_cost_sum", 0)
|
||||
percentage = (cat_data["average"] / total * 100) if total > 0 else 0
|
||||
benchmark_tco_list.append({
|
||||
"category_id": 0,
|
||||
"category_code": cat_code,
|
||||
"category_name": cat_data.get("name", cat_code),
|
||||
"amount": cat_data["average"],
|
||||
"currency": benchmark_result.get("currency", "HUF"),
|
||||
"amount_huf": cat_data["average"],
|
||||
"percentage": round(percentage, 2),
|
||||
})
|
||||
|
||||
# Calculate cost per km if odometer data available
|
||||
cost_per_km = None
|
||||
if vehicle.odometer and vehicle.odometer > 0:
|
||||
total_cost = user_tco_result.get("total_amount", 0)
|
||||
cost_per_km = total_cost / vehicle.odometer
|
||||
|
||||
stats = {
|
||||
"total_cost": user_tco_result.get("total_amount", 0),
|
||||
"cost_per_km": cost_per_km,
|
||||
"total_transactions": user_tco_result.get("total_transactions", 0),
|
||||
"date_range": user_tco_result.get("date_range"),
|
||||
}
|
||||
|
||||
return TCOSummaryResponse(
|
||||
vehicle_id=vehicle_id,
|
||||
user_tco=user_tco_list,
|
||||
lifetime_tco=lifetime_tco_list,
|
||||
benchmark_tco=benchmark_tco_list,
|
||||
stats=stats,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error in TCO summary for vehicle {vehicle_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Internal server error: {str(e)}"
|
||||
)
|
||||
@@ -10,7 +10,7 @@ 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'")
|
||||
stmt_limit = text("SELECT (value->>:plan)::int FROM system.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
|
||||
|
||||
|
||||
77
backend/app/api/v1/endpoints/finance_admin.py
Normal file
77
backend/app/api/v1/endpoints/finance_admin.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/finance_admin.py
|
||||
"""
|
||||
Finance Admin API endpoints for managing Issuers with strict RBAC protection.
|
||||
Only users with rank >= 90 (Superadmin/Finance Admin) can access these endpoints.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import List
|
||||
|
||||
from app.api import deps
|
||||
from app.models.identity import User, UserRole
|
||||
from app.models.finance import Issuer
|
||||
from app.schemas.finance import IssuerResponse, IssuerUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def check_finance_admin_access(
|
||||
current_user: User = Depends(deps.get_current_active_user)
|
||||
):
|
||||
"""
|
||||
RBAC protection: only users with rank >= 90 (Superadmin/Finance Admin) can access.
|
||||
In our system, this translates to role being 'superadmin' or 'admin'.
|
||||
"""
|
||||
if current_user.role not in [UserRole.superadmin, UserRole.admin]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not enough permissions. Rank >= 90 (Superadmin/Finance Admin) required."
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
@router.get("/", response_model=List[IssuerResponse], tags=["finance-admin"])
|
||||
async def list_issuers(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_finance_admin_access)
|
||||
):
|
||||
"""
|
||||
List all Issuers (billing entities).
|
||||
Only accessible by Superadmin/Finance Admin (rank >= 90).
|
||||
"""
|
||||
result = await db.execute(select(Issuer).order_by(Issuer.id))
|
||||
issuers = result.scalars().all()
|
||||
return issuers
|
||||
|
||||
|
||||
@router.patch("/{issuer_id}", response_model=IssuerResponse, tags=["finance-admin"])
|
||||
async def update_issuer(
|
||||
issuer_id: int,
|
||||
issuer_update: IssuerUpdate,
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
admin: User = Depends(check_finance_admin_access)
|
||||
):
|
||||
"""
|
||||
Update an Issuer's details (activate/deactivate, revenue limit, API config).
|
||||
Only accessible by Superadmin/Finance Admin (rank >= 90).
|
||||
"""
|
||||
result = await db.execute(select(Issuer).where(Issuer.id == issuer_id))
|
||||
issuer = result.scalar_one_or_none()
|
||||
|
||||
if not issuer:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Issuer with ID {issuer_id} not found."
|
||||
)
|
||||
|
||||
# Update fields if provided
|
||||
update_data = issuer_update.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(issuer, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(issuer)
|
||||
|
||||
return issuer
|
||||
@@ -15,7 +15,7 @@ async def get_vehicle_summary(vehicle_id: str, db: AsyncSession = Depends(get_db
|
||||
category,
|
||||
SUM(amount) as total_amount,
|
||||
COUNT(*) as transaction_count
|
||||
FROM data.vehicle_expenses
|
||||
FROM vehicle.vehicle_expenses
|
||||
WHERE vehicle_id = :v_id
|
||||
GROUP BY category
|
||||
""")
|
||||
@@ -40,7 +40,7 @@ async def get_monthly_trends(vehicle_id: str, db: AsyncSession = Depends(get_db)
|
||||
SELECT
|
||||
TO_CHAR(date, 'YYYY-MM') as month,
|
||||
SUM(amount) as monthly_total
|
||||
FROM data.vehicle_expenses
|
||||
FROM vehicle.vehicle_expenses
|
||||
WHERE vehicle_id = :v_id
|
||||
GROUP BY month
|
||||
ORDER BY month DESC
|
||||
|
||||
@@ -10,12 +10,12 @@ 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)
|
||||
# PostGIS alapú keresés a fleet.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
|
||||
FROM fleet.organizations o
|
||||
JOIN fleet.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
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
from fastapi import APIRouter, Depends, Form, Query, HTTPException
|
||||
from fastapi import APIRouter, Depends, Form, Query, HTTPException, status
|
||||
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
|
||||
from app.services.marketplace_service import (
|
||||
create_verified_review,
|
||||
get_service_reviews,
|
||||
can_user_review_service
|
||||
)
|
||||
from app.schemas.social import ServiceReviewCreate, ServiceReviewResponse
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.identity import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -19,7 +27,7 @@ async def register_service_hunt(
|
||||
""" Ú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)
|
||||
INSERT INTO marketplace.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})
|
||||
|
||||
@@ -55,4 +63,76 @@ async def search_services(
|
||||
result = await db.execute(query.distinct())
|
||||
services = result.scalars().all()
|
||||
|
||||
return services
|
||||
return services
|
||||
|
||||
|
||||
# --- ⭐ VERIFIED SERVICE REVIEWS (Social 3 - #66) ---
|
||||
|
||||
@router.post("/{service_id}/reviews", response_model=ServiceReviewResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_service_review(
|
||||
service_id: int,
|
||||
review_data: ServiceReviewCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Verifikált szerviz értékelés beküldése.
|
||||
Csak igazolt pénzügyi tranzakció után lehetséges (transaction_id kötelező).
|
||||
"""
|
||||
try:
|
||||
review = await create_verified_review(
|
||||
db=db,
|
||||
service_id=service_id,
|
||||
user_id=current_user.id,
|
||||
transaction_id=review_data.transaction_id,
|
||||
review_data=review_data
|
||||
)
|
||||
return review
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
except IntegrityError as e:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{service_id}/reviews", response_model=dict)
|
||||
async def list_service_reviews(
|
||||
service_id: int,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
verified_only: bool = Query(True),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Szerviz értékeléseinek lapozható listázása.
|
||||
"""
|
||||
reviews, total = await get_service_reviews(
|
||||
db=db,
|
||||
service_id=service_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
verified_only=verified_only
|
||||
)
|
||||
return {
|
||||
"reviews": reviews,
|
||||
"total": total,
|
||||
"skip": skip,
|
||||
"limit": limit
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{service_id}/reviews/check")
|
||||
async def check_review_eligibility(
|
||||
service_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Ellenőrzi, hogy a felhasználó értékelheti‑e a szervizt.
|
||||
"""
|
||||
can_review, reason = await can_user_review_service(db, current_user.id, service_id)
|
||||
return {
|
||||
"can_review": can_review,
|
||||
"reason": reason,
|
||||
"user_id": current_user.id,
|
||||
"service_id": service_id
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Dict, Any
|
||||
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.schemas.user import UserResponse
|
||||
from app.models.user import User
|
||||
from app.models.identity import User
|
||||
from app.services.trust_engine import TrustEngine
|
||||
|
||||
router = APIRouter()
|
||||
trust_engine = TrustEngine()
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def read_users_me(
|
||||
@@ -14,3 +17,26 @@ async def read_users_me(
|
||||
):
|
||||
"""Visszaadja a bejelentkezett felhasználó profilját"""
|
||||
return current_user
|
||||
|
||||
@router.get("/me/trust")
|
||||
async def get_user_trust(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
force_recalculate: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Visszaadja a felhasználó Gondos Gazda Index (Trust Score) értékét.
|
||||
|
||||
A számítás dinamikusan betölti a paramétereket a SystemParameter rendszerből
|
||||
(Global/Country/Region/User hierarchia).
|
||||
|
||||
Paraméterek:
|
||||
- force_recalculate: Ha True, akkor újraszámolja a trust score-t
|
||||
(alapértelmezetten cache-elt értéket ad vissza, ha kevesebb mint 24 órája számoltuk)
|
||||
"""
|
||||
trust_data = await trust_engine.calculate_user_trust(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
force_recalculate=force_recalculate
|
||||
)
|
||||
return trust_data
|
||||
|
||||
142
backend/app/api/v1/endpoints/vehicles.py
Normal file
142
backend/app/api/v1/endpoints/vehicles.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
Jármű értékelési végpontok a Social 1 modulhoz.
|
||||
"""
|
||||
import uuid
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.vehicle import VehicleUserRating
|
||||
from app.models.vehicle_definitions import VehicleModelDefinition
|
||||
from app.models.identity import User
|
||||
from app.schemas.vehicle import VehicleRatingCreate, VehicleRatingResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/{vehicle_id}/ratings", response_model=VehicleRatingResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_vehicle_rating(
|
||||
vehicle_id: int,
|
||||
rating: VehicleRatingCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Értékelés beküldése egy járműhöz.
|
||||
Csak a jármű tulajdonosa (vagy jogosult felhasználó) értékelhet.
|
||||
Egy felhasználó csak egyszer értékelhet egy adott járművet.
|
||||
"""
|
||||
# 1. Ellenőrizzük, hogy a jármű létezik-e
|
||||
vehicle = await db.scalar(
|
||||
select(VehicleModelDefinition).where(VehicleModelDefinition.id == vehicle_id)
|
||||
)
|
||||
if not vehicle:
|
||||
raise HTTPException(status_code=404, detail="Jármű nem található")
|
||||
|
||||
# 2. Ellenőrizzük, hogy a felhasználó jogosult-e értékelni (jelenleg csak tulajdonos)
|
||||
# TODO: Később kibővíthető más jogosultságokkal is
|
||||
# Most feltételezzük, hogy mindenki értékelhet, de csak egyszer
|
||||
|
||||
# 3. Ellenőrizzük, hogy már létezik-e értékelés ettől a felhasználótól ehhez a járműhöz
|
||||
existing_rating = await db.scalar(
|
||||
select(VehicleUserRating).where(
|
||||
and_(
|
||||
VehicleUserRating.vehicle_id == vehicle_id,
|
||||
VehicleUserRating.user_id == current_user.id
|
||||
)
|
||||
)
|
||||
)
|
||||
if existing_rating:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Már értékelted ezt a járművet. Csak egy értékelés engedélyezett felhasználónként."
|
||||
)
|
||||
|
||||
# 4. Hozzuk létre az új értékelést
|
||||
new_rating = VehicleUserRating(
|
||||
vehicle_id=vehicle_id,
|
||||
user_id=current_user.id,
|
||||
driving_experience=rating.driving_experience,
|
||||
reliability=rating.reliability,
|
||||
comfort=rating.comfort,
|
||||
consumption_satisfaction=rating.consumption_satisfaction,
|
||||
comment=rating.comment
|
||||
)
|
||||
|
||||
db.add(new_rating)
|
||||
await db.commit()
|
||||
await db.refresh(new_rating)
|
||||
|
||||
# 5. Átlagpontszám számítása
|
||||
average_score = new_rating.average_score
|
||||
|
||||
# 6. Válasz összeállítása
|
||||
return VehicleRatingResponse(
|
||||
id=new_rating.id,
|
||||
vehicle_id=new_rating.vehicle_id,
|
||||
user_id=new_rating.user_id,
|
||||
driving_experience=new_rating.driving_experience,
|
||||
reliability=new_rating.reliability,
|
||||
comfort=new_rating.comfort,
|
||||
consumption_satisfaction=new_rating.consumption_satisfaction,
|
||||
comment=new_rating.comment,
|
||||
average_score=average_score,
|
||||
created_at=new_rating.created_at,
|
||||
updated_at=new_rating.updated_at
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{vehicle_id}/ratings", response_model=List[VehicleRatingResponse])
|
||||
async def get_vehicle_ratings(
|
||||
vehicle_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Az összes értékelés lekérése egy adott járműhöz.
|
||||
"""
|
||||
# Ellenőrizzük, hogy a jármű létezik-e
|
||||
vehicle = await db.scalar(
|
||||
select(VehicleModelDefinition).where(VehicleModelDefinition.id == vehicle_id)
|
||||
)
|
||||
if not vehicle:
|
||||
raise HTTPException(status_code=404, detail="Jármű nem található")
|
||||
|
||||
# Lekérjük az értékeléseket
|
||||
stmt = (
|
||||
select(VehicleUserRating)
|
||||
.where(VehicleUserRating.vehicle_id == vehicle_id)
|
||||
.order_by(VehicleUserRating.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
result = await db.scalars(stmt)
|
||||
ratings = result.all()
|
||||
|
||||
# Átalakítás válasz sémává
|
||||
response_ratings = []
|
||||
for rating in ratings:
|
||||
response_ratings.append(
|
||||
VehicleRatingResponse(
|
||||
id=rating.id,
|
||||
vehicle_id=rating.vehicle_id,
|
||||
user_id=rating.user_id,
|
||||
driving_experience=rating.driving_experience,
|
||||
reliability=rating.reliability,
|
||||
comfort=rating.comfort,
|
||||
consumption_satisfaction=rating.consumption_satisfaction,
|
||||
comment=rating.comment,
|
||||
average_score=rating.average_score,
|
||||
created_at=rating.created_at,
|
||||
updated_at=rating.updated_at
|
||||
)
|
||||
)
|
||||
|
||||
return response_ratings
|
||||
Reference in New Issue
Block a user