admin_szolgáltatók_

This commit is contained in:
Roo
2026-07-01 02:27:38 +00:00
parent 189cbfd7ca
commit 6e627d0ebe
491 changed files with 20965 additions and 9703 deletions

View File

@@ -286,8 +286,8 @@ class AssetService:
))
# Gamification
reward = await config.get_setting(db, "xp_reward_asset_register", default=250)
await GamificationService.award_points(db, user_id, int(reward), "NEW_ASSET_REG")
# A pontérték a point_rules táblából jön (action_key='ASSET_REGISTER')
await GamificationService.award_points(db, user_id, amount=0, reason="ASSET_REGISTER", action_key="ASSET_REGISTER")
# Check if this is user's first vehicle and award "First Car" badge
await AssetService._award_first_car_badge(db, user_id, target_org_id)

View File

@@ -295,19 +295,21 @@ class AuthService:
user.region_code = kyc_in.region_code
# Gamification XP jóváírás (commit=False, mert a külső metódus kezeli a tranzakciót)
await GamificationService.award_points(db, user_id=user.id, amount=int(kyc_reward), reason="KYC_VERIFICATION", commit=False)
# A pontérték a point_rules táblából jön (action_key='KYC_VERIFICATION')
await GamificationService.award_points(db, user_id=user.id, amount=0, reason="KYC_VERIFICATION", commit=False, action_key="KYC_VERIFICATION")
# P2P Referral XP a meghívónak (ha van referred_by_id)
if user.referred_by_id:
p2p_xp = await config.get_setting(db, "gamification_p2p_invite_xp", default=50)
# A pontérték a point_rules táblából jön (action_key='P2P_REFERRAL_SUCCESS')
await GamificationService.award_points(
db,
user_id=user.referred_by_id,
amount=int(p2p_xp),
amount=0,
reason="P2P_REFERRAL_SUCCESS",
commit=False
commit=False,
action_key="P2P_REFERRAL_SUCCESS"
)
logger.info(f"P2P XP ({p2p_xp}) awarded to referrer ID {user.referred_by_id} for user {user.id}")
logger.info(f"P2P XP awarded to referrer ID {user.referred_by_id} for user {user.id}")
await db.commit()
return user

View File

@@ -25,8 +25,6 @@ class CostService:
try:
# 1. Dinamikus konfiguráció lekérése
base_currency = await config.get_setting(db, "finance_base_currency", default="EUR")
base_xp = await config.get_setting(db, "xp_per_cost_log", default=50)
ocr_multiplier = await config.get_setting(db, "xp_multiplier_ocr_cost", default=1.5)
# 2. Intelligens Árfolyamkezelés
exchange_rate = Decimal("1.0")
@@ -66,12 +64,10 @@ class CostService:
await self._sync_telemetry(db, cost_in.asset_id, cost_in.mileage_at_cost)
# 5. Gamification (Értékesebb az adat, ha van róla fotó/OCR)
final_xp = base_xp
if new_cost.is_ai_generated:
final_xp = int(base_xp * float(ocr_multiplier))
# A pontérték a point_rules táblából jön (action_key='EXPENSE_LOG')
# Az OCR bónusz továbbra is érvényesül a process_activity szorzóin keresztül
await GamificationService.award_points(
db, user_id=user_id, amount=final_xp, reason=f"EXPENSE_LOG_{cost_in.cost_type}"
db, user_id=user_id, amount=0, reason=f"EXPENSE_LOG_{cost_in.cost_type}", action_key="EXPENSE_LOG"
)
await db.commit()

View File

@@ -82,15 +82,17 @@ class FleetService:
db.add(new_event)
# 6. DINAMIKUS GAMIFIKÁCIÓ
# Kikeresjük a konkrét eseménytípushoz tartozó pontokat
# A pontérték a point_rules táblából jön (action_key='FLEET_EVENT')
# A social pont továbbra is az event_rewards konfigurációból jön
rewards = event_rewards.get(event_data.event_type, event_rewards["default"])
await gamification_service.process_activity(
db,
user_id,
xp_amount=rewards["xp"],
social_amount=rewards["social"],
reason=f"FLEET_EVENT_{event_data.event_type.upper()}"
db,
user_id,
xp_amount=0,
social_amount=rewards["social"],
reason=f"FLEET_EVENT_{event_data.event_type.upper()}",
action_key="FLEET_EVENT"
)
await db.commit()

View File

@@ -1,6 +1,7 @@
# /opt/docker/dev/service_finder/backend/app/services/gamification_service.py
import logging
import math
from copy import deepcopy
from decimal import Decimal
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
@@ -12,6 +13,25 @@ from app.services.config_service import config # 2.0 Központi konfigurátor
logger = logging.getLogger("Gamification-Service-2.0")
def _deep_merge_config(base: dict, overlay: dict) -> dict:
"""
Rekurzívan egyesít két dictionary-t.
Az overlay kulcsai felülírják a base kulcsait, de a base-ben lévő
hiányzó kulcsok megmaradnak az overlay-ből.
Ez biztosítja, hogy a GAMIFICATION_MASTER_CONFIG minden szükséges
kulcsot tartalmazzon, még akkor is, ha az adatbázisban lévő konfiguráció
hiányos (pl. régebbi verzióból származik).
"""
result = deepcopy(base)
for key, value in overlay.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = _deep_merge_config(result[key], value)
elif key not in result:
result[key] = deepcopy(value)
return result
class GamificationService:
"""
Gamification Service 2.0 - A 'Jövevény' lelke.
@@ -22,22 +42,24 @@ class GamificationService:
"""
@staticmethod
async def award_points(db: AsyncSession, user_id: int, amount: int, reason: str, social_points: int = 0, commit: bool = True):
async def award_points(db: AsyncSession, user_id: int, amount: int, reason: str, social_points: int = 0, commit: bool = True, action_key: str | None = None):
""" Statikus segédfüggvény a Robotok számára az egyszerűbb híváshoz.
Args:
commit: If True (default), commits the transaction internally.
Set to False when called from within a larger transaction
(e.g., from AuthService.complete_kyc).
action_key: Opcionális. Ha meg van adva, a point_rules táblából
olvassa a pontértékeket a master config helyett.
"""
service = GamificationService()
return await service.process_activity(db, user_id, xp_amount=amount, social_amount=social_points, reason=reason, commit=commit)
return await service.process_activity(db, user_id, xp_amount=amount, social_amount=social_points, reason=reason, commit=commit, action_key=action_key)
async def _get_point_rule(self, db: AsyncSession, action_key: str) -> dict | None:
"""Lekér egy pontszabályt a point_rules táblából action_key alapján.
Returns:
dict with 'points' and 'description' or None if not found/inactive.
dict with 'points', 'description', 'rule_id' or None if not found/inactive.
"""
stmt = select(PointRule).where(
PointRule.action_key == action_key,
@@ -46,7 +68,7 @@ class GamificationService:
result = await db.execute(stmt)
rule = result.scalar_one_or_none()
if rule:
return {"points": rule.points, "description": rule.description}
return {"points": rule.points, "description": rule.description, "rule_id": rule.id}
return None
async def process_activity(
@@ -80,7 +102,7 @@ class GamificationService:
try:
# 1. ADMIN KONFIGURÁCIÓ BETÖLTÉSE
# Minden paraméter az admin felületről módosítható JSON-ként
cfg = await config.get_setting(db, "GAMIFICATION_MASTER_CONFIG", default={
_DEFAULT_GAMIFICATION_CONFIG = {
"xp_logic": {"base_xp": 500, "exponent": 1.5},
"penalty_logic": {
"recovery_rate": 0.5,
@@ -89,15 +111,28 @@ class GamificationService:
},
"conversion_logic": {"social_to_credit_rate": 100},
"level_rewards": {"credits_per_10_levels": 50}
})
}
cfg = await config.get_setting(db, "GAMIFICATION_MASTER_CONFIG", default=_DEFAULT_GAMIFICATION_CONFIG)
# 🔐 HIÁNYZÓ KULCSOK VÉDELME (Deep Merge)
# Ha a GAMIFICATION_MASTER_CONFIG már létezik az adatbázisban, de hiányoznak
# belőle kulcsok (pl. penalty_logic egy régebbi verzióból), a default értékek
# automatikusan kiegészítik a hiányzó részeket.
# Ez megakadályozza a KeyError kivételeket a cfg["penalty_logic"] hívásoknál.
if isinstance(cfg, dict):
cfg = _deep_merge_config(cfg, _DEFAULT_GAMIFICATION_CONFIG)
# 1/b. POINT RULES TÁBLA LEKÉRÉSE (ha action_key meg van adva)
# A point_rules tábla elsőbbséget élvez a master config-gal szemben!
point_rule_id = None
points_at_time = None
if action_key:
rule = await self._get_point_rule(db, action_key)
if rule:
# A point_rules-ból jövő pontok felülírják a paraméterként kapott értékeket
xp_amount = rule["points"]
point_rule_id = rule.get("rule_id")
points_at_time = rule["points"]
if rule.get("description"):
reason = f"{action_key}: {rule['description']}"
logger.debug(f"Point rule applied: {action_key} -> {xp_amount} XP")
@@ -152,7 +187,18 @@ class GamificationService:
stats.social_points %= rate # A maradék pont megmarad
await self._add_earned_credits(db, user_id, credits_to_add, "SOCIAL_ACTIVITY_CONVERSION")
# 7. NAPLÓZÁS (kibővítve xp, source_type, source_id mezőkkel)
# 7. NAPLÓZÁS (kibővítve xp, source_type, source_id, points_snapshot mezőkkel)
# A points_snapshot JSONB tárolja a kiosztáskor érvényes point_rules adatokat,
# hogy a pontértékek megőrződjenek a ledger-ben, még akkor is, ha a
# point_rules táblában később módosítják a pontértékeket.
snapshot = None
if action_key and point_rule_id and points_at_time is not None:
snapshot = {
"action_key": action_key,
"point_rule_id": point_rule_id,
"points_at_time": points_at_time,
"multiplier": multiplier,
}
db.add(PointsLedger(
user_id=user_id,
points=final_xp,
@@ -160,6 +206,7 @@ class GamificationService:
reason=reason,
source_type=source_type,
source_id=source_id,
points_snapshot=snapshot,
))
# Only commit if caller wants us to manage the transaction

View File

@@ -40,7 +40,7 @@ from sqlalchemy.orm import joinedload
from app.models.marketplace.organization import Organization, OrgType, Branch, OrganizationMember, OrgUserRole
from app.models.marketplace.service import ServiceProfile, ServiceStatus, ExpertiseTag, ServiceExpertise, ServiceStaging
from app.models.identity.social import ServiceProvider
from app.models.identity.social import ServiceProvider, ModerationStatus, SourceType
from app.models.gamification.gamification import PointRule, UserStats
from app.schemas.provider import (
ProviderSearchResult,
@@ -64,9 +64,12 @@ async def _award_provider_points(
"""
Dinamikus pontjóváírás a gamification.point_rules tábla alapján.
Ez a függvény az adatbázisból olvassa ki a pontértéket az action_key
alapján, így a pontok NINCSENEK beégetve a kódba. Az admin felületről
bármikor módosíthatók a point_rules táblában.
REFAKTOR (2026-06-30): A duplikált point_rules olvasási logika eltávolításra
került. A pontértékeket most a GamificationService.award_points() olvassa ki
a point_rules táblából az action_key alapján a process_activity() hívás során.
Ez a függvény már csak a providers_added_count statisztikát kezeli.
A pontok kiszámítása és naplózása a GamificationService-ben történik.
Args:
db: AsyncSession
@@ -76,39 +79,19 @@ async def _award_provider_points(
Returns:
int: A jóváírt pontok száma (0 ha a szabály nem található vagy inaktív)
"""
# 1. Pontszabály lekérdezése az adatbázisból (dinamikus!)
rule_stmt = select(PointRule).where(
PointRule.action_key == action_key,
PointRule.is_active == True,
)
rule_result = await db.execute(rule_stmt)
rule = rule_result.scalar_one_or_none()
if not rule:
logger.warning(
f"Pontszabály '{action_key}' nem található vagy inaktív. "
f"Pontjóváírás kihagyva user_id={user_id}."
)
return 0
points_to_award = rule.points
logger.info(
f"Dinamikus pont lekérés: action_key='{action_key}', "
f"points={points_to_award} (forrás: gamification.point_rules tábla)"
)
# 2. Pontok jóváírása a Gamification Service-en keresztül
# A GamificationService.award_points() kezeli a szorzókat, szintlépést,
# büntetés ledolgozást és a naplózást (PointsLedger).
# 1. Pontok jóváírása a Gamification Service-en keresztül (action_key alapján)
# A GamificationService.award_points() kezeli a point_rules olvasást, szorzókat,
# szintlépést, büntetés ledolgozást és a naplózást (PointsLedger).
await gamification_service.award_points(
db=db,
user_id=user_id,
amount=points_to_award,
reason=f"ADD_NEW_PROVIDER: +{points_to_award} pont új szolgáltató rögzítéséért",
commit=False, # A hívó (quick_add_provider) kezeli a commit-ot
amount=0,
reason=f"{action_key}",
commit=False, # A hívó kezeli a commit-ot
action_key=action_key,
)
# 3. Statisztika növelése (providers_added_count)
# 2. Statisztika növelése (providers_added_count)
stats_stmt = select(UserStats).where(UserStats.user_id == user_id)
stats = (await db.execute(stats_stmt)).scalar_one_or_none()
@@ -122,7 +105,7 @@ async def _award_provider_points(
# Ha még nincs UserStats rekordja, létrehozzuk
stats = UserStats(
user_id=user_id,
total_xp=points_to_award,
total_xp=0,
current_level=1,
providers_added_count=1,
)
@@ -132,7 +115,102 @@ async def _award_provider_points(
f"providers_added_count=1"
)
return points_to_award
return 0 # Visszatérési érték már nem a pontszám, mert az award_points kezeli
async def find_or_create_provider_by_name(
db: AsyncSession,
name: str,
added_by_user_id: int,
) -> tuple:
"""
Keres vagy létrehoz egy ServiceProvider-t a megadott név alapján.
Ez a függvény az expense auto-discovery hook része. Amikor egy user
költséget rögzít és megad egy external_vendor_name-t, ez a függvény
megkeresi, hogy létezik-e már a provider, és ha nem, létrehozza.
Logika:
1. Pontos match keresése ServiceProvider.name alapján (kisbetűs, space-sztrippelt)
2. Ha nem létezik → új provider létrehozása PENDING státusszal,
validation_score=10, source=import, vissza: PROVIDER_DISCOVERY
3. Ha létezik és APPROVED → vissza: PROVIDER_VERIFIED_USE
4. Ha létezik és nem APPROVED → vissza: PROVIDER_CONFIRMATION
5. A függvény meghívja a _award_provider_points()-t a megfelelő action_key-kel
Args:
db: AsyncSession
name: A provider neve (external_vendor_name)
added_by_user_id: A költséget rögzítő user ID-ja
Returns:
tuple[ServiceProvider, str]: (provider, action_key)
- action_key: "PROVIDER_DISCOVERY" | "PROVIDER_CONFIRMATION" | "PROVIDER_VERIFIED_USE"
"""
# 1. Pontos match keresése (kisbetűsen, space-sztrippelten)
clean_name = name.strip()
provider_stmt = select(ServiceProvider).where(
func.lower(ServiceProvider.name) == func.lower(clean_name)
)
provider_result = await db.execute(provider_stmt)
provider = provider_result.scalar_one_or_none()
if not provider:
# 2. Ha nem létezik → létrehozás + PROVIDER_DISCOVERY
new_provider = ServiceProvider(
name=clean_name,
address=clean_name,
status=ModerationStatus.pending,
source=SourceType.api,
validation_score=10,
added_by_user_id=added_by_user_id,
)
db.add(new_provider)
await db.flush()
logger.info(
f"Új provider felfedezve: name='{clean_name}', "
f"provider_id={new_provider.id}, user_id={added_by_user_id}"
)
# Gamification pont kiosztása
await _award_provider_points(
db=db,
user_id=added_by_user_id,
action_key="PROVIDER_DISCOVERY",
)
return new_provider, "PROVIDER_DISCOVERY"
# 3. Ha létezik, státusz alapján döntés
# P0 CRITICAL (Card #362): USE_UNVERIFIED_PROVIDER vs PROVIDER_CONFIRMATION
# - Ha a provider APPROVED -> PROVIDER_VERIFIED_USE (100 XP)
# - Ha a provider PENDING es a hasznalo user a provider letrehozoja -> PROVIDER_CONFIRMATION (150 XP)
# - Ha a provider PENDING es a hasznalo user NEM a provider letrehozoja -> USE_UNVERIFIED_PROVIDER (200 XP)
# Ez megakadalyozza az XP farmingot (sajat provider hasznalata sajat maganak).
if provider.status == ModerationStatus.approved:
action_key = "PROVIDER_VERIFIED_USE"
elif provider.added_by_user_id == added_by_user_id:
# A provider letrehozoja hasznalja a sajat pending provideret
action_key = "PROVIDER_CONFIRMATION"
else:
# Mas user hasznal egy pending providert
action_key = "USE_UNVERIFIED_PROVIDER"
logger.info(
f"Meglevo provider hasznalata: name='{clean_name}', "
f"provider_id={provider.id}, action_key='{action_key}', "
f"user_id={added_by_user_id}, provider_creator_id={provider.added_by_user_id}"
)
# Gamification pont kiosztása
await _award_provider_points(
db=db,
user_id=added_by_user_id,
action_key=action_key,
)
return provider, action_key
async def search_providers(

View File

@@ -10,36 +10,36 @@ from app.schemas.social import ServiceProviderCreate
logger = logging.getLogger(__name__)
class SocialService:
"""
"""
SocialService: Kezeli a közösségi interakciókat, szavazatokat és a moderációt.
Az importok a metódusokon belül vannak a körkörös függőség elkerülése érdekében.
"""
async def create_service_provider(self, db: AsyncSession, obj_in: ServiceProviderCreate, user_id: int):
from app.services.gamification_service import gamification_service
from app.services.provider_service import _award_provider_points
new_provider = ServiceProvider(**obj_in.model_dump(), added_by_user_id=user_id)
db.add(new_provider)
await db.flush()
await db.flush()
# Alappontszám az új beküldésért
await gamification_service.process_activity(db, user_id, 50, 10, f"New Provider: {new_provider.name}")
# Dinamikus pontjóváírás a point_rules táblából (ADD_NEW_PROVIDER)
await _award_provider_points(db, user_id, "ADD_NEW_PROVIDER")
await db.commit()
await db.refresh(new_provider)
return new_provider
async def vote_for_provider(self, db: AsyncSession, voter_id: int, provider_id: int, vote_value: int):
from app.services.gamification_service import gamification_service
from app.services.provider_service import _award_provider_points
# Duplikált szavazat ellenőrzése
exists = (await db.execute(select(Vote).where(and_(Vote.user_id == voter_id, Vote.provider_id == provider_id)))).scalar()
if exists:
if exists:
return {"message": "Már szavaztál erre a szolgáltatóra!"}
db.add(Vote(user_id=voter_id, provider_id=provider_id, vote_value=vote_value))
provider = (await db.execute(select(ServiceProvider).where(ServiceProvider.id == provider_id))).scalar_one_or_none()
if not provider:
if not provider:
return {"error": "Szolgáltató nem található."}
provider.validation_score += vote_value
@@ -53,6 +53,9 @@ class SocialService:
provider.status = ModerationStatus.rejected
await self._penalize_user(db, provider.added_by_user_id, provider.name)
# RATE_PROVIDER pontok kiosztása a sikeres szavazatért
await _award_provider_points(db, voter_id, "RATE_PROVIDER")
await db.commit()
return {"status": "success", "score": provider.validation_score, "new_status": provider.status}
@@ -67,7 +70,8 @@ class SocialService:
from app.services.gamification_service import gamification_service
if not user_id: return
await gamification_service.process_activity(db, user_id, 100, 20, f"Validated: {provider_name}")
# A pontérték a point_rules táblából jön (action_key='PROVIDER_VALIDATED')
await gamification_service.process_activity(db, user_id, xp_amount=0, social_amount=20, reason=f"Validated: {provider_name}", action_key="PROVIDER_VALIDATED")
# Aktuális verseny keresése és pontozása
now = datetime.now(timezone.utc)
@@ -91,8 +95,8 @@ class SocialService:
from app.services.gamification_service import gamification_service
if not user_id: return
# JAVÍTVA: is_penalty=True hozzáadva a gamification híváshoz
await gamification_service.process_activity(db, user_id, 50, 0, f"Rejected: {provider_name}", is_penalty=True)
# A pontérték a point_rules táblából jön (action_key='PROVIDER_REJECTED')
await gamification_service.process_activity(db, user_id, xp_amount=0, social_amount=0, reason=f"Rejected: {provider_name}", is_penalty=True, action_key="PROVIDER_REJECTED")
user = (await db.execute(select(User).where(User.id == user_id))).scalar_one_or_none()
if user and hasattr(user, 'reputation_score'):