201 előtti mentés
This commit is contained in:
@@ -4,7 +4,7 @@ from app.api.v1.endpoints import (
|
||||
auth, catalog, assets, organizations, documents,
|
||||
services, admin, expenses, evidence, social, security,
|
||||
billing, finance_admin, analytics, vehicles, system_parameters,
|
||||
gamification, translations
|
||||
gamification, translations, users, reports
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
@@ -26,4 +26,6 @@ api_router.include_router(analytics.router, prefix="/analytics", tags=["Analytic
|
||||
api_router.include_router(vehicles.router, prefix="/vehicles", tags=["Vehicles"])
|
||||
api_router.include_router(system_parameters.router, prefix="/system/parameters", tags=["System Parameters"])
|
||||
api_router.include_router(gamification.router, prefix="/gamification", tags=["Gamification"])
|
||||
api_router.include_router(translations.router, prefix="/translations", tags=["i18n"])
|
||||
api_router.include_router(translations.router, prefix="/translations", tags=["i18n"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["Users"])
|
||||
api_router.include_router(reports.router, prefix="/reports", tags=["Reports"])
|
||||
@@ -9,7 +9,7 @@ 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.schemas.analytics import TCOSummaryResponse, TCOErrorResponse, DashboardResponse
|
||||
from app.services.analytics_service import TCOAnalytics
|
||||
from app.models import Vehicle
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
@@ -190,6 +190,102 @@ async def get_tco_summary(
|
||||
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)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/dashboard",
|
||||
response_model=DashboardResponse,
|
||||
responses={
|
||||
500: {"model": TCOErrorResponse, "description": "Internal server error"},
|
||||
},
|
||||
summary="Get dashboard analytics data",
|
||||
description="Returns aggregated dashboard data including monthly costs, fuel efficiency trends, "
|
||||
"and business metrics for the user's fleet."
|
||||
)
|
||||
async def get_dashboard_analytics(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
current_user = Depends(deps.get_current_active_user),
|
||||
):
|
||||
"""
|
||||
Retrieve dashboard analytics for the user's fleet.
|
||||
|
||||
This endpoint returns mock data for now, but will be connected to real
|
||||
analytics services in the future.
|
||||
"""
|
||||
try:
|
||||
# For now, return mock data matching the frontend expectations
|
||||
# In production, this would query the database and aggregate real data
|
||||
|
||||
# Import the new schema
|
||||
from app.schemas.analytics import (
|
||||
DashboardResponse, DashboardMonthlyCost, DashboardFuelEfficiency,
|
||||
DashboardCostPerKm, DashboardFunFacts, DashboardBusinessMetrics
|
||||
)
|
||||
|
||||
# Mock monthly costs (last 6 months)
|
||||
monthly_costs = [
|
||||
DashboardMonthlyCost(month="Oct", maintenance=450, fuel=320, insurance=180, total=950),
|
||||
DashboardMonthlyCost(month="Nov", maintenance=520, fuel=310, insurance=180, total=1010),
|
||||
DashboardMonthlyCost(month="Dec", maintenance=380, fuel=290, insurance=180, total=850),
|
||||
DashboardMonthlyCost(month="Jan", maintenance=620, fuel=350, insurance=200, total=1170),
|
||||
DashboardMonthlyCost(month="Feb", maintenance=410, fuel=280, insurance=180, total=870),
|
||||
DashboardMonthlyCost(month="Mar", maintenance=480, fuel=330, insurance=180, total=990),
|
||||
]
|
||||
|
||||
# Mock fuel efficiency trends
|
||||
fuel_efficiency_trends = [
|
||||
DashboardFuelEfficiency(month="Oct", efficiency=12.5),
|
||||
DashboardFuelEfficiency(month="Nov", efficiency=12.8),
|
||||
DashboardFuelEfficiency(month="Dec", efficiency=13.2),
|
||||
DashboardFuelEfficiency(month="Jan", efficiency=12.9),
|
||||
DashboardFuelEfficiency(month="Feb", efficiency=13.5),
|
||||
DashboardFuelEfficiency(month="Mar", efficiency=13.8),
|
||||
]
|
||||
|
||||
# Mock cost per km trends
|
||||
cost_per_km_trends = [
|
||||
DashboardCostPerKm(month="Oct", cost=0.42),
|
||||
DashboardCostPerKm(month="Nov", cost=0.45),
|
||||
DashboardCostPerKm(month="Dec", cost=0.38),
|
||||
DashboardCostPerKm(month="Jan", cost=0.51),
|
||||
DashboardCostPerKm(month="Feb", cost=0.39),
|
||||
DashboardCostPerKm(month="Mar", cost=0.41),
|
||||
]
|
||||
|
||||
# Mock fun facts
|
||||
fun_facts = DashboardFunFacts(
|
||||
total_km_driven=384400,
|
||||
total_trees_saved=42,
|
||||
total_co2_saved=8.5,
|
||||
total_money_saved=12500,
|
||||
moon_trips=1,
|
||||
earth_circuits=10
|
||||
)
|
||||
|
||||
# Mock business metrics
|
||||
business_metrics = DashboardBusinessMetrics(
|
||||
fleet_size=24,
|
||||
average_vehicle_age=3.2,
|
||||
total_monthly_cost=23500,
|
||||
average_cost_per_km=0.43,
|
||||
utilization_rate=78,
|
||||
downtime_hours=42
|
||||
)
|
||||
|
||||
return DashboardResponse(
|
||||
monthly_costs=monthly_costs,
|
||||
fuel_efficiency_trends=fuel_efficiency_trends,
|
||||
cost_per_km_trends=cost_per_km_trends,
|
||||
fun_facts=fun_facts,
|
||||
business_metrics=business_metrics
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error in dashboard analytics: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Internal server error: {str(e)}"
|
||||
|
||||
@@ -18,6 +18,52 @@ from app.schemas.asset import AssetResponse, AssetCreate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/vehicles", response_model=List[AssetResponse])
|
||||
async def get_user_vehicles(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get all vehicles/assets belonging to the current user or their organization.
|
||||
|
||||
This endpoint returns a paginated list of vehicles that the authenticated user
|
||||
has access to (either as owner or through organization membership).
|
||||
"""
|
||||
# Query assets where user is owner or organization member
|
||||
from sqlalchemy import or_
|
||||
|
||||
# First, get user's organization memberships
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
org_stmt = select(OrganizationMember.organization_id).where(
|
||||
OrganizationMember.user_id == current_user.id
|
||||
)
|
||||
org_result = await db.execute(org_stmt)
|
||||
user_org_ids = [row[0] for row in org_result.all()]
|
||||
|
||||
# Build query: assets owned by user OR assets in user's organizations
|
||||
stmt = (
|
||||
select(Asset)
|
||||
.where(
|
||||
or_(
|
||||
Asset.owner_person_id == current_user.id,
|
||||
Asset.owner_org_id.in_(user_org_ids) if user_org_ids else False,
|
||||
Asset.operator_person_id == current_user.id,
|
||||
Asset.operator_org_id.in_(user_org_ids) if user_org_ids else False
|
||||
)
|
||||
)
|
||||
.order_by(Asset.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.options(selectinload(Asset.catalog))
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
assets = result.scalars().all()
|
||||
|
||||
return assets
|
||||
|
||||
@router.get("/{asset_id}/financial-summary", response_model=Dict[str, Any])
|
||||
async def get_asset_financial_report(
|
||||
asset_id: uuid.UUID,
|
||||
|
||||
@@ -3,39 +3,65 @@ 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 import Asset, AssetCost # JAVÍTVA
|
||||
from pydantic import BaseModel
|
||||
from datetime import date
|
||||
from app.models import Asset, AssetCost
|
||||
from app.schemas.asset_cost import AssetCostCreate
|
||||
from datetime import datetime
|
||||
|
||||
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)):
|
||||
@router.post("/", status_code=201)
|
||||
async def create_expense(
|
||||
expense: AssetCostCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Create a new expense (fuel, service, tax, insurance) for an asset.
|
||||
Uses AssetCostCreate schema which includes mileage_at_cost, cost_type, etc.
|
||||
"""
|
||||
# Validate asset exists
|
||||
stmt = select(Asset).where(Asset.id == expense.asset_id)
|
||||
result = await db.execute(stmt)
|
||||
asset = result.scalar_one_or_none()
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="Jármű nem található.")
|
||||
raise HTTPException(status_code=404, detail="Asset not found.")
|
||||
|
||||
# Determine organization_id from asset
|
||||
# Determine organization_id from asset (required by AssetCost model)
|
||||
organization_id = asset.current_organization_id or asset.owner_org_id
|
||||
if not organization_id:
|
||||
raise HTTPException(status_code=400, detail="Az eszközhez nincs társított szervezet.")
|
||||
raise HTTPException(status_code=400, detail="Asset has no associated organization.")
|
||||
|
||||
# Map cost_type to cost_category (AssetCost uses cost_category)
|
||||
cost_category = expense.cost_type
|
||||
|
||||
# Prepare data JSON for extra fields (mileage_at_cost, description, etc.)
|
||||
data = expense.data.copy() if expense.data else {}
|
||||
if expense.mileage_at_cost is not None:
|
||||
data["mileage_at_cost"] = expense.mileage_at_cost
|
||||
if expense.description:
|
||||
data["description"] = expense.description
|
||||
|
||||
# Create AssetCost instance
|
||||
new_cost = AssetCost(
|
||||
asset_id=expense.asset_id,
|
||||
cost_category=expense.category,
|
||||
amount_net=expense.amount,
|
||||
currency="HUF",
|
||||
organization_id=organization_id,
|
||||
cost_category=cost_category,
|
||||
amount_net=expense.amount_local,
|
||||
currency=expense.currency_local,
|
||||
date=expense.date,
|
||||
organization_id=organization_id
|
||||
invoice_number=data.get("invoice_number"),
|
||||
data=data
|
||||
)
|
||||
|
||||
db.add(new_cost)
|
||||
await db.commit()
|
||||
return {"status": "success"}
|
||||
await db.refresh(new_cost)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"id": new_cost.id,
|
||||
"asset_id": new_cost.asset_id,
|
||||
"cost_category": new_cost.cost_category,
|
||||
"amount_net": new_cost.amount_net,
|
||||
"date": new_cost.date
|
||||
}
|
||||
@@ -472,4 +472,455 @@ async def get_leaderboard_top10(
|
||||
current_level=stats.current_level
|
||||
)
|
||||
)
|
||||
return leaderboard
|
||||
return leaderboard
|
||||
|
||||
|
||||
# --- QUIZ ENDPOINTS FOR DAILY QUIZ GAMIFICATION ---
|
||||
|
||||
@router.get("/quiz/daily")
|
||||
async def get_daily_quiz(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returns daily quiz questions for the user.
|
||||
Checks if user has already played today.
|
||||
"""
|
||||
# Check if user has already played today
|
||||
today = datetime.now().date()
|
||||
stmt = select(PointsLedger).where(
|
||||
PointsLedger.user_id == current_user.id,
|
||||
func.date(PointsLedger.created_at) == today,
|
||||
PointsLedger.reason.ilike("%quiz%")
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
already_played = result.scalar_one_or_none()
|
||||
|
||||
if already_played:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="You have already played the daily quiz today. Try again tomorrow."
|
||||
)
|
||||
|
||||
# Return quiz questions (for now, using mock questions - in production these would come from a database)
|
||||
quiz_questions = [
|
||||
{
|
||||
"id": 1,
|
||||
"question": "Melyik alkatrész felelős a motor levegő‑üzemanyag keverékének szabályozásáért?",
|
||||
"options": ["Generátor", "Lambda‑szonda", "Féktárcsa", "Olajszűrő"],
|
||||
"correctAnswer": 1,
|
||||
"explanation": "A lambda‑szonda méri a kipufogógáz oxigéntartalmát, és ezen alapul a befecskendezés."
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"question": "Mennyi ideig érvényes egy gépjármű műszaki vizsgája Magyarországon?",
|
||||
"options": ["1 év", "2 év", "4 év", "6 év"],
|
||||
"correctAnswer": 1,
|
||||
"explanation": "A személygépkocsik műszaki vizsgája 2 évre érvényes, kivéve az újonnan forgalomba helyezett autókat."
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"question": "Melyik anyag NEM része a hibrid autók akkumulátorának?",
|
||||
"options": ["Lítium", "Nikkel", "Ólom", "Kobalt"],
|
||||
"correctAnswer": 2,
|
||||
"explanation": "A hibrid és elektromos autók akkumulátoraiban általában lítium, nikkel és kobalt található, ólom az ólom‑savas akkukban van."
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
"questions": quiz_questions,
|
||||
"total_questions": len(quiz_questions),
|
||||
"date": today.isoformat()
|
||||
}
|
||||
|
||||
|
||||
@router.post("/quiz/answer")
|
||||
async def submit_quiz_answer(
|
||||
question_id: int = Body(...),
|
||||
selected_option: int = Body(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Submit answer to a quiz question and award points if correct.
|
||||
"""
|
||||
# Check if user has already played today
|
||||
today = datetime.now().date()
|
||||
stmt = select(PointsLedger).where(
|
||||
PointsLedger.user_id == current_user.id,
|
||||
func.date(PointsLedger.created_at) == today,
|
||||
PointsLedger.reason.ilike("%quiz%")
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
already_played = result.scalar_one_or_none()
|
||||
|
||||
if already_played:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="You have already played the daily quiz today. Try again tomorrow."
|
||||
)
|
||||
|
||||
# Mock quiz data - in production this would come from a database
|
||||
quiz_data = {
|
||||
1: {"correct_answer": 1, "points": 10, "explanation": "A lambda‑szonda méri a kipufogógáz oxigéntartalmát, és ezen alapul a befecskendezés."},
|
||||
2: {"correct_answer": 1, "points": 10, "explanation": "A személygépkocsik műszaki vizsgája 2 évre érvényes, kivéve az újonnan forgalomba helyezett autókat."},
|
||||
3: {"correct_answer": 2, "points": 10, "explanation": "A hibrid és elektromos autók akkumulátoraiban általában lítium, nikkel és kobalt található, ólom az ólom‑savas akkukban van."}
|
||||
}
|
||||
|
||||
if question_id not in quiz_data:
|
||||
raise HTTPException(status_code=404, detail="Question not found")
|
||||
|
||||
question_info = quiz_data[question_id]
|
||||
is_correct = selected_option == question_info["correct_answer"]
|
||||
|
||||
# Award points if correct
|
||||
if is_correct:
|
||||
# Update user stats
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
stats_result = await db.execute(stats_stmt)
|
||||
user_stats = stats_result.scalar_one_or_none()
|
||||
|
||||
if not user_stats:
|
||||
# Create user stats if they don't exist
|
||||
user_stats = UserStats(
|
||||
user_id=current_user.id,
|
||||
total_xp=question_info["points"],
|
||||
current_level=1
|
||||
)
|
||||
db.add(user_stats)
|
||||
else:
|
||||
user_stats.total_xp += question_info["points"]
|
||||
|
||||
# Add points ledger entry
|
||||
points_ledger = PointsLedger(
|
||||
user_id=current_user.id,
|
||||
points=question_info["points"],
|
||||
reason=f"Daily quiz correct answer - Question {question_id}",
|
||||
created_at=datetime.now()
|
||||
)
|
||||
db.add(points_ledger)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"is_correct": is_correct,
|
||||
"correct_answer": question_info["correct_answer"],
|
||||
"points_awarded": question_info["points"] if is_correct else 0,
|
||||
"explanation": question_info["explanation"]
|
||||
}
|
||||
|
||||
|
||||
@router.post("/quiz/complete")
|
||||
async def complete_daily_quiz(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Mark daily quiz as completed for today.
|
||||
This prevents the user from playing again today.
|
||||
"""
|
||||
today = datetime.now().date()
|
||||
|
||||
# Check if already completed today
|
||||
stmt = select(PointsLedger).where(
|
||||
PointsLedger.user_id == current_user.id,
|
||||
func.date(PointsLedger.created_at) == today,
|
||||
PointsLedger.reason == "Daily quiz completed"
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
already_completed = result.scalar_one_or_none()
|
||||
|
||||
if already_completed:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Daily quiz already marked as completed today."
|
||||
)
|
||||
|
||||
# Add completion entry
|
||||
completion_ledger = PointsLedger(
|
||||
user_id=current_user.id,
|
||||
points=0,
|
||||
reason="Daily quiz completed",
|
||||
created_at=datetime.now()
|
||||
)
|
||||
db.add(completion_ledger)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Daily quiz marked as completed for today."}
|
||||
|
||||
|
||||
@router.get("/quiz/stats")
|
||||
async def get_quiz_stats(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get user's quiz statistics including points, streak, and last played date.
|
||||
"""
|
||||
# Get user stats
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
stats_result = await db.execute(stats_stmt)
|
||||
user_stats = stats_result.scalar_one_or_none()
|
||||
|
||||
# Get quiz points from ledger
|
||||
points_stmt = select(func.sum(PointsLedger.points)).where(
|
||||
PointsLedger.user_id == current_user.id,
|
||||
PointsLedger.reason.ilike("%quiz%")
|
||||
)
|
||||
points_result = await db.execute(points_stmt)
|
||||
quiz_points = points_result.scalar() or 0
|
||||
|
||||
# Get last played date
|
||||
last_played_stmt = select(PointsLedger.created_at).where(
|
||||
PointsLedger.user_id == current_user.id,
|
||||
PointsLedger.reason.ilike("%quiz%")
|
||||
).order_by(desc(PointsLedger.created_at)).limit(1)
|
||||
last_played_result = await db.execute(last_played_stmt)
|
||||
last_played = last_played_result.scalar()
|
||||
|
||||
# Calculate streak (simplified - in production would be more sophisticated)
|
||||
streak = 0
|
||||
if last_played:
|
||||
# Simple streak calculation - check last 7 days
|
||||
streak = 1 # Placeholder
|
||||
|
||||
return {
|
||||
"total_quiz_points": quiz_points,
|
||||
"total_xp": user_stats.total_xp if user_stats else 0,
|
||||
"current_level": user_stats.current_level if user_stats else 1,
|
||||
"last_played": last_played.isoformat() if last_played else None,
|
||||
"current_streak": streak,
|
||||
"can_play_today": not await has_played_today(db, current_user.id)
|
||||
}
|
||||
|
||||
|
||||
async def has_played_today(db: AsyncSession, user_id: int) -> bool:
|
||||
"""Check if user has already played quiz today."""
|
||||
today = datetime.now().date()
|
||||
stmt = select(PointsLedger).where(
|
||||
PointsLedger.user_id == user_id,
|
||||
func.date(PointsLedger.created_at) == today,
|
||||
PointsLedger.reason.ilike("%quiz%")
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
|
||||
# --- BADGE/TROPHY ENDPOINTS ---
|
||||
|
||||
@router.get("/badges")
|
||||
async def get_all_badges(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get all available badges in the system.
|
||||
"""
|
||||
stmt = select(Badge)
|
||||
result = await db.execute(stmt)
|
||||
badges = result.scalars().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": badge.id,
|
||||
"name": badge.name,
|
||||
"description": badge.description,
|
||||
"icon_url": badge.icon_url
|
||||
}
|
||||
for badge in badges
|
||||
]
|
||||
|
||||
|
||||
@router.get("/my-badges")
|
||||
async def get_my_badges(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get badges earned by the current user.
|
||||
"""
|
||||
stmt = (
|
||||
select(UserBadge, Badge)
|
||||
.join(Badge, UserBadge.badge_id == Badge.id)
|
||||
.where(UserBadge.user_id == current_user.id)
|
||||
.order_by(desc(UserBadge.earned_at))
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
user_badges = result.all()
|
||||
|
||||
return [
|
||||
{
|
||||
"badge_id": badge.id,
|
||||
"badge_name": badge.name,
|
||||
"badge_description": badge.description,
|
||||
"badge_icon_url": badge.icon_url,
|
||||
"earned_at": user_badge.earned_at.isoformat() if user_badge.earned_at else None
|
||||
}
|
||||
for user_badge, badge in user_badges
|
||||
]
|
||||
|
||||
|
||||
@router.post("/badges/award/{badge_id}")
|
||||
async def award_badge_to_user(
|
||||
badge_id: int,
|
||||
user_id: int = Body(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Award a badge to a user (admin only or automated system).
|
||||
"""
|
||||
# Check if badge exists
|
||||
badge_stmt = select(Badge).where(Badge.id == badge_id)
|
||||
badge_result = await db.execute(badge_stmt)
|
||||
badge = badge_result.scalar_one_or_none()
|
||||
|
||||
if not badge:
|
||||
raise HTTPException(status_code=404, detail="Badge not found")
|
||||
|
||||
# Determine target user (default to current user if not specified)
|
||||
target_user_id = user_id if user_id else current_user.id
|
||||
|
||||
# Check if user already has this badge
|
||||
existing_stmt = select(UserBadge).where(
|
||||
UserBadge.user_id == target_user_id,
|
||||
UserBadge.badge_id == badge_id
|
||||
)
|
||||
existing_result = await db.execute(existing_stmt)
|
||||
existing = existing_result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="User already has this badge")
|
||||
|
||||
# Award the badge
|
||||
user_badge = UserBadge(
|
||||
user_id=target_user_id,
|
||||
badge_id=badge_id,
|
||||
earned_at=datetime.now()
|
||||
)
|
||||
db.add(user_badge)
|
||||
|
||||
# Also add points for earning a badge
|
||||
points_ledger = PointsLedger(
|
||||
user_id=target_user_id,
|
||||
points=50, # Points for earning a badge
|
||||
reason=f"Badge earned: {badge.name}",
|
||||
created_at=datetime.now()
|
||||
)
|
||||
db.add(points_ledger)
|
||||
|
||||
# Update user stats
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == target_user_id)
|
||||
stats_result = await db.execute(stats_stmt)
|
||||
user_stats = stats_result.scalar_one_or_none()
|
||||
|
||||
if user_stats:
|
||||
user_stats.total_xp += 50
|
||||
else:
|
||||
user_stats = UserStats(
|
||||
user_id=target_user_id,
|
||||
total_xp=50,
|
||||
current_level=1
|
||||
)
|
||||
db.add(user_stats)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"message": f"Badge '{badge.name}' awarded to user",
|
||||
"badge_id": badge.id,
|
||||
"badge_name": badge.name,
|
||||
"points_awarded": 50
|
||||
}
|
||||
|
||||
|
||||
@router.get("/achievements")
|
||||
async def get_achievements_progress(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get user's progress on various achievements (combines badges and other metrics).
|
||||
"""
|
||||
# Get user badges
|
||||
badges_stmt = select(UserBadge.badge_id).where(UserBadge.user_id == current_user.id)
|
||||
badges_result = await db.execute(badges_stmt)
|
||||
user_badge_ids = [row[0] for row in badges_result.all()]
|
||||
|
||||
# Get all badges
|
||||
all_badges_stmt = select(Badge)
|
||||
all_badges_result = await db.execute(all_badges_stmt)
|
||||
all_badges = all_badges_result.scalars().all()
|
||||
|
||||
# Get user stats
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == current_user.id)
|
||||
stats_result = await db.execute(stats_stmt)
|
||||
user_stats = stats_result.scalar_one_or_none()
|
||||
|
||||
# Define achievement categories
|
||||
achievements = []
|
||||
|
||||
# Badge-based achievements
|
||||
for badge in all_badges:
|
||||
achievements.append({
|
||||
"id": f"badge_{badge.id}",
|
||||
"title": badge.name,
|
||||
"description": badge.description,
|
||||
"icon_url": badge.icon_url,
|
||||
"is_earned": badge.id in user_badge_ids,
|
||||
"category": "badge",
|
||||
"progress": 100 if badge.id in user_badge_ids else 0
|
||||
})
|
||||
|
||||
# XP-based achievements
|
||||
xp_levels = [
|
||||
{"title": "Novice", "xp_required": 100, "description": "Earn 100 XP"},
|
||||
{"title": "Apprentice", "xp_required": 500, "description": "Earn 500 XP"},
|
||||
{"title": "Expert", "xp_required": 2000, "description": "Earn 2000 XP"},
|
||||
{"title": "Master", "xp_required": 5000, "description": "Earn 5000 XP"},
|
||||
]
|
||||
|
||||
current_xp = user_stats.total_xp if user_stats else 0
|
||||
for level in xp_levels:
|
||||
progress = min((current_xp / level["xp_required"]) * 100, 100)
|
||||
achievements.append({
|
||||
"id": f"xp_{level['xp_required']}",
|
||||
"title": level["title"],
|
||||
"description": level["description"],
|
||||
"icon_url": None,
|
||||
"is_earned": current_xp >= level["xp_required"],
|
||||
"category": "xp",
|
||||
"progress": progress
|
||||
})
|
||||
|
||||
# Quiz-based achievements
|
||||
quiz_points_stmt = select(func.sum(PointsLedger.points)).where(
|
||||
PointsLedger.user_id == current_user.id,
|
||||
PointsLedger.reason.ilike("%quiz%")
|
||||
)
|
||||
quiz_points_result = await db.execute(quiz_points_stmt)
|
||||
quiz_points = quiz_points_result.scalar() or 0
|
||||
|
||||
quiz_achievements = [
|
||||
{"title": "Quiz Beginner", "points_required": 50, "description": "Earn 50 quiz points"},
|
||||
{"title": "Quiz Enthusiast", "points_required": 200, "description": "Earn 200 quiz points"},
|
||||
{"title": "Quiz Master", "points_required": 500, "description": "Earn 500 quiz points"},
|
||||
]
|
||||
|
||||
for achievement in quiz_achievements:
|
||||
progress = min((quiz_points / achievement["points_required"]) * 100, 100)
|
||||
achievements.append({
|
||||
"id": f"quiz_{achievement['points_required']}",
|
||||
"title": achievement["title"],
|
||||
"description": achievement["description"],
|
||||
"icon_url": None,
|
||||
"is_earned": quiz_points >= achievement["points_required"],
|
||||
"category": "quiz",
|
||||
"progress": progress
|
||||
})
|
||||
|
||||
return {
|
||||
"achievements": achievements,
|
||||
"total_achievements": len(achievements),
|
||||
"earned_count": sum(1 for a in achievements if a["is_earned"]),
|
||||
"progress_percentage": round((sum(1 for a in achievements if a["is_earned"]) / len(achievements)) * 100, 1) if achievements else 0
|
||||
}
|
||||
@@ -37,7 +37,7 @@ async def get_monthly_trends(vehicle_id: str, db: AsyncSession = Depends(get_db)
|
||||
Visszaadja az utolsó 6 hónap költéseit havi bontásban.
|
||||
"""
|
||||
query = text("""
|
||||
SELECT
|
||||
SELECT
|
||||
TO_CHAR(date, 'YYYY-MM') as month,
|
||||
SUM(amount) as monthly_total
|
||||
FROM vehicle.vehicle_expenses
|
||||
@@ -47,4 +47,19 @@ async def get_monthly_trends(vehicle_id: str, db: AsyncSession = Depends(get_db)
|
||||
LIMIT 6
|
||||
""")
|
||||
result = await db.execute(query, {"v_id": vehicle_id})
|
||||
return [dict(row._mapping) for row in result.fetchall()]
|
||||
return [dict(row._mapping) for row in result.fetchall()]
|
||||
|
||||
@router.get("/summary/latest")
|
||||
async def get_latest_summary(db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
"""
|
||||
Returns a simple summary for the dashboard (mock data for now).
|
||||
This endpoint is called by the frontend dashboard.
|
||||
"""
|
||||
# For now, return mock data to satisfy the frontend
|
||||
return {
|
||||
"total_vehicles": 4,
|
||||
"total_cost_this_month": 1250.50,
|
||||
"most_expensive_category": "Fuel",
|
||||
"trend": "down",
|
||||
"trend_percentage": -5.2
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
#/opt/docker/dev/service_finder/backend/app/api/v1/endpoints/users.py
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
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.schemas.user import UserResponse, UserUpdate
|
||||
from app.models.identity import User
|
||||
from app.services.trust_engine import TrustEngine
|
||||
|
||||
@@ -41,3 +41,34 @@ async def get_user_trust(
|
||||
force_recalculate=force_recalculate
|
||||
)
|
||||
return trust_data
|
||||
|
||||
|
||||
@router.patch("/me/preferences", response_model=UserResponse)
|
||||
async def update_user_preferences(
|
||||
update_data: UserUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update user preferences (ui_mode, preferred_language, etc.)
|
||||
"""
|
||||
# Filter out None values
|
||||
update_dict = update_data.dict(exclude_unset=True)
|
||||
if not update_dict:
|
||||
raise HTTPException(status_code=400, detail="No fields to update")
|
||||
|
||||
# Validate ui_mode if present
|
||||
if "ui_mode" in update_dict:
|
||||
if update_dict["ui_mode"] not in ["personal", "fleet"]:
|
||||
raise HTTPException(status_code=422, detail="ui_mode must be 'personal' or 'fleet'")
|
||||
|
||||
# Update user fields
|
||||
for field, value in update_dict.items():
|
||||
if hasattr(current_user, field):
|
||||
setattr(current_user, field, value)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid field: {field}")
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(current_user)
|
||||
return current_user
|
||||
|
||||
@@ -85,11 +85,18 @@ class Settings(BaseSettings):
|
||||
SMTP_PASSWORD: Optional[str] = None
|
||||
|
||||
# --- External URLs ---
|
||||
FRONTEND_BASE_URL: str = "https://dev.profibot.hu"
|
||||
FRONTEND_BASE_URL: str = "https://dev.servicefinder.hu"
|
||||
BACKEND_CORS_ORIGINS: List[str] = Field(
|
||||
default=[
|
||||
"http://localhost:3001",
|
||||
"https://dev.profibot.hu"
|
||||
# Production domains
|
||||
"https://app.servicefinder.hu", # Production Public UI
|
||||
"https://admin.servicefinder.hu", # Production Admin UI
|
||||
"https://dev.servicefinder.hu", # API domain itself
|
||||
|
||||
# Development and internal fallbacks
|
||||
"http://192.168.100.10:8503", # Internal IP fallback
|
||||
"http://localhost:5173", # Local dev fallback (Vite)
|
||||
"http://localhost:3001", # Local dev fallback (Nuxt/other)
|
||||
],
|
||||
description="Comma-separated list of allowed CORS origins. Set via ALLOWED_ORIGINS environment variable."
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ if TYPE_CHECKING:
|
||||
from .gamification import UserStats
|
||||
from .payment import PaymentIntent, WithdrawalRequest
|
||||
from .social import ServiceReview, SocialAccount
|
||||
from ..marketplace.service_request import ServiceRequest
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
superadmin = "superadmin"
|
||||
@@ -135,6 +136,7 @@ class User(Base):
|
||||
preferred_language: Mapped[str] = mapped_column(String(5), server_default="hu")
|
||||
region_code: Mapped[str] = mapped_column(String(5), server_default="HU")
|
||||
preferred_currency: Mapped[str] = mapped_column(String(3), server_default="HUF")
|
||||
ui_mode: Mapped[str] = mapped_column(String(20), server_default="personal")
|
||||
|
||||
scope_level: Mapped[str] = mapped_column(String(30), server_default="individual")
|
||||
scope_id: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
@@ -182,6 +184,7 @@ class User(Base):
|
||||
# Pénzügyi és egyéb kapcsolatok
|
||||
withdrawal_requests: Mapped[List["WithdrawalRequest"]] = relationship("WithdrawalRequest", foreign_keys="[WithdrawalRequest.user_id]", back_populates="user", cascade="all, delete-orphan")
|
||||
service_reviews: Mapped[List["ServiceReview"]] = relationship("ServiceReview", back_populates="user", cascade="all, delete-orphan")
|
||||
service_requests: Mapped[List["ServiceRequest"]] = relationship("ServiceRequest", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
class Wallet(Base):
|
||||
""" Felhasználói pénztárca. """
|
||||
|
||||
@@ -15,6 +15,7 @@ from .service import (
|
||||
ServiceProfile,
|
||||
ExpertiseTag,
|
||||
ServiceExpertise,
|
||||
Cost,
|
||||
)
|
||||
|
||||
from .logistics import Location, LocationType
|
||||
@@ -44,6 +45,7 @@ __all__ = [
|
||||
"ServiceProfile",
|
||||
"ExpertiseTag",
|
||||
"ServiceExpertise",
|
||||
"Cost",
|
||||
"ServiceStaging",
|
||||
"DiscoveryParameter",
|
||||
"Location",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import enum
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, List, Optional
|
||||
from typing import Any, List, Optional, TYPE_CHECKING
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Numeric, BigInteger, Float
|
||||
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID, JSONB
|
||||
@@ -13,6 +13,9 @@ from geoalchemy2 import Geometry
|
||||
# MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
|
||||
from app.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .service_request import ServiceRequest
|
||||
|
||||
class OrgType(str, enum.Enum):
|
||||
individual = "individual"
|
||||
service = "service"
|
||||
@@ -222,6 +225,13 @@ class Branch(Base):
|
||||
|
||||
# Kapcsolatok (Primaryjoin tartva a rating rendszerhez)
|
||||
reviews: Mapped[List["Rating"]] = relationship(
|
||||
"Rating",
|
||||
"Rating",
|
||||
primaryjoin="and_(Branch.id==foreign(Rating.target_branch_id))"
|
||||
)
|
||||
|
||||
# Kapcsolat a ServiceRequest modellel
|
||||
service_requests: Mapped[List["ServiceRequest"]] = relationship(
|
||||
"ServiceRequest",
|
||||
back_populates="branch",
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
@@ -156,4 +156,21 @@ class DiscoveryParameter(Base):
|
||||
city: Mapped[str] = mapped_column(String(100))
|
||||
keyword: Mapped[str] = mapped_column(String(100))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
|
||||
class Cost(Base):
|
||||
""" Költségnapló a trust engine számára. """
|
||||
__tablename__ = "costs"
|
||||
__table_args__ = {"schema": "marketplace"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
vehicle_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||||
category: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
||||
amount: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
|
||||
currency: Mapped[str] = mapped_column(String(3), default="HUF")
|
||||
odometer_km: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
occurrence_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
|
||||
@@ -4,7 +4,7 @@ ServiceRequest - Piactér központi tranzakciós modellje.
|
||||
Epic 7: Marketplace ServiceRequest dedikált modell.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from datetime import datetime
|
||||
from sqlalchemy import String, ForeignKey, Text, DateTime, Numeric, Integer, Index
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -12,6 +12,11 @@ from sqlalchemy.sql import func
|
||||
|
||||
from app.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..identity.identity import User
|
||||
from ..vehicle.asset import Asset
|
||||
from ..marketplace.service import Branch
|
||||
|
||||
|
||||
class ServiceRequest(Base):
|
||||
"""
|
||||
@@ -87,9 +92,9 @@ class ServiceRequest(Base):
|
||||
)
|
||||
|
||||
# Relationships (opcionális, de ajánlott a lazy loading miatt)
|
||||
user = relationship("User", back_populates="service_requests", lazy="selectin")
|
||||
asset = relationship("Asset", back_populates="service_requests", lazy="selectin")
|
||||
branch = relationship("Branch", back_populates="service_requests", lazy="selectin")
|
||||
user: Mapped["User"] = relationship("User", back_populates="service_requests", lazy="selectin")
|
||||
asset: Mapped[Optional["Asset"]] = relationship("Asset", back_populates="service_requests", lazy="selectin")
|
||||
branch: Mapped[Optional["Branch"]] = relationship("Branch", back_populates="service_requests", lazy="selectin")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ServiceRequest(id={self.id}, status='{self.status}', user_id={self.user_id})>"
|
||||
@@ -13,6 +13,7 @@ class ParameterScope(str, Enum):
|
||||
GLOBAL = "global"
|
||||
COUNTRY = "country"
|
||||
REGION = "region"
|
||||
ORGANIZATION = "organization"
|
||||
USER = "user"
|
||||
|
||||
class SystemParameter(Base):
|
||||
|
||||
@@ -39,7 +39,7 @@ class Asset(Base):
|
||||
__table_args__ = {"schema": "vehicle"}
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
vin: Mapped[str] = mapped_column(String(17), unique=True, index=True, nullable=False)
|
||||
vin: Mapped[Optional[str]] = mapped_column(String(17), unique=True, index=True, nullable=True)
|
||||
license_plate: Mapped[Optional[str]] = mapped_column(String(20), index=True)
|
||||
name: Mapped[Optional[str]] = mapped_column(String)
|
||||
|
||||
@@ -79,6 +79,7 @@ class Asset(Base):
|
||||
telemetry: Mapped[Optional["AssetTelemetry"]] = relationship("AssetTelemetry", back_populates="asset", uselist=False)
|
||||
assignments: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="asset")
|
||||
ownership_history: Mapped[List["VehicleOwnership"]] = relationship("VehicleOwnership", back_populates="asset")
|
||||
service_requests: Mapped[List["ServiceRequest"]] = relationship("ServiceRequest", back_populates="asset", cascade="all, delete-orphan")
|
||||
|
||||
# --- COMPUTED PROPERTIES (for Pydantic schema compatibility) ---
|
||||
@property
|
||||
@@ -255,4 +256,21 @@ class CatalogDiscovery(Base):
|
||||
attempts: Mapped[int] = mapped_column(Integer, server_default=text("0"))
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
class VehicleExpenses(Base):
|
||||
""" Jármű költségek a jelentésekhez. """
|
||||
__tablename__ = "vehicle_expenses"
|
||||
__table_args__ = {"schema": "vehicle"}
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
vehicle_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), nullable=False, index=True)
|
||||
category: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
||||
amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
|
||||
date: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationship
|
||||
asset: Mapped["Asset"] = relationship("Asset")
|
||||
@@ -43,4 +43,72 @@ class TCOSummaryResponse(BaseModel):
|
||||
class TCOErrorResponse(BaseModel):
|
||||
"""Error response for TCO endpoints."""
|
||||
detail: str = Field(..., description="Error description")
|
||||
vehicle_id: Optional[int] = Field(None, description="Related vehicle ID if applicable")
|
||||
vehicle_id: Optional[int] = Field(None, description="Related vehicle ID if applicable")
|
||||
|
||||
|
||||
class DashboardMonthlyCost(BaseModel):
|
||||
"""Monthly cost data for dashboard charts."""
|
||||
month: str = Field(..., description="Month abbreviation (e.g., 'Jan', 'Feb')")
|
||||
maintenance: float = Field(..., description="Maintenance costs")
|
||||
fuel: float = Field(..., description="Fuel costs")
|
||||
insurance: float = Field(..., description="Insurance costs")
|
||||
total: float = Field(..., description="Total monthly cost")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class DashboardFuelEfficiency(BaseModel):
|
||||
"""Fuel efficiency trend data."""
|
||||
month: str = Field(..., description="Month abbreviation")
|
||||
efficiency: float = Field(..., description="Fuel efficiency in km per liter")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class DashboardCostPerKm(BaseModel):
|
||||
"""Cost per km trend data."""
|
||||
month: str = Field(..., description="Month abbreviation")
|
||||
cost: float = Field(..., description="Cost per kilometer")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class DashboardFunFacts(BaseModel):
|
||||
"""Fun facts for dashboard."""
|
||||
total_km_driven: float = Field(..., description="Total kilometers driven")
|
||||
total_trees_saved: int = Field(..., description="Total trees saved (eco metric)")
|
||||
total_co2_saved: float = Field(..., description="Total CO2 saved in tons")
|
||||
total_money_saved: float = Field(..., description="Total money saved in EUR")
|
||||
moon_trips: int = Field(..., description="Number of moon trips equivalent")
|
||||
earth_circuits: int = Field(..., description="Number of Earth circuits equivalent")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class DashboardBusinessMetrics(BaseModel):
|
||||
"""Business metrics for fleet management."""
|
||||
fleet_size: int = Field(..., description="Number of vehicles in fleet")
|
||||
average_vehicle_age: float = Field(..., description="Average vehicle age in years")
|
||||
total_monthly_cost: float = Field(..., description="Total monthly cost for fleet")
|
||||
average_cost_per_km: float = Field(..., description="Average cost per kilometer")
|
||||
utilization_rate: float = Field(..., description="Fleet utilization rate in percentage")
|
||||
downtime_hours: int = Field(..., description="Total downtime hours per month")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class DashboardResponse(BaseModel):
|
||||
"""Complete dashboard data response."""
|
||||
monthly_costs: List[DashboardMonthlyCost] = Field(..., description="Monthly cost breakdown")
|
||||
fuel_efficiency_trends: List[DashboardFuelEfficiency] = Field(..., description="Fuel efficiency trends")
|
||||
cost_per_km_trends: List[DashboardCostPerKm] = Field(..., description="Cost per km trends")
|
||||
fun_facts: DashboardFunFacts = Field(..., description="Fun facts and eco metrics")
|
||||
business_metrics: DashboardBusinessMetrics = Field(..., description="Business metrics")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -32,7 +32,7 @@ class AssetCatalogResponse(BaseModel):
|
||||
class AssetResponse(BaseModel):
|
||||
""" A konkrét járműpéldány (Asset) teljes válaszmodellje. """
|
||||
id: UUID
|
||||
vin: str = Field(..., min_length=17, max_length=17)
|
||||
vin: str = Field(..., min_length=1, max_length=50)
|
||||
license_plate: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
year_of_manufacture: Optional[int] = None
|
||||
@@ -58,7 +58,7 @@ class AssetResponse(BaseModel):
|
||||
|
||||
class AssetCreate(BaseModel):
|
||||
""" Jármű létrehozásához szükséges adatok. """
|
||||
vin: str = Field(..., min_length=17, max_length=17, description="VIN szám (17 karakter)")
|
||||
vin: Optional[str] = Field(None, min_length=1, max_length=50, description="VIN szám (1-50 karakter, opcionális draft módban)")
|
||||
license_plate: str = Field(..., min_length=2, max_length=20, description="Rendszám")
|
||||
catalog_id: Optional[int] = Field(None, description="Opcionális katalógus ID (ha ismert a modell)")
|
||||
organization_id: int = Field(..., description="Szervezet ID, amelyhez a jármű tartozik")
|
||||
@@ -17,9 +17,11 @@ class UserResponse(UserBase):
|
||||
subscription_plan: str
|
||||
scope_level: str
|
||||
scope_id: Optional[str] = None
|
||||
ui_mode: str = "personal"
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
preferred_language: Optional[str] = None
|
||||
preferred_language: Optional[str] = None
|
||||
ui_mode: Optional[str] = None
|
||||
95
backend/app/scripts/reset_admin_pass.py
Normal file
95
backend/app/scripts/reset_admin_pass.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reset admin password script.
|
||||
Updates the hashed_password for superadmin@profibot.hu in the identity.users table.
|
||||
Sets password to Admin123! using the system's get_password_hash function.
|
||||
Ensures is_active is set to True.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the backend directory to the Python path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from sqlalchemy import update, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import AsyncSessionLocal, engine
|
||||
from app.models.identity.identity import User
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
|
||||
async def reset_admin_password():
|
||||
"""Reset password for superadmin@profibot.hu"""
|
||||
email = "superadmin@profibot.hu"
|
||||
new_password = "Admin123!"
|
||||
|
||||
print(f"🔧 Resetting password for {email}...")
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
# First, check if the user exists
|
||||
stmt = select(User).where(User.email == email)
|
||||
result = await session.execute(stmt)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
print(f"❌ User {email} not found in database!")
|
||||
return False
|
||||
|
||||
print(f"✅ Found user: ID={user.id}, email={user.email}, is_active={user.is_active}")
|
||||
|
||||
# Generate new password hash
|
||||
hashed_password = get_password_hash(new_password)
|
||||
print(f"🔐 Generated password hash: {hashed_password[:20]}...")
|
||||
|
||||
# Update the user
|
||||
update_stmt = (
|
||||
update(User)
|
||||
.where(User.email == email)
|
||||
.values(
|
||||
hashed_password=hashed_password,
|
||||
is_active=True
|
||||
)
|
||||
)
|
||||
await session.execute(update_stmt)
|
||||
await session.commit()
|
||||
|
||||
print(f"✅ Password updated successfully!")
|
||||
print(f"📋 New credentials:")
|
||||
print(f" Email: {email}")
|
||||
print(f" Password: {new_password}")
|
||||
print(f" is_active: True")
|
||||
|
||||
# Verify the update
|
||||
result = await session.execute(stmt)
|
||||
updated_user = result.scalar_one_or_none()
|
||||
if updated_user:
|
||||
print(f"✅ Verification: User is_active={updated_user.is_active}")
|
||||
if updated_user.hashed_password == hashed_password:
|
||||
print(f"✅ Password hash matches!")
|
||||
else:
|
||||
print(f"⚠️ Password hash mismatch (should not happen)")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def main():
|
||||
try:
|
||||
success = await reset_admin_password()
|
||||
if success:
|
||||
print("\n🎉 Password reset completed successfully!")
|
||||
print("You can now log in with superadmin@profibot.hu / Admin123!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\n❌ Password reset failed!")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"💥 Error during password reset: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
355
backend/app/scripts/seed_integration_data.py
Normal file
355
backend/app/scripts/seed_integration_data.py
Normal file
@@ -0,0 +1,355 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Service Finder Integration Data Seeding Script
|
||||
Populates PostgreSQL DB with real test data for frontend integration.
|
||||
|
||||
Inserts:
|
||||
1. User: tester_pro@profibot.hu (Password: Tester123!, Role: admin)
|
||||
2. Organization: "Profibot Test Fleet" in fleet.organizations
|
||||
3. Vehicles: 4 real vehicles (BMW, Audi, Mercedes, Tesla) in vehicle.assets
|
||||
4. Logs: Initial logs in audit.process_logs
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Tuple
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select, delete, text
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.identity.identity import User, Person, UserRole, Wallet
|
||||
from app.models.marketplace.organization import Organization
|
||||
from app.models.vehicle.asset import Asset
|
||||
from app.models.audit import ProcessLog
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
# Environment safety check
|
||||
ENVIRONMENT = "development"
|
||||
|
||||
|
||||
async def cleanup_existing_integration_data(db):
|
||||
"""Clean up previously seeded integration data (only in non-production environments)."""
|
||||
if ENVIRONMENT == "production":
|
||||
print("⚠️ Production environment detected - skipping cleanup.")
|
||||
return
|
||||
|
||||
print("🧹 Cleaning up previously seeded integration data...")
|
||||
|
||||
# We need to delete in the correct order due to foreign key constraints:
|
||||
# There's a circular reference between User and Person:
|
||||
# - User has person_id (foreign key to Person)
|
||||
# - Person has user_id (foreign key to User, but optional)
|
||||
# So we need to break the circular reference by setting person.user_id = NULL first
|
||||
|
||||
# 1. Delete integration test vehicles (by VIN pattern)
|
||||
result = await db.execute(
|
||||
delete(Asset).where(Asset.vin.like("INTEG%"))
|
||||
)
|
||||
print(f" Deleted {result.rowcount} integration test vehicles")
|
||||
|
||||
# 2. Delete integration test organization
|
||||
result = await db.execute(
|
||||
delete(Organization).where(Organization.name == "Profibot Test Fleet")
|
||||
)
|
||||
print(f" Deleted {result.rowcount} integration test organization")
|
||||
|
||||
# 3. Find integration test users and break circular references
|
||||
user_stmt = select(User).where(
|
||||
(User.email == "tester_pro@profibot.hu") |
|
||||
(User.email == "superadmin@profibot.hu")
|
||||
)
|
||||
user_result = await db.execute(user_stmt)
|
||||
integration_users = user_result.scalars().all()
|
||||
|
||||
for user in integration_users:
|
||||
# Find the person associated with this user
|
||||
person_stmt = select(Person).where(Person.user_id == user.id)
|
||||
person_result = await db.execute(person_stmt)
|
||||
person = person_result.scalar_one_or_none()
|
||||
if person:
|
||||
# Break the circular reference: set person.user_id = NULL
|
||||
person.user_id = None
|
||||
person.active_user_account = None
|
||||
await db.flush()
|
||||
print(f" Broke circular reference for person {person.id}")
|
||||
|
||||
# 4. Delete wallets for integration test users
|
||||
for user in integration_users:
|
||||
wallet_stmt = select(Wallet).where(Wallet.user_id == user.id)
|
||||
wallet_result = await db.execute(wallet_stmt)
|
||||
wallet = wallet_result.scalar_one_or_none()
|
||||
if wallet:
|
||||
await db.execute(delete(Wallet).where(Wallet.id == wallet.id))
|
||||
print(f" Deleted wallet for user {user.email}")
|
||||
|
||||
# 5. Now delete the users
|
||||
result = await db.execute(
|
||||
delete(User).where(
|
||||
(User.email == "tester_pro@profibot.hu") |
|
||||
(User.email == "superadmin@profibot.hu")
|
||||
)
|
||||
)
|
||||
print(f" Deleted {result.rowcount} integration test users")
|
||||
|
||||
# 6. Delete the persons (now that user references are broken)
|
||||
# Find persons that were associated with the deleted users
|
||||
# We need to join with users to find persons based on user email
|
||||
person_stmt = select(Person).join(User, Person.user_id == User.id).where(
|
||||
(User.email == "tester_pro@profibot.hu") |
|
||||
(User.email == "superadmin@profibot.hu")
|
||||
)
|
||||
person_result = await db.execute(person_stmt)
|
||||
integration_persons = person_result.scalars().all()
|
||||
|
||||
for person in integration_persons:
|
||||
# Double-check that no user references this person
|
||||
user_check_stmt = select(User).where(User.person_id == person.id)
|
||||
user_check_result = await db.execute(user_check_stmt)
|
||||
remaining_users = user_check_result.scalars().all()
|
||||
|
||||
if remaining_users:
|
||||
print(f"⚠️ Person {person.id} still referenced by users: {[u.email for u in remaining_users]}")
|
||||
# Try to break the reference by setting user.person_id = NULL
|
||||
for user in remaining_users:
|
||||
user.person_id = None
|
||||
await db.flush()
|
||||
|
||||
await db.execute(delete(Person).where(Person.id == person.id))
|
||||
print(f" Deleted person {person.first_name} {person.last_name}")
|
||||
|
||||
# 7. Delete integration test logs
|
||||
result = await db.execute(
|
||||
delete(ProcessLog).where(ProcessLog.process_name == "integration_seeding")
|
||||
)
|
||||
print(f" Deleted {result.rowcount} integration test logs")
|
||||
|
||||
|
||||
async def seed_integration_data():
|
||||
"""Main seeding function for integration test data."""
|
||||
print("🚀 Starting integration data seeding...")
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
# Clean up old integration data first
|
||||
await cleanup_existing_integration_data(db)
|
||||
|
||||
# 1. Create Person for the tester
|
||||
print("👤 Creating Person for tester...")
|
||||
person = Person(
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone="+36123456789",
|
||||
is_active=True,
|
||||
lifetime_xp=1000,
|
||||
penalty_points=0,
|
||||
social_reputation=4.5
|
||||
)
|
||||
db.add(person)
|
||||
await db.flush() # Get the person ID
|
||||
|
||||
# 2. Create User with admin role (tester)
|
||||
print("👤 Creating User tester_pro@profibot.hu...")
|
||||
user = User(
|
||||
email="tester_pro@profibot.hu",
|
||||
hashed_password=get_password_hash("Tester123!"),
|
||||
role=UserRole.admin,
|
||||
person_id=person.id,
|
||||
is_active=True,
|
||||
subscription_plan="PREMIUM",
|
||||
subscription_expires_at=datetime.now() + timedelta(days=365),
|
||||
is_vip=True,
|
||||
preferred_language="hu",
|
||||
region_code="HU",
|
||||
preferred_currency="HUF",
|
||||
scope_level="organization",
|
||||
custom_permissions={},
|
||||
created_at=datetime.now()
|
||||
)
|
||||
db.add(user)
|
||||
await db.flush() # Get the user ID
|
||||
|
||||
# Update person with active user reference
|
||||
person.user_id = user.id
|
||||
person.active_user_account = user
|
||||
|
||||
# 2b. Create superadmin user (separate person)
|
||||
print("👑 Creating superadmin user superadmin@profibot.hu...")
|
||||
superadmin_person = Person(
|
||||
first_name="Super",
|
||||
last_name="Admin",
|
||||
phone="+36123456788",
|
||||
is_active=True,
|
||||
lifetime_xp=5000,
|
||||
penalty_points=0,
|
||||
social_reputation=5.0
|
||||
)
|
||||
db.add(superadmin_person)
|
||||
await db.flush()
|
||||
|
||||
superadmin_user = User(
|
||||
email="superadmin@profibot.hu",
|
||||
hashed_password=get_password_hash("Superadmin123!"),
|
||||
role=UserRole.superadmin,
|
||||
person_id=superadmin_person.id,
|
||||
is_active=True,
|
||||
subscription_plan="ENTERPRISE",
|
||||
subscription_expires_at=datetime.now() + timedelta(days=365),
|
||||
is_vip=True,
|
||||
preferred_language="hu",
|
||||
region_code="HU",
|
||||
preferred_currency="HUF",
|
||||
scope_level="system",
|
||||
custom_permissions={},
|
||||
created_at=datetime.now()
|
||||
)
|
||||
db.add(superadmin_user)
|
||||
await db.flush()
|
||||
|
||||
superadmin_person.user_id = superadmin_user.id
|
||||
superadmin_person.active_user_account = superadmin_user
|
||||
|
||||
# 3. Create Organization
|
||||
print("🏢 Creating Organization 'Profibot Test Fleet'...")
|
||||
organization = Organization(
|
||||
name="Profibot Test Fleet",
|
||||
full_name="Profibot Test Fleet Kft.",
|
||||
owner_id=user.id,
|
||||
legal_owner_id=person.id,
|
||||
default_currency="HUF",
|
||||
country_code="HU",
|
||||
language="hu",
|
||||
folder_slug="profibot",
|
||||
first_registered_at=datetime.now(),
|
||||
current_lifecycle_started_at=datetime.now(),
|
||||
subscription_plan="PREMIUM",
|
||||
base_asset_limit=10,
|
||||
purchased_extra_slots=0,
|
||||
notification_settings={"notify_owner": True, "alert_days_before": [30, 15, 7, 1]},
|
||||
external_integration_config={},
|
||||
org_type="fleet_owner",
|
||||
status="active",
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
created_at=datetime.now(),
|
||||
is_ownership_transferable=True
|
||||
)
|
||||
db.add(organization)
|
||||
await db.flush()
|
||||
|
||||
# 4. Create 4 real vehicles
|
||||
print("🚗 Creating 4 real vehicles...")
|
||||
|
||||
vehicles_data = [
|
||||
{
|
||||
"vin": "INTEGBMW123456", # 13 chars
|
||||
"license_plate": "ABC-123",
|
||||
"name": "BMW X5",
|
||||
"year_of_manufacture": 2022,
|
||||
"owner_person_id": person.id,
|
||||
"owner_org_id": organization.id,
|
||||
"current_organization_id": organization.id,
|
||||
"status": "active",
|
||||
"current_mileage": 45000,
|
||||
"currency": "EUR",
|
||||
"individual_equipment": {},
|
||||
"created_at": datetime.now()
|
||||
},
|
||||
{
|
||||
"vin": "INTEGAUDI789012", # 14 chars
|
||||
"license_plate": "DEF-456",
|
||||
"name": "Audi A6",
|
||||
"year_of_manufacture": 2021,
|
||||
"owner_person_id": person.id,
|
||||
"owner_org_id": organization.id,
|
||||
"current_organization_id": organization.id,
|
||||
"status": "active",
|
||||
"current_mileage": 32000,
|
||||
"currency": "EUR",
|
||||
"individual_equipment": {},
|
||||
"created_at": datetime.now()
|
||||
},
|
||||
{
|
||||
"vin": "INTEGMB345678", # 12 chars
|
||||
"license_plate": "GHI-789",
|
||||
"name": "Mercedes E-Class",
|
||||
"year_of_manufacture": 2023,
|
||||
"owner_person_id": person.id,
|
||||
"owner_org_id": organization.id,
|
||||
"current_organization_id": organization.id,
|
||||
"status": "active",
|
||||
"current_mileage": 15000,
|
||||
"currency": "EUR",
|
||||
"individual_equipment": {},
|
||||
"created_at": datetime.now()
|
||||
},
|
||||
{
|
||||
"vin": "INTEGTESLA90123", # 15 chars
|
||||
"license_plate": "JKL-012",
|
||||
"name": "Tesla Model 3",
|
||||
"year_of_manufacture": 2023,
|
||||
"owner_person_id": person.id,
|
||||
"owner_org_id": organization.id,
|
||||
"current_organization_id": organization.id,
|
||||
"status": "active",
|
||||
"current_mileage": 25000,
|
||||
"currency": "EUR",
|
||||
"individual_equipment": {},
|
||||
"created_at": datetime.now()
|
||||
}
|
||||
]
|
||||
|
||||
for i, vehicle_data in enumerate(vehicles_data, 1):
|
||||
vehicle = Asset(**vehicle_data)
|
||||
db.add(vehicle)
|
||||
print(f" Created vehicle {i}: {vehicle_data['name']}")
|
||||
|
||||
# 5. Create initial process logs
|
||||
print("📝 Creating initial process logs...")
|
||||
|
||||
# Create a single process log for the entire seeding process
|
||||
log = ProcessLog(
|
||||
process_name="integration_seeding",
|
||||
start_time=datetime.now(),
|
||||
end_time=datetime.now(),
|
||||
items_processed=7, # 1 user + 1 org + 4 vehicles + 1 log
|
||||
items_failed=0,
|
||||
details={
|
||||
"user_email": "tester_pro@profibot.hu",
|
||||
"organization": "Profibot Test Fleet",
|
||||
"vehicle_count": 4,
|
||||
"makes": ["BMW", "Audi", "Mercedes-Benz", "Tesla"]
|
||||
}
|
||||
)
|
||||
db.add(log)
|
||||
|
||||
# Commit all changes
|
||||
await db.commit()
|
||||
print("✅ Integration data seeding completed successfully!")
|
||||
|
||||
# Print summary
|
||||
print("\n📊 Seeding Summary:")
|
||||
print(f" • User: tester_pro@profibot.hu (Password: Tester123!)")
|
||||
print(f" • Organization: Profibot Test Fleet")
|
||||
print(f" • Vehicles: 4 real vehicles (BMW X5, Audi A6, Mercedes E-Class, Tesla Model 3)")
|
||||
print(f" • Logs: 3 process logs created")
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
print(f"❌ Error during integration data seeding: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def main():
|
||||
"""Entry point for the seeding script."""
|
||||
try:
|
||||
await seed_integration_data()
|
||||
except Exception as e:
|
||||
print(f"💥 Fatal error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -30,22 +30,25 @@ class AssetService:
|
||||
|
||||
@staticmethod
|
||||
async def create_or_claim_vehicle(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
org_id: int,
|
||||
vin: str,
|
||||
license_plate: str,
|
||||
catalog_id: int = None
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
org_id: int,
|
||||
vin: Optional[str] = None,
|
||||
license_plate: Optional[str] = None,
|
||||
catalog_id: int = None,
|
||||
draft: bool = False
|
||||
):
|
||||
"""
|
||||
Intelligens Jármű Rögzítés:
|
||||
Ha új: létrehozza.
|
||||
Intelligens Jármű Rögzítés:
|
||||
Ha új: létrehozza.
|
||||
Ha már létezik: Transzfer folyamatot indít.
|
||||
Ha draft=True vagy VIN hiányzik: draft státuszban hozza létre.
|
||||
"""
|
||||
try:
|
||||
vin_clean = vin.strip().upper()
|
||||
vin_clean = vin.strip().upper() if vin else None
|
||||
license_plate_clean = license_plate.strip().upper() if license_plate else None
|
||||
|
||||
# 1. ADMIN LIMIT ELLENŐRZÉS
|
||||
# 1. ADMIN LIMIT ELLENŐRZÉS (csak aktív járművek számítanak)
|
||||
user_stmt = select(User).where(User.id == user_id)
|
||||
user = (await db.execute(user_stmt)).scalar_one()
|
||||
|
||||
@@ -53,15 +56,21 @@ class AssetService:
|
||||
user_role = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
||||
allowed_limit = limits.get(user_role, 1)
|
||||
|
||||
count_stmt = select(func.count(Asset.id)).where(Asset.current_organization_id == org_id)
|
||||
# Csak aktív járművek számítanak a limitbe (draft-ok nem)
|
||||
count_stmt = select(func.count(Asset.id)).where(
|
||||
Asset.current_organization_id == org_id,
|
||||
Asset.status == "active"
|
||||
)
|
||||
current_count = (await db.execute(count_stmt)).scalar()
|
||||
|
||||
if current_count >= allowed_limit:
|
||||
if current_count >= allowed_limit and not draft:
|
||||
raise ValueError(f"Limit túllépés! A csomagod {allowed_limit} autót engedélyez.")
|
||||
|
||||
# 2. LÉTEZIK-E MÁR A JÁRMŰ?
|
||||
stmt = select(Asset).where(Asset.vin == vin_clean)
|
||||
existing_asset = (await db.execute(stmt)).scalar_one_or_none()
|
||||
# 2. LÉTEZIK-E MÁR A JÁRMŰ? (csak ha van VIN)
|
||||
existing_asset = None
|
||||
if vin_clean:
|
||||
stmt = select(Asset).where(Asset.vin == vin_clean)
|
||||
existing_asset = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if existing_asset:
|
||||
# HA MÁR A JELENLEGI SZERVEZETNÉL VAN
|
||||
@@ -70,16 +79,17 @@ class AssetService:
|
||||
|
||||
# TRANSZFER FOLYAMAT INDÍTÁSA
|
||||
return await AssetService.initiate_ownership_transfer(
|
||||
db, existing_asset, user_id, org_id, license_plate
|
||||
db, existing_asset, user_id, org_id, license_plate_clean or ""
|
||||
)
|
||||
|
||||
# 3. ÚJ JÁRMŰ LÉTREHOZÁSA (Standard Flow)
|
||||
# 3. ÚJ JÁRMŰ LÉTREHOZÁSA (Standard vagy Draft Flow)
|
||||
status = "draft" if draft or not vin_clean else "active"
|
||||
new_asset = Asset(
|
||||
vin=vin_clean,
|
||||
license_plate=license_plate.strip().upper(),
|
||||
license_plate=license_plate_clean,
|
||||
catalog_id=catalog_id,
|
||||
current_organization_id=org_id,
|
||||
status="active",
|
||||
status=status,
|
||||
individual_equipment={},
|
||||
created_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ OLLAMA_URL = "http://sf_ollama:11434/api/generate"
|
||||
OLLAMA_MODEL = "qwen2.5-coder:14b" # A 14b paraméteres modell az agy
|
||||
MAX_ATTEMPTS = 3
|
||||
TIMEOUT_SECONDS = 45 # Megemelt timeout a 14b modell lassabb válaszideje miatt
|
||||
BATCH_SIZE = 10 # Maximum 10 párhuzamos AI hívás a CPU fagyás elkerülésére
|
||||
BATCH_SIZE = 4 # Maximum 4 párhuzamos AI hívás a CPU fagyás elkerülésére
|
||||
|
||||
class AlchemistPro:
|
||||
def __init__(self):
|
||||
@@ -222,7 +222,7 @@ class AlchemistPro:
|
||||
await self.process_batch(db, vehicles)
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
await asyncio.sleep(10)
|
||||
await asyncio.sleep(2)
|
||||
except Exception as e:
|
||||
logger.error(f"Főciklus hiba: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
Reference in New Issue
Block a user