céges meghívó kezelése,
This commit is contained in:
@@ -19,20 +19,27 @@ P1 CRITICAL ALIGN (2026-06-17): Atomizált címmezők.
|
||||
- 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
|
||||
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, ExpertiseTag, ServiceExpertise, ServiceStaging
|
||||
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 (
|
||||
@@ -42,6 +49,7 @@ from app.schemas.provider import (
|
||||
ProviderQuickAddResponse,
|
||||
ProviderUpdateIn,
|
||||
ProviderUpdateResponse,
|
||||
CategoryInfo,
|
||||
)
|
||||
from app.services.gamification_service import gamification_service
|
||||
|
||||
@@ -132,6 +140,7 @@ async def search_providers(
|
||||
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:
|
||||
@@ -144,11 +153,27 @@ async def search_providers(
|
||||
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
|
||||
|
||||
@@ -175,18 +200,42 @@ async def search_providers(
|
||||
or_(
|
||||
Organization.name.ilike(f"%{token}%"),
|
||||
cast(Organization.aliases, Text).ilike(f"%{token}%"),
|
||||
cast(Organization.tags, 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.ilike(f"%{city}%"),
|
||||
Organization.address_zip.ilike(f"%{city}%"),
|
||||
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
|
||||
@@ -210,6 +259,7 @@ async def search_providers(
|
||||
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"),
|
||||
@@ -234,10 +284,14 @@ async def search_providers(
|
||||
)
|
||||
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.ilike(f"%{city}%"),
|
||||
ServiceStaging.postal_code.ilike(f"%{city}%"),
|
||||
ServiceStaging.city == city_clean,
|
||||
ServiceStaging.city.ilike(f"{city_clean}%"),
|
||||
ServiceStaging.postal_code == city_clean,
|
||||
ServiceStaging.postal_code.ilike(f"{city_clean}%"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -250,6 +304,7 @@ async def search_providers(
|
||||
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"),
|
||||
@@ -270,8 +325,13 @@ async def search_providers(
|
||||
)
|
||||
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(
|
||||
ServiceProvider.address.ilike(f"%{city}%")
|
||||
or_(
|
||||
ServiceProvider.address == city_clean,
|
||||
ServiceProvider.address.ilike(f"{city_clean}%"),
|
||||
)
|
||||
)
|
||||
|
||||
crowd_stmt = select(
|
||||
@@ -283,6 +343,7 @@ async def search_providers(
|
||||
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"),
|
||||
@@ -315,6 +376,43 @@ async def search_providers(
|
||||
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)
|
||||
@@ -323,6 +421,19 @@ async def search_providers(
|
||||
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,
|
||||
@@ -333,10 +444,12 @@ async def search_providers(
|
||||
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,
|
||||
)
|
||||
@@ -364,11 +477,16 @@ async def quick_add_provider(
|
||||
- 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. Kategória ellenőrzése (expertise_tags)
|
||||
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 kapcsolat 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
|
||||
|
||||
@@ -383,10 +501,15 @@ async def quick_add_provider(
|
||||
Raises:
|
||||
ValueError: Ha a kategória nem létezik
|
||||
"""
|
||||
# 1. Kategória ellenőrzése
|
||||
category = await db.get(ExpertiseTag, data.category_id)
|
||||
if not category:
|
||||
raise ValueError(f"Category with id={data.category_id} not found")
|
||||
# 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(
|
||||
@@ -414,6 +537,7 @@ async def quick_add_provider(
|
||||
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),
|
||||
@@ -428,7 +552,9 @@ async def quick_add_provider(
|
||||
).hexdigest()[:64]
|
||||
|
||||
# specialization_tags összeállítása: primary_category + user-supplied tags
|
||||
spec_tags = {"primary_category": category.key}
|
||||
spec_tags: dict = {}
|
||||
if primary_category_key:
|
||||
spec_tags["primary_category"] = primary_category_key
|
||||
if data.tags:
|
||||
spec_tags["user_tags"] = data.tags
|
||||
|
||||
@@ -445,13 +571,49 @@ async def quick_add_provider(
|
||||
db.add(profile)
|
||||
await db.flush()
|
||||
|
||||
# 4. ServiceExpertise kapcsolat
|
||||
expertise = ServiceExpertise(
|
||||
service_id=profile.id,
|
||||
expertise_id=data.category_id,
|
||||
confidence_level=50, # Közepes bizonyosság (crowdsourced)
|
||||
)
|
||||
db.add(expertise)
|
||||
# =====================================================================
|
||||
# 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(
|
||||
@@ -467,6 +629,28 @@ async def quick_add_provider(
|
||||
)
|
||||
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)
|
||||
# =====================================================================
|
||||
@@ -494,6 +678,173 @@ async def quick_add_provider(
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
@@ -507,6 +858,11 @@ async def update_provider(
|
||||
- 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
|
||||
@@ -527,15 +883,43 @@ async def update_provider(
|
||||
|
||||
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],
|
||||
@@ -549,7 +933,16 @@ async def update_provider(
|
||||
address_house_number=data.address_house_number,
|
||||
status="pending_verification",
|
||||
is_verified=False,
|
||||
folder_slug=f"sp-{staging.id}-{uuid.uuid4().hex[:6]}",
|
||||
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()
|
||||
@@ -562,6 +955,7 @@ async def update_provider(
|
||||
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],
|
||||
@@ -575,7 +969,16 @@ async def update_provider(
|
||||
address_house_number=data.address_house_number,
|
||||
status="pending_verification",
|
||||
is_verified=False,
|
||||
folder_slug=f"cr-{crowd.id}-{uuid.uuid4().hex[:6]}",
|
||||
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()
|
||||
@@ -605,6 +1008,8 @@ async def update_provider(
|
||||
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(
|
||||
@@ -625,26 +1030,156 @@ async def update_provider(
|
||||
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"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"Only Organization fields updated."
|
||||
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}"
|
||||
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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user