refaktor címtár
This commit is contained in:
@@ -34,14 +34,16 @@ 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 import select, func, or_, literal, union_all, cast, String, Text, null, and_, case, delete, join
|
||||
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.identity.address import Address, GeoPostalCode
|
||||
from app.models.marketplace.service import ServiceProfile, ServiceStatus, ExpertiseTag, ServiceExpertise, ServiceStaging
|
||||
from app.models.identity.social import ServiceProvider, ModerationStatus, SourceType
|
||||
from app.models.gamification.gamification import PointRule, UserStats
|
||||
from app.schemas.address import AddressOut
|
||||
from app.schemas.provider import (
|
||||
ProviderSearchResult,
|
||||
ProviderSearchResponse,
|
||||
@@ -52,6 +54,8 @@ from app.schemas.provider import (
|
||||
CategoryInfo,
|
||||
)
|
||||
from app.services.gamification_service import gamification_service
|
||||
from app.services.address_manager import AddressManager
|
||||
from app.schemas.address import AddressIn
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -126,17 +130,28 @@ async def find_or_create_provider_by_name(
|
||||
"""
|
||||
Keres vagy létrehoz egy ServiceProvider-t a megadott név alapján.
|
||||
|
||||
P0 ARCHITECTURE CLEANUP (2026-07-01):
|
||||
======================================
|
||||
Gamification/XP logika ELTÁVOLÍTVA ebből a service rétegből.
|
||||
A gamification hívások az API rétegbe (expenses.py) kerültek át.
|
||||
Ez a függvény immár tiszta adatbázis service — nem tud a gamification-ről.
|
||||
|
||||
Visszatérési érték módosítva: (provider, created) tuple.
|
||||
- created=True: új provider lett létrehozva (felfedezés)
|
||||
- created=False: meglévő provider lett lekérve
|
||||
|
||||
pg_trgm similarity threshold: 0.3 -> 0.6 (hamis pozitív találatok csökkentése)
|
||||
|
||||
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
|
||||
1. Pontos match keresése ServiceProvider.name alapján (ILIKE, kisbetűs)
|
||||
2. Fuzzy match trigram similarity-vel (pg_trgm) — ha similarity > 0.6
|
||||
3. Ha nem létezik → új provider létrehozása PENDING státusszal,
|
||||
validation_score=0, source=import, vissza: created=True
|
||||
4. Ha létezik → vissza: created=False
|
||||
|
||||
Args:
|
||||
db: AsyncSession
|
||||
@@ -144,25 +159,37 @@ async def find_or_create_provider_by_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"
|
||||
tuple[ServiceProvider, bool]: (provider, created)
|
||||
- created=True: új provider lett létrehozva
|
||||
- created=False: meglévő provider lett lekérve
|
||||
"""
|
||||
# 1. Pontos match keresése (kisbetűsen, space-sztrippelten)
|
||||
# 1. Pontos match keresése (ILIKE, kisbetűsen, space-sztrippelten)
|
||||
clean_name = name.strip()
|
||||
provider_stmt = select(ServiceProvider).where(
|
||||
func.lower(ServiceProvider.name) == func.lower(clean_name)
|
||||
ServiceProvider.name.ilike(clean_name)
|
||||
)
|
||||
provider_result = await db.execute(provider_stmt)
|
||||
provider = provider_result.scalar_one_or_none()
|
||||
|
||||
# 2. Fuzzy match trigram similarity-vel (pg_trgm), ha nincs pontos találat
|
||||
# P0 ARCHITECTURE CLEANUP: threshold 0.3 -> 0.6 a hamis pozitívok csökkentésére
|
||||
if not provider:
|
||||
# 2. Ha nem létezik → létrehozás + PROVIDER_DISCOVERY
|
||||
fuzzy_stmt = select(ServiceProvider).where(
|
||||
func.similarity(ServiceProvider.name, clean_name) > 0.6
|
||||
).order_by(
|
||||
func.similarity(ServiceProvider.name, clean_name).desc()
|
||||
).limit(1)
|
||||
fuzzy_result = await db.execute(fuzzy_stmt)
|
||||
provider = fuzzy_result.scalar_one_or_none()
|
||||
|
||||
if not provider:
|
||||
# 3. Ha nem létezik → létrehozás, created=True
|
||||
new_provider = ServiceProvider(
|
||||
name=clean_name,
|
||||
address=clean_name,
|
||||
status=ModerationStatus.pending,
|
||||
source=SourceType.api,
|
||||
validation_score=10,
|
||||
source=SourceType.api_import, # "import" — API import forrás
|
||||
validation_score=0,
|
||||
added_by_user_id=added_by_user_id,
|
||||
)
|
||||
db.add(new_provider)
|
||||
@@ -173,44 +200,16 @@ async def find_or_create_provider_by_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"
|
||||
return new_provider, True
|
||||
|
||||
# 4. Ha létezik, visszaadjuk created=False
|
||||
logger.info(
|
||||
f"Meglevo provider hasznalata: name='{clean_name}', "
|
||||
f"provider_id={provider.id}, action_key='{action_key}', "
|
||||
f"provider_id={provider.id}, "
|
||||
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
|
||||
return provider, False
|
||||
|
||||
|
||||
async def search_providers(
|
||||
@@ -287,14 +286,16 @@ async def search_providers(
|
||||
)
|
||||
org_conditions.append(and_(*token_conditions))
|
||||
if city:
|
||||
# SZIGORÍTÁS (2026-06-17): City keresés exact match vagy StartsWith
|
||||
# P0 UNIFIED ADDRESS REFACTOR (2026-07-01): A denormalizált címmezők
|
||||
# helyett az Address → GeoPostalCode kapcsolaton keresztül érjük el
|
||||
# a város és irányítószám adatokat.
|
||||
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}%"),
|
||||
GeoPostalCode.city == city_clean,
|
||||
GeoPostalCode.city.ilike(f"{city_clean}%"),
|
||||
GeoPostalCode.zip_code == city_clean,
|
||||
GeoPostalCode.zip_code.ilike(f"{city_clean}%"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -319,30 +320,30 @@ async def search_providers(
|
||||
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").
|
||||
# P0 UNIFIED ADDRESS REFACTOR (2026-07-01): A denormalizált címmezők
|
||||
# helyett az Address → GeoPostalCode kapcsolaton keresztül érjük el
|
||||
# a város, irányítószám és utca adatokat.
|
||||
# LEFT JOIN-eket használunk, mert az address_id lehet NULL.
|
||||
org_stmt = select(
|
||||
Organization.id.label("id"),
|
||||
Organization.name.label("name"),
|
||||
Organization.address_city.label("city"),
|
||||
GeoPostalCode.city.label("city"),
|
||||
func.concat(
|
||||
Organization.address_zip,
|
||||
GeoPostalCode.zip_code,
|
||||
literal(' '),
|
||||
Organization.address_city,
|
||||
GeoPostalCode.city,
|
||||
literal(', '),
|
||||
Organization.address_street_name,
|
||||
Address.street_name,
|
||||
literal(' '),
|
||||
Organization.address_street_type,
|
||||
Address.street_type,
|
||||
literal(' '),
|
||||
Organization.address_house_number,
|
||||
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"),
|
||||
GeoPostalCode.zip_code.label("address_zip"),
|
||||
Address.street_name.label("address_street_name"),
|
||||
Address.street_type.label("address_street_type"),
|
||||
Address.house_number.label("address_house_number"),
|
||||
literal(None).label("plus_code"),
|
||||
Organization.is_verified.label("is_verified"),
|
||||
ServiceProfile.contact_phone.label("contact_phone"),
|
||||
ServiceProfile.contact_email.label("contact_email"),
|
||||
@@ -355,6 +356,12 @@ async def search_providers(
|
||||
).outerjoin(
|
||||
ServiceProfile,
|
||||
ServiceProfile.organization_id == Organization.id,
|
||||
).outerjoin(
|
||||
Address,
|
||||
Address.id == Organization.address_id,
|
||||
).outerjoin(
|
||||
GeoPostalCode,
|
||||
GeoPostalCode.id == Address.postal_code_id,
|
||||
).where(*org_conditions)
|
||||
|
||||
# --- 2. LEKÉRDEZÉS: Robot által gyűjtött adatok (marketplace.service_staging) ---
|
||||
@@ -517,17 +524,36 @@ async def search_providers(
|
||||
for cat in row_categories
|
||||
]
|
||||
|
||||
# P3: Egységes cím objektum összeállítása a flat mezőkből
|
||||
row_address_zip = getattr(row, "address_zip", None)
|
||||
row_address_street_name = getattr(row, "address_street_name", None)
|
||||
row_address_street_type = getattr(row, "address_street_type", None)
|
||||
row_address_house_number = getattr(row, "address_house_number", None)
|
||||
row_city = row.city
|
||||
row_plus_code = getattr(row, "plus_code", None)
|
||||
|
||||
address_detail = None
|
||||
if any([row_address_zip, row_address_street_name, row_address_street_type, row_address_house_number, row_city]):
|
||||
address_detail = AddressOut(
|
||||
zip=row_address_zip,
|
||||
city=row_city,
|
||||
street_name=row_address_street_name,
|
||||
street_type=row_address_street_type,
|
||||
house_number=row_address_house_number,
|
||||
full_address_text=row.address,
|
||||
)
|
||||
|
||||
results.append(
|
||||
ProviderSearchResult(
|
||||
id=row.id,
|
||||
name=row.name,
|
||||
city=row.city,
|
||||
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),
|
||||
address_zip=row_address_zip,
|
||||
address_street_name=row_address_street_name,
|
||||
address_street_type=row_address_street_type,
|
||||
address_house_number=row_address_house_number,
|
||||
plus_code=row_plus_code,
|
||||
contact_phone=getattr(row, "contact_phone", None),
|
||||
contact_email=getattr(row, "contact_email", None),
|
||||
website=getattr(row, "website", None),
|
||||
@@ -535,6 +561,7 @@ async def search_providers(
|
||||
categories=categories_out,
|
||||
source=row.source,
|
||||
is_verified=row.is_verified,
|
||||
address_detail=address_detail,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -954,17 +981,23 @@ async def update_provider(
|
||||
if staging:
|
||||
# Migráljuk Organization-be
|
||||
now = datetime.now(timezone.utc)
|
||||
# ── AddressManager: Cím létrehozása a staging adataiból ──
|
||||
staging_addr_in = AddressIn(
|
||||
city=staging.city or "",
|
||||
zip=data.address_zip or "",
|
||||
street_name=data.address_street_name or "",
|
||||
street_type=data.address_street_type or "",
|
||||
house_number=data.address_house_number or "",
|
||||
)
|
||||
staging_address_id = await AddressManager.create_or_update(db, None, staging_addr_in)
|
||||
|
||||
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,
|
||||
address_id=staging_address_id,
|
||||
status="pending_verification",
|
||||
is_verified=False,
|
||||
folder_slug=hashlib.md5(f"sp-{staging.id}-{uuid.uuid4()}".encode()).hexdigest()[:12],
|
||||
@@ -988,6 +1021,16 @@ async def update_provider(
|
||||
# 1c. Ha nincs staging-ben sem, ServiceProvider-ben keresünk
|
||||
crowd = await db.get(ServiceProvider, provider_id)
|
||||
if crowd:
|
||||
# ── AddressManager: Cím létrehozása a crowd adataiból ──
|
||||
crowd_addr_in = AddressIn(
|
||||
city=data.city or "",
|
||||
zip=data.address_zip or "",
|
||||
street_name=data.address_street_name or "",
|
||||
street_type=data.address_street_type or "",
|
||||
house_number=data.address_house_number or "",
|
||||
)
|
||||
crowd_address_id = await AddressManager.create_or_update(db, None, crowd_addr_in)
|
||||
|
||||
# Migráljuk Organization-be
|
||||
now = datetime.now(timezone.utc)
|
||||
org = Organization(
|
||||
@@ -996,11 +1039,7 @@ async def update_provider(
|
||||
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,
|
||||
address_id=crowd_address_id,
|
||||
status="pending_verification",
|
||||
is_verified=False,
|
||||
folder_slug=hashlib.md5(f"cr-{crowd.id}-{uuid.uuid4()}".encode()).hexdigest()[:12],
|
||||
@@ -1024,26 +1063,41 @@ async def update_provider(
|
||||
# 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.
|
||||
# 2. Organization mezők frissítése AddressManager segítségével
|
||||
# P3 UNIFIED ADDRESS REFACTOR (2026-07-01): A denormalizált címmezők
|
||||
# helyett az AddressManager.create_or_update() kezeli a címet.
|
||||
# A flat mezők (city, address_zip, stb.) továbbra is támogatottak
|
||||
# a ProviderUpdateIn sémában a backward compatibility miatt.
|
||||
org.name = data.name[:100]
|
||||
org.full_name = data.name
|
||||
org.display_name = data.name[:50]
|
||||
|
||||
# ── Cím frissítése AddressManager segítségével ──
|
||||
# Összegyűjtjük a flat mezőkből az AddressIn adatokat
|
||||
addr_fields = {}
|
||||
if data.city is not None:
|
||||
org.address_city = data.city
|
||||
addr_fields["city"] = data.city
|
||||
if data.address_zip is not None:
|
||||
org.address_zip = data.address_zip
|
||||
addr_fields["zip"] = data.address_zip
|
||||
if data.address_street_name is not None:
|
||||
org.address_street_name = data.address_street_name
|
||||
addr_fields["street_name"] = data.address_street_name
|
||||
if data.address_street_type is not None:
|
||||
org.address_street_type = data.address_street_type
|
||||
addr_fields["street_type"] = data.address_street_type
|
||||
if data.address_house_number is not None:
|
||||
org.address_house_number = data.address_house_number
|
||||
addr_fields["house_number"] = data.address_house_number
|
||||
if data.plus_code is not None:
|
||||
org.plus_code = data.plus_code
|
||||
addr_fields["plus_code"] = data.plus_code
|
||||
|
||||
# Ha van address_detail, az elsőbbséget élvez a flat mezőkkel szemben
|
||||
if data.address_detail is not None:
|
||||
org.address_id = await AddressManager.create_or_update(
|
||||
db, org.address_id, data.address_detail
|
||||
)
|
||||
elif addr_fields:
|
||||
addr_in = AddressIn(**addr_fields)
|
||||
org.address_id = await AddressManager.create_or_update(
|
||||
db, org.address_id, addr_in
|
||||
)
|
||||
|
||||
# 3. ServiceProfile lekérdezése (ha létezik)
|
||||
profile_stmt = select(ServiceProfile).where(
|
||||
|
||||
Reference in New Issue
Block a user