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'):
|
||||
|
||||
186
backend/backend/tests/active/test_use_unverified_provider.py
Normal file
186
backend/backend/tests/active/test_use_unverified_provider.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Test: USE_UNVERIFIED_PROVIDER pontszabály bekötése (Card #362)
|
||||
=============================================================
|
||||
Ellenőrzi, hogy a find_or_create_provider_by_name() függvény helyesen
|
||||
differenciál a PROVIDER_CONFIRMATION és USE_UNVERIFIED_PROVIDER között.
|
||||
|
||||
Logika:
|
||||
1. Ha a provider APPROVED -> PROVIDER_VERIFIED_USE (100 XP)
|
||||
2. Ha a provider PENDING és a használó user a provider létrehozója -> PROVIDER_CONFIRMATION (150 XP)
|
||||
3. Ha a provider PENDING és a használó user NEM a provider létrehozója -> USE_UNVERIFIED_PROVIDER (200 XP)
|
||||
|
||||
Használat:
|
||||
docker compose exec sf_api python3 /app/backend/tests/active/test_use_unverified_provider.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "backend"))
|
||||
|
||||
from sqlalchemy import select, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from app.core.config import settings
|
||||
from app.models.identity.social import ServiceProvider, ModerationStatus, SourceType
|
||||
from app.models.gamification.gamification import PointRule, UserStats, PointsLedger
|
||||
from app.services.provider_service import find_or_create_provider_by_name, _award_provider_points
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("Test-USE_UNVERIFIED_PROVIDER")
|
||||
|
||||
TEST_USER_ID = 1 # Provider creator (superadmin@profibot.hu)
|
||||
TEST_OTHER_USER_ID = 2 # Other user using the provider (admin@profibot.hu)
|
||||
TEST_PROVIDER_NAME = "Test Unverified Provider Card362"
|
||||
|
||||
|
||||
async def cleanup_test_data(db: AsyncSession):
|
||||
"""Clean up any leftover test data."""
|
||||
await db.execute(
|
||||
delete(PointsLedger).where(
|
||||
PointsLedger.user_id.in_([TEST_USER_ID, TEST_OTHER_USER_ID])
|
||||
)
|
||||
)
|
||||
await db.execute(
|
||||
delete(UserStats).where(
|
||||
UserStats.user_id.in_([TEST_USER_ID, TEST_OTHER_USER_ID])
|
||||
)
|
||||
)
|
||||
await db.execute(
|
||||
delete(ServiceProvider).where(ServiceProvider.name == TEST_PROVIDER_NAME)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def test_use_unverified_provider():
|
||||
"""
|
||||
Test scenario:
|
||||
1. User A (TEST_USER_ID) creates a provider via find_or_create_provider_by_name()
|
||||
-> Should return PROVIDER_DISCOVERY (300 XP) - new provider
|
||||
2. User A uses the same provider again
|
||||
-> Should return PROVIDER_CONFIRMATION (150 XP) - same user, pending provider
|
||||
3. User B (TEST_OTHER_USER_ID) uses the same provider
|
||||
-> Should return USE_UNVERIFIED_PROVIDER (200 XP) - different user, pending provider
|
||||
"""
|
||||
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:
|
||||
# Clean up first
|
||||
await cleanup_test_data(db)
|
||||
|
||||
# =========================================================
|
||||
# STEP 1: User A creates a new provider (PROVIDER_DISCOVERY)
|
||||
# =========================================================
|
||||
logger.info("=" * 60)
|
||||
logger.info("STEP 1: User A creates a new provider")
|
||||
logger.info("=" * 60)
|
||||
|
||||
provider, action_key = await find_or_create_provider_by_name(
|
||||
db=db,
|
||||
name=TEST_PROVIDER_NAME,
|
||||
added_by_user_id=TEST_USER_ID,
|
||||
)
|
||||
|
||||
assert provider is not None, "Provider should be created"
|
||||
assert action_key == "PROVIDER_DISCOVERY", (
|
||||
f"Expected PROVIDER_DISCOVERY, got {action_key}"
|
||||
)
|
||||
assert provider.status == ModerationStatus.pending, (
|
||||
f"Expected pending status, got {provider.status}"
|
||||
)
|
||||
assert provider.added_by_user_id == TEST_USER_ID, (
|
||||
f"Expected added_by_user_id={TEST_USER_ID}, got {provider.added_by_user_id}"
|
||||
)
|
||||
logger.info(f"✅ STEP 1 PASS: action_key={action_key}, provider_id={provider.id}")
|
||||
|
||||
# Check that points were awarded
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == TEST_USER_ID)
|
||||
stats = (await db.execute(stats_stmt)).scalar_one_or_none()
|
||||
assert stats is not None, "UserStats should exist"
|
||||
assert stats.total_xp >= 300, (
|
||||
f"Expected at least 300 XP, got {stats.total_xp}"
|
||||
)
|
||||
logger.info(f"✅ STEP 1 XP CHECK: total_xp={stats.total_xp}")
|
||||
|
||||
# =========================================================
|
||||
# STEP 2: User A uses the same provider again (PROVIDER_CONFIRMATION)
|
||||
# =========================================================
|
||||
logger.info("=" * 60)
|
||||
logger.info("STEP 2: User A uses the same provider again")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# Re-fetch to get clean state
|
||||
await db.refresh(provider)
|
||||
|
||||
provider2, action_key2 = await find_or_create_provider_by_name(
|
||||
db=db,
|
||||
name=TEST_PROVIDER_NAME,
|
||||
added_by_user_id=TEST_USER_ID,
|
||||
)
|
||||
|
||||
assert provider2.id == provider.id, "Should be the same provider"
|
||||
assert action_key2 == "PROVIDER_CONFIRMATION", (
|
||||
f"Expected PROVIDER_CONFIRMATION, got {action_key2}"
|
||||
)
|
||||
logger.info(f"✅ STEP 2 PASS: action_key={action_key2}")
|
||||
|
||||
# =========================================================
|
||||
# STEP 3: User B uses the same provider (USE_UNVERIFIED_PROVIDER)
|
||||
# =========================================================
|
||||
logger.info("=" * 60)
|
||||
logger.info("STEP 3: User B uses the same provider")
|
||||
logger.info("=" * 60)
|
||||
|
||||
provider3, action_key3 = await find_or_create_provider_by_name(
|
||||
db=db,
|
||||
name=TEST_PROVIDER_NAME,
|
||||
added_by_user_id=TEST_OTHER_USER_ID,
|
||||
)
|
||||
|
||||
assert provider3.id == provider.id, "Should be the same provider"
|
||||
assert action_key3 == "USE_UNVERIFIED_PROVIDER", (
|
||||
f"Expected USE_UNVERIFIED_PROVIDER, got {action_key3}"
|
||||
)
|
||||
logger.info(f"✅ STEP 3 PASS: action_key={action_key3}")
|
||||
|
||||
# Check that User B got points
|
||||
stats_stmt2 = select(UserStats).where(UserStats.user_id == TEST_OTHER_USER_ID)
|
||||
stats2 = (await db.execute(stats_stmt2)).scalar_one_or_none()
|
||||
assert stats2 is not None, "UserStats should exist for User B"
|
||||
assert stats2.total_xp >= 200, (
|
||||
f"Expected at least 200 XP for User B, got {stats2.total_xp}"
|
||||
)
|
||||
logger.info(f"✅ STEP 3 XP CHECK: User B total_xp={stats2.total_xp}")
|
||||
|
||||
# =========================================================
|
||||
# SUMMARY
|
||||
# =========================================================
|
||||
logger.info("=" * 60)
|
||||
logger.info("🎉 ALL TESTS PASSED!")
|
||||
logger.info(f" - PROVIDER_DISCOVERY (300 XP): ✅ User A created provider")
|
||||
logger.info(f" - PROVIDER_CONFIRMATION (150 XP): ✅ User A reused own provider")
|
||||
logger.info(f" - USE_UNVERIFIED_PROVIDER (200 XP): ✅ User B used pending provider")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# Clean up
|
||||
await cleanup_test_data(db)
|
||||
|
||||
except AssertionError as e:
|
||||
logger.error(f"❌ TEST FAILED: {e}")
|
||||
await db.rollback()
|
||||
await cleanup_test_data(db)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ UNEXPECTED ERROR: {e}", exc_info=True)
|
||||
await db.rollback()
|
||||
await cleanup_test_data(db)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_use_unverified_provider())
|
||||
@@ -0,0 +1,28 @@
|
||||
"""feat: add vehicle classes and specializations to providers
|
||||
|
||||
Revision ID: 2387cd18fde7
|
||||
Revises: 7cd9b8a65ce8
|
||||
Create Date: 2026-07-01 00:38:21.828081
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '2387cd18fde7'
|
||||
down_revision: Union[str, Sequence[str], None] = '7cd9b8a65ce8'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
pass
|
||||
131
backend/static/locales/cz.json
Normal file
131
backend/static/locales/cz.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"AUTH": {
|
||||
"LOGIN": {
|
||||
"SUCCESS": "Přihlášení úspěšné. Vítejte zpět!"
|
||||
},
|
||||
"ERROR": {
|
||||
"EMAIL_EXISTS": "Tento email je již zaregistrován.",
|
||||
"UNAUTHORIZED": "Neoprávněný přístup."
|
||||
}
|
||||
},
|
||||
"SENTINEL": {
|
||||
"LOCK": {
|
||||
"MSG": "Účet byl z bezpečnostních důvodů uzamčen."
|
||||
},
|
||||
"APPROVAL": {
|
||||
"REQUIRED": "Vyžadováno schválení"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"SAVE_SUCCESS": "Úspěšně uloženo!"
|
||||
},
|
||||
"EMAIL": {
|
||||
"REG": {
|
||||
"SUBJECT": "Potvrďte svou registraci - Service Finder",
|
||||
"GREETING": "Ahoj {{first_name}}!",
|
||||
"BODY": "Děkujeme za registraci! Kliknutím na tlačítko níže aktivujete svůj účet:",
|
||||
"BUTTON": "Aktivovat účet",
|
||||
"FOOTER": "Pokud jste se nezaregistrovali, ignorujte prosím tento email."
|
||||
},
|
||||
"PWD_RESET": {
|
||||
"SUBJECT": "Žádost o obnovení hesla",
|
||||
"GREETING": "Ahoj!",
|
||||
"BODY": "Požádali jste o obnovení hesla. Kliknutím na tlačítko níže nastavíte nové heslo:",
|
||||
"BUTTON": "Obnovit heslo",
|
||||
"FOOTER": "Pokud jste o obnovení hesla nežádali, ignorujte prosím tento email."
|
||||
},
|
||||
"TEST": {
|
||||
"SUBJECT": "🧪 Testovací email - Service Finder",
|
||||
"GREETING": "Ahoj {{first_name}}!",
|
||||
"BODY": "Toto je testovací email ze systému Service Finder. Pokud to vidíte, odesílání emailů funguje!",
|
||||
"BUTTON": "Přejít na Dashboard",
|
||||
"FOOTER": "Service Finder - Testovací systémová zpráva"
|
||||
}
|
||||
},
|
||||
"ORGANIZATION": {
|
||||
"ERROR": {
|
||||
"NOT_FOUND": "Organizace nenalezena.",
|
||||
"INVALID_TAX_FORMAT": "Neplatný formát maďarského daňového čísla!",
|
||||
"TAX_LOOKUP_FAILED": "Ověření daňového čísla selhalo.",
|
||||
"FORBIDDEN": "Nemáte oprávnění k přístupu k této organizaci.",
|
||||
"INVALID_ROLE": "Neplatná role.",
|
||||
"CANNOT_REMOVE_OWNER": "Vlastník nemůže být odstraněn z organizace.",
|
||||
"CANNOT_DEMOTE_OWNER": "Role vlastníka nemůže být změněna.",
|
||||
"ALREADY_MEMBER": "Již jste členem této organizace.",
|
||||
"NO_ACTIVE_ADMIN": "Tato organizace nemá aktivního správce. Použijte koncový bod /claim.",
|
||||
"HAS_ACTIVE_ADMIN": "Tato organizace má aktivního správce. Použijte koncový bod /join-request.",
|
||||
"EMAIL_MISMATCH": "Email neodpovídá emailu vlastníka organizace.",
|
||||
"INVALID_CODE": "Neplatný nebo expirovaný kód.",
|
||||
"CODE_ORG_MISMATCH": "Kód nepatří k této organizaci.",
|
||||
"INVITE_EXPIRED": "Pozvánka vypršela.",
|
||||
"INVITE_INVALID": "Neplatný token pozvánky."
|
||||
},
|
||||
"SUCCESS": {
|
||||
"ONBOARDED": "Společnost úspěšně vytvořena a zaregistrována.",
|
||||
"MEMBER_REMOVED": "Člen úspěšně odstraněn.",
|
||||
"ROLE_UPDATED": "Role úspěšně aktualizována.",
|
||||
"UPDATED": "Data organizace úspěšně aktualizována.",
|
||||
"VISUAL_SETTINGS_UPDATED": "Vizuální nastavení úspěšně aktualizováno."
|
||||
},
|
||||
"INVITATION": {
|
||||
"CREATED": "Pozvánka úspěšně vytvořena.",
|
||||
"ACCEPTED": "Pozvánka přijata.",
|
||||
"REVOKED": "Pozvánka odvolána."
|
||||
},
|
||||
"JOIN_REQUEST": {
|
||||
"SENT": "Vaše žádost o připojení byla odeslána. Čeká na schválení správcem."
|
||||
},
|
||||
"CLAIM": {
|
||||
"OTP_SENT": "6místný ověřovací kód byl odeslán na email vlastníka organizace.",
|
||||
"SUCCESS": "Úspěšně jste převzali organizaci. Nyní jste správcem."
|
||||
}
|
||||
},
|
||||
"REGISTRATION_DOCUMENTS": {
|
||||
"CERTIFICATE_NUMBER": "Číslo osvědčení o registraci",
|
||||
"CERTIFICATE_VALIDITY": "Platnost osvědčení o registraci",
|
||||
"DOCUMENT_NUMBER": "Číslo dokladu vozidla",
|
||||
"TITLE": "Osvědčení o registraci & Doklad vozidla"
|
||||
},
|
||||
"GAMIFICATION": {
|
||||
"SUBMIT_SERVICE": {
|
||||
"SELF_DEFENSE_BLOCKED": "Z důvodu omezení sebeobrany nemůžete odesílat nová servisní data.",
|
||||
"SUCCESS": "Servis odeslán do systému k analýze!",
|
||||
"POINTS_ERROR": "Chyba při bodování: {error}",
|
||||
"SUBMIT_ERROR": "Chyba při odesílání: {error}"
|
||||
},
|
||||
"QUIZ": {
|
||||
"ALREADY_PLAYED": "Dnes jste již hráli denní kvíz. Vraťte se zítra!",
|
||||
"NO_QUIZ_AVAILABLE": "Dnes není k dispozici žádný kvíz.",
|
||||
"ALREADY_COMPLETED": "Denní kvíz byl dnes již označen jako dokončený.",
|
||||
"COMPLETED_SUCCESS": "Denní kvíz označen jako dokončený pro dnešek.",
|
||||
"QUESTION_NOT_FOUND": "Otázka nenalezena.",
|
||||
"POINTS_FAILED": "Nepodařilo se udělit body za kvíz: {error}"
|
||||
},
|
||||
"BADGE": {
|
||||
"NOT_FOUND": "Odznak nenalezen.",
|
||||
"ALREADY_AWARDED": "Odznak již byl tomuto uživateli udělen.",
|
||||
"AWARDED_SUCCESS": "Odznak '{badge_name}' byl udělen uživateli.",
|
||||
"POINTS_FAILED": "Nepodařilo se udělit body za odznak: {error}"
|
||||
},
|
||||
"SEASON": {
|
||||
"NOT_FOUND": "Sezóna nenalezena.",
|
||||
"NO_ACTIVE": "Nebyla nalezena žádná aktivní sezóna."
|
||||
},
|
||||
"ACHIEVEMENTS": {
|
||||
"XP_NOVICE": "Začátečník",
|
||||
"XP_APPRENTICE": "Učeň",
|
||||
"XP_EXPERT": "Expert",
|
||||
"XP_MASTER": "Mistr",
|
||||
"XP_NOVICE_DESC": "Získej 100 XP",
|
||||
"XP_APPRENTICE_DESC": "Získej 500 XP",
|
||||
"XP_EXPERT_DESC": "Získej 2000 XP",
|
||||
"XP_MASTER_DESC": "Získej 5000 XP",
|
||||
"QUIZ_BEGINNER": "Kvízový začátečník",
|
||||
"QUIZ_ENTHUSIAST": "Kvízový nadšenec",
|
||||
"QUIZ_MASTER": "Kvízový mistr",
|
||||
"QUIZ_BEGINNER_DESC": "Získej 50 kvízových bodů",
|
||||
"QUIZ_ENTHUSIAST_DESC": "Získej 200 kvízových bodů",
|
||||
"QUIZ_MASTER_DESC": "Získej 500 kvízových bodů"
|
||||
}
|
||||
}
|
||||
}
|
||||
131
backend/static/locales/de.json
Normal file
131
backend/static/locales/de.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"AUTH": {
|
||||
"LOGIN": {
|
||||
"SUCCESS": "Anmeldung erfolgreich. Willkommen zurück!"
|
||||
},
|
||||
"ERROR": {
|
||||
"EMAIL_EXISTS": "Diese E-Mail ist bereits registriert.",
|
||||
"UNAUTHORIZED": "Unbefugter Zugriff."
|
||||
}
|
||||
},
|
||||
"SENTINEL": {
|
||||
"LOCK": {
|
||||
"MSG": "Konto aus Sicherheitsgründen gesperrt."
|
||||
},
|
||||
"APPROVAL": {
|
||||
"REQUIRED": "Genehmigung erforderlich"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"SAVE_SUCCESS": "Erfolgreich gespeichert!"
|
||||
},
|
||||
"EMAIL": {
|
||||
"REG": {
|
||||
"SUBJECT": "Bestätigen Sie Ihre Registrierung - Service Finder",
|
||||
"GREETING": "Hallo {{first_name}}!",
|
||||
"BODY": "Vielen Dank für Ihre Registrierung! Klicken Sie auf die Schaltfläche unten, um Ihr Konto zu aktivieren:",
|
||||
"BUTTON": "Konto aktivieren",
|
||||
"FOOTER": "Wenn Sie sich nicht registriert haben, ignorieren Sie bitte diese E-Mail."
|
||||
},
|
||||
"PWD_RESET": {
|
||||
"SUBJECT": "Passwort zurücksetzen",
|
||||
"GREETING": "Hallo!",
|
||||
"BODY": "Sie haben eine Passwortzurücksetzung angefordert. Klicken Sie auf die Schaltfläche unten, um ein neues Passwort festzulegen:",
|
||||
"BUTTON": "Passwort zurücksetzen",
|
||||
"FOOTER": "Wenn Sie keine Passwortzurücksetzung angefordert haben, ignorieren Sie bitte diese E-Mail."
|
||||
},
|
||||
"TEST": {
|
||||
"SUBJECT": "🧪 Test-E-Mail - Service Finder",
|
||||
"GREETING": "Hallo {{first_name}}!",
|
||||
"BODY": "Dies ist eine Test-E-Mail vom Service Finder System. Wenn Sie dies sehen, funktioniert der E-Mail-Versand!",
|
||||
"BUTTON": "Zum Dashboard",
|
||||
"FOOTER": "Service Finder - Test-Systemnachricht"
|
||||
}
|
||||
},
|
||||
"ORGANIZATION": {
|
||||
"ERROR": {
|
||||
"NOT_FOUND": "Organisation nicht gefunden.",
|
||||
"INVALID_TAX_FORMAT": "Ungültiges ungarisches Steuernummernformat!",
|
||||
"TAX_LOOKUP_FAILED": "Steuernummernprüfung fehlgeschlagen.",
|
||||
"FORBIDDEN": "Sie haben keine Berechtigung für diese Organisation.",
|
||||
"INVALID_ROLE": "Ungültige Rolle angegeben.",
|
||||
"CANNOT_REMOVE_OWNER": "Der Eigentümer kann nicht aus der Organisation entfernt werden.",
|
||||
"CANNOT_DEMOTE_OWNER": "Die Rolle des Eigentümers kann nicht geändert werden.",
|
||||
"ALREADY_MEMBER": "Sie sind bereits Mitglied dieser Organisation.",
|
||||
"NO_ACTIVE_ADMIN": "Diese Organisation hat keinen aktiven Administrator. Verwenden Sie den /claim-Endpunkt.",
|
||||
"HAS_ACTIVE_ADMIN": "Diese Organisation hat einen aktiven Administrator. Verwenden Sie den /join-request-Endpunkt.",
|
||||
"EMAIL_MISMATCH": "Die E-Mail stimmt nicht mit der E-Mail des Organisationsinhabers überein.",
|
||||
"INVALID_CODE": "Ungültiger oder abgelaufener Code.",
|
||||
"CODE_ORG_MISMATCH": "Der Code gehört nicht zu dieser Organisation.",
|
||||
"INVITE_EXPIRED": "Die Einladung ist abgelaufen.",
|
||||
"INVITE_INVALID": "Ungültiges Einladungstoken."
|
||||
},
|
||||
"SUCCESS": {
|
||||
"ONBOARDED": "Unternehmen erfolgreich erstellt und registriert.",
|
||||
"MEMBER_REMOVED": "Mitglied erfolgreich entfernt.",
|
||||
"ROLE_UPDATED": "Rolle erfolgreich aktualisiert.",
|
||||
"UPDATED": "Organisationsdaten erfolgreich aktualisiert.",
|
||||
"VISUAL_SETTINGS_UPDATED": "Visuelle Einstellungen erfolgreich aktualisiert."
|
||||
},
|
||||
"INVITATION": {
|
||||
"CREATED": "Einladung erfolgreich erstellt.",
|
||||
"ACCEPTED": "Einladung angenommen.",
|
||||
"REVOKED": "Einladung widerrufen."
|
||||
},
|
||||
"JOIN_REQUEST": {
|
||||
"SENT": "Ihre Beitrittsanfrage wurde übermittelt. Warten auf Administratorgenehmigung."
|
||||
},
|
||||
"CLAIM": {
|
||||
"OTP_SENT": "Ein 6-stelliger Bestätigungscode wurde an die E-Mail des Organisationsinhabers gesendet.",
|
||||
"SUCCESS": "Sie haben die Organisation erfolgreich übernommen. Sie sind jetzt der Administrator."
|
||||
}
|
||||
},
|
||||
"REGISTRATION_DOCUMENTS": {
|
||||
"CERTIFICATE_NUMBER": "Zulassungsbescheinigung Nummer",
|
||||
"CERTIFICATE_VALIDITY": "Gültigkeit der Zulassungsbescheinigung",
|
||||
"DOCUMENT_NUMBER": "Fahrzeugdokument Nummer",
|
||||
"TITLE": "Zulassungsbescheinigung & Fahrzeugdokument"
|
||||
},
|
||||
"GAMIFICATION": {
|
||||
"SUBMIT_SERVICE": {
|
||||
"SELF_DEFENSE_BLOCKED": "Sie können aufgrund einer Selbstverteidigungseinschränkung keine neuen Servicedaten einreichen.",
|
||||
"SUCCESS": "Service zur Analyse an das System übermittelt!",
|
||||
"POINTS_ERROR": "Fehler bei der Bewertung: {error}",
|
||||
"SUBMIT_ERROR": "Fehler bei der Übermittlung: {error}"
|
||||
},
|
||||
"QUIZ": {
|
||||
"ALREADY_PLAYED": "Sie haben heute bereits am täglichen Quiz teilgenommen. Kommen Sie morgen wieder!",
|
||||
"NO_QUIZ_AVAILABLE": "Heute ist kein Quiz verfügbar.",
|
||||
"ALREADY_COMPLETED": "Das tägliche Quiz wurde heute bereits als abgeschlossen markiert.",
|
||||
"COMPLETED_SUCCESS": "Tägliches Quiz wurde heute als abgeschlossen markiert.",
|
||||
"QUESTION_NOT_FOUND": "Frage nicht gefunden.",
|
||||
"POINTS_FAILED": "Fehler bei der Vergabe von Quizpunkten: {error}"
|
||||
},
|
||||
"BADGE": {
|
||||
"NOT_FOUND": "Abzeichen nicht gefunden.",
|
||||
"ALREADY_AWARDED": "Abzeichen wurde diesem Benutzer bereits verliehen.",
|
||||
"AWARDED_SUCCESS": "Abzeichen '{badge_name}' wurde dem Benutzer verliehen.",
|
||||
"POINTS_FAILED": "Fehler bei der Vergabe von Abzeichenpunkten: {error}"
|
||||
},
|
||||
"SEASON": {
|
||||
"NOT_FOUND": "Saison nicht gefunden.",
|
||||
"NO_ACTIVE": "Keine aktive Saison gefunden."
|
||||
},
|
||||
"ACHIEVEMENTS": {
|
||||
"XP_NOVICE": "Anfänger",
|
||||
"XP_APPRENTICE": "Lehrling",
|
||||
"XP_EXPERT": "Experte",
|
||||
"XP_MASTER": "Meister",
|
||||
"XP_NOVICE_DESC": "Sammle 100 XP",
|
||||
"XP_APPRENTICE_DESC": "Sammle 500 XP",
|
||||
"XP_EXPERT_DESC": "Sammle 2000 XP",
|
||||
"XP_MASTER_DESC": "Sammle 5000 XP",
|
||||
"QUIZ_BEGINNER": "Quiz-Anfänger",
|
||||
"QUIZ_ENTHUSIAST": "Quiz-Enthusiast",
|
||||
"QUIZ_MASTER": "Quiz-Meister",
|
||||
"QUIZ_BEGINNER_DESC": "Sammle 50 Quizpunkte",
|
||||
"QUIZ_ENTHUSIAST_DESC": "Sammle 200 Quizpunkte",
|
||||
"QUIZ_MASTER_DESC": "Sammle 500 Quizpunkte"
|
||||
}
|
||||
}
|
||||
}
|
||||
131
backend/static/locales/fr.json
Normal file
131
backend/static/locales/fr.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"AUTH": {
|
||||
"LOGIN": {
|
||||
"SUCCESS": "Connexion réussie. Bon retour parmi nous!"
|
||||
},
|
||||
"ERROR": {
|
||||
"EMAIL_EXISTS": "Cet email est déjà enregistré.",
|
||||
"UNAUTHORIZED": "Accès non autorisé."
|
||||
}
|
||||
},
|
||||
"SENTINEL": {
|
||||
"LOCK": {
|
||||
"MSG": "Compte verrouillé pour des raisons de sécurité."
|
||||
},
|
||||
"APPROVAL": {
|
||||
"REQUIRED": "Approbation requise"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"SAVE_SUCCESS": "Enregistré avec succès!"
|
||||
},
|
||||
"EMAIL": {
|
||||
"REG": {
|
||||
"SUBJECT": "Confirmez votre inscription - Service Finder",
|
||||
"GREETING": "Bonjour {{first_name}}!",
|
||||
"BODY": "Merci de vous être inscrit! Cliquez sur le bouton ci-dessous pour activer votre compte:",
|
||||
"BUTTON": "Activer le compte",
|
||||
"FOOTER": "Si vous ne vous êtes pas inscrit, veuillez ignorer cet email."
|
||||
},
|
||||
"PWD_RESET": {
|
||||
"SUBJECT": "Demande de réinitialisation du mot de passe",
|
||||
"GREETING": "Bonjour!",
|
||||
"BODY": "Vous avez demandé une réinitialisation de mot de passe. Cliquez sur le bouton ci-dessous pour définir un nouveau mot de passe:",
|
||||
"BUTTON": "Réinitialiser le mot de passe",
|
||||
"FOOTER": "Si vous n'avez pas demandé de réinitialisation, veuillez ignorer cet email."
|
||||
},
|
||||
"TEST": {
|
||||
"SUBJECT": "🧪 Email de test - Service Finder",
|
||||
"GREETING": "Bonjour {{first_name}}!",
|
||||
"BODY": "Ceci est un email de test du système Service Finder. Si vous voyez ceci, l'envoi d'emails fonctionne!",
|
||||
"BUTTON": "Aller au tableau de bord",
|
||||
"FOOTER": "Service Finder - Message système de test"
|
||||
}
|
||||
},
|
||||
"ORGANIZATION": {
|
||||
"ERROR": {
|
||||
"NOT_FOUND": "Organisation non trouvée.",
|
||||
"INVALID_TAX_FORMAT": "Format de numéro de TVA hongrois invalide!",
|
||||
"TAX_LOOKUP_FAILED": "La vérification du numéro de TVA a échoué.",
|
||||
"FORBIDDEN": "Vous n'avez pas la permission d'accéder à cette organisation.",
|
||||
"INVALID_ROLE": "Rôle spécifié invalide.",
|
||||
"CANNOT_REMOVE_OWNER": "Le propriétaire ne peut pas être retiré de l'organisation.",
|
||||
"CANNOT_DEMOTE_OWNER": "Le rôle du propriétaire ne peut pas être modifié.",
|
||||
"ALREADY_MEMBER": "Vous êtes déjà membre de cette organisation.",
|
||||
"NO_ACTIVE_ADMIN": "Cette organisation n'a pas d'administrateur actif. Utilisez le point de terminaison /claim.",
|
||||
"HAS_ACTIVE_ADMIN": "Cette organisation a un administrateur actif. Utilisez le point de terminaison /join-request.",
|
||||
"EMAIL_MISMATCH": "L'email ne correspond pas à l'email du propriétaire de l'organisation.",
|
||||
"INVALID_CODE": "Code invalide ou expiré.",
|
||||
"CODE_ORG_MISMATCH": "Le code n'appartient pas à cette organisation.",
|
||||
"INVITE_EXPIRED": "L'invitation a expiré.",
|
||||
"INVITE_INVALID": "Jeton d'invitation invalide."
|
||||
},
|
||||
"SUCCESS": {
|
||||
"ONBOARDED": "Entreprise créée et enregistrée avec succès.",
|
||||
"MEMBER_REMOVED": "Membre supprimé avec succès.",
|
||||
"ROLE_UPDATED": "Rôle mis à jour avec succès.",
|
||||
"UPDATED": "Données de l'organisation mises à jour avec succès.",
|
||||
"VISUAL_SETTINGS_UPDATED": "Paramètres visuels mis à jour avec succès."
|
||||
},
|
||||
"INVITATION": {
|
||||
"CREATED": "Invitation créée avec succès.",
|
||||
"ACCEPTED": "Invitation acceptée.",
|
||||
"REVOKED": "Invitation révoquée."
|
||||
},
|
||||
"JOIN_REQUEST": {
|
||||
"SENT": "Votre demande d'adhésion a été soumise. En attente de l'approbation de l'administrateur."
|
||||
},
|
||||
"CLAIM": {
|
||||
"OTP_SENT": "Un code de vérification à 6 chiffres a été envoyé à l'email du propriétaire de l'organisation.",
|
||||
"SUCCESS": "Vous avez repris l'organisation avec succès. Vous êtes maintenant l'administrateur."
|
||||
}
|
||||
},
|
||||
"REGISTRATION_DOCUMENTS": {
|
||||
"CERTIFICATE_NUMBER": "Numéro du certificat d'immatriculation",
|
||||
"CERTIFICATE_VALIDITY": "Validité du certificat d'immatriculation",
|
||||
"DOCUMENT_NUMBER": "Numéro du document du véhicule",
|
||||
"TITLE": "Certificat d'immatriculation & Document du véhicule"
|
||||
},
|
||||
"GAMIFICATION": {
|
||||
"SUBMIT_SERVICE": {
|
||||
"SELF_DEFENSE_BLOCKED": "Vous ne pouvez pas soumettre de nouvelles données de service en raison d'une restriction d'autodéfense.",
|
||||
"SUCCESS": "Service soumis au système pour analyse!",
|
||||
"POINTS_ERROR": "Erreur lors de l'attribution des points: {error}",
|
||||
"SUBMIT_ERROR": "Erreur lors de la soumission: {error}"
|
||||
},
|
||||
"QUIZ": {
|
||||
"ALREADY_PLAYED": "Vous avez déjà participé au quiz quotidien aujourd'hui. Revenez demain!",
|
||||
"NO_QUIZ_AVAILABLE": "Aucun quiz disponible aujourd'hui.",
|
||||
"ALREADY_COMPLETED": "Le quiz quotidien a déjà été marqué comme terminé aujourd'hui.",
|
||||
"COMPLETED_SUCCESS": "Quiz quotidien marqué comme terminé pour aujourd'hui.",
|
||||
"QUESTION_NOT_FOUND": "Question non trouvée.",
|
||||
"POINTS_FAILED": "Échec de l'attribution des points de quiz: {error}"
|
||||
},
|
||||
"BADGE": {
|
||||
"NOT_FOUND": "Badge non trouvé.",
|
||||
"ALREADY_AWARDED": "Badge déjà attribué à cet utilisateur.",
|
||||
"AWARDED_SUCCESS": "Badge '{badge_name}' attribué à l'utilisateur.",
|
||||
"POINTS_FAILED": "Échec de l'attribution des points de badge: {error}"
|
||||
},
|
||||
"SEASON": {
|
||||
"NOT_FOUND": "Saison non trouvée.",
|
||||
"NO_ACTIVE": "Aucune saison active trouvée."
|
||||
},
|
||||
"ACHIEVEMENTS": {
|
||||
"XP_NOVICE": "Novice",
|
||||
"XP_APPRENTICE": "Apprenti",
|
||||
"XP_EXPERT": "Expert",
|
||||
"XP_MASTER": "Maître",
|
||||
"XP_NOVICE_DESC": "Gagnez 100 XP",
|
||||
"XP_APPRENTICE_DESC": "Gagnez 500 XP",
|
||||
"XP_EXPERT_DESC": "Gagnez 2000 XP",
|
||||
"XP_MASTER_DESC": "Gagnez 5000 XP",
|
||||
"QUIZ_BEGINNER": "Débutant Quiz",
|
||||
"QUIZ_ENTHUSIAST": "Passionné de Quiz",
|
||||
"QUIZ_MASTER": "Maître du Quiz",
|
||||
"QUIZ_BEGINNER_DESC": "Gagnez 50 points de quiz",
|
||||
"QUIZ_ENTHUSIAST_DESC": "Gagnez 200 points de quiz",
|
||||
"QUIZ_MASTER_DESC": "Gagnez 500 points de quiz"
|
||||
}
|
||||
}
|
||||
}
|
||||
131
backend/static/locales/ro.json
Normal file
131
backend/static/locales/ro.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"AUTH": {
|
||||
"LOGIN": {
|
||||
"SUCCESS": "Autentificare reușită. Bun venit înapoi!"
|
||||
},
|
||||
"ERROR": {
|
||||
"EMAIL_EXISTS": "Acest email este deja înregistrat.",
|
||||
"UNAUTHORIZED": "Acces neautorizat."
|
||||
}
|
||||
},
|
||||
"SENTINEL": {
|
||||
"LOCK": {
|
||||
"MSG": "Cont blocat din motive de securitate."
|
||||
},
|
||||
"APPROVAL": {
|
||||
"REQUIRED": "Aprobare necesară"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"SAVE_SUCCESS": "Salvat cu succes!"
|
||||
},
|
||||
"EMAIL": {
|
||||
"REG": {
|
||||
"SUBJECT": "Confirmați înregistrarea - Service Finder",
|
||||
"GREETING": "Salut {{first_name}}!",
|
||||
"BODY": "Vă mulțumim pentru înregistrare! Faceți clic pe butonul de mai jos pentru a vă activa contul:",
|
||||
"BUTTON": "Activează Contul",
|
||||
"FOOTER": "Dacă nu v-ați înregistrat, vă rugăm să ignorați acest email."
|
||||
},
|
||||
"PWD_RESET": {
|
||||
"SUBJECT": "Solicitare resetare parolă",
|
||||
"GREETING": "Salut!",
|
||||
"BODY": "Ați solicitat resetarea parolei. Faceți clic pe butonul de mai jos pentru a seta o nouă parolă:",
|
||||
"BUTTON": "Resetează Parola",
|
||||
"FOOTER": "Dacă nu ați solicitat resetarea parolei, vă rugăm să ignorați acest email."
|
||||
},
|
||||
"TEST": {
|
||||
"SUBJECT": "🧪 Email de test - Service Finder",
|
||||
"GREETING": "Salut {{first_name}}!",
|
||||
"BODY": "Acesta este un email de test de la sistemul Service Finder. Dacă vedeți acest mesaj, trimiterea de emailuri funcționează!",
|
||||
"BUTTON": "Mergi la Dashboard",
|
||||
"FOOTER": "Service Finder - Mesaj de sistem de test"
|
||||
}
|
||||
},
|
||||
"ORGANIZATION": {
|
||||
"ERROR": {
|
||||
"NOT_FOUND": "Organizație negăsită.",
|
||||
"INVALID_TAX_FORMAT": "Format invalid al numărului de TVA maghiar!",
|
||||
"TAX_LOOKUP_FAILED": "Verificarea numărului de TVA a eșuat.",
|
||||
"FORBIDDEN": "Nu aveți permisiunea de a accesa această organizație.",
|
||||
"INVALID_ROLE": "Rol specificat invalid.",
|
||||
"CANNOT_REMOVE_OWNER": "Proprietarul nu poate fi eliminat din organizație.",
|
||||
"CANNOT_DEMOTE_OWNER": "Rolul proprietarului nu poate fi modificat.",
|
||||
"ALREADY_MEMBER": "Sunteți deja membru al acestei organizații.",
|
||||
"NO_ACTIVE_ADMIN": "Această organizație nu are un administrator activ. Utilizați punctul final /claim.",
|
||||
"HAS_ACTIVE_ADMIN": "Această organizație are un administrator activ. Utilizați punctul final /join-request.",
|
||||
"EMAIL_MISMATCH": "Emailul nu corespunde cu emailul proprietarului organizației.",
|
||||
"INVALID_CODE": "Cod invalid sau expirat.",
|
||||
"CODE_ORG_MISMATCH": "Codul nu aparține acestei organizații.",
|
||||
"INVITE_EXPIRED": "Invitația a expirat.",
|
||||
"INVITE_INVALID": "Token de invitație invalid."
|
||||
},
|
||||
"SUCCESS": {
|
||||
"ONBOARDED": "Companie creată și înregistrată cu succes.",
|
||||
"MEMBER_REMOVED": "Membru eliminat cu succes.",
|
||||
"ROLE_UPDATED": "Rol actualizat cu succes.",
|
||||
"UPDATED": "Datele organizației actualizate cu succes.",
|
||||
"VISUAL_SETTINGS_UPDATED": "Setările vizuale actualizate cu succes."
|
||||
},
|
||||
"INVITATION": {
|
||||
"CREATED": "Invitație creată cu succes.",
|
||||
"ACCEPTED": "Invitație acceptată.",
|
||||
"REVOKED": "Invitație revocată."
|
||||
},
|
||||
"JOIN_REQUEST": {
|
||||
"SENT": "Cererea dvs. de aderare a fost trimisă. Se așteaptă aprobarea administratorului."
|
||||
},
|
||||
"CLAIM": {
|
||||
"OTP_SENT": "Un cod de verificare din 6 cifre a fost trimis pe emailul proprietarului organizației.",
|
||||
"SUCCESS": "Ați preluat cu succes organizația. Acum sunteți administratorul."
|
||||
}
|
||||
},
|
||||
"REGISTRATION_DOCUMENTS": {
|
||||
"CERTIFICATE_NUMBER": "Numărul certificatului de înmatriculare",
|
||||
"CERTIFICATE_VALIDITY": "Valabilitatea certificatului de înmatriculare",
|
||||
"DOCUMENT_NUMBER": "Numărul documentului vehiculului",
|
||||
"TITLE": "Certificat de înmatriculare & Document vehicul"
|
||||
},
|
||||
"GAMIFICATION": {
|
||||
"SUBMIT_SERVICE": {
|
||||
"SELF_DEFENSE_BLOCKED": "Nu puteți trimite date noi de service din cauza unei restricții de autoapărare.",
|
||||
"SUCCESS": "Service trimis sistemului pentru analiză!",
|
||||
"POINTS_ERROR": "Eroare la punctare: {error}",
|
||||
"SUBMIT_ERROR": "Eroare la trimitere: {error}"
|
||||
},
|
||||
"QUIZ": {
|
||||
"ALREADY_PLAYED": "Ați jucat deja quizul zilnic astăzi. Reveniți mâine!",
|
||||
"NO_QUIZ_AVAILABLE": "Niciun quiz disponibil pentru astăzi.",
|
||||
"ALREADY_COMPLETED": "Quizul zilnic a fost deja marcat ca finalizat astăzi.",
|
||||
"COMPLETED_SUCCESS": "Quizul zilnic marcat ca finalizat pentru astăzi.",
|
||||
"QUESTION_NOT_FOUND": "Întrebare negăsită.",
|
||||
"POINTS_FAILED": "Eroare la acordarea punctelor de quiz: {error}"
|
||||
},
|
||||
"BADGE": {
|
||||
"NOT_FOUND": "Insignă negăsită.",
|
||||
"ALREADY_AWARDED": "Insigna a fost deja acordată acestui utilizator.",
|
||||
"AWARDED_SUCCESS": "Insigna '{badge_name}' a fost acordată utilizatorului.",
|
||||
"POINTS_FAILED": "Eroare la acordarea punctelor insignei: {error}"
|
||||
},
|
||||
"SEASON": {
|
||||
"NOT_FOUND": "Sezon negăsit.",
|
||||
"NO_ACTIVE": "Niciun sezon activ găsit."
|
||||
},
|
||||
"ACHIEVEMENTS": {
|
||||
"XP_NOVICE": "Începător",
|
||||
"XP_APPRENTICE": "Ucenic",
|
||||
"XP_EXPERT": "Expert",
|
||||
"XP_MASTER": "Maestru",
|
||||
"XP_NOVICE_DESC": "Câștigă 100 XP",
|
||||
"XP_APPRENTICE_DESC": "Câștigă 500 XP",
|
||||
"XP_EXPERT_DESC": "Câștigă 2000 XP",
|
||||
"XP_MASTER_DESC": "Câștigă 5000 XP",
|
||||
"QUIZ_BEGINNER": "Începător Quiz",
|
||||
"QUIZ_ENTHUSIAST": "Entuziast Quiz",
|
||||
"QUIZ_MASTER": "Maestru Quiz",
|
||||
"QUIZ_BEGINNER_DESC": "Câștigă 50 de puncte de quiz",
|
||||
"QUIZ_ENTHUSIAST_DESC": "Câștigă 200 de puncte de quiz",
|
||||
"QUIZ_MASTER_DESC": "Câștigă 500 de puncte de quiz"
|
||||
}
|
||||
}
|
||||
}
|
||||
131
backend/static/locales/sk.json
Normal file
131
backend/static/locales/sk.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"AUTH": {
|
||||
"LOGIN": {
|
||||
"SUCCESS": "Prihlásenie úspešné. Vitajte späť!"
|
||||
},
|
||||
"ERROR": {
|
||||
"EMAIL_EXISTS": "Tento email je už zaregistrovaný.",
|
||||
"UNAUTHORIZED": "Neoprávnený prístup."
|
||||
}
|
||||
},
|
||||
"SENTINEL": {
|
||||
"LOCK": {
|
||||
"MSG": "Účet bol z bezpečnostných dôvodov uzamknutý."
|
||||
},
|
||||
"APPROVAL": {
|
||||
"REQUIRED": "Vyžaduje sa schválenie"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"SAVE_SUCCESS": "Úspešne uložené!"
|
||||
},
|
||||
"EMAIL": {
|
||||
"REG": {
|
||||
"SUBJECT": "Potvrďte svoju registráciu - Service Finder",
|
||||
"GREETING": "Ahoj {{first_name}}!",
|
||||
"BODY": "Ďakujeme za registráciu! Kliknutím na tlačidlo nižšie aktivujete svoj účet:",
|
||||
"BUTTON": "Aktivovať účet",
|
||||
"FOOTER": "Ak ste sa nezaregistrovali, ignorujte prosím tento email."
|
||||
},
|
||||
"PWD_RESET": {
|
||||
"SUBJECT": "Žiadosť o obnovenie hesla",
|
||||
"GREETING": "Ahoj!",
|
||||
"BODY": "Požiadali ste o obnovenie hesla. Kliknutím na tlačidlo nižšie nastavíte nové heslo:",
|
||||
"BUTTON": "Obnoviť heslo",
|
||||
"FOOTER": "Ak ste nepožiadali o obnovenie hesla, ignorujte prosím tento email."
|
||||
},
|
||||
"TEST": {
|
||||
"SUBJECT": "🧪 Testovací email - Service Finder",
|
||||
"GREETING": "Ahoj {{first_name}}!",
|
||||
"BODY": "Toto je testovací email zo systému Service Finder. Ak to vidíte, odosielanie emailov funguje!",
|
||||
"BUTTON": "Prejsť na Dashboard",
|
||||
"FOOTER": "Service Finder - Testovacia systémová správa"
|
||||
}
|
||||
},
|
||||
"ORGANIZATION": {
|
||||
"ERROR": {
|
||||
"NOT_FOUND": "Organizácia nenájdená.",
|
||||
"INVALID_TAX_FORMAT": "Neplatný formát maďarského daňového čísla!",
|
||||
"TAX_LOOKUP_FAILED": "Overenie daňového čísla zlyhalo.",
|
||||
"FORBIDDEN": "Nemáte oprávnenie na prístup k tejto organizácii.",
|
||||
"INVALID_ROLE": "Neplatná rola.",
|
||||
"CANNOT_REMOVE_OWNER": "Vlastník nemôže byť odstránený z organizácie.",
|
||||
"CANNOT_DEMOTE_OWNER": "Rola vlastníka nemôže byť zmenená.",
|
||||
"ALREADY_MEMBER": "Už ste členom tejto organizácie.",
|
||||
"NO_ACTIVE_ADMIN": "Táto organizácia nemá aktívneho správcu. Použite koncový bod /claim.",
|
||||
"HAS_ACTIVE_ADMIN": "Táto organizácia má aktívneho správcu. Použite koncový bod /join-request.",
|
||||
"EMAIL_MISMATCH": "Email nezodpovedá emailu vlastníka organizácie.",
|
||||
"INVALID_CODE": "Neplatný alebo expirovaný kód.",
|
||||
"CODE_ORG_MISMATCH": "Kód nepatrí k tejto organizácii.",
|
||||
"INVITE_EXPIRED": "Pozvánka vypršala.",
|
||||
"INVITE_INVALID": "Neplatný token pozvánky."
|
||||
},
|
||||
"SUCCESS": {
|
||||
"ONBOARDED": "Spoločnosť úspešne vytvorená a zaregistrovaná.",
|
||||
"MEMBER_REMOVED": "Člen úspešne odstránený.",
|
||||
"ROLE_UPDATED": "Rola úspešne aktualizovaná.",
|
||||
"UPDATED": "Dáta organizácie úspešne aktualizované.",
|
||||
"VISUAL_SETTINGS_UPDATED": "Vizuálne nastavenia úspešne aktualizované."
|
||||
},
|
||||
"INVITATION": {
|
||||
"CREATED": "Pozvánka úspešne vytvorená.",
|
||||
"ACCEPTED": "Pozvánka prijatá.",
|
||||
"REVOKED": "Pozvánka odvolaná."
|
||||
},
|
||||
"JOIN_REQUEST": {
|
||||
"SENT": "Vaša žiadosť o pripojenie bola odoslaná. Čaká na schválenie správcom."
|
||||
},
|
||||
"CLAIM": {
|
||||
"OTP_SENT": "6-miestny overovací kód bol odoslaný na email vlastníka organizácie.",
|
||||
"SUCCESS": "Úspešne ste prevzali organizáciu. Teraz ste správcom."
|
||||
}
|
||||
},
|
||||
"REGISTRATION_DOCUMENTS": {
|
||||
"CERTIFICATE_NUMBER": "Číslo osvedčenia o registrácii",
|
||||
"CERTIFICATE_VALIDITY": "Platnosť osvedčenia o registrácii",
|
||||
"DOCUMENT_NUMBER": "Číslo dokladu vozidla",
|
||||
"TITLE": "Osvedčenie o registrácii & Doklad vozidla"
|
||||
},
|
||||
"GAMIFICATION": {
|
||||
"SUBMIT_SERVICE": {
|
||||
"SELF_DEFENSE_BLOCKED": "Z dôvodu obmedzenia sebaobrany nemôžete odosielať nové servisné dáta.",
|
||||
"SUCCESS": "Servis odoslaný do systému na analýzu!",
|
||||
"POINTS_ERROR": "Chyba pri bodovaní: {error}",
|
||||
"SUBMIT_ERROR": "Chyba pri odosielaní: {error}"
|
||||
},
|
||||
"QUIZ": {
|
||||
"ALREADY_PLAYED": "Dnes ste už hrali denný kvíz. Vráťte sa zajtra!",
|
||||
"NO_QUIZ_AVAILABLE": "Dnes nie je k dispozícii žiadny kvíz.",
|
||||
"ALREADY_COMPLETED": "Denný kvíz bol dnes už označený ako dokončený.",
|
||||
"COMPLETED_SUCCESS": "Denný kvíz označený ako dokončený pre dnešok.",
|
||||
"QUESTION_NOT_FOUND": "Otázka nenájdená.",
|
||||
"POINTS_FAILED": "Nepodarilo sa udeliť body za kvíz: {error}"
|
||||
},
|
||||
"BADGE": {
|
||||
"NOT_FOUND": "Odznak nenájdený.",
|
||||
"ALREADY_AWARDED": "Odznak už bol tomuto používateľovi udelený.",
|
||||
"AWARDED_SUCCESS": "Odznak '{badge_name}' bol udelený používateľovi.",
|
||||
"POINTS_FAILED": "Nepodarilo sa udeliť body za odznak: {error}"
|
||||
},
|
||||
"SEASON": {
|
||||
"NOT_FOUND": "Sezóna nenájdená.",
|
||||
"NO_ACTIVE": "Nebola nájdená žiadna aktívna sezóna."
|
||||
},
|
||||
"ACHIEVEMENTS": {
|
||||
"XP_NOVICE": "Začiatočník",
|
||||
"XP_APPRENTICE": "Učeň",
|
||||
"XP_EXPERT": "Expert",
|
||||
"XP_MASTER": "Majster",
|
||||
"XP_NOVICE_DESC": "Získaj 100 XP",
|
||||
"XP_APPRENTICE_DESC": "Získaj 500 XP",
|
||||
"XP_EXPERT_DESC": "Získaj 2000 XP",
|
||||
"XP_MASTER_DESC": "Získaj 5000 XP",
|
||||
"QUIZ_BEGINNER": "Kvízový začiatočník",
|
||||
"QUIZ_ENTHUSIAST": "Kvízový nadšenec",
|
||||
"QUIZ_MASTER": "Kvízový majster",
|
||||
"QUIZ_BEGINNER_DESC": "Získaj 50 kvízových bodov",
|
||||
"QUIZ_ENTHUSIAST_DESC": "Získaj 200 kvízových bodov",
|
||||
"QUIZ_MASTER_DESC": "Získaj 500 kvízových bodov"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user