1186 lines
46 KiB
Python
1186 lines
46 KiB
Python
"""
|
||
Provider Service – Szolgáltató keresés, gyors felvétel és szerkesztés logikája.
|
||
|
||
Gamification Integration (2026-06-16):
|
||
=======================================
|
||
A Vezető Tervező utasítása szerint a crowdsourcing funkciókba
|
||
dinamikus Gamification horgok kerültek beépítésre.
|
||
|
||
ALAPELV: A pontértékek SOHA nincsenek beégetve (hardcoded) a kódba!
|
||
Minden pontszámot a gamification.point_rules táblából olvasunk ki
|
||
action_key alapján futásidőben.
|
||
|
||
Jelenlegi horgok:
|
||
- ADD_NEW_PROVIDER: 500 pont (adatbázisból) + providers_added_count növelés
|
||
|
||
P1 CRITICAL ALIGN (2026-06-17): Atomizált címmezők.
|
||
=====================================================
|
||
- quick_add_provider: data.street -> data.address_street_name, address_street_type, address_house_number
|
||
- update_provider: data.street -> data.address_street_name, address_street_type, address_house_number
|
||
- search_providers: visszaadja az address_street_name, address_street_type, address_house_number mezőket
|
||
- A kapcsolatfelvételi adatok (contact_phone, contact_email, website, tags) a ServiceProfile-ba kerülnek.
|
||
|
||
4-Level Category Hybrid Save (2026-06-17):
|
||
===========================================
|
||
- update_provider: category_ids -> ServiceExpertise kapcsolatok frissítése
|
||
- update_provider: new_tags -> új ExpertiseTag létrehozása (is_official=False, level=3)
|
||
- A hibrid logika biztosítja, hogy a meglévő kapcsolatok ne sérüljenek
|
||
"""
|
||
|
||
import logging
|
||
import re
|
||
import uuid
|
||
import hashlib
|
||
from datetime import datetime, timezone
|
||
from typing import Optional, List, Tuple
|
||
|
||
from sqlalchemy import select, func, or_, literal, union_all, cast, String, Text, null, and_, case, delete
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
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.gamification.gamification import PointRule, UserStats
|
||
from app.schemas.provider import (
|
||
ProviderSearchResult,
|
||
ProviderSearchResponse,
|
||
ProviderQuickAddIn,
|
||
ProviderQuickAddResponse,
|
||
ProviderUpdateIn,
|
||
ProviderUpdateResponse,
|
||
CategoryInfo,
|
||
)
|
||
from app.services.gamification_service import gamification_service
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
async def _award_provider_points(
|
||
db: AsyncSession,
|
||
user_id: int,
|
||
action_key: str,
|
||
) -> int:
|
||
"""
|
||
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.
|
||
|
||
Args:
|
||
db: AsyncSession
|
||
user_id: A felhasználó ID-ja
|
||
action_key: A pontszabály azonosítója (pl. 'ADD_NEW_PROVIDER')
|
||
|
||
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).
|
||
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
|
||
)
|
||
|
||
# 3. 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()
|
||
|
||
if stats:
|
||
stats.providers_added_count = UserStats.providers_added_count + 1
|
||
logger.info(
|
||
f"Statisztika frissítve: user_id={user_id}, "
|
||
f"providers_added_count={stats.providers_added_count}"
|
||
)
|
||
else:
|
||
# Ha még nincs UserStats rekordja, létrehozzuk
|
||
stats = UserStats(
|
||
user_id=user_id,
|
||
total_xp=points_to_award,
|
||
current_level=1,
|
||
providers_added_count=1,
|
||
)
|
||
db.add(stats)
|
||
logger.info(
|
||
f"Új UserStats rekord létrehozva: user_id={user_id}, "
|
||
f"providers_added_count=1"
|
||
)
|
||
|
||
return points_to_award
|
||
|
||
|
||
async def search_providers(
|
||
db: AsyncSession,
|
||
q: Optional[str] = None,
|
||
category: Optional[str] = None,
|
||
city: Optional[str] = None,
|
||
category_ids: Optional[List[int]] = None,
|
||
limit: int = 20,
|
||
offset: int = 0,
|
||
) -> ProviderSearchResponse:
|
||
"""
|
||
Egyesített szolgáltató keresés három forrásból:
|
||
1. fleet.organizations (verified orgs with org_type='service_provider')
|
||
2. marketplace.service_staging (robot által gyűjtött adatok)
|
||
3. marketplace.service_providers (crowdsourced adatok)
|
||
|
||
P1 CRITICAL ALIGN (2026-06-17): Az org lekérdezés most már visszaadja
|
||
az address_street_name, address_street_type, address_house_number mezőket is.
|
||
|
||
FEATURE (2026-06-17): category_ids szűrő — SZIGORÚ AND KAPCSOLAT.
|
||
- Ha több kategória ID van megadva, a szolgáltatónak MINDEGYIKHEZ
|
||
tartoznia kell (AND), nem elég, ha csak az egyikhez (OR).
|
||
- Nincs hierarchikus terjeszkedés: csak a pontosan megadott ID-kat
|
||
ellenőrizzük a ServiceExpertise táblában.
|
||
|
||
FEATURE (2026-06-17): Keresés szigorítása.
|
||
- A q (free-text) keresés csak a name és aliases mezőkben keres (ILIKE),
|
||
a tags mezőt NEM vizsgálja.
|
||
- A city keresés exact match vagy StartsWith, nem ILIKE.
|
||
|
||
FEATURE (2026-06-17): Kategória adatok visszaadása.
|
||
- Minden ProviderSearchResult tartalmazza a kapcsolódó kategóriák
|
||
(ExpertiseTag) ID-ját, nevét és szintjét a categories mezőben.
|
||
|
||
Args:
|
||
db: AsyncSession
|
||
q: Keresőszó (tokenizálva, AND kapcsolat) — opcionális
|
||
category: Kategória szűrő
|
||
city: Város szűrő — AND kapcsolat a q-val
|
||
category_ids: Kategória ID-k listája (SZIGORÚ AND szűrés)
|
||
limit: Lapozási limit
|
||
offset: Lapozási offset
|
||
|
||
Returns:
|
||
ProviderSearchResponse lapozott találatokkal
|
||
"""
|
||
results = []
|
||
total = 0
|
||
|
||
# Tokenizáljuk a q paramétert: szóközök mentén feldaraboljuk
|
||
q_tokens: list[str] = []
|
||
if q:
|
||
q_tokens = [token.strip() for token in q.split() if token.strip()]
|
||
|
||
# --- 1. LEKÉRDEZÉS: Szervezetek (fleet.organizations) ---
|
||
org_conditions = [
|
||
Organization.org_type == OrgType.service_provider,
|
||
Organization.is_deleted == False,
|
||
]
|
||
if q_tokens:
|
||
token_conditions = []
|
||
for token in q_tokens:
|
||
token_conditions.append(
|
||
or_(
|
||
Organization.name.ilike(f"%{token}%"),
|
||
cast(Organization.aliases, Text).ilike(f"%{token}%"),
|
||
)
|
||
)
|
||
org_conditions.append(and_(*token_conditions))
|
||
if city:
|
||
# SZIGORÍTÁS (2026-06-17): City keresés exact match vagy StartsWith
|
||
city_clean = city.strip()
|
||
org_conditions.append(
|
||
or_(
|
||
Organization.address_city == city_clean,
|
||
Organization.address_city.ilike(f"{city_clean}%"),
|
||
Organization.address_zip == city_clean,
|
||
Organization.address_zip.ilike(f"{city_clean}%"),
|
||
)
|
||
)
|
||
|
||
# FEATURE (2026-06-17): Kategória szűrés category_ids alapján.
|
||
# SZIGORÚ AND KAPCSOLAT (2026-06-17): Ha több kategória ID van megadva,
|
||
# a szolgáltatónak MINDEGYIKHEZ tartoznia kell. Nincs hierarchikus
|
||
# terjeszkedés (path LIKE), csak a pontosan megadott ID-kat ellenőrizzük.
|
||
# P0 CRITICAL BUGFIX (2026-06-17): Null-safety — ha category_ids None,
|
||
# nem iterálunk rajta.
|
||
safe_category_ids = category_ids or []
|
||
if safe_category_ids:
|
||
# Minden egyes megadott category_id-hoz külön subquery-t készítünk,
|
||
# és AND kapcsolattal fűzzük össze őket. Ez biztosítja, hogy a
|
||
# szolgáltató minden kategóriával rendelkezzen.
|
||
for cid in safe_category_ids:
|
||
profile_subq = (
|
||
select(ServiceExpertise.service_id)
|
||
.where(ServiceExpertise.expertise_id == cid)
|
||
.subquery()
|
||
)
|
||
org_conditions.append(
|
||
ServiceProfile.id.in_(select(profile_subq.c.service_id))
|
||
)
|
||
|
||
# P1 CRITICAL ALIGN: Az org lekérdezés most már tartalmazza az atomizált
|
||
# címmezőket (address_street_name, address_street_type, address_house_number).
|
||
# Az 'address' mezőt SQL összefűzéssé alakítjuk, hogy a frontend kártyán
|
||
# ne duplikálódjon a városnév (pl. "Dunakeszi, Dunakeszi").
|
||
org_stmt = select(
|
||
Organization.id.label("id"),
|
||
Organization.name.label("name"),
|
||
Organization.address_city.label("city"),
|
||
func.concat(
|
||
Organization.address_zip,
|
||
literal(' '),
|
||
Organization.address_city,
|
||
literal(', '),
|
||
Organization.address_street_name,
|
||
literal(' '),
|
||
Organization.address_street_type,
|
||
literal(' '),
|
||
Organization.address_house_number,
|
||
).label("address"),
|
||
Organization.address_zip.label("address_zip"),
|
||
Organization.address_street_name.label("address_street_name"),
|
||
Organization.address_street_type.label("address_street_type"),
|
||
Organization.address_house_number.label("address_house_number"),
|
||
Organization.plus_code.label("plus_code"),
|
||
Organization.is_verified.label("is_verified"),
|
||
ServiceProfile.contact_phone.label("contact_phone"),
|
||
ServiceProfile.contact_email.label("contact_email"),
|
||
ServiceProfile.website.label("website"),
|
||
ServiceProfile.specialization_tags.label("specialization_tags"),
|
||
case(
|
||
(cast(Organization.external_integration_config["source"], String) == "crowdsourced", literal("crowd_added")),
|
||
else_=literal("verified_org"),
|
||
).label("source"),
|
||
).outerjoin(
|
||
ServiceProfile,
|
||
ServiceProfile.organization_id == Organization.id,
|
||
).where(*org_conditions)
|
||
|
||
# --- 2. LEKÉRDEZÉS: Robot által gyűjtött adatok (marketplace.service_staging) ---
|
||
staging_conditions = []
|
||
if q_tokens:
|
||
token_conditions = []
|
||
for token in q_tokens:
|
||
token_conditions.append(
|
||
ServiceStaging.name.ilike(f"%{token}%")
|
||
)
|
||
staging_conditions.append(and_(*token_conditions))
|
||
if city:
|
||
# SZIGORÍTÁS (2026-06-17): City keresés exact match vagy StartsWith
|
||
city_clean = city.strip()
|
||
staging_conditions.append(
|
||
or_(
|
||
ServiceStaging.city == city_clean,
|
||
ServiceStaging.city.ilike(f"{city_clean}%"),
|
||
ServiceStaging.postal_code == city_clean,
|
||
ServiceStaging.postal_code.ilike(f"{city_clean}%"),
|
||
)
|
||
)
|
||
|
||
staging_stmt = select(
|
||
ServiceStaging.id.label("id"),
|
||
ServiceStaging.name.label("name"),
|
||
ServiceStaging.city.label("city"),
|
||
ServiceStaging.full_address.label("address"),
|
||
literal(None).label("address_zip"),
|
||
literal(None).label("address_street_name"),
|
||
literal(None).label("address_street_type"),
|
||
literal(None).label("address_house_number"),
|
||
literal(None).label("plus_code"),
|
||
literal(False).label("is_verified"),
|
||
literal(None).label("contact_phone"),
|
||
literal(None).label("contact_email"),
|
||
literal(None).label("website"),
|
||
literal(None).label("specialization_tags"),
|
||
literal("staged_data").label("source"),
|
||
)
|
||
if staging_conditions:
|
||
staging_stmt = staging_stmt.where(*staging_conditions)
|
||
|
||
# --- 3. LEKÉRDEZÉS: Crowdsourced adatok (marketplace.service_providers) ---
|
||
crowd_conditions = []
|
||
if q_tokens:
|
||
token_conditions = []
|
||
for token in q_tokens:
|
||
token_conditions.append(
|
||
ServiceProvider.name.ilike(f"%{token}%")
|
||
)
|
||
crowd_conditions.append(and_(*token_conditions))
|
||
if city:
|
||
# SZIGORÍTÁS (2026-06-17): City keresés exact match vagy StartsWith
|
||
city_clean = city.strip()
|
||
crowd_conditions.append(
|
||
or_(
|
||
ServiceProvider.address == city_clean,
|
||
ServiceProvider.address.ilike(f"{city_clean}%"),
|
||
)
|
||
)
|
||
|
||
crowd_stmt = select(
|
||
ServiceProvider.id.label("id"),
|
||
ServiceProvider.name.label("name"),
|
||
literal(None).label("city"),
|
||
ServiceProvider.address.label("address"),
|
||
literal(None).label("address_zip"),
|
||
literal(None).label("address_street_name"),
|
||
literal(None).label("address_street_type"),
|
||
literal(None).label("address_house_number"),
|
||
literal(None).label("plus_code"),
|
||
literal(False).label("is_verified"),
|
||
literal(None).label("contact_phone"),
|
||
literal(None).label("contact_email"),
|
||
literal(None).label("website"),
|
||
literal(None).label("specialization_tags"),
|
||
literal("crowd_added").label("source"),
|
||
)
|
||
if crowd_conditions:
|
||
crowd_stmt = crowd_stmt.where(*crowd_conditions)
|
||
|
||
# --- UNION ---
|
||
union_parts = [org_stmt]
|
||
union_parts.append(staging_stmt)
|
||
union_parts.append(crowd_stmt)
|
||
|
||
# UNION végrehajtása
|
||
if len(union_parts) == 1:
|
||
base_query = union_parts[0]
|
||
else:
|
||
base_query = union_all(*union_parts)
|
||
|
||
# Teljes darabszám
|
||
count_subquery = base_query.subquery()
|
||
count_stmt = select(func.count()).select_from(count_subquery)
|
||
total_result = await db.execute(count_stmt)
|
||
total = total_result.scalar() or 0
|
||
|
||
# Lapozott lekérdezés
|
||
paginated_query = base_query.order_by("source").offset(offset).limit(limit)
|
||
result = await db.execute(paginated_query)
|
||
rows = result.fetchall()
|
||
|
||
# FEATURE (2026-06-17): Kategória adatok előtöltése.
|
||
# Lekérdezzük az összes olyan ServiceProfile-hoz tartozó ExpertiseTag-eket,
|
||
# amelyek a találati listában szereplő organization ID-khoz tartoznak.
|
||
# Ezt egyetlen batch lekérdezéssel tesszük a N+1 probléma elkerülése érdekében.
|
||
org_ids = [row.id for row in rows if row.source in ("verified_org", "crowd_added")]
|
||
org_categories_map: dict[int, list[dict]] = {}
|
||
if org_ids:
|
||
cat_stmt = (
|
||
select(
|
||
ServiceProfile.organization_id,
|
||
ExpertiseTag.id,
|
||
ExpertiseTag.name_hu,
|
||
ExpertiseTag.name_en,
|
||
ExpertiseTag.level,
|
||
ExpertiseTag.key,
|
||
)
|
||
.select_from(ServiceExpertise)
|
||
.join(ExpertiseTag, ServiceExpertise.expertise_id == ExpertiseTag.id)
|
||
.join(ServiceProfile, ServiceExpertise.service_id == ServiceProfile.id)
|
||
.where(
|
||
ServiceProfile.organization_id.in_(org_ids),
|
||
)
|
||
)
|
||
cat_result = await db.execute(cat_stmt)
|
||
cat_rows = cat_result.fetchall()
|
||
for cat_row in cat_rows:
|
||
org_id = cat_row.organization_id
|
||
if org_id not in org_categories_map:
|
||
org_categories_map[org_id] = []
|
||
org_categories_map[org_id].append({
|
||
"id": cat_row.id,
|
||
"name_hu": cat_row.name_hu,
|
||
"name_en": cat_row.name_en,
|
||
"level": cat_row.level,
|
||
"key": cat_row.key,
|
||
})
|
||
|
||
for row in rows:
|
||
tags_list: list = []
|
||
spec_tags = getattr(row, "specialization_tags", None)
|
||
if spec_tags and isinstance(spec_tags, dict):
|
||
user_tags = spec_tags.get("user_tags", [])
|
||
if user_tags:
|
||
tags_list = list(user_tags)
|
||
|
||
# Kategória adatok összeállítása
|
||
row_categories = org_categories_map.get(row.id, [])
|
||
categories_out = [
|
||
CategoryInfo(
|
||
id=cat["id"],
|
||
name_hu=cat["name_hu"],
|
||
name_en=cat["name_en"],
|
||
level=cat["level"],
|
||
key=cat["key"],
|
||
)
|
||
for cat in row_categories
|
||
]
|
||
|
||
results.append(
|
||
ProviderSearchResult(
|
||
id=row.id,
|
||
name=row.name,
|
||
city=row.city,
|
||
address=row.address,
|
||
address_zip=getattr(row, "address_zip", None),
|
||
address_street_name=getattr(row, "address_street_name", None),
|
||
address_street_type=getattr(row, "address_street_type", None),
|
||
address_house_number=getattr(row, "address_house_number", None),
|
||
plus_code=getattr(row, "plus_code", None),
|
||
contact_phone=getattr(row, "contact_phone", None),
|
||
contact_email=getattr(row, "contact_email", None),
|
||
website=getattr(row, "website", None),
|
||
tags=tags_list,
|
||
categories=categories_out,
|
||
source=row.source,
|
||
is_verified=row.is_verified,
|
||
)
|
||
)
|
||
|
||
page = (offset // limit) + 1 if limit > 0 else 1
|
||
|
||
return ProviderSearchResponse(
|
||
results=results,
|
||
total=total,
|
||
page=page,
|
||
per_page=limit,
|
||
)
|
||
|
||
|
||
async def quick_add_provider(
|
||
db: AsyncSession,
|
||
data: ProviderQuickAddIn,
|
||
user_id: int,
|
||
) -> ProviderQuickAddResponse:
|
||
"""
|
||
Gyors szolgáltató felvétel crowdsourcingból.
|
||
|
||
P1 CRITICAL ALIGN (2026-06-17): Atomizált címmezők.
|
||
- data.street -> data.address_street_name, data.address_street_type, data.address_house_number
|
||
- A kapcsolatfelvételi adatok a ServiceProfile-ba kerülnek.
|
||
|
||
P0 CRITICAL BUGFIX (2026-06-17): category_id most már opcionális.
|
||
- Ha category_id nincs megadva, a primary_category kihagyásra kerül.
|
||
- A 4-level kategória rendszerből érkező category_ids és new_tags
|
||
feldolgozásra kerülnek a ServiceExpertise kapcsolatok létrehozásához.
|
||
|
||
Folyamat:
|
||
1. (Opcionális) Elsődleges kategória ellenőrzése (expertise_tags)
|
||
2. Organization létrehozása (is_verified=False, org_type='service_provider')
|
||
3. ServiceProfile létrehozása
|
||
4. ServiceExpertise kapcsolatok létrehozása (category_id + category_ids + new_tags)
|
||
5. Branch létrehozása a címmel
|
||
6. OrganizationMember létrehozása a felhasználóhoz
|
||
|
||
Args:
|
||
db: AsyncSession
|
||
data: ProviderQuickAddIn
|
||
user_id: A felhasználó ID-ja
|
||
|
||
Returns:
|
||
ProviderQuickAddResponse
|
||
|
||
Raises:
|
||
ValueError: Ha a kategória nem létezik
|
||
"""
|
||
# 1. (Opcionális) Elsődleges kategória ellenőrzése
|
||
primary_category_key = None
|
||
if data.category_id is not None:
|
||
category = await db.get(ExpertiseTag, data.category_id)
|
||
if not category:
|
||
raise ValueError(f"Category with id={data.category_id} not found")
|
||
primary_category_key = category.key
|
||
else:
|
||
category = None
|
||
|
||
# 2. Organization létrehozása
|
||
folder_slug = hashlib.md5(
|
||
f"{data.name}-{uuid.uuid4()}".encode()
|
||
).hexdigest()[:12]
|
||
|
||
# P1 CRITICAL ALIGN: address_street_name, address_street_type, address_house_number
|
||
# használata a régi street helyett.
|
||
org = Organization(
|
||
name=data.name[:100],
|
||
full_name=data.name,
|
||
display_name=data.name[:50],
|
||
folder_slug=folder_slug,
|
||
org_type=OrgType.service_provider,
|
||
is_verified=False,
|
||
status="pending_verification",
|
||
subscription_plan="FREE",
|
||
base_asset_limit=1,
|
||
purchased_extra_slots=0,
|
||
notification_settings={"notify_owner": True, "alert_days_before": [30, 15, 7, 1]},
|
||
external_integration_config={"source": "crowdsourced"},
|
||
is_ownership_transferable=True,
|
||
address_city=data.city,
|
||
address_zip=data.address_zip,
|
||
address_street_name=data.address_street_name,
|
||
address_street_type=data.address_street_type,
|
||
address_house_number=data.address_house_number,
|
||
plus_code=data.plus_code,
|
||
owner_id=None,
|
||
first_registered_at=datetime.now(timezone.utc),
|
||
current_lifecycle_started_at=datetime.now(timezone.utc),
|
||
created_at=datetime.now(timezone.utc),
|
||
)
|
||
db.add(org)
|
||
await db.flush()
|
||
|
||
# 3. ServiceProfile létrehozása
|
||
fingerprint = hashlib.sha256(
|
||
f"quick-add-{org.id}-{datetime.now().isoformat()}".encode()
|
||
).hexdigest()[:64]
|
||
|
||
# specialization_tags összeállítása: primary_category + user-supplied tags
|
||
spec_tags: dict = {}
|
||
if primary_category_key:
|
||
spec_tags["primary_category"] = primary_category_key
|
||
if data.tags:
|
||
spec_tags["user_tags"] = data.tags
|
||
|
||
profile = ServiceProfile(
|
||
organization_id=org.id,
|
||
fingerprint=fingerprint,
|
||
status="ghost",
|
||
location=func.ST_SetSRID(func.ST_MakePoint(19.040236, 47.497913), 4326),
|
||
specialization_tags=spec_tags,
|
||
contact_phone=data.contact_phone,
|
||
contact_email=data.contact_email,
|
||
website=data.website,
|
||
)
|
||
db.add(profile)
|
||
await db.flush()
|
||
|
||
# =====================================================================
|
||
# 4. 🏗️ ServiceExpertise kapcsolatok létrehozása (4-LEVEL CATEGORY)
|
||
# =====================================================================
|
||
# Összegyűjtjük az összes kategória ID-t a ServiceExpertise kapcsolatokhoz.
|
||
all_category_ids: List[int] = []
|
||
|
||
# 4a. Elsődleges kategória (ha meg van adva)
|
||
if data.category_id is not None:
|
||
all_category_ids.append(data.category_id)
|
||
|
||
# 4b. category_ids a 4-level checkboxokból
|
||
safe_category_ids = data.category_ids or []
|
||
if safe_category_ids:
|
||
all_category_ids.extend(safe_category_ids)
|
||
|
||
# 4c. new_tags -> Új ExpertiseTag létrehozása + kapcsolat
|
||
safe_new_tags = data.new_tags or []
|
||
new_tag_ids: List[int] = []
|
||
if safe_new_tags:
|
||
new_tag_ids = await _create_new_tags(
|
||
db=db,
|
||
service_profile_id=profile.id,
|
||
new_tag_names=safe_new_tags,
|
||
)
|
||
if new_tag_ids:
|
||
all_category_ids.extend(new_tag_ids)
|
||
|
||
# 4d. ServiceExpertise kapcsolatok létrehozása
|
||
if all_category_ids:
|
||
# Deduplikáció: csak egyedi ID-k
|
||
unique_ids = list(set(all_category_ids))
|
||
for expertise_id in unique_ids:
|
||
expertise = ServiceExpertise(
|
||
service_id=profile.id,
|
||
expertise_id=expertise_id,
|
||
confidence_level=50, # Közepes bizonyosság (crowdsourced)
|
||
)
|
||
db.add(expertise)
|
||
logger.info(
|
||
f"ServiceExpertise kapcsolatok létrehozva: service_id={profile.id}, "
|
||
f"expertise_ids={unique_ids}"
|
||
)
|
||
# =====================================================================
|
||
|
||
# 5. Branch létrehozása atomizált címmezőkkel
|
||
branch = Branch(
|
||
organization_id=org.id,
|
||
name=data.name[:100],
|
||
is_main=True,
|
||
city=data.city,
|
||
postal_code=data.address_zip,
|
||
street_name=data.address_street_name,
|
||
street_type=data.address_street_type,
|
||
house_number=data.address_house_number,
|
||
status="active",
|
||
)
|
||
db.add(branch)
|
||
|
||
# =====================================================================
|
||
# 6. 👤 ORGANIZATION MEMBER LÉTREHOZÁSA (P0 CRITICAL BUGFIX 2026-06-17)
|
||
# =====================================================================
|
||
# A létrehozó felhasználó ADMIN szerepkörű tag lesz (NEM OWNER).
|
||
# Crowdsourcingból felvett szolgáltatóknak nincs tulajdonosa (owner_id=NULL).
|
||
# Az ADMIN jogosultság elegendő a későbbi szerkesztéshez, de a cég
|
||
# nem jelenik meg a "Cégeim" garázsválasztóban.
|
||
member = OrganizationMember(
|
||
organization_id=org.id,
|
||
user_id=user_id,
|
||
role=OrgUserRole.ADMIN,
|
||
is_permanent=True,
|
||
is_verified=True,
|
||
)
|
||
db.add(member)
|
||
await db.flush()
|
||
logger.info(
|
||
f"OrganizationMember created: org_id={org.id}, user_id={user_id}, "
|
||
f"role=ADMIN (crowdsourced provider has no owner)"
|
||
)
|
||
# =====================================================================
|
||
|
||
# =====================================================================
|
||
# 7. 🎮 GAMIFICATION INTEGRÁCIÓ (Dinamikus pontozás)
|
||
# =====================================================================
|
||
earned_points = await _award_provider_points(
|
||
db=db,
|
||
user_id=user_id,
|
||
action_key="ADD_NEW_PROVIDER",
|
||
)
|
||
# =====================================================================
|
||
|
||
await db.commit()
|
||
await db.refresh(org)
|
||
|
||
logger.info(
|
||
f"Quick-add provider created: org_id={org.id}, name={data.name}, "
|
||
f"user_id={user_id}, earned_points={earned_points}"
|
||
)
|
||
|
||
return ProviderQuickAddResponse(
|
||
id=org.id,
|
||
name=data.name,
|
||
status="pending_verification",
|
||
message="Szolgáltató sikeresen rögzítve. Ellenőrzés alatt.",
|
||
earned_points=earned_points,
|
||
)
|
||
|
||
|
||
async def _sync_service_expertises(
|
||
db: AsyncSession,
|
||
service_profile_id: int,
|
||
category_ids: List[int],
|
||
) -> None:
|
||
"""
|
||
Szinkronizálja a ServiceExpertise kapcsolatokat a megadott kategória ID-k alapján.
|
||
|
||
4-Level Category Hybrid Save (2026-06-17):
|
||
- Eltávolítja a meglévő kapcsolatokat, amelyek nincsenek az új listában
|
||
- Hozzáadja az új kapcsolatokat, amelyek még nem léteznek
|
||
|
||
Args:
|
||
db: AsyncSession
|
||
service_profile_id: A ServiceProfile ID-ja
|
||
category_ids: Az új kategória ID-k listája
|
||
"""
|
||
if not category_ids:
|
||
return
|
||
|
||
# Meglévő kapcsolatok lekérése
|
||
existing_stmt = select(ServiceExpertise).where(
|
||
ServiceExpertise.service_id == service_profile_id
|
||
)
|
||
existing_result = await db.execute(existing_stmt)
|
||
existing_expertises = existing_result.scalars().all()
|
||
existing_ids = {e.expertise_id for e in existing_expertises}
|
||
|
||
# Eltávolítandó kapcsolatok (amik nincsenek az új listában)
|
||
ids_to_remove = existing_ids - set(category_ids)
|
||
if ids_to_remove:
|
||
delete_stmt = delete(ServiceExpertise).where(
|
||
ServiceExpertise.service_id == service_profile_id,
|
||
ServiceExpertise.expertise_id.in_(ids_to_remove),
|
||
)
|
||
await db.execute(delete_stmt)
|
||
logger.info(
|
||
f"Eltávolított ServiceExpertise kapcsolatok: service_id={service_profile_id}, "
|
||
f"expertise_ids={ids_to_remove}"
|
||
)
|
||
|
||
# Hozzáadandó kapcsolatok (amik újak)
|
||
ids_to_add = set(category_ids) - existing_ids
|
||
for expertise_id in ids_to_add:
|
||
new_expertise = ServiceExpertise(
|
||
service_id=service_profile_id,
|
||
expertise_id=expertise_id,
|
||
confidence_level=50,
|
||
)
|
||
db.add(new_expertise)
|
||
logger.info(
|
||
f"Új ServiceExpertise kapcsolat: service_id={service_profile_id}, "
|
||
f"expertise_id={expertise_id}"
|
||
)
|
||
|
||
|
||
def _slugify(text: str) -> str:
|
||
"""
|
||
Biztonságos slug létrehozása magyar ékezetes karakterekkel.
|
||
|
||
- Kisbetűsítés
|
||
- Ékezetes karakterek cseréje (á→a, é→e, í→i, ó→o, ö→o, ő→o, ú→u, ü→u, ű→u)
|
||
- Nem alfanumerikus karakterek cseréje alulvonásra
|
||
- Maximum 50 karakter
|
||
"""
|
||
replacements = {
|
||
'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ö': 'o', 'ő': 'o',
|
||
'ú': 'u', 'ü': 'u', 'ű': 'u',
|
||
'Á': 'a', 'É': 'e', 'Í': 'i', 'Ó': 'o', 'Ö': 'o', 'Ő': 'o',
|
||
'Ú': 'u', 'Ü': 'u', 'Ű': 'u',
|
||
}
|
||
result = text.lower().strip()
|
||
for hungarian, ascii_char in replacements.items():
|
||
result = result.replace(hungarian, ascii_char)
|
||
# Replace any non-alphanumeric character (except underscore) with underscore
|
||
result = re.sub(r'[^a-z0-9_]', '_', result)
|
||
# Collapse multiple underscores
|
||
result = re.sub(r'_+', '_', result)
|
||
# Strip leading/trailing underscores
|
||
result = result.strip('_')
|
||
return result[:50]
|
||
|
||
|
||
async def _create_new_tags(
|
||
db: AsyncSession,
|
||
service_profile_id: int,
|
||
new_tag_names: List[str],
|
||
) -> List[int]:
|
||
"""
|
||
Létrehoz új ExpertiseTag rekordokat a user által gépelt címkékből.
|
||
|
||
P0 CRITICAL BUGFIX (2026-06-17): try-except blokk + slugify + hibanaplózás.
|
||
|
||
4-Level Category Hybrid Save (2026-06-17):
|
||
- Ha a címke string már létezik az ExpertiseTag táblában, visszaadja az ID-ját
|
||
- Ha a címke vadiúj, létrehozza is_official=False és level=3 értékekkel
|
||
|
||
Args:
|
||
db: AsyncSession
|
||
service_profile_id: A ServiceProfile ID-ja (a kapcsolathoz)
|
||
new_tag_names: A user által gépelt új címkenevek listája
|
||
|
||
Returns:
|
||
List[int]: A létrehozott/megtalált ExpertiseTag ID-k listája
|
||
"""
|
||
if not new_tag_names:
|
||
return []
|
||
|
||
created_ids = []
|
||
|
||
for tag_name in new_tag_names:
|
||
tag_name = tag_name.strip()
|
||
if not tag_name:
|
||
continue
|
||
|
||
try:
|
||
# Ellenőrizzük, hogy létezik-e már
|
||
existing_stmt = select(ExpertiseTag).where(
|
||
or_(
|
||
ExpertiseTag.key == _slugify(tag_name),
|
||
ExpertiseTag.name_hu == tag_name,
|
||
ExpertiseTag.name_en == tag_name,
|
||
)
|
||
)
|
||
existing_result = await db.execute(existing_stmt)
|
||
existing_tag = existing_result.scalar_one_or_none()
|
||
|
||
if existing_tag:
|
||
# Már létezik, használjuk a meglévő ID-t
|
||
created_ids.append(existing_tag.id)
|
||
logger.info(
|
||
f"Meglévő címke használata: tag_name={tag_name}, "
|
||
f"existing_id={existing_tag.id}"
|
||
)
|
||
else:
|
||
# Vadiúj címke létrehozása slugify-jal
|
||
new_key = _slugify(tag_name)
|
||
new_tag = ExpertiseTag(
|
||
key=new_key,
|
||
name_hu=tag_name,
|
||
name_en=tag_name,
|
||
level=3,
|
||
is_official=False,
|
||
category="user_created",
|
||
parent_id=None,
|
||
path=None,
|
||
search_keywords=[tag_name.lower()],
|
||
description=f"User-created tag: {tag_name}",
|
||
)
|
||
db.add(new_tag)
|
||
await db.flush()
|
||
created_ids.append(new_tag.id)
|
||
logger.info(
|
||
f"Új címke létrehozva: tag_name={tag_name}, "
|
||
f"new_id={new_tag.id}, key={new_key}"
|
||
)
|
||
except Exception as e:
|
||
logger.error(
|
||
f"HIBA az új címke létrehozásakor: tag_name={tag_name}, "
|
||
f"error={e}", exc_info=True
|
||
)
|
||
# Nem szakítjuk meg a teljes folyamatot egyetlen hibás címke miatt
|
||
continue
|
||
|
||
return created_ids
|
||
|
||
|
||
async def update_provider(
|
||
db: AsyncSession,
|
||
provider_id: int,
|
||
data: ProviderUpdateIn,
|
||
user_id: int,
|
||
) -> ProviderUpdateResponse:
|
||
"""
|
||
Szolgáltató adatainak szerkesztése.
|
||
|
||
P1 CRITICAL ALIGN (2026-06-17): Atomizált címmezők.
|
||
- data.street -> data.address_street_name, data.address_street_type, data.address_house_number
|
||
- A kapcsolatfelvételi adatok a ServiceProfile-ba kerülnek.
|
||
|
||
4-Level Category Hybrid Save (2026-06-17):
|
||
- category_ids: ServiceExpertise kapcsolatok frissítése
|
||
- new_tags: Új ExpertiseTag létrehozása (is_official=False, level=3)
|
||
majd ServiceExpertise kapcsolat létrehozása
|
||
|
||
Multi-source update (2026-06-17): Támogatja mindhárom forrást:
|
||
1. fleet.organizations (verified orgs) - közvetlen frissítés
|
||
2. marketplace.service_staging (robot adatok) - migrálás Organization-be
|
||
3. marketplace.service_providers (crowdsourced) - migrálás Organization-be
|
||
|
||
Frissíti az Organization és a ServiceProfile rekordokat a megadott
|
||
adatokkal. A forrást (source) a backend állítja be, a frontend SOHA
|
||
nem küldheti azt!
|
||
|
||
Args:
|
||
db: AsyncSession
|
||
provider_id: A szolgáltató ID-ja
|
||
data: ProviderUpdateIn séma
|
||
user_id: A szerkesztő felhasználó ID-ja
|
||
|
||
Returns:
|
||
ProviderUpdateResponse
|
||
|
||
Raises:
|
||
ValueError: Ha a szolgáltató nem található
|
||
PermissionError: Ha a felhasználó nem tagja a szervezetnek
|
||
"""
|
||
# 1. Organization lekérdezése
|
||
org = await db.get(Organization, provider_id)
|
||
|
||
if org:
|
||
# =====================================================================
|
||
# 1a. 🔐 ACCESS CONTROL ELLENŐRZÉS (P0 CRITICAL BUGFIX 2026-06-17)
|
||
# =====================================================================
|
||
# Csak azok a felhasználók szerkeszthetik a szolgáltatót, akik
|
||
# OWNER vagy ADMIN szerepkörű tagok a szervezetben.
|
||
member_stmt = select(OrganizationMember).where(
|
||
OrganizationMember.organization_id == org.id,
|
||
OrganizationMember.user_id == user_id,
|
||
OrganizationMember.role.in_([OrgUserRole.OWNER, OrgUserRole.ADMIN]),
|
||
)
|
||
member_result = await db.execute(member_stmt)
|
||
member = member_result.scalar_one_or_none()
|
||
|
||
if not member:
|
||
logger.warning(
|
||
f"Access denied: user_id={user_id} attempted to update "
|
||
f"org_id={org.id} but is not an OWNER or ADMIN member"
|
||
)
|
||
raise PermissionError(
|
||
f"Nincs jogosultságod szerkeszteni ezt a szolgáltatót. "
|
||
f"Csak a szervezet tulajdonosa vagy adminisztrátora "
|
||
f"módosíthatja az adatokat."
|
||
)
|
||
# =====================================================================
|
||
|
||
if not org:
|
||
# 1b. Ha nincs Organization-ben, ServiceStaging-ben keresünk
|
||
staging = await db.get(ServiceStaging, provider_id)
|
||
if staging:
|
||
# Migráljuk Organization-be
|
||
now = datetime.now(timezone.utc)
|
||
org = Organization(
|
||
id=staging.id,
|
||
name=staging.name[:100],
|
||
full_name=staging.name,
|
||
display_name=staging.name[:50],
|
||
org_type=OrgType.service_provider,
|
||
address_city=staging.city,
|
||
address_zip=data.address_zip,
|
||
address_street_name=data.address_street_name,
|
||
address_street_type=data.address_street_type,
|
||
address_house_number=data.address_house_number,
|
||
status="pending_verification",
|
||
is_verified=False,
|
||
folder_slug=hashlib.md5(f"sp-{staging.id}-{uuid.uuid4()}".encode()).hexdigest()[:12],
|
||
first_registered_at=now,
|
||
current_lifecycle_started_at=now,
|
||
created_at=now,
|
||
subscription_plan="FREE",
|
||
base_asset_limit=1,
|
||
purchased_extra_slots=0,
|
||
notification_settings={"notify_owner": True, "alert_days_before": [30, 15, 7, 1]},
|
||
external_integration_config={},
|
||
is_ownership_transferable=True,
|
||
)
|
||
db.add(org)
|
||
await db.flush()
|
||
logger.info(
|
||
f"Provider migrated from ServiceStaging: staging_id={provider_id}, "
|
||
f"new org_id={org.id}, user_id={user_id}"
|
||
)
|
||
else:
|
||
# 1c. Ha nincs staging-ben sem, ServiceProvider-ben keresünk
|
||
crowd = await db.get(ServiceProvider, provider_id)
|
||
if crowd:
|
||
# Migráljuk Organization-be
|
||
now = datetime.now(timezone.utc)
|
||
org = Organization(
|
||
id=crowd.id,
|
||
name=crowd.name[:100],
|
||
full_name=crowd.name,
|
||
display_name=crowd.name[:50],
|
||
org_type=OrgType.service_provider,
|
||
address_city=data.city,
|
||
address_zip=data.address_zip,
|
||
address_street_name=data.address_street_name,
|
||
address_street_type=data.address_street_type,
|
||
address_house_number=data.address_house_number,
|
||
status="pending_verification",
|
||
is_verified=False,
|
||
folder_slug=hashlib.md5(f"cr-{crowd.id}-{uuid.uuid4()}".encode()).hexdigest()[:12],
|
||
first_registered_at=now,
|
||
current_lifecycle_started_at=now,
|
||
created_at=now,
|
||
subscription_plan="FREE",
|
||
base_asset_limit=1,
|
||
purchased_extra_slots=0,
|
||
notification_settings={"notify_owner": True, "alert_days_before": [30, 15, 7, 1]},
|
||
external_integration_config={},
|
||
is_ownership_transferable=True,
|
||
)
|
||
db.add(org)
|
||
await db.flush()
|
||
logger.info(
|
||
f"Provider migrated from ServiceProvider: crowd_id={provider_id}, "
|
||
f"new org_id={org.id}, user_id={user_id}"
|
||
)
|
||
else:
|
||
# 1d. Ha egyikben sem található
|
||
raise ValueError(f"Provider with id={provider_id} not found")
|
||
|
||
# 2. Organization mezők frissítése atomizált címmezőkkel
|
||
# ADATVÉDELMI SZABÁLY (2026-06-17): Csak akkor írjuk felül a mezőt,
|
||
# ha a frontend explicit értéket küldött (nem None). Ez megakadályozza,
|
||
# hogy a meglévő címadatok véletlenül null-ra állítódjanak, amikor
|
||
# a felhasználó csak más mezőket szerkeszt.
|
||
org.name = data.name[:100]
|
||
org.full_name = data.name
|
||
org.display_name = data.name[:50]
|
||
if data.city is not None:
|
||
org.address_city = data.city
|
||
if data.address_zip is not None:
|
||
org.address_zip = data.address_zip
|
||
if data.address_street_name is not None:
|
||
org.address_street_name = data.address_street_name
|
||
if data.address_street_type is not None:
|
||
org.address_street_type = data.address_street_type
|
||
if data.address_house_number is not None:
|
||
org.address_house_number = data.address_house_number
|
||
if data.plus_code is not None:
|
||
org.plus_code = data.plus_code
|
||
|
||
# 3. ServiceProfile lekérdezése (ha létezik)
|
||
profile_stmt = select(ServiceProfile).where(
|
||
ServiceProfile.organization_id == org.id
|
||
)
|
||
profile_result = await db.execute(profile_stmt)
|
||
profile = profile_result.scalar_one_or_none()
|
||
|
||
if profile:
|
||
# 4. ServiceProfile mezők frissítése
|
||
profile.contact_phone = data.contact_phone
|
||
profile.contact_email = data.contact_email
|
||
profile.website = data.website
|
||
|
||
# specialization_tags frissítése
|
||
if data.tags is not None:
|
||
spec_tags = profile.specialization_tags or {}
|
||
spec_tags["user_tags"] = data.tags
|
||
profile.specialization_tags = spec_tags
|
||
|
||
# =====================================================================
|
||
# 5. 🏗️ 4-LEVEL CATEGORY HYBRID SAVE (2026-06-17)
|
||
# =====================================================================
|
||
|
||
# P0 CRITICAL BUGFIX (2026-06-17): Null-safety — ha category_ids vagy
|
||
# new_tags None, nem iterálunk rajtuk, és nem hívjuk meg az extend-et.
|
||
|
||
# Összegyűjtjük az összes kategória ID-t (meglévő + új)
|
||
all_category_ids: List[int] = []
|
||
|
||
# 5a. category_ids -> ServiceExpertise kapcsolatok frissítése
|
||
safe_category_ids = data.category_ids or []
|
||
if safe_category_ids:
|
||
all_category_ids.extend(safe_category_ids)
|
||
|
||
# 5b. new_tags -> Új ExpertiseTag létrehozása + kapcsolat
|
||
safe_new_tags = data.new_tags or []
|
||
if safe_new_tags:
|
||
new_tag_ids = await _create_new_tags(
|
||
db=db,
|
||
service_profile_id=profile.id,
|
||
new_tag_names=safe_new_tags,
|
||
)
|
||
if new_tag_ids:
|
||
all_category_ids.extend(new_tag_ids)
|
||
|
||
# 5c. Egyszeri szinkronizálás az összes kategória ID-val
|
||
if all_category_ids:
|
||
await _sync_service_expertises(
|
||
db=db,
|
||
service_profile_id=profile.id,
|
||
category_ids=all_category_ids,
|
||
)
|
||
# =====================================================================
|
||
|
||
logger.info(
|
||
f"Provider update: org_id={org.id}, profile_id={profile.id}, "
|
||
f"contact_phone={data.contact_phone}, contact_email={data.contact_email}, "
|
||
f"website={data.website}, tags={data.tags}, "
|
||
f"category_ids={data.category_ids}, new_tags={data.new_tags}"
|
||
)
|
||
else:
|
||
# P0 CRITICAL BUGFIX (2026-06-17): Ha nincs ServiceProfile, létrehozzuk.
|
||
# Ez biztosítja, hogy a contact_phone, contact_email, website, tags
|
||
# és category_ids adatok mentésre kerüljenek, még akkor is, ha a
|
||
# szolgáltató korábban csak Organization-ként létezett.
|
||
logger.warning(
|
||
f"Provider update: org_id={org.id} has no ServiceProfile. "
|
||
f"Creating one now."
|
||
)
|
||
fingerprint = hashlib.sha256(
|
||
f"update-create-{org.id}-{datetime.now().isoformat()}".encode()
|
||
).hexdigest()[:64]
|
||
profile = ServiceProfile(
|
||
organization_id=org.id,
|
||
fingerprint=fingerprint,
|
||
location=func.ST_SetSRID(func.ST_MakePoint(19.040236, 47.497913), 4326),
|
||
contact_phone=data.contact_phone,
|
||
contact_email=data.contact_email,
|
||
website=data.website,
|
||
specialization_tags={"user_tags": data.tags} if data.tags else {},
|
||
status=ServiceStatus.active,
|
||
)
|
||
db.add(profile)
|
||
await db.flush()
|
||
logger.info(
|
||
f"ServiceProfile created for org_id={org.id}, profile_id={profile.id}"
|
||
)
|
||
|
||
# 4. ServiceProfile mezők frissítése
|
||
profile.contact_phone = data.contact_phone
|
||
profile.contact_email = data.contact_email
|
||
profile.website = data.website
|
||
|
||
# specialization_tags frissítése
|
||
if data.tags is not None:
|
||
spec_tags = profile.specialization_tags or {}
|
||
spec_tags["user_tags"] = data.tags
|
||
profile.specialization_tags = spec_tags
|
||
|
||
# =====================================================================
|
||
# 5. 🏗️ 4-LEVEL CATEGORY HYBRID SAVE (2026-06-17)
|
||
# =====================================================================
|
||
|
||
# P0 CRITICAL BUGFIX (2026-06-17): Null-safety — ha category_ids vagy
|
||
# new_tags None, nem iterálunk rajtuk, és nem hívjuk meg az extend-et.
|
||
|
||
# Összegyűjtjük az összes kategória ID-t (meglévő + új)
|
||
all_category_ids: List[int] = []
|
||
|
||
# 5a. category_ids -> ServiceExpertise kapcsolatok frissítése
|
||
safe_category_ids = data.category_ids or []
|
||
if safe_category_ids:
|
||
all_category_ids.extend(safe_category_ids)
|
||
|
||
# 5b. new_tags -> Új ExpertiseTag létrehozása + kapcsolat
|
||
safe_new_tags = data.new_tags or []
|
||
if safe_new_tags:
|
||
new_tag_ids = await _create_new_tags(
|
||
db=db,
|
||
service_profile_id=profile.id,
|
||
new_tag_names=safe_new_tags,
|
||
)
|
||
if new_tag_ids:
|
||
all_category_ids.extend(new_tag_ids)
|
||
|
||
# 5c. Egyszeri szinkronizálás az összes kategória ID-val
|
||
if all_category_ids:
|
||
await _sync_service_expertises(
|
||
db=db,
|
||
service_profile_id=profile.id,
|
||
category_ids=all_category_ids,
|
||
)
|
||
# =====================================================================
|
||
|
||
logger.info(
|
||
f"Provider update: org_id={org.id}, profile_id={profile.id}, "
|
||
f"contact_phone={data.contact_phone}, contact_email={data.contact_email}, "
|
||
f"website={data.website}, tags={data.tags}, "
|
||
f"category_ids={data.category_ids}, new_tags={data.new_tags}"
|
||
)
|
||
|
||
# =====================================================================
|
||
# 6. 🎮 GAMIFICATION INTEGRÁCIÓ (Dinamikus pontozás szerkesztésért)
|
||
# =====================================================================
|
||
# A felhasználó aktuális penalty_level-jétől (restriction_level) függően
|
||
# kalkuláljuk a kapott pontot. A GamificationService.process_activity()
|
||
# automatikusan alkalmazza a büntetési szorzót a _calculate_multiplier-en
|
||
# keresztül, így:
|
||
# - Szint 0 (L0) = 100% pont
|
||
# - Szint 1 (L1) = 50% pont
|
||
# - Szint 2 (L2) = 10% pont
|
||
# - Szint 3 (L3) = 0% pont (blokkolt)
|
||
earned_points = await _award_provider_points(
|
||
db=db,
|
||
user_id=user_id,
|
||
action_key="UPDATE_PROVIDER",
|
||
)
|
||
# =====================================================================
|
||
|
||
await db.commit()
|
||
|
||
logger.info(
|
||
f"Provider updated successfully: org_id={org.id}, "
|
||
f"name={data.name}, user_id={user_id}, earned_points={earned_points}"
|
||
)
|
||
|
||
return ProviderUpdateResponse(
|
||
id=org.id,
|
||
name=data.name,
|
||
message="Szolgáltató adatai sikeresen frissítve.",
|
||
earned_points=earned_points,
|
||
)
|