Initial commit - Migrated to Dev environment

This commit is contained in:
2026-02-03 19:55:45 +00:00
commit a34e5b7976
3518 changed files with 481663 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
from typing import Any, Optional
from sqlalchemy import text
from app.db.session import SessionLocal
class ConfigService:
@staticmethod
async def get_setting(
key: str,
org_id: Optional[int] = None,
region_code: Optional[str] = None,
tier_id: Optional[int] = None,
default: Any = None
) -> Any:
query = text("""
SELECT value_json
FROM data.system_settings
WHERE key_name = :key
AND (
(org_id = :org_id) OR
(org_id IS NULL AND tier_id = :tier_id) OR
(org_id IS NULL AND tier_id IS NULL AND region_code = :region_code) OR
(org_id IS NULL AND tier_id IS NULL AND region_code IS NULL)
)
ORDER BY
(org_id IS NOT NULL) DESC,
(tier_id IS NOT NULL) DESC,
(region_code IS NOT NULL) DESC
LIMIT 1
""")
async with SessionLocal() as db:
result = await db.execute(query, {
"key": key,
"org_id": org_id,
"tier_id": tier_id,
"region_code": region_code
})
row = result.fetchone()
return row[0] if row else default
config = ConfigService()

View File

@@ -0,0 +1,84 @@
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from app.core.config import settings
class EmailManager:
@staticmethod
def _render_template(template_key: str, variables: dict, lang: str = "hu") -> str:
base_dir = "/app/app/templates/emails"
file_path = f"{base_dir}/{lang}/{template_key}.html"
if not os.path.exists(file_path):
return ""
with open(file_path, "r", encoding="utf-8") as f:
body_html = f.read()
for k, v in variables.items():
body_html = body_html.replace(f"{{{{{k}}}}}", str(v))
body_html = body_html.replace(f"{{{k}}}", str(v))
return body_html
@staticmethod
def _subject(template_key: str) -> str:
subjects = {
"registration": "Regisztráció - Service Finder",
"password_reset": "Jelszó visszaállítás - Service Finder",
"notification": "Értesítés - Service Finder",
}
return subjects.get(template_key, "Értesítés - Service Finder")
@staticmethod
async def send_email(recipient: str, template_key: str, variables: dict, user_id: int = None, lang: str = "hu"):
if settings.EMAIL_PROVIDER == "disabled":
return {"status": "disabled"}
html = EmailManager._render_template(template_key, variables, lang=lang)
subject = EmailManager._subject(template_key)
provider = settings.EMAIL_PROVIDER
if provider == "auto":
provider = "sendgrid" if settings.SENDGRID_API_KEY else "smtp"
# 1) SendGrid API (stabil)
if provider == "sendgrid" and settings.SENDGRID_API_KEY:
try:
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
message = Mail(
from_email=(settings.EMAILS_FROM_EMAIL, settings.EMAILS_FROM_NAME),
to_emails=recipient,
subject=subject,
html_content=html or "<p>Üzenet</p>",
)
sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
sg.send(message)
return {"status": "success", "provider": "sendgrid"}
except Exception as e:
# ha auto módban vagyunk, esünk vissza smtp-re
if settings.EMAIL_PROVIDER != "auto":
return {"status": "error", "provider": "sendgrid", "message": str(e)}
# 2) SMTP fallback
if not settings.SMTP_HOST or not settings.SMTP_USER or not settings.SMTP_PASSWORD:
return {"status": "error", "provider": "smtp", "message": "SMTP not configured"}
try:
msg = MIMEMultipart()
msg["From"] = f"{settings.EMAILS_FROM_NAME} <{settings.EMAILS_FROM_EMAIL}>"
msg["To"] = recipient
msg["Subject"] = subject
msg.attach(MIMEText(html or "Üzenet", "html"))
with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT, timeout=15) as server:
if settings.SMTP_USE_TLS:
server.starttls()
server.login(settings.SMTP_USER, settings.SMTP_PASSWORD)
server.send_message(msg)
return {"status": "success", "provider": "smtp"}
except Exception as e:
return {"status": "error", "provider": "smtp", "message": str(e)}
email_manager = EmailManager()

View File

@@ -0,0 +1,40 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.models.vehicle import UserVehicle
from app.models.expense import VehicleEvent
from app.models.social import ServiceProvider, SourceType, ModerationStatus
from app.schemas.fleet import EventCreate, TCOStats
from app.services.gamification_service import GamificationService
async def add_vehicle_event(db: AsyncSession, vehicle_id: int, event_data: EventCreate, user_id: int):
v_res = await db.execute(select(UserVehicle).where(UserVehicle.id == vehicle_id))
vehicle = v_res.scalars().first()
if not vehicle: return {"error": "Vehicle not found"}
final_provider_id = event_data.provider_id
if event_data.is_diy: final_provider_id = None
elif event_data.provider_name and not final_provider_id:
p_res = await db.execute(select(ServiceProvider).where(func.lower(ServiceProvider.name) == event_data.provider_name.lower()))
existing = p_res.scalars().first()
if existing: final_provider_id = existing.id
else:
new_p = ServiceProvider(name=event_data.provider_name, added_by_user_id=user_id, status=ModerationStatus.pending)
db.add(new_p); await db.flush(); final_provider_id = new_p.id
await GamificationService.award_points(db, user_id, 50, f"Új helyszín: {event_data.provider_name}")
anomaly = event_data.odometer_value < vehicle.current_odometer
new_event = VehicleEvent(vehicle_id=vehicle_id, service_provider_id=final_provider_id, odometer_anomaly=anomaly, **event_data.model_dump(exclude={"provider_id", "provider_name"}))
db.add(new_event)
if event_data.odometer_value > vehicle.current_odometer: vehicle.current_odometer = event_data.odometer_value
await GamificationService.award_points(db, user_id, 20, f"Esemény: {event_data.event_type}")
await db.commit(); await db.refresh(new_event)
return new_event
async def calculate_tco(db: AsyncSession, vehicle_id: int) -> TCOStats:
result = await db.execute(select(VehicleEvent.event_type, func.sum(VehicleEvent.cost_amount)).where(VehicleEvent.vehicle_id == vehicle_id).group_by(VehicleEvent.event_type))
breakdown = {row[0]: row[1] for row in result.all()}
v_res = await db.execute(select(UserVehicle).where(UserVehicle.id == vehicle_id))
v = v_res.scalars().first()
km = (v.current_odometer - v.initial_odometer) if v else 0
cpk = sum(breakdown.values()) / km if km > 0 else 0
return TCOStats(vehicle_id=vehicle_id, total_cost=sum(breakdown.values()), breakdown=breakdown, cost_per_km=round(cpk, 2))

View File

@@ -0,0 +1,40 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.gamification import UserStats, PointsLedger
from sqlalchemy import select
class GamificationService:
@staticmethod
async def award_points(db: AsyncSession, user_id: int, points: int, reason: str):
"""Pontok jóváírása és a UserStats frissítése"""
# 1. Bejegyzés a naplóba (Mezőnevek szinkronizálva a modellel)
new_entry = PointsLedger(
user_id=user_id,
points_change=points,
reason=reason
)
db.add(new_entry)
# 2. Összesített statisztika lekérése/létrehozása
result = await db.execute(select(UserStats).where(UserStats.user_id == user_id))
stats = result.scalar_one_or_none()
if not stats:
# Ha új a user, létrehozzuk az alap statisztikát
stats = UserStats(
user_id=user_id,
total_points=0,
current_level=1
)
db.add(stats)
# 3. Pontok hozzáadása
stats.total_points += points
# Itt fogjuk később meghívni a szintlépési logikát
# await GamificationService._check_level_up(stats)
# Fontos: Nem commitolunk itt, hanem hagyjuk, hogy a hívó (SocialService)
# egy tranzakcióban mentse el a szolgáltatót és a pontokat!
await db.flush()
return stats.total_points

View File

@@ -0,0 +1,25 @@
import os
import time
from datetime import datetime, timedelta
class MaintenanceService:
@staticmethod
async def cleanup_old_files(storage_path: str):
"""1 évnél régebbi fájlok törlése a NAS-ról"""
limit = datetime.now() - timedelta(days=365)
for root, dirs, files in os.walk(storage_path):
for file in files:
file_path = os.path.join(root, file)
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
if file_time < limit:
os.remove(file_path)
print(f"🗑️ Törölve (lejárt): {file}")
@staticmethod
async def delete_validated_evidence(service_id: int, photo_path: str):
"""Döntésed: Validáció után a szervizkép törölhető"""
if os.path.exists(photo_path):
os.remove(photo_path)
# Logoljuk az adatbázisba, hogy a kép már nincs meg, de az adat valid

View File

@@ -0,0 +1,35 @@
from typing import List, Dict, Any
from sqlalchemy import text
from app.db.session import SessionLocal
from app.services.config_service import config
class MatchingService:
@staticmethod
async def rank_services(services: List[Dict[str, Any]], org_id: int = None) -> List[Dict[str, Any]]:
# 1. Dinamikus paraméterek lekérése az Admin beállításokból
w_dist = await config.get_setting('weight_distance', org_id=org_id, default=0.5)
w_rate = await config.get_setting('weight_rating', org_id=org_id, default=0.5)
b_gold = await config.get_setting('bonus_gold_service', org_id=org_id, default=500)
ranked_list = []
for s in services:
# Normalizált pontszámok (példa logika)
# Távolság pont (P_dist): 100 / (távolság + 1) -> közelebb = több pont
p_dist = 100 / (s.get('distance', 1) + 1)
# Értékelés pont (P_rate): csillagok * 20 -> 5 csillag = 100 pont
p_rate = s.get('rating', 0) * 20
# Bónusz (B_tier): ha Gold, megkapja a bónuszt
tier_bonus = b_gold if s.get('tier') == 'gold' else 0
# A Mester Képlet:
total_score = (p_dist * float(w_dist)) + (p_rate * float(w_rate)) + tier_bonus
s['total_score'] = round(total_score, 2)
ranked_list.append(s)
# Sorbarendezés pontszám szerint csökkenőben
return sorted(ranked_list, key=lambda x: x['total_score'], reverse=True)
matching_service = MatchingService()

View File

@@ -0,0 +1,14 @@
from datetime import datetime, timedelta
from sqlalchemy import select
from app.models.user import User
from app.models.vehicle import Vehicle
from app.core.email import send_expiry_notification
async def check_expiring_documents(db: AsyncSession, background_tasks: BackgroundTasks):
# Példa: Műszaki vizsga lejárata 30 napon belül
threshold = datetime.now().date() + timedelta(days=30)
result = await db.execute(
select(Vehicle, User).join(User).where(Vehicle.mot_expiry_date <= threshold)
)
for vehicle, user in result.all():
send_expiry_notification(background_tasks, user.email, f"Műszaki vizsga ({vehicle.license_plate})")

View File

@@ -0,0 +1,64 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from app.models.social import ServiceProvider, Vote, ModerationStatus, Competition, UserScore
from app.models.user import User
from datetime import datetime
from app.services.gamification_service import GamificationService
from app.schemas.social import ServiceProviderCreate
async def create_service_provider(db: AsyncSession, obj_in: ServiceProviderCreate, user_id: int):
new_provider = ServiceProvider(**obj_in.dict(), added_by_user_id=user_id)
db.add(new_provider)
await db.flush()
await GamificationService.award_points(db, user_id, 50, f"Új szolgáltató: {new_provider.name}")
await db.commit()
await db.refresh(new_provider)
return new_provider
async def vote_for_provider(db: AsyncSession, voter_id: int, provider_id: int, vote_value: int):
res = await db.execute(select(Vote).where(and_(Vote.user_id == voter_id, Vote.provider_id == provider_id)))
if res.scalars().first(): return {"message": "User already voted"}
new_vote = Vote(user_id=voter_id, provider_id=provider_id, vote_value=vote_value)
db.add(new_vote)
p_res = await db.execute(select(ServiceProvider).where(ServiceProvider.id == provider_id))
provider = p_res.scalars().first()
if not provider: return {"error": "Provider not found"}
provider.validation_score += vote_value
if provider.status == ModerationStatus.pending:
if provider.validation_score >= 5:
provider.status = ModerationStatus.approved
await _reward_submitter(db, provider.added_by_user_id, provider.name)
elif provider.validation_score <= -3:
provider.status = ModerationStatus.rejected
await _penalize_user(db, provider.added_by_user_id, provider.name)
await db.commit()
return {"message": "Vote cast", "new_score": provider.validation_score, "status": provider.status}
async def get_leaderboard(db: AsyncSession, limit: int = 10):
return await GamificationService.get_top_users(db, limit)
async def _reward_submitter(db: AsyncSession, user_id: int, provider_name: str):
if not user_id: return
await GamificationService.award_points(db, user_id, 100, f"Validált szolgáltató: {provider_name}")
u_res = await db.execute(select(User).where(User.id == user_id))
user = u_res.scalars().first()
if user: user.reputation_score = (user.reputation_score or 0) + 1
now = datetime.utcnow()
c_res = await db.execute(select(Competition).where(and_(Competition.is_active == True, Competition.start_date <= now, Competition.end_date >= now)))
comp = c_res.scalars().first()
if comp:
s_res = await db.execute(select(UserScore).where(and_(UserScore.user_id == user_id, UserScore.competition_id == comp.id)))
us = s_res.scalars().first()
if not us:
us = UserScore(user_id=user_id, competition_id=comp.id, points=0)
db.add(us)
us.points += 10
async def _penalize_user(db: AsyncSession, user_id: int, provider_name: str):
if not user_id: return
await GamificationService.award_points(db, user_id, -50, f"Elutasított szolgáltató: {provider_name}")
u_res = await db.execute(select(User).where(User.id == user_id))
user = u_res.scalars().first()
if user:
user.reputation_score = (user.reputation_score or 0) - 2
if user.reputation_score <= -10: user.is_active = False

View File

@@ -0,0 +1,46 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from app.models.translation import Translation
from typing import Dict
class TranslationService:
# Ez a memória-cache tárolja az élesített szövegeket
_published_cache: Dict[str, Dict[str, str]] = {}
@classmethod
async def load_cache(cls, db: AsyncSession):
"""Betölti az összes PUBLIKÁLT fordítást az adatbázisból a memóriába."""
result = await db.execute(
select(Translation).where(Translation.is_published == True)
)
translations = result.scalars().all()
cls._published_cache = {}
for t in translations:
if t.lang_code not in cls._published_cache:
cls._published_cache[t.lang_code] = {}
cls._published_cache[t.lang_code][t.key] = t.value
print(f"🌍 i18n Cache: {len(translations)} szöveg élesítve.")
@classmethod
def get_text(cls, key: str, lang: str = "en") -> str:
"""Villámgyors lekérés a memóriából Fallback logikával."""
# 1. Kért nyelv
text = cls._published_cache.get(lang, {}).get(key)
if text: return text
# 2. Fallback: Angol
if lang != "en":
text = cls._published_cache.get("en", {}).get(key)
if text: return text
return f"[{key}]"
@classmethod
async def publish_all(cls, db: AsyncSession):
"""Élesíti a piszkozatokat és frissíti a szerver memóriáját."""
await db.execute(
update(Translation).where(Translation.is_published == False).values(is_published=True)
)
await db.commit()
await cls.load_cache(db)