admin_szolgáltatók_
This commit is contained in:
@@ -7,7 +7,7 @@ from app.api.v1.endpoints import (
|
||||
gamification, translations, users, reports, dictionaries,
|
||||
admin_packages, admin_services, admin_organizations, admin_users, admin_persons, constants, providers,
|
||||
subscriptions, marketing, admin_permissions, regions,
|
||||
admin_gamification,
|
||||
admin_gamification, admin_providers,
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
@@ -45,4 +45,5 @@ api_router.include_router(providers.router, prefix="/providers", tags=["Provider
|
||||
api_router.include_router(subscriptions.router, prefix="/subscriptions", tags=["Subscriptions"])
|
||||
api_router.include_router(marketing.router, prefix="/marketing", tags=["Marketing & Ads"])
|
||||
api_router.include_router(admin_permissions.router, prefix="", tags=["Admin Permissions (RBAC)"])
|
||||
api_router.include_router(admin_gamification.router, prefix="/admin/gamification", tags=["Admin Gamification"])
|
||||
api_router.include_router(admin_gamification.router, prefix="/admin/gamification", tags=["Admin Gamification"])
|
||||
api_router.include_router(admin_providers.router, prefix="/admin/providers", tags=["Admin Provider Moderation"])
|
||||
1321
backend/app/api/v1/endpoints/admin_providers.py
Normal file
1321
backend/app/api/v1/endpoints/admin_providers.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ from sqlalchemy import select, func, desc
|
||||
from app.api.deps import get_db, get_current_user, RequireOrgCapability
|
||||
from app.models import Asset, AssetCost, AssetEvent, OrganizationMember, SystemParameter, OrgRole, CostCategory, Organization
|
||||
from app.schemas.asset_cost import AssetCostCreate, AssetCostUpdate
|
||||
from app.services.provider_service import find_or_create_provider_by_name
|
||||
from datetime import datetime, timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -563,6 +564,32 @@ async def create_expense(
|
||||
if expense.description:
|
||||
data["description"] = expense.description
|
||||
|
||||
# ── PROVIDER AUTO-DISCOVERY HOOK (Card #360) ──
|
||||
# Ha external_vendor_name meg van adva, de service_provider_id nincs,
|
||||
# automatikusan felfedezzük vagy létrehozzuk a providert.
|
||||
# A find_or_create_provider_by_name() kezeli a gamification pontok kiosztását is
|
||||
# (PROVIDER_DISCOVERY / PROVIDER_CONFIRMATION / PROVIDER_VERIFIED_USE).
|
||||
resolved_provider_id = expense.service_provider_id
|
||||
if expense.external_vendor_name and not expense.service_provider_id:
|
||||
try:
|
||||
provider, action_key = await find_or_create_provider_by_name(
|
||||
db=db,
|
||||
name=expense.external_vendor_name,
|
||||
added_by_user_id=current_user.id,
|
||||
)
|
||||
resolved_provider_id = provider.id
|
||||
logger.info(
|
||||
f"Provider auto-discovery: name='{expense.external_vendor_name}', "
|
||||
f"provider_id={provider.id}, action_key='{action_key}', "
|
||||
f"user_id={current_user.id}"
|
||||
)
|
||||
except Exception as e:
|
||||
# Ha a provider felderítés hibázik, ne blokkolja a költség rögzítését
|
||||
logger.warning(
|
||||
f"Provider auto-discovery failed for '{expense.external_vendor_name}': {e}. "
|
||||
f"Expense will be created without provider link."
|
||||
)
|
||||
|
||||
try:
|
||||
# Create AssetCost instance with new fields
|
||||
new_cost = AssetCost(
|
||||
@@ -580,7 +607,8 @@ async def create_expense(
|
||||
# === B2B VENDOR FIELDS ===
|
||||
vendor_organization_id=expense.vendor_organization_id,
|
||||
# P0 HYBRID VENDOR REFACTOR: service_provider_id az expense creation-ben
|
||||
service_provider_id=expense.service_provider_id,
|
||||
# Ha az auto-discovery hook feloldotta a providert, a resolved_provider_id-t használjuk
|
||||
service_provider_id=resolved_provider_id,
|
||||
external_vendor_name=expense.external_vendor_name,
|
||||
# === INVOICE DATES ===
|
||||
invoice_date=expense.invoice_date,
|
||||
|
||||
@@ -36,8 +36,8 @@ async def register_service_hunt(
|
||||
"""), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng, "user_id": current_user.id})
|
||||
|
||||
# MB 2.0 Gamification: Dinamikus pontszám a felfedezésért
|
||||
reward_points = await ConfigService.get_int(db, "GAMIFICATION_HUNT_REWARD", 50)
|
||||
await GamificationService.award_points(db, current_user.id, reward_points, f"Service Hunt: {name}")
|
||||
# A pontérték a point_rules táblából jön (action_key='SERVICE_HUNT')
|
||||
await GamificationService.award_points(db, current_user.id, amount=0, reason=f"Service Hunt: {name}", action_key="SERVICE_HUNT")
|
||||
await db.commit()
|
||||
return {"status": "success", "message": "Discovery registered and points awarded."}
|
||||
|
||||
@@ -86,8 +86,8 @@ async def validate_staged_service(
|
||||
)
|
||||
|
||||
# 5. Adományozz dinamikus XP-t a current_user-nek a GamificationService-en keresztül
|
||||
validation_reward = await ConfigService.get_int(db, "GAMIFICATION_VALIDATE_REWARD", 10)
|
||||
await GamificationService.award_points(db, current_user.id, validation_reward, f"Service Validation: staging #{staging_id}")
|
||||
# A pontérték a point_rules táblából jön (action_key='SERVICE_VALIDATION')
|
||||
await GamificationService.award_points(db, current_user.id, amount=0, reason=f"Service Validation: staging #{staging_id}", action_key="SERVICE_VALIDATION")
|
||||
|
||||
# 6. Növeld a current_user places_validated értékét a UserStats-ban
|
||||
await db.execute(
|
||||
|
||||
@@ -327,6 +327,23 @@ def reload_quiz_data() -> None:
|
||||
logger.info("Quiz: Quiz data reloaded successfully")
|
||||
|
||||
|
||||
# ── LocaleManager wrapper for email_manager compatibility ──────────────
|
||||
class _LocaleManager:
|
||||
"""
|
||||
Wrapper providing a ``.get()`` interface around the ``t()`` function.
|
||||
|
||||
The ``email_manager`` module imports ``locale_manager`` and calls
|
||||
``locale_manager.get(key, lang=lang, **variables)``. This class
|
||||
delegates to the existing ``t()`` function so that no import breaks.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get(key: str, lang: Optional[str] = None, **kwargs: Any) -> str:
|
||||
return t(key, lang=lang, **kwargs)
|
||||
|
||||
|
||||
locale_manager = _LocaleManager()
|
||||
|
||||
# ── Preload on import ──────────────────────────────────────────────────
|
||||
_load_locales()
|
||||
_load_quiz_data()
|
||||
|
||||
@@ -48,6 +48,13 @@ class PointsLedger(Base):
|
||||
source_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
source_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
|
||||
# SNAPSHOT MECHANIZMUS (2026-06-30, #369)
|
||||
# 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.
|
||||
# Tartalma: {"action_key": str, "point_rule_id": int, "points_at_time": int, "multiplier": float}
|
||||
points_snapshot: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
user: Mapped["User"] = relationship("User")
|
||||
|
||||
@@ -30,6 +30,7 @@ from .social import (
|
||||
ServiceReview,
|
||||
ModerationStatus,
|
||||
SourceType,
|
||||
ProviderValidation,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -58,4 +59,5 @@ __all__ = [
|
||||
"ServiceReview",
|
||||
"ModerationStatus",
|
||||
"SourceType",
|
||||
"ProviderValidation",
|
||||
]
|
||||
@@ -5,7 +5,7 @@ from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import String, Integer, ForeignKey, DateTime, Boolean, Text, UniqueConstraint, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID
|
||||
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID, JSONB, ARRAY
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
@@ -17,6 +17,7 @@ class ModerationStatus(str, enum.Enum):
|
||||
class SourceType(str, enum.Enum):
|
||||
manual = "manual"
|
||||
ocr = "ocr"
|
||||
api = "api"
|
||||
api_import = "import"
|
||||
|
||||
class ServiceProvider(Base):
|
||||
@@ -26,7 +27,7 @@ class ServiceProvider(Base):
|
||||
kapcsolatfelvételi adatokkal a quick_add_provider() refactorhoz.
|
||||
"""
|
||||
__tablename__ = "service_providers"
|
||||
__table_args__ = {"schema": "marketplace"}
|
||||
__table_args__ = {"schema": "marketplace", "extend_existing": True}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
@@ -58,6 +59,15 @@ class ServiceProvider(Base):
|
||||
validation_score: Mapped[int] = mapped_column(Integer, default=0)
|
||||
evidence_image_path: Mapped[Optional[str]] = mapped_column(String)
|
||||
added_by_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
|
||||
|
||||
# === P0 3D FILTERING MATRIX: Járműosztályok és specializációk ===
|
||||
supported_vehicle_classes: Mapped[Optional[list]] = mapped_column(
|
||||
ARRAY(String), server_default=text("'{}'"), nullable=True
|
||||
)
|
||||
specializations: Mapped[Optional[dict]] = mapped_column(
|
||||
JSONB, server_default=text("'{}'::jsonb"), nullable=True
|
||||
)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
class Vote(Base):
|
||||
@@ -73,6 +83,45 @@ class Vote(Base):
|
||||
provider_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_providers.id"), nullable=False)
|
||||
vote_value: Mapped[int] = mapped_column(Integer, nullable=False) # +1 vagy -1
|
||||
|
||||
|
||||
class ProviderValidation(Base):
|
||||
"""
|
||||
Provider validációs rekordok (XP farming prevention).
|
||||
|
||||
Minden egyes admin moderációs akció (approve/reject/flag) egy rekordot hoz létre
|
||||
ebben a táblában. A Vote táblával ellentétben itt az admin által végzett
|
||||
validációk kerülnek rögzítésre, súlyozott értékkel.
|
||||
|
||||
Features:
|
||||
- UniqueConstraint(voter_user_id, provider_id): egy user csak egyszer validálhat
|
||||
- weight: az admin szintjétől függő súly (pl. superadmin=10, admin=5)
|
||||
- metadata: JSONB a validáció részleteivel (reason, evidence, stb.)
|
||||
- Küszöbérték: ha a weight-ek összege >= VALIDATION_THRESHOLD, a provider auto-approved
|
||||
"""
|
||||
__tablename__ = "provider_validations"
|
||||
__table_args__ = (
|
||||
UniqueConstraint('voter_user_id', 'provider_id', name='uq_voter_provider_validation'),
|
||||
{"schema": "marketplace", "extend_existing": True}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
provider_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("marketplace.service_providers.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
voter_user_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("identity.users.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
validation_type: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="approve"
|
||||
) # approve | reject | flag
|
||||
weight: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
||||
validation_metadata: Mapped[Optional[dict]] = mapped_column("metadata", JSONB, server_default=text("'{}'::jsonb"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
provider: Mapped["ServiceProvider"] = relationship("ServiceProvider", foreign_keys=[provider_id])
|
||||
voter: Mapped["User"] = relationship("User", foreign_keys=[voter_user_id])
|
||||
|
||||
class Competition(Base):
|
||||
""" Gamifikált versenyek (pl. Januári Feltöltő Verseny). """
|
||||
__tablename__ = "competitions"
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import datetime
|
||||
from typing import Any, List, Optional
|
||||
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, text, Text, Float, Index, Numeric, BigInteger
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB, ENUM as SQLEnum
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB, ENUM as SQLEnum, ARRAY
|
||||
from geoalchemy2 import Geometry
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
@@ -72,6 +72,14 @@ class ServiceProfile(Base):
|
||||
website: Mapped[Optional[str]] = mapped_column(String)
|
||||
bio: Mapped[Optional[str]] = mapped_column(Text)
|
||||
|
||||
# === P0 3D FILTERING MATRIX: Járműosztályok és specializációk ===
|
||||
supported_vehicle_classes: Mapped[Optional[list]] = mapped_column(
|
||||
ARRAY(String), server_default=text("'{}'"), nullable=True
|
||||
)
|
||||
specializations: Mapped[Optional[dict]] = mapped_column(
|
||||
JSONB, server_default=text("'{}'::jsonb"), nullable=True
|
||||
)
|
||||
|
||||
# Kapcsolatok
|
||||
organization: Mapped["Organization"] = relationship("Organization", back_populates="service_profile")
|
||||
expertises: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="service")
|
||||
|
||||
@@ -120,6 +120,8 @@ class ProviderSearchResult(BaseModel):
|
||||
source: str = Field(..., description="Forrás: verified_org | staged_data | crowd_added")
|
||||
is_verified: bool = False
|
||||
rating: Optional[float] = None
|
||||
supported_vehicle_classes: Optional[List[str]] = None
|
||||
specializations: Optional[dict] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -159,6 +161,12 @@ class ProviderQuickAddIn(BaseModel):
|
||||
contact_email: Optional[str] = Field(None, max_length=255, description="Email cím")
|
||||
website: Optional[str] = Field(None, max_length=255, description="Weboldal URL")
|
||||
tags: Optional[List[str]] = Field(None, description="Szolgáltatás címkék (specialization_tags)")
|
||||
supported_vehicle_classes: Optional[List[str]] = Field(
|
||||
None, description="Támogatott járműosztályok (pl. ['car', 'hgv', 'van'])"
|
||||
)
|
||||
specializations: Optional[dict] = Field(
|
||||
None, description="Specializációk (pl. {'brands': ['Volvo'], 'propulsion': ['EV']})"
|
||||
)
|
||||
|
||||
|
||||
class ProviderQuickAddResponse(BaseModel):
|
||||
@@ -198,6 +206,12 @@ class ProviderUpdateIn(BaseModel):
|
||||
new_tags: Optional[List[str]] = Field(
|
||||
None, description="User által gépelt új címkenevek (is_official=False, level=3)"
|
||||
)
|
||||
supported_vehicle_classes: Optional[List[str]] = Field(
|
||||
None, description="Támogatott járműosztályok (pl. ['car', 'hgv', 'van'])"
|
||||
)
|
||||
specializations: Optional[dict] = Field(
|
||||
None, description="Specializációk (pl. {'brands': ['Volvo'], 'propulsion': ['EV']})"
|
||||
)
|
||||
|
||||
|
||||
class ProviderUpdateResponse(BaseModel):
|
||||
|
||||
53
backend/app/scripts/check_provider_validations.py
Normal file
53
backend/app/scripts/check_provider_validations.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Check if provider_validations table exists and create if not.
|
||||
"""
|
||||
import asyncio
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
async def main():
|
||||
engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI))
|
||||
|
||||
async with engine.connect() as conn:
|
||||
# Check if table exists
|
||||
result = await conn.execute(
|
||||
text("SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'marketplace' AND table_name = 'provider_validations')")
|
||||
)
|
||||
exists = result.scalar()
|
||||
print(f"provider_validations table exists: {exists}")
|
||||
|
||||
if not exists:
|
||||
# Create the table
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS marketplace.provider_validations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
provider_id INTEGER NOT NULL REFERENCES marketplace.service_providers(id) ON DELETE CASCADE,
|
||||
voter_user_id INTEGER NOT NULL REFERENCES identity.users(id) ON DELETE CASCADE,
|
||||
validation_type VARCHAR(20) NOT NULL DEFAULT 'approve',
|
||||
weight INTEGER NOT NULL DEFAULT 1,
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(voter_user_id, provider_id)
|
||||
)
|
||||
"""))
|
||||
await conn.commit()
|
||||
print("Created provider_validations table")
|
||||
|
||||
# Check columns
|
||||
result = await conn.execute(text("""
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'marketplace' AND table_name = 'provider_validations'
|
||||
ORDER BY ordinal_position
|
||||
"""))
|
||||
print("\nColumns:")
|
||||
for row in result:
|
||||
print(f" {row.column_name}: {row.data_type} (nullable: {row.is_nullable})")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
157
backend/app/scripts/seed_gamification_action_keys.py
Normal file
157
backend/app/scripts/seed_gamification_action_keys.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Gamification Action Keys Seed Script
|
||||
=====================================
|
||||
Cél: Hiányzó action_key-ek felvétele a gamification.point_rules táblába.
|
||||
|
||||
Az audit során az alábbi akciókhoz NEM létezik action_key a point_rules táblában:
|
||||
- KYC_VERIFICATION (auth_service KYC completion)
|
||||
- P2P_REFERRAL_SUCCESS (auth_service P2P referral)
|
||||
- EXPENSE_LOG (cost_service expense recording)
|
||||
- FLEET_EVENT (fleet_service vehicle event)
|
||||
- PROVIDER_VALIDATED (social_service provider validation)
|
||||
- PROVIDER_REJECTED (social_service provider rejection)
|
||||
- SERVICE_HUNT (services.py service hunt)
|
||||
- SERVICE_VALIDATION (services.py service validation)
|
||||
|
||||
Használat:
|
||||
docker compose exec sf_api python3 /app/backend/app/scripts/seed_gamification_action_keys.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Projekt gyökér hozzáadása a Python path-hoz
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from app.core.config import settings
|
||||
from app.models.gamification.gamification import PointRule
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("Seed-Gamification-Action-Keys")
|
||||
|
||||
# Hiányzó action_key-ek a point_rules táblába
|
||||
# Pontértékek a meglévő ConfigService default értékek alapján:
|
||||
# KYC_VERIFICATION = 100 (auth_service: kyc_reward)
|
||||
# P2P_REFERRAL_SUCCESS = 50 (auth_service: gamification_p2p_invite_xp default)
|
||||
# EXPENSE_LOG = 50 (cost_service: xp_per_cost_log default)
|
||||
# FLEET_EVENT = 20 (fleet_service: event_rewards["default"]["xp"])
|
||||
# PROVIDER_VALIDATED = 100 (social_service: hardcoded 100XP)
|
||||
# PROVIDER_REJECTED = 50 (social_service: hardcoded 50XP)
|
||||
# SERVICE_HUNT = 50 (services.py: GAMIFICATION_HUNT_REWARD default)
|
||||
# SERVICE_VALIDATION = 10 (services.py: GAMIFICATION_VALIDATE_REWARD default)
|
||||
SEED_RULES = [
|
||||
{
|
||||
"action_key": "KYC_VERIFICATION",
|
||||
"points": 100,
|
||||
"description": "Sikeres KYC (Know Your Customer) folyamat teljesítése",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"action_key": "P2P_REFERRAL_SUCCESS",
|
||||
"points": 50,
|
||||
"description": "Sikeres P2P meghívó (referred_by_id alapján)",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"action_key": "EXPENSE_LOG",
|
||||
"points": 50,
|
||||
"description": "Költség rögzítése (base_xp, OCR bónusz nélkül)",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"action_key": "FLEET_EVENT",
|
||||
"points": 20,
|
||||
"description": "Flotta esemény rögzítése (alapértelmezett pont)",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"action_key": "PROVIDER_VALIDATED",
|
||||
"points": 100,
|
||||
"description": "Provider adatainak közösségi validálása (jóváhagyás)",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"action_key": "PROVIDER_REJECTED",
|
||||
"points": 50,
|
||||
"description": "Provider adatainak közösségi elutasítása (büntetés)",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"action_key": "SERVICE_HUNT",
|
||||
"points": 50,
|
||||
"description": "Új szerviz felfedezése (service hunt)",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"action_key": "SERVICE_VALIDATION",
|
||||
"points": 10,
|
||||
"description": "Szerviz validálása (más user által beküldött staging rekord)",
|
||||
"is_active": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def seed_gamification_action_keys():
|
||||
"""
|
||||
Feltölti a gamification.point_rules táblát a hiányzó action_key-ekkel.
|
||||
Csak azokat a rekordokat szúrja be, amelyek még nem léteznek (action_key alapján).
|
||||
"""
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
||||
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as db:
|
||||
try:
|
||||
inserted_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for rule_data in SEED_RULES:
|
||||
# Ellenőrizzük, hogy létezik-e már ez a szabály
|
||||
stmt = select(PointRule).where(
|
||||
PointRule.action_key == rule_data["action_key"]
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(
|
||||
f"⏭️ Szabály már létezik: {rule_data['action_key']} "
|
||||
f"(pont: {existing.points}, ID: {existing.id})"
|
||||
)
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Új szabály beszúrása
|
||||
new_rule = PointRule(
|
||||
action_key=rule_data["action_key"],
|
||||
points=rule_data["points"],
|
||||
description=rule_data["description"],
|
||||
is_active=rule_data["is_active"],
|
||||
)
|
||||
db.add(new_rule)
|
||||
await db.flush()
|
||||
logger.info(
|
||||
f"✅ Szabály létrehozva: {rule_data['action_key']} "
|
||||
f"(pont: {rule_data['points']}, ID: {new_rule.id})"
|
||||
)
|
||||
inserted_count += 1
|
||||
|
||||
await db.commit()
|
||||
logger.info(
|
||||
f"\n📊 Összegzés: {inserted_count} új szabály beszúrva, "
|
||||
f"{skipped_count} már létezett."
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"❌ Hiba a seedelés során: {e}", exc_info=True)
|
||||
raise
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_gamification_action_keys())
|
||||
124
backend/app/scripts/seed_provider_point_rules.py
Normal file
124
backend/app/scripts/seed_provider_point_rules.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Provider Gamification Point Rules Seed Script
|
||||
=============================================
|
||||
Cél: Új gamification pontszabályok beszúrása a provider discovery rendszerhez.
|
||||
|
||||
Új szabályok:
|
||||
- PROVIDER_DISCOVERY (300 pont) — Új provider felfedezése expense rögzítéskor
|
||||
- PROVIDER_CONFIRMATION (150 pont) — Meglévő provider adatainak megerősítése
|
||||
- PROVIDER_VERIFIED_USE (100 pont) — Már validált provider használata
|
||||
|
||||
Használat:
|
||||
docker compose exec sf_api python3 /app/backend/app/scripts/seed_provider_point_rules.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Projekt gyökér hozzáadása a Python path-hoz
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from app.core.config import settings
|
||||
from app.models.gamification.gamification import PointRule
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("Seed-Provider-Point-Rules")
|
||||
|
||||
# Új provider discovery pontszabályok
|
||||
# A pontértékek a kártya specifikációja szerint (#356, #362):
|
||||
# PROVIDER_DISCOVERY = 300 — Új provider felfedezése
|
||||
# PROVIDER_CONFIRMATION = 150 — Saját pending provider használata
|
||||
# PROVIDER_VERIFIED_USE = 100 — Már validált provider használata
|
||||
# USE_UNVERIFIED_PROVIDER = 200 — Más user által létrehozott, még pending provider használata
|
||||
SEED_RULES = [
|
||||
{
|
||||
"action_key": "PROVIDER_DISCOVERY",
|
||||
"points": 300,
|
||||
"description": "Új, még nem létező provider felfedezése expense rögzítéskor (external_vendor_name alapján)",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"action_key": "PROVIDER_CONFIRMATION",
|
||||
"points": 150,
|
||||
"description": "Saját magad által létrehozott, még pending státuszú provider használata",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"action_key": "PROVIDER_VERIFIED_USE",
|
||||
"points": 100,
|
||||
"description": "Már jóváhagyott (approved) provider használata költség rögzítésnél",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"action_key": "USE_UNVERIFIED_PROVIDER",
|
||||
"points": 200,
|
||||
"description": "Más user által létrehozott, még pending státuszú provider használata (XP farming prevention)",
|
||||
"is_active": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def seed_provider_point_rules():
|
||||
"""
|
||||
Feltölti a gamification.point_rules táblát a provider discovery szabályokkal.
|
||||
Csak azokat a rekordokat szúrja be, amelyek még nem léteznek (action_key alapján).
|
||||
"""
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
||||
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as db:
|
||||
try:
|
||||
inserted_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for rule_data in SEED_RULES:
|
||||
# Ellenőrizzük, hogy létezik-e már ez a szabály
|
||||
stmt = select(PointRule).where(
|
||||
PointRule.action_key == rule_data["action_key"]
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(
|
||||
f"⏭️ Szabály már létezik: {rule_data['action_key']} "
|
||||
f"(pont: {existing.points}, ID: {existing.id})"
|
||||
)
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Új szabály beszúrása
|
||||
new_rule = PointRule(
|
||||
action_key=rule_data["action_key"],
|
||||
points=rule_data["points"],
|
||||
description=rule_data["description"],
|
||||
is_active=rule_data["is_active"],
|
||||
)
|
||||
db.add(new_rule)
|
||||
await db.flush()
|
||||
logger.info(
|
||||
f"✅ Szabály létrehozva: {rule_data['action_key']} "
|
||||
f"(pont: {rule_data['points']}, ID: {new_rule.id})"
|
||||
)
|
||||
inserted_count += 1
|
||||
|
||||
await db.commit()
|
||||
logger.info(
|
||||
f"\n📊 Összegzés: {inserted_count} új szabály beszúrva, "
|
||||
f"{skipped_count} már létezett."
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"❌ Hiba a seedelés során: {e}", exc_info=True)
|
||||
raise
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_provider_point_rules())
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'):
|
||||
|
||||
Reference in New Issue
Block a user