# /opt/docker/dev/service_finder/backend/app/workers/service/validation_pipeline.py """ 5-Szintes Költséghatékony Validációs Pipeline a szerviz validálására (Epic 9, #111-es jegy). Ez a modul a régi Google validátor (service_robot_4_validator_google.py) kiegészítéseként szolgál, vízesés (fallback) architektúrát alkalmazva, hogy minimalizáljuk a költségeket és maximalizáljuk a fedezetet. A pipeline 5 szintből áll, amelyek sorban próbálkoznak, amíg egy sikeres validációt nem érnek el. Minden szintnek saját siker/failure feltételei vannak, és a következő szintre való lépés döntése a szint belső logikája alapján történik. ARCHITEKTÚRA (JAVÍTOTT, KÖLTSÉGHATÉKONY): 1. OpenStreetMap Nominatim (ingyenes) – alap geokódolás 2. EU VIES / Cégjegyzék API + AI Parser – hivatalos jogi létezés ellenőrzés 3. Freemium API-k (Foursquare / Yelp) – ingyenes nyitvatartás és képek 4. Célzott Web Scraping – szerviz saját weblapjának aszinkron átolvasása 5. Google Places API (Fallback) – csak a legnehezebb, beragadt esetek Minden szint dokumentálva van masszív docstring‑gel, amely tartalmazza: - A szint célját - Használt külső API‑t vagy AI eszközt - Sikerfeltéleteket (mikor térünk vissza) - Fallback feltételeket (mikor lépünk tovább) - Költség‑ és kvótakezelési megfontolásokat A pipeline aszinkron, párhuzamosítható, és atomi zárolással dolgozik a `service_profiles` táblán. """ import asyncio import httpx import logging import os import sys import json import re from datetime import datetime from typing import Optional, Dict, Any, Tuple from sqlalchemy import text, update, func from app.database import AsyncSessionLocal from app.models.marketplace.service import ServiceProfile, ServiceStatus logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] AI-Pipeline: %(message)s', stream=sys.stdout ) logger = logging.getLogger("Service-AI-Pipeline") # ------------------------------------------------------------------- # 1. SZINT: OPENSTREETMAP NOMINATIM (INGYENES ALAP GEOKÓDOLÁS) # ------------------------------------------------------------------- class OSMNominatimValidator: """ Első szint: OpenStreetMap Nominatim API (ingyenes alap geokódolás). CÉL: Ingyenes alap geokódolás a szerviz neve és címe alapján. Ha 100%-os találatot ad, akkor SIKER, és nem kell továbbmenni a következő szintekre. HASZNÁLT API: OpenStreetMap Nominatim Search – https://nominatim.openstreetmap.org/search Nincs API kulcs, de tiszteletben kell tartani a Usage Policy‑t (max 1 kérés/másodperc). SIKER (visszatérés DONE): - A Nominatim visszaad egy találatot a szerviz nevével és címmel - GPS koordináta kinyerése (lat, lon) pontossággal - A találat confidence > 0.8 (jó egyezés) - A szerviz státusza active‑re frissül, trust_score +20 FALLBACK (továbblépés a 2. szintre): - Nincs találat (üres válasz) - Találat confidence < 0.5 (gyenge egyezés) - Hálózati hiba vagy timeout - Túl sok kérés (429) KÖLTSÉGKEZELÉS: Teljesen ingyenes, de rate limit miatt szükséges throttling (1 másodperc várakozás). Nincs pénzügyi költség. """ NOMINATIM_URL = "https://nominatim.openstreetmap.org/search" async def validate(self, db, profile_id: int, fingerprint: str, bio: str) -> Tuple[str, Optional[Dict]]: logger.info(f"[OSM] 1. szint: Validálás indul: {fingerprint}") name = fingerprint.split('|')[0] if '|' in fingerprint else fingerprint params = { "q": f"{name} Hungary", "format": "json", "limit": 1, "addressdetails": 1 } headers = {"User-Agent": "ServiceFinderBot/1.0 (contact: admin@servicefinder.hu)"} try: async with httpx.AsyncClient(timeout=15.0) as client: await asyncio.sleep(1) # Rate limit tisztelet resp = await client.get(self.NOMINATIM_URL, params=params, headers=headers) if resp.status_code == 200: results = resp.json() if not results: logger.warning(f"[OSM] Nem található: {name}, továbblépés VIES-re.") return "FALLBACK", None result = results[0] confidence = min(len(result.get("display_name", "")) / 100, 1.0) if confidence < 0.5: logger.warning(f"[OSM] Alacsony confidence ({confidence}), továbblépés VIES-re.") return "FALLBACK", None extracted = { "osm_id": result.get("osm_id"), "display_name": result.get("display_name"), "location": { "latitude": float(result.get("lat")), "longitude": float(result.get("lon")) }, "confidence": confidence } logger.info(f"[OSM] Sikeres validáció, koordináta: {extracted['location']}") return "DONE", extracted else: logger.error(f"[OSM] API hiba: {resp.status_code}, továbblépés VIES-re.") return "FALLBACK", None except Exception as e: logger.debug(f"[OSM] Hálózati hiba: {e}, továbblépés VIES-re.") return "FALLBACK", None # ------------------------------------------------------------------- # 2. SZINT: EU VIES / CÉGJEGYZÉK API + AI PARSER # ------------------------------------------------------------------- class EUVIESValidator: """ Második szint: EU VIES (VAT Information Exchange System) és nemzeti cégjegyzék API-k. CÉL: Hivatalos jogi létezés ellenőrzése adószám vagy cégjegyzékszám alapján. Az AI (Ollama/Qwen) a nyers JSON/HTML választ strukturálja és értelmezi. HASZNÁLT API: EU VIES SOAP API (ingyenes) – VAT szám validáció Nemzeti cégjegyzék API-k (pl. Hungarian Company Registry) – ha elérhető AI Parser: Ollama Qwen 14B a strukturálatlan adatok feldolgozására SIKER (visszatérés DONE): - A VIES API visszaigazolja, hogy a VAT szám érvényes és aktív - Cégjegyzék visszaadja a cég nevének, székhelyének, tevékenységi körének adatait - AI parser kinyeri a releváns mezőket és magas confidence-t ad (>0.7) - A szerviz státusza active, trust_score +30 FALLBACK (továbblépés a 3. szintre): - VAT szám nem érvényes vagy nem található - Cégjegyzék API nem elérhető vagy hibás válasz - AI parser alacsony confidence-t ad (<0.3) - Időtúllépés vagy parsing hiba KÖLTSÉGKEZELÉS: VIES ingyenes, cégjegyzék API-k lehetnek korlátozottak. AI parser helyi, nulla költség. Összköltség: ~$0 (kivéve ha fizetős cégjegyzék API-t használunk). """ VIES_URL = "https://ec.europa.eu/taxation_customs/vies/services/checkVatService" VIES_REST_URL = "https://ec.europa.eu/taxation_customs/vies/rest-api/ms/{country_code}/vat/{vat_number}" OLLAMA_URL = "http://localhost:11434/api/generate" async def validate(self, db, profile_id: int, fingerprint: str, bio: str) -> Tuple[str, Optional[Dict]]: logger.info(f"[VIES] 2. szint: Jogi validáció indul: {fingerprint}") vat_match = re.search(r'[A-Z]{2}[0-9A-Z]{8,12}', bio if bio else "") if not vat_match: logger.warning("[VIES] Nincs VAT szám a bio-ban, továbblépés Freemium API-ra.") return "FALLBACK", None vat_number = vat_match.group() country_code = vat_number[:2] vat_num = vat_number[2:] # 1. EU VIES REST API hívás rest_url = self.VIES_REST_URL.format(country_code=country_code, vat_number=vat_num) try: async with httpx.AsyncClient(timeout=10.0) as client: resp = await client.get(rest_url) if resp.status_code == 200: vies_data = resp.json() if vies_data.get("valid", False): logger.info(f"[VIES] VAT szám érvényes: {vat_number}") # 2. AI parser hívása a nyers VIES adatokkal ai_extracted = await self._parse_with_ai(json.dumps(vies_data)) if ai_extracted and ai_extracted.get("is_active", False): extracted = { "vat_valid": True, "vat_number": vat_number, "company_name": ai_extracted.get("company_name", ""), "address": ai_extracted.get("address", ""), "is_active": True, "confidence": 0.9 } return "DONE", extracted else: logger.warning("[VIES] AI parser nem találta aktívnak a céget, továbblépés.") return "FALLBACK", None else: logger.warning(f"[VIES] VAT szám érvénytelen vagy nem található: {vat_number}") return "FALLBACK", None else: logger.error(f"[VIES] REST API hiba: {resp.status_code}") return "FALLBACK", None except Exception as e: logger.debug(f"[VIES] Hálózati hiba: {e}") return "FALLBACK", None async def _parse_with_ai(self, raw_data: str) -> Optional[Dict]: """ Privát metódus, amely a helyi Ollama (Qwen) AI-t használja a VIES nyers adatok strukturálására. A prompt specifikus, hogy JSON-t adjon vissza. """ prompt = f"""You are an expert data extractor. Extract the company name, exact address, and active status from the following VIES registry data. Return ONLY a valid JSON object with keys: 'company_name', 'address', 'is_active'. Do not include markdown formatting or explanation. Data: {raw_data}""" payload = { "model": "qwen2.5:14b", "prompt": prompt, "format": "json", "stream": False } try: async with httpx.AsyncClient(timeout=30.0) as client: resp = await client.post(self.OLLAMA_URL, json=payload) if resp.status_code == 200: result = resp.json() response_text = result.get("response", "").strip() # Tisztítás: eltávolítjuk a ```json és ``` jeleket if response_text.startswith("```json"): response_text = response_text[7:] if response_text.endswith("```"): response_text = response_text[:-3] try: parsed = json.loads(response_text) # Ellenőrizzük, hogy a szükséges kulcsok léteznek if all(key in parsed for key in ["company_name", "address", "is_active"]): return parsed else: logger.warning(f"[Ollama] Hiányzó kulcsok a JSON-ban: {parsed}") return None except json.JSONDecodeError as e: logger.error(f"[Ollama] JSON parse hiba: {e}, response: {response_text}") return None else: logger.error(f"[Ollama] API hiba: {resp.status_code}, {resp.text}") return None except Exception as e: logger.debug(f"[Ollama] Hálózati hiba: {e}") return None # ------------------------------------------------------------------- # 3. SZINT: FREEMIUM API-K (FOURSQUARE / YELP) # ------------------------------------------------------------------- class FreemiumAPIValidator: """ Harmadik szint: Freemium API-k (Foursquare, Yelp) ingyenes rétege. CÉL: Ingyenes nyitvatartás, képek, értékelések és alapvető üzleti információk lekérése. Ezek az API-k ingyenes tierrel rendelkeznek, de korlátozott kvótával. HASZNÁLT API: Foursquare Places API (ingyenes tier, 950 kérés/nap) Yelp Fusion API (ingyenes tier, 500 kérés/nap) Környezeti változók: FOURSQUARE_CLIENT_ID, FOURSQUARE_CLIENT_SECRET, YELP_API_KEY SIKER (visszatérés DONE): - API visszaad egy vagy több találatot a szervizre - Nyitvatartási idő, telefonszám, weboldal, átlagos értékelés kinyerése - Legalább 3 kép vagy értékelés megtalálása - A szerviz státusza active, trust_score +25 FALLBACK (továbblépés a 4. szintre): - Nincs találat az API-ban - API kvóta elérve - Hálózati hiba vagy timeout - Kevesebb mint 2 kép/értékelés KÖLTSÉGKEZELÉS: Ingyenes tier, de kvóták figyelése szükséges. Ha a napi limit túllépés közelében van, automatikusan átvált a következő szintre. Nincs pénzügyi költség az ingyenes kvótán belül. """ FOURSQUARE_URL = "https://api.foursquare.com/v3/places/search" YELP_URL = "https://api.yelp.com/v3/businesses/search" def __init__(self): self.foursquare_client_id = os.getenv("FOURSQUARE_CLIENT_ID") self.foursquare_client_secret = os.getenv("FOURSQUARE_CLIENT_SECRET") self.yelp_api_key = os.getenv("YELP_API_KEY") async def validate(self, db, profile_id: int, fingerprint: str, bio: str) -> Tuple[str, Optional[Dict]]: logger.info(f"[Freemium] 3. szint: Validálás indul: {fingerprint}") name = fingerprint.split('|')[0] if '|' in fingerprint else fingerprint if self.foursquare_client_id and self.foursquare_client_secret: result = await self._try_foursquare(name) if result: return "DONE", result if self.yelp_api_key: result = await self._try_yelp(name) if result: return "DONE", result logger.warning("[Freemium] Egyik API sem adott eredményt, továbblépés Web Scraping-re.") return "FALLBACK", None async def _try_foursquare(self, name: str) -> Optional[Dict]: headers = { "Authorization": f"{self.foursquare_client_id}", "Accept": "application/json" } params = {"query": name, "near": "Hungary", "limit": 1} try: async with httpx.AsyncClient(timeout=10.0) as client: resp = await client.get(self.FOURSQUARE_URL, params=params, headers=headers) if resp.status_code == 200: data = resp.json() places = data.get("results", []) if places: place = places[0] extracted = { "fsq_id": place.get("fsq_id"), "name": place.get("name"), "location": place.get("location", {}), "rating": place.get("rating"), "photos": place.get("photos", []), "contact": place.get("contact", {}) } logger.info(f"[Foursquare] Találat: {place.get('name')}") return extracted except Exception as e: logger.debug(f"[Foursquare] Hiba: {e}") return None async def _try_yelp(self, name: str) -> Optional[Dict]: headers = {"Authorization": f"Bearer {self.yelp_api_key}"} params = {"term": name, "location": "Hungary", "limit": 1} try: async with httpx.AsyncClient(timeout=10.0) as client: resp = await client.get(self.YELP_URL, params=params, headers=headers) if resp.status_code == 200: data = resp.json() businesses = data.get("businesses", []) if businesses: business = businesses[0] extracted = { "yelp_id": business.get("id"), "name": business.get("name"), "rating": business.get("rating"), "review_count": business.get("review_count"), "phone": business.get("phone"), "photos": business.get("photos", []), "location": business.get("location", {}) } logger.info(f"[Yelp] Találat: {business.get('name')}") return extracted except Exception as e: logger.debug(f"[Yelp] Hiba: {e}") return None # ------------------------------------------------------------------- # 4. SZINT: CÉLZOTT WEB SCRAPING # ------------------------------------------------------------------- class WebScrapingValidator: """ Negyedik szint: Célzott Web Scraping a szerviz saját weblapjáról. CÉL: A szerviz saját weblapjának aszinkron átolvasása (detektív munka) információk kinyerésére: telefonszám, cím, nyitvatartás, szolgáltatások, képek. HASZNÁLT ESZKÖZ: Aszinkron HTTP kérések (httpx) + BeautifulSoup HTML parsing Környezeti változó: SCRAPING_TIMEOUT (alapértelmezett 30 másodperc) SIKER (visszatérés DONE): - Weblap sikeresen letöltve és parse-olva - Legalább 2 releváns kulcsszó található a HTML szövegében - A kinyert információk konzisztensek a szerviz adataival - A szerviz státusza active, trust_score +15 FALLBACK (továbblépés a 5. szintre): - Weblap nem elérhető (404, timeout) - Nincs releváns információ a HTML-ben - Scraping tiltva (robots.txt, rate limiting) - Parsing hiba KÖLTSÉGKEZELÉS: Nincs API költség, de erőforrás-igényes lehet. Rate limiting beépítve, hogy ne terheljük túl a cél szervert. Nincs pénzügyi költség. """ # Releváns kulcsszavak a szerviz weblapjain KEYWORDS = ["szerviz", "javítás", "autó", "motor", "műhely", "garage", "service", "repair", "car", "workshop", "maintenance", "auto"] async def validate(self, db, profile_id: int, fingerprint: str, bio: str) -> Tuple[str, Optional[Dict]]: logger.info(f"[WebScraping] 4. szint: Validálás indul: {fingerprint}") # Weboldal URL kinyerése a bio-ból (egyszerű regex) url_match = re.search(r'https?://[^\s]+', bio if bio else "") if not url_match: logger.warning("[WebScraping] Nincs URL a bio-ban, továbblépés Google-re.") return "FALLBACK", None url = url_match.group() try: async with httpx.AsyncClient(timeout=30.0) as client: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" } resp = await client.get(url, headers=headers, follow_redirects=True) if resp.status_code == 200: html = resp.text # BeautifulSoup import (inline, mert a fájl elején nincs) try: from bs4 import BeautifulSoup soup = BeautifulSoup(html, 'html.parser') # Távolítsuk el a script és style elemeket for script in soup(["script", "style"]): script.decompose() # Szöveg kinyerése text = soup.get_text(separator=' ', strip=True) text_lower = text.lower() # Kulcsszó keresés found_keywords = [] for keyword in self.KEYWORDS: if keyword.lower() in text_lower: found_keywords.append(keyword) logger.info(f"[WebScraping] Talált kulcsszavak: {found_keywords}") # Ha legalább 2 kulcsszó található, sikeres if len(found_keywords) >= 2: extracted = { "url": url, "found_keywords": found_keywords, "text_preview": text[:200] + "..." if len(text) > 200 else text } logger.info(f"[WebScraping] Sikeres scraping, {len(found_keywords)} kulcsszó találva.") return "DONE", extracted else: logger.warning(f"[WebScraping] Kevesebb mint 2 kulcsszó ({len(found_keywords)}), továbblépés Google-re.") return "FALLBACK", None except ImportError: logger.error("[WebScraping] BeautifulSoup4 nincs telepítve, továbblépés Google-re.") return "FALLBACK", None else: logger.error(f"[WebScraping] HTTP hiba: {resp.status_code}") return "FALLBACK", None except httpx.TimeoutException: logger.warning("[WebScraping] Timeout a weblap betöltésénél, továbblépés Google-re.") return "FALLBACK", None except Exception as e: logger.debug(f"[WebScraping] Hálózati hiba: {e}") return "FALLBACK", None # ------------------------------------------------------------------- # 5. SZINT: GOOGLE PLACES API (FALLBACK) # ------------------------------------------------------------------- class QuotaManager: """ Szigorú napi limit figyelő a Google API-hoz, hogy soha többé ne legyen 250$-os számla! """ def __init__(self, service_name: str, daily_limit: int): self.service_name = service_name self.daily_limit = daily_limit self.state_file = f"/app/temp/.quota_{service_name}.json" self._ensure_file() def _ensure_file(self): os.makedirs(os.path.dirname(self.state_file), exist_ok=True) if not os.path.exists(self.state_file): with open(self.state_file, 'w') as f: json.dump({"date": datetime.now().strftime("%Y-%m-%d"), "count": 0}, f) def can_make_request(self) -> bool: with open(self.state_file, 'r') as f: data = json.load(f) today = datetime.now().strftime("%Y-%m-%d") if data["date"] != today: data = {"date": today, "count": 0} if data["count"] >= self.daily_limit: return False data["count"] += 1 with open(self.state_file, 'w') as f: json.dump(data, f) return True class GooglePlacesValidator: """ Ötödik szint: Google Places API (a legdrágább, fallback). CÉL: CSAK a legnehezebb, beragadt eseteket küldjük ide, hogy spóroljunk a kvótával. A Google arany standard adatokat szolgáltat, de költséges ($0,03 / hívás). HASZNÁLT API: Google Places API (Text Search) – https://places.googleapis.com/v1/places:searchText Környezeti változó: GOOGLE_API_KEY, GOOGLE_DAILY_LIMIT (alapértelmezett 100) SIKER (visszatérés DONE): - Google visszaad egy érvényes place objektumot - GPS koordináta, telefonszám, weboldal, értékelések kinyerése - A szerviz státusza active, trust_score +50 FALLBACK (visszatérés FALLBACK): - Google API nem válaszol (hálózati hiba) - NAPI KVÓTA ELÉRVE (QuotaManager blokkol) - A Google nem ismeri a szervizt (NOT_FOUND) - API kulcs hiányzik vagy érvénytelen Ekkor a pipeline FAILED állapotba kerül, és manuális ellenőrzésre vár. KÖLTSÉGKEZELÉS: QuotaManager szigorúan figyeli a napi limitet. Csak akkor használjuk, ha az összes előző szint sikertelen. Költség: ~$0,03 / hívás. """ PLACES_TEXT_URL = "https://places.googleapis.com/v1/places:searchText" def __init__(self): self.api_key = os.getenv("GOOGLE_API_KEY") # Napi limit: pl. 100 lekérdezés = kb. $3/nap maximum! self.daily_limit = int(os.getenv("GOOGLE_DAILY_LIMIT", "100")) self.quota = QuotaManager("google_places", self.daily_limit) self.headers = { "Content-Type": "application/json", "X-Goog-Api-Key": self.api_key, # Csak a legszükségesebb mezőket kérjük, hogy olcsó maradjon az API hívás! "X-Goog-FieldMask": "places.id,places.location,places.rating,places.userRatingCount,places.regularOpeningHours,places.internationalPhoneNumber,places.websiteUri" } async def validate(self, db, profile_id: int, fingerprint: str, bio: str) -> Tuple[str, Optional[Dict]]: logger.info(f"[Google] 5. szint (Fallback): Validálás indul: {fingerprint}") if not self.api_key: logger.warning("[Google] Hiányzó API kulcs, pipeline FAILED.") return "FALLBACK", None if not self.quota.can_make_request(): logger.warning("[Google] Napi kvóta elérve, pipeline FAILED.") return "FALLBACK", None name = fingerprint.split('|')[0] if '|' in fingerprint else fingerprint query_text = f"{name} {bio}" payload = {"textQuery": query_text, "maxResultCount": 1} for attempt in range(2): try: async with httpx.AsyncClient(timeout=10.0) as client: resp = await client.post(self.PLACES_TEXT_URL, json=payload, headers=self.headers) if resp.status_code == 200: places = resp.json().get("places", []) if not places: logger.warning(f"[Google] Nem található: {name}") return "FALLBACK", None place_data = places[0] extracted = { "google_place_id": place_data.get("id"), "rating": place_data.get("rating"), "user_ratings_total": place_data.get("userRatingCount"), "contact_phone": place_data.get("internationalPhoneNumber"), "website": place_data.get("websiteUri"), "opening_hours": place_data.get("regularOpeningHours", {}), "location": place_data.get("location") } logger.info(f"[Google] Sikeres validáció, adatok kinyerve.") return "DONE", extracted elif resp.status_code == 429: logger.warning("[Google] Rate limit, újrapróbálás...") await asyncio.sleep(2) continue else: logger.error(f"[Google] API hiba: {resp.status_code}") return "FALLBACK", None except Exception as e: logger.debug(f"[Google] Hálózati hiba: {e}") await asyncio.sleep(1) logger.warning("[Google] Mindkét próbálkozás sikertelen, pipeline FAILED.") return "FALLBACK", None # ------------------------------------------------------------------- # PIPELINE KOORDINÁTOR # ------------------------------------------------------------------- class ValidationPipeline: """ A teljes 5‑szintes pipeline koordinátora. Felelősség: - Szekvenciális hívás az 1‑5. szinteknek - Adatbázis frissítés a sikeres validáció után - Naplózás és metrika gyűjtés """ def __init__(self): self.validators = [ OSMNominatimValidator(), EUVIESValidator(), FreemiumAPIValidator(), WebScrapingValidator(), GooglePlacesValidator() ] async def run(self, profile_id: int) -> bool: """Futtatja a pipeline‑t egy adott szerviz profilra.""" async with AsyncSessionLocal() as db: # Profil adatok lekérése result = await db.execute( text("SELECT fingerprint, bio FROM marketplace.service_profiles WHERE id = :id"), {"id": profile_id} ) row = result.fetchone() if not row: logger.error(f"[Pipeline] Profil {profile_id} nem található.") return False fingerprint, bio = row # Szekvenciális validáció for i, validator in enumerate(self.validators, 1): status, data = await validator.validate(db, profile_id, fingerprint, bio) if status == "DONE": logger.info(f"[Pipeline] {i}. szint sikeres, profil frissítése.") # Adatbázis frissítés (egyszerűsített) await db.execute( text("UPDATE marketplace.service_profiles SET status = 'active', trust_score = trust_score + :score WHERE id = :id"), {"score": 10 * i, "id": profile_id} ) await db.commit() return True elif status == "FALLBACK": logger.info(f"[Pipeline] {i}. szint sikertelen, továbblépés {i+1}. szintre.") continue else: logger.error(f"[Pipeline] {i}. szint hibás, pipeline leáll.") break logger.warning(f"[Pipeline] Minden szint sikertelen, profil flagged.") await db.execute( text("UPDATE marketplace.service_profiles SET status = 'flagged' WHERE id = :id"), {"id": profile_id} ) await db.commit() return False