Initial commit - Migrated to Dev environment
This commit is contained in:
BIN
backend/app/services/__pycache__/config_service.cpython-312.pyc
Executable file
BIN
backend/app/services/__pycache__/config_service.cpython-312.pyc
Executable file
Binary file not shown.
BIN
backend/app/services/__pycache__/email_manager.cpython-312.pyc
Executable file
BIN
backend/app/services/__pycache__/email_manager.cpython-312.pyc
Executable file
Binary file not shown.
41
backend/app/services/config_service.py
Executable file
41
backend/app/services/config_service.py
Executable 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()
|
||||
84
backend/app/services/email_manager.py
Executable file
84
backend/app/services/email_manager.py
Executable 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()
|
||||
40
backend/app/services/fleet_service.py
Executable file
40
backend/app/services/fleet_service.py
Executable 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))
|
||||
40
backend/app/services/gamification_service.py
Executable file
40
backend/app/services/gamification_service.py
Executable 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
|
||||
25
backend/app/services/maintenance_service.py
Executable file
25
backend/app/services/maintenance_service.py
Executable 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
|
||||
35
backend/app/services/matching_service.py
Executable file
35
backend/app/services/matching_service.py
Executable 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()
|
||||
14
backend/app/services/notification_service.py
Executable file
14
backend/app/services/notification_service.py
Executable 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})")
|
||||
64
backend/app/services/social_service.py
Executable file
64
backend/app/services/social_service.py
Executable 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
|
||||
46
backend/app/services/translation_service.py
Executable file
46
backend/app/services/translation_service.py
Executable 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)
|
||||
Reference in New Issue
Block a user