201 előtti mentés

This commit is contained in:
Roo
2026-03-26 07:09:44 +00:00
parent 89668a9beb
commit 03258db091
124 changed files with 13619 additions and 13347 deletions

View File

@@ -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"])

View File

@@ -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)}"

View File

@@ -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,

View File

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

View File

@@ -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", "Lambdaszonda", "Féktárcsa", "Olajszűrő"],
"correctAnswer": 1,
"explanation": "A lambdaszonda 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 ólomsavas 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 lambdaszonda 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 ólomsavas 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
}

View File

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

View File

@@ -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

View File

@@ -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."
)

View File

@@ -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. """

View File

@@ -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",

View File

@@ -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"
)

View File

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

View File

@@ -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})>"

View File

@@ -13,6 +13,7 @@ class ParameterScope(str, Enum):
GLOBAL = "global"
COUNTRY = "country"
REGION = "region"
ORGANIZATION = "organization"
USER = "user"
class SystemParameter(Base):

View File

@@ -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")

View File

@@ -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

View File

@@ -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")

View File

@@ -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

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

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
"""Add ui_mode column to users table
Revision ID: 51fb2de6b6b2
Revises: ee76703cb1c6
Create Date: 2026-03-25 01:01:10.473313
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '51fb2de6b6b2'
down_revision: Union[str, Sequence[str], None] = 'ee76703cb1c6'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass