feat: v1.7 overhaul - identity hash, triple wallet, financial ledger, and security audit system

This commit is contained in:
2026-02-16 00:42:49 +00:00
parent bb02d4ed59
commit d574d3297d
63 changed files with 3710 additions and 565 deletions

2
.env
View File

@@ -1,3 +1,5 @@
COMPOSE_PROJECT_NAME=service_finder
# --- ADATBÁZIS KAPCSOLAT (Központi) --- # --- ADATBÁZIS KAPCSOLAT (Központi) ---
# Itt a 'shared-postgres' nevet használjuk, ami a központi konténer neve # Itt a 'shared-postgres' nevet használjuk, ami a központi konténer neve
APP_DB_HOST=shared-postgres APP_DB_HOST=shared-postgres

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
n8n/db_data/
N8N/db_data/
*.pyc
__pycache__/

View File

@@ -1,3 +0,0 @@
{
"encryptionKey": "54T2Q0sTamS0GDOb8vyTtOQXBJxq3d0/"
}

View File

@@ -1,5 +0,0 @@
{
"name": "installed-nodes",
"private": true,
"dependencies": {}
}

View File

@@ -11,35 +11,35 @@ from app.models.asset import Asset, AssetCost, AssetTelemetry
from app.models.identity import User from app.models.identity import User
from app.services.cost_service import cost_service from app.services.cost_service import cost_service
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
# --- IMPORT JAVÍTVA: Behozzuk a jármű sémát a dúsított adatokhoz ---
from app.schemas.asset import AssetResponse
router = APIRouter() router = APIRouter()
# --- 1. MODUL: IDENTITÁS (Alapadatok) --- # --- 1. MODUL: IDENTITÁS (Alapadatok & Technikai katalógus) ---
@router.get("/{asset_id}", response_model=Dict[str, Any]) @router.get("/{asset_id}", response_model=AssetResponse)
async def get_asset_identity( async def get_asset_identity(
asset_id: uuid.UUID, asset_id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
"""Csak a jármű alapadatai és katalógus információi.""" """
stmt = select(Asset).where(Asset.id == asset_id).options(selectinload(Asset.catalog)) Visszaadja a jármű alapadatokat és a dúsított katalógus információkat (kW, CCM, tengelyek).
A selectinload(Asset.catalog) biztosítja, hogy a technikai adatok is betöltődjenek.
"""
stmt = (
select(Asset)
.where(Asset.id == asset_id)
.options(selectinload(Asset.catalog))
)
asset = (await db.execute(stmt)).scalar_one_or_none() asset = (await db.execute(stmt)).scalar_one_or_none()
if not asset: if not asset:
raise HTTPException(status_code=404, detail="Jármű nem található") raise HTTPException(status_code=404, detail="Jármű nem található")
return { # Közvetlenül az objektumot adjuk vissza, a Pydantic AssetResponse
"id": asset.id, # modellje fogja formázni a kimenetet a dúsított adatokkal együtt.
"vin": asset.vin, return asset
"license_plate": asset.license_plate,
"name": asset.name,
"catalog": {
"make": asset.catalog.make,
"model": asset.catalog.model,
"type": asset.catalog.vehicle_class,
"factory_data": getattr(asset.catalog, 'factory_data', {})
}
}
# --- 2. MODUL: PÉNZÜGY (Költségek) --- # --- 2. MODUL: PÉNZÜGY (Költségek) ---
@router.get("/{asset_id}/costs", response_model=Dict[str, Any]) @router.get("/{asset_id}/costs", response_model=Dict[str, Any])
@@ -89,11 +89,7 @@ async def create_asset_cost(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
""" """Új költség rögzítése automatikus EUR konverzióval."""
Új költség rögzítése.
Automatikus: EUR konverzió, Telemetria frissítés, XP jóváírás.
"""
# Validáció: az asset_id-nak egyeznie kell a path-szal
if cost_in.asset_id != asset_id: if cost_in.asset_id != asset_id:
raise HTTPException(status_code=400, detail="Asset ID mismatch") raise HTTPException(status_code=400, detail="Asset ID mismatch")

View File

@@ -153,7 +153,7 @@ async def complete_kyc(
): ):
""" """
Step 2: KYC Aktiválás. Step 2: KYC Aktiválás.
Itt használjuk a get_current_user-t (nem active), mert a user még inaktív. It használjuk a get_current_user-t (nem active), mert a user még inaktív.
""" """
user = await AuthService.complete_kyc(db, current_user.id, kyc_in) user = await AuthService.complete_kyc(db, current_user.id, kyc_in)
if not user: if not user:

View File

@@ -1,3 +1,5 @@
import hashlib
import unicodedata
import re import re
class VINValidator: class VINValidator:
@@ -45,3 +47,30 @@ class VINValidator:
"year_code": vin[9], # Modellév kódja "year_code": vin[9], # Modellév kódja
"wmi": vin[0:3] # World Manufacturer Identifier "wmi": vin[0:3] # World Manufacturer Identifier
} }
class IdentityNormalizer:
@staticmethod
def normalize_text(text: str) -> str:
"""Tisztítja a szöveget: kisbetű, ékezetmentesítés, szóközök és jelek törlése."""
if not text:
return ""
# 1. Kisbetűre alakítás
text = text.lower().strip()
# 2. Ékezetek eltávolítása (Unicode normalizálás)
text = "".join(
c for c in unicodedata.normalize('NFD', text)
if unicodedata.category(c) != 'Mn'
)
# 3. Csak az angol ABC betűi és számok maradjanak
return re.sub(r'[^a-z0-9]', '', text)
@classmethod
def generate_person_hash(cls, last_name: str, first_name: str, mothers_name: str, birth_date: str) -> str:
"""Létrehozza az egyedi SHA256 ujjlenyomatot a személyhez."""
raw_combined = (
cls.normalize_text(last_name) +
cls.normalize_text(first_name) +
cls.normalize_text(mothers_name) +
cls.normalize_text(birth_date)
)
return hashlib.sha256(raw_combined.encode()).hexdigest()

View File

@@ -1,10 +1,11 @@
# /opt/docker/dev/service_finder/backend/app/db/base.py # /opt/docker/dev/service_finder/backend/app/db/base.py
from app.db.base_class import Base # noqa from app.db.base_class import Base # noqa
# Közvetlen importok a fájlokból (Circular Import elkerülése) # Közvetlen importok (HOZZÁADVA az audit és sales modellek)
from app.models.address import Address, GeoPostalCode, GeoStreet, GeoStreetType # noqa from app.models.address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Branch # noqa
from app.models.identity import User, Person, VerificationToken, Wallet # noqa from app.models.identity import User, Person, VerificationToken, Wallet # noqa
from app.models.organization import Organization, OrganizationMember # noqa from app.models.organization import Organization, OrganizationMember, OrganizationSalesAssignment # noqa
from app.models.audit import SecurityAuditLog, OperationalLog, FinancialLedger # noqa <--- KRITIKUS!
from app.models.asset import ( # noqa from app.models.asset import ( # noqa
Asset, AssetCatalog, AssetCost, AssetEvent, Asset, AssetCatalog, AssetCost, AssetEvent,
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
@@ -12,11 +13,11 @@ from app.models.asset import ( # noqa
from app.models.gamification import ( # noqa from app.models.gamification import ( # noqa
PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger
) )
from app.models.system_config import SystemParameter # noqa from app.models.system import SystemParameter # noqa (system.py használata)
from app.models.history import AuditLog, VehicleOwnership # noqa from app.models.history import AuditLog, VehicleOwnership # noqa
from app.models.document import Document # noqa from app.models.document import Document # noqa
from app.models.translation import Translation # noqa <--- HOZZÁADVA from app.models.translation import Translation # noqa
from app.models.core_logic import ( # noqa from app.models.core_logic import ( # noqa
SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
) )
from app.models.security import PendingAction # noqa <--- CSAK A BIZTONSÁG KEDVÉÉRT, HA EZ IS HIÁNYZOTT VOLNA from app.models.security import PendingAction # noqa

View File

@@ -1,11 +1,12 @@
# /opt/docker/dev/service_finder/backend/app/models/__init__.py # /opt/docker/dev/service_finder/backend/app/models/__init__.py
from app.db.base_class import Base from app.db.base_class import Base
# Identitás és Jogosultság # Identitás és Jogosultság
from .identity import User, Person, Wallet, UserRole, VerificationToken, SocialAccount from .identity import User, Person, Wallet, UserRole, VerificationToken, SocialAccount
# Szervezeti struktúra # Szervezeti struktúra (HOZZÁADVA: OrganizationSalesAssignment)
from .organization import Organization, OrganizationMember from .organization import Organization, OrganizationMember, OrganizationSalesAssignment
# Járművek és Eszközök (Digital Twin) # Járművek és Eszközök (Digital Twin)
from .asset import ( from .asset import (
@@ -13,24 +14,25 @@ from .asset import (
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
) )
# Szerviz és Szakértelem (ÚJ) # Szerviz és Szakértelem
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging
# Földrajzi adatok és Címek # Földrajzi adatok és Címek
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Branch
# Gamification és Economy # Gamification és Economy
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger
# Rendszerkonfiguráció és Alapok # Rendszerkonfiguráció (HASZNÁLJUK a frissített system.py-t!)
from .system_config import SystemParameter from .system import SystemParameter
from .document import Document from .document import Document
from .translation import Translation from .translation import Translation
# Üzleti logika és Előfizetés # Üzleti logika és Előfizetés
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
# Naplózás és Biztonság # Naplózás és Biztonság (HOZZÁADVA: audit.py modellek)
from .audit import SecurityAuditLog, OperationalLog, FinancialLedger # <--- KRITIKUS!
from .history import AuditLog, VehicleOwnership from .history import AuditLog, VehicleOwnership
from .security import PendingAction from .security import PendingAction
@@ -42,16 +44,15 @@ ServiceRecord = AssetEvent
__all__ = [ __all__ = [
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount", "Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
"Organization", "OrganizationMember", "Organization", "OrganizationMember", "OrganizationSalesAssignment",
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials", "Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
"AssetTelemetry", "AssetReview", "ExchangeRate", "AssetTelemetry", "AssetReview", "ExchangeRate",
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger", "Point_Rule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"SystemParameter", "Document", "Translation", "PendingAction", "SystemParameter", "Document", "Translation", "PendingAction",
"SubscriptionTier", "OrganizationSubscription", "SubscriptionTier", "OrganizationSubscription",
"CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership", "CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership",
# --- SZERVIZ MODUL (Tisztítva) --- "SecurityAuditLog", "OperationalLog", "FinancialLedger", # <--- KRITIKUS!
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging",
# --- ALIASOK ---
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord" "Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord"
] ]

View File

@@ -1,7 +1,9 @@
import uuid import uuid
from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime, Float # Hozzáadva: Boolean, text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime, Float, Boolean, text, func
from sqlalchemy.sql import func # PostgreSQL specifikus típusok
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from sqlalchemy.orm import relationship
from app.db.base_class import Base from app.db.base_class import Base
class GeoPostalCode(Base): class GeoPostalCode(Base):
@@ -46,3 +48,43 @@ class Address(Base):
longitude = Column(Float) longitude = Column(Float)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
# Add to /app/models/address.py
class Branch(Base):
"""
Telephely entitás. A fizikai helyszín, ahol a szolgáltatás vagy flotta-kezelés zajlik.
Minden cégnek van legalább egy 'Main' telephelye.
"""
__tablename__ = "branches"
__table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
name = Column(String(100), nullable=False) # pl. "Központi iroda", "Dunakeszi Szerviz"
is_main = Column(Boolean, default=False)
# Részletes címadatok (Denormalizált a gyors kereséshez)
postal_code = Column(String(10), index=True)
city = Column(String(100), index=True)
street_name = Column(String(150))
street_type = Column(String(50))
house_number = Column(String(20))
stairwell = Column(String(20))
floor = Column(String(20))
door = Column(String(20))
hrsz = Column(String(50)) # Helyrajzi szám
# Telephely specifikus adatok
opening_hours = Column(JSONB, server_default=text("'{}'::jsonb"))
branch_rating = Column(Float, default=0.0)
status = Column(String(30), default="active")
is_deleted = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
organization = relationship("Organization", back_populates="branches")
address = relationship("Address")
# Kapcsolat a szerviz értékelésekkel
reviews = relationship("Rating", primaryjoin="and_(Branch.id==foreign(Rating.target_id), Rating.target_type=='branch')")

View File

@@ -24,6 +24,16 @@ class AssetCatalog(Base):
year_to = Column(Integer) year_to = Column(Integer)
vehicle_class = Column(String) vehicle_class = Column(String)
fuel_type = Column(String, index=True) fuel_type = Column(String, index=True)
# --- ÚJ OSZLOPOK (Ezeket add hozzá!) ---
power_kw = Column(Integer, index=True)
engine_capacity = Column(Integer, index=True)
max_weight_kg = Column(Integer)
axle_count = Column(Integer)
euro_class = Column(String(20))
body_type = Column(String(100))
# ---------------------------------------
engine_code = Column(String) engine_code = Column(String)
factory_data = Column(JSONB, server_default=text("'{}'::jsonb")) factory_data = Column(JSONB, server_default=text("'{}'::jsonb"))
@@ -101,11 +111,17 @@ class AssetAssignment(Base):
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False) asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False) organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
# ÚJ: Telephelyi hozzárendelés
branch_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"), nullable=True)
assigned_at = Column(DateTime(timezone=True), server_default=func.now()) assigned_at = Column(DateTime(timezone=True), server_default=func.now())
released_at = Column(DateTime(timezone=True), nullable=True) released_at = Column(DateTime(timezone=True), nullable=True)
status = Column(String(30), default="active") status = Column(String(30), default="active")
asset = relationship("Asset", back_populates="assignments") asset = relationship("Asset", back_populates="assignments")
organization = relationship("Organization") organization = relationship("Organization")
branch = relationship("Branch") # Új kapcsolat
class AssetEvent(Base): class AssetEvent(Base):
__tablename__ = "asset_events" __tablename__ = "asset_events"
@@ -145,3 +161,26 @@ class ExchangeRate(Base):
base_currency = Column(String(3), default="EUR") base_currency = Column(String(3), default="EUR")
target_currency = Column(String(3), unique=True) target_currency = Column(String(3), unique=True)
rate = Column(Numeric(18, 6), nullable=False) rate = Column(Numeric(18, 6), nullable=False)
class CatalogDiscovery(Base):
"""
Discovery tábla: Ide gyűjtjük a piaci 'neveket' (pl. Citroen C3).
A Robot innen indulva keresi meg az összes létező technikai variánst.
"""
__tablename__ = "catalog_discovery"
id = Column(Integer, primary_key=True, index=True)
make = Column(String(100), nullable=False, index=True)
model = Column(String(100), nullable=False, index=True)
vehicle_class = Column(String(50), index=True) # car, motorcycle, truck, stb.
source = Column(String(50)) # 'hasznaltauto', 'mobile.de'
status = Column(String(20), server_default=text("'pending'"), index=True)
attempts = Column(Integer, default=0)
last_attempt = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
# EGYESÍTETT __table_args__
__table_args__ = (
UniqueConstraint('make', 'model', 'vehicle_class', name='_make_model_class_uc'),
{"schema": "data"}
)

View File

@@ -1,16 +1,56 @@
from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey, text from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey, text, Numeric, Boolean, BigInteger
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.db.base_class import Base from app.db.base_class import Base
class AuditLog(Base): class SecurityAuditLog(Base):
__tablename__ = "audit_logs" """ Kiemelt biztonsági események és a 4-szem elv. """
__table_args__ = {"schema": "data"} __tablename__ = "security_audit_logs"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True)
action = Column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST', 'SUB_EXTEND'
actor_id = Column(Integer, ForeignKey("data.users.id")) # Aki kezdeményezte
target_id = Column(Integer, ForeignKey("data.users.id")) # Akivel történt
# 4-szem elv: csak akkor válik élessé, ha ez nem NULL
confirmed_by_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
is_critical = Column(Boolean, default=False) # Szuperadmin hívásoknál True
payload_before = Column(JSON)
payload_after = Column(JSON)
created_at = Column(DateTime(timezone=True), server_default=func.now())
class OperationalLog(Base):
""" Napi üzemi események (Operational). """
__tablename__ = "operational_logs"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True, index=True) # <--- EZ HIÁNYZOTT!
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="SET NULL"), nullable=True) user_id = Column(Integer, ForeignKey("data.users.id", ondelete="SET NULL"), nullable=True)
action = Column(String(100), nullable=False) # pl. "LOGIN", "REGISTER", "DELETE_ASSET" action = Column(String(100), nullable=False) # pl. "ADD_VEHICLE", "UPDATE_COST"
resource_type = Column(String(50)) # pl. "User", "Asset", "Organization" resource_type = Column(String(50)) # pl. "Asset", "Expense"
resource_id = Column(String(100)) resource_id = Column(String(100))
details = Column(JSON, server_default=text("'{}'::jsonb")) details = Column(JSON, server_default=text("'{}'::jsonb"))
ip_address = Column(String(45)) ip_address = Column(String(45))
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
class FinancialLedger(Base):
""" Minden pénz- és kreditmozgás központi naplója. """
__tablename__ = "financial_ledger"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("data.users.id"))
person_id = Column(BigInteger, ForeignKey("data.persons.id"))
amount = Column(Numeric(18, 4), nullable=False)
currency = Column(String(10)) # 'HUF', 'CREDIT', 'COIN'
transaction_type = Column(String(50)) # 'PURCHASE', 'HUNTING_COMMISSION', 'FARMING_COMMISSION'
# Üzletkötői követhetőség
related_agent_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
details = Column(JSON, server_default=text("'{}'::jsonb"))
created_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@@ -9,29 +9,34 @@ from app.db.base_class import Base
class UserRole(str, enum.Enum): class UserRole(str, enum.Enum):
superadmin = "superadmin" superadmin = "superadmin"
admin = "admin" admin = "admin"
region_admin = "region_admin"
country_admin = "country_admin"
moderator = "moderator"
sales_agent = "sales_agent"
user = "user" user = "user"
service = "service" service_owner = "service_owner"
fleet_manager = "fleet_manager" fleet_manager = "fleet_manager"
driver = "driver" driver = "driver"
class Person(Base): class Person(Base):
""" """
Természetes személy identitása. Természetes személy identitása. A DNS szint.
A bot által talált személyek is ide kerülnek (is_ghost=True). Itt tároljuk az örök adatokat, amik nem vesznek el account törléskor.
Azonosítás: Név + Anyja neve + Születési adatok alapján.
""" """
__tablename__ = "persons" __tablename__ = "persons"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data", "extend_existing": True}
id = Column(BigInteger, primary_key=True, index=True) id = Column(BigInteger, primary_key=True, index=True)
id_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False) id_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True) address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
# --- KRITIKUS: EGYEDI AZONOSÍTÓ HASH (Normalizált adatokból) ---
identity_hash = Column(String(64), unique=True, index=True, nullable=True)
last_name = Column(String, nullable=False) last_name = Column(String, nullable=False)
first_name = Column(String, nullable=False) first_name = Column(String, nullable=False)
phone = Column(String, nullable=True) phone = Column(String, nullable=True)
# --- TERMÉSZETES AZONOSÍTÓK (Azonosításhoz, nem publikus) ---
mothers_last_name = Column(String) mothers_last_name = Column(String)
mothers_first_name = Column(String) mothers_first_name = Column(String)
birth_place = Column(String) birth_place = Column(String)
@@ -40,8 +45,14 @@ class Person(Base):
identity_docs = Column(JSON, server_default=text("'{}'::jsonb")) identity_docs = Column(JSON, server_default=text("'{}'::jsonb"))
ice_contact = Column(JSON, server_default=text("'{}'::jsonb")) ice_contact = Column(JSON, server_default=text("'{}'::jsonb"))
is_active = Column(Boolean, default=False, nullable=False) # --- ÖRÖK ADATOK (Person szint) ---
is_ghost = Column(Boolean, default=True, nullable=False) # Bot találta = True, Regisztrált = False lifetime_xp = Column(BigInteger, server_default=text("0"))
penalty_points = Column(Integer, server_default=text("0")) # 0-3 szint
social_reputation = Column(Numeric(3, 2), server_default=text("1.00")) # 1.00 = 100%
is_sales_agent = Column(Boolean, server_default=text("false"))
is_active = Column(Boolean, default=True, nullable=False)
is_ghost = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now())
@@ -50,28 +61,39 @@ class Person(Base):
memberships = relationship("OrganizationMember", back_populates="person") memberships = relationship("OrganizationMember", back_populates="person")
class User(Base): class User(Base):
"""
Login entitás. Bármikor törölhető (GDPR), de Person-höz kötött.
"""
__tablename__ = "users" __tablename__ = "users"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False) email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=True) hashed_password = Column(String, nullable=True)
role = Column(Enum(UserRole), default=UserRole.user) role = Column(Enum(UserRole), default=UserRole.user)
is_active = Column(Boolean, default=False)
is_deleted = Column(Boolean, default=False)
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True) person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
folder_slug = Column(String(12), unique=True, index=True)
refresh_token_hash = Column(String(255), nullable=True) # --- ELŐFIZETÉS ÉS VIP (Időkorlátos logika) ---
two_factor_secret = Column(String(100), nullable=True) subscription_plan = Column(String(30), server_default=text("'FREE'"))
two_factor_enabled = Column(Boolean, default=False) subscription_expires_at = Column(DateTime(timezone=True), nullable=True)
is_vip = Column(Boolean, server_default=text("false"))
# --- REFERRAL ÉS SALES (Üzletkötői hálózat) ---
referral_code = Column(String(20), unique=True)
referred_by_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
# Farming üzletkötő (Átruházható cégkezelő)
current_sales_agent_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
is_active = Column(Boolean, default=False)
is_deleted = Column(Boolean, default=False)
folder_slug = Column(String(12), unique=True, index=True)
preferred_language = Column(String(5), server_default="hu") preferred_language = Column(String(5), server_default="hu")
region_code = Column(String(5), server_default="HU") region_code = Column(String(5), server_default="HU")
preferred_currency = Column(String(3), server_default="HUF") preferred_currency = Column(String(3), server_default="HUF")
scope_level = Column(String(30), server_default="individual") scope_level = Column(String(30), server_default="individual") # global, region, country, entity, individual
scope_id = Column(String(50)) scope_id = Column(String(50))
custom_permissions = Column(JSON, server_default=text("'{}'::jsonb")) custom_permissions = Column(JSON, server_default=text("'{}'::jsonb"))
@@ -79,18 +101,25 @@ class User(Base):
person = relationship("Person", back_populates="users") person = relationship("Person", back_populates="users")
wallet = relationship("Wallet", back_populates="user", uselist=False) wallet = relationship("Wallet", back_populates="user", uselist=False)
stats = relationship("UserStats", back_populates="user", uselist=False)
ownership_history = relationship("VehicleOwnership", back_populates="user")
owned_organizations = relationship("Organization", back_populates="owner")
social_accounts = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan") social_accounts = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan")
class Wallet(Base): class Wallet(Base):
__tablename__ = "wallets"; __table_args__ = {"schema": "data"} """ A 3-as felosztású pénztárca. """
__tablename__ = "wallets"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id"), unique=True) user_id = Column(Integer, ForeignKey("data.users.id"), unique=True)
coin_balance = Column(Numeric(18, 2), default=0.00); credit_balance = Column(Numeric(18, 2), default=0.00); currency = Column(String(3), default="HUF")
earned_credits = Column(Numeric(18, 4), server_default=text("0")) # Munka + Referral
purchased_credits = Column(Numeric(18, 4), server_default=text("0")) # Vásárolt
service_coins = Column(Numeric(18, 4), server_default=text("0")) # Csak hirdetésre!
currency = Column(String(3), default="HUF")
user = relationship("User", back_populates="wallet") user = relationship("User", back_populates="wallet")
# ... (VerificationToken és SocialAccount változatlan) ...
class VerificationToken(Base): class VerificationToken(Base):
__tablename__ = "verification_tokens"; __table_args__ = {"schema": "data"} __tablename__ = "verification_tokens"; __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)

View File

@@ -61,6 +61,11 @@ class Organization(Base):
status = Column(String(30), default="pending_verification") status = Column(String(30), default="pending_verification")
is_deleted = Column(Boolean, default=False) is_deleted = Column(Boolean, default=False)
# --- ÚJ: Előfizetés és Méret korlátok ---
subscription_plan = Column(String(30), server_default=text("'FREE'"), index=True)
base_asset_limit = Column(Integer, server_default=text("1"))
purchased_extra_slots = Column(Integer, server_default=text("0"))
notification_settings = Column(JSON, server_default=text("'{\"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1]}'::jsonb")) notification_settings = Column(JSON, server_default=text("'{\"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1]}'::jsonb"))
external_integration_config = Column(JSON, server_default=text("'{}'::jsonb")) external_integration_config = Column(JSON, server_default=text("'{}'::jsonb"))
@@ -71,12 +76,17 @@ class Organization(Base):
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# --- ÚJ: Dual Twin Tulajdonjog logika ---
# Individual esetén False, Business esetén True
is_ownership_transferable = Column(Boolean, server_default=text("true"))
# Kapcsolatok # Kapcsolatok
assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan") assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan")
members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan") members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan")
owner = relationship("User", back_populates="owned_organizations") owner = relationship("User", back_populates="owned_organizations")
financials = relationship("OrganizationFinancials", back_populates="organization", cascade="all, delete-orphan") financials = relationship("OrganizationFinancials", back_populates="organization", cascade="all, delete-orphan")
service_profile = relationship("ServiceProfile", back_populates="organization", uselist=False) service_profile = relationship("ServiceProfile", back_populates="organization", uselist=False)
branches = relationship("Branch", back_populates="organization", cascade="all, delete-orphan")
class OrganizationFinancials(Base): class OrganizationFinancials(Base):
"""Cégek éves gazdasági adatai elemzéshez.""" """Cégek éves gazdasági adatai elemzéshez."""
@@ -112,3 +122,14 @@ class OrganizationMember(Base):
organization = relationship("Organization", back_populates="members") organization = relationship("Organization", back_populates="members")
user = relationship("User") user = relationship("User")
person = relationship("Person", back_populates="memberships") person = relationship("Person", back_populates="memberships")
class OrganizationSalesAssignment(Base):
"""Összeköti a céget az aktuális üzletkötővel a jutalék miatt."""
__tablename__ = "org_sales_assignments"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
organization_id = Column(Integer, ForeignKey("data.organizations.id"))
agent_user_id = Column(Integer, ForeignKey("data.users.id")) # Ő kapja a Farming díjat
assigned_at = Column(DateTime(timezone=True), server_default=func.now())
is_active = Column(Boolean, default=True)

View File

@@ -86,12 +86,17 @@ class ServiceStaging(Base):
name = Column(String, nullable=False, index=True) name = Column(String, nullable=False, index=True)
# --- Strukturált cím adatok (A kérésedre bontva) --- # --- Strukturált cím adatok (A kérésedre bontva) ---
postal_code = Column(String(10), nullable=True, index=True) # Irányítószám postal_code = Column(String(10), index=True)
city = Column(String(100), nullable=True, index=True) # Település city = Column(String(100), index=True)
street = Column(String(255), nullable=True) # Utca és közterület jellege (pl. Diófa utca) street_name = Column(String(150))
house_number = Column(String(50), nullable=True) # Házszám, emelet, ajtó street_type = Column(String(50)) # utca, út, tér...
full_address = Column(String, nullable=True) # Az eredeti, egybefüggő cím (ha van) house_number = Column(String(20))
stairwell = Column(String(20)) # lépcsőház
floor = Column(String(20)) # emelet
door = Column(String(20)) # ajtó
hrsz = Column(String(50)) # helyrajzi szám
full_address = Column(String) # Eredeti string (audit célból)
# --- Elérhetőségek --- # --- Elérhetőségek ---
contact_phone = Column(String, nullable=True) contact_phone = Column(String, nullable=True)
email = Column(String, nullable=True) email = Column(String, nullable=True)
@@ -112,3 +117,13 @@ class ServiceStaging(Base):
trust_score = Column(Integer, default=0) trust_score = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
class DiscoveryParameter(Base):
__tablename__ = "discovery_parameters"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
city = Column(String(100), nullable=False)
keyword = Column(String(100), nullable=False) # pl. "autóvillamosság"
country_code = Column(String(2), default="HU")
is_active = Column(Boolean, default=True)
last_run_at = Column(DateTime(timezone=True))

View File

@@ -7,7 +7,11 @@ class SystemParameter(Base):
__table_args__ = {"schema": "data", "extend_existing": True} __table_args__ = {"schema": "data", "extend_existing": True}
key = Column(String, primary_key=True, index=True, nullable=False) key = Column(String, primary_key=True, index=True, nullable=False)
# Csoportosítás az Admin felületnek (pl. 'xp', 'scout', 'routing')
category = Column(String, index=True, server_default="general")
value = Column(JSON, nullable=False) value = Column(JSON, nullable=False)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
description = Column(String) description = Column(String)
# Kötelező audit mező: ki módosította utoljára?
last_modified_by = Column(String, nullable=True)
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())

View File

@@ -5,6 +5,7 @@ from datetime import datetime
# --- KATALÓGUS SÉMÁK (Gyári adatok) --- # --- KATALÓGUS SÉMÁK (Gyári adatok) ---
class AssetCatalogBase(BaseModel): class AssetCatalogBase(BaseModel):
"""Alap katalógus adatok, amik a technikai dúsításból származnak."""
make: str make: str
model: str model: str
generation: Optional[str] = None generation: Optional[str] = None
@@ -14,38 +15,56 @@ class AssetCatalogBase(BaseModel):
fuel_type: Optional[str] = None fuel_type: Optional[str] = None
engine_code: Optional[str] = None engine_code: Optional[str] = None
class AssetCatalogResponse(AssetCatalogBase): # --- ÚJ TECHNIKAI MEZŐK (Robot v1.0.8 Smart Hunter adatai) ---
id: int power_kw: Optional[int] = None
factory_data: Optional[Dict[str, Any]] = None # A robot által gyűjtött adatok engine_capacity: Optional[int] = None
max_weight_kg: Optional[int] = None
axle_count: Optional[int] = None
body_type: Optional[str] = None
class AssetCatalogResponse(AssetCatalogBase):
"""Katalógus válasz séma azonosítóval és extra gyári adatokkal."""
id: int
factory_data: Optional[Dict[str, Any]] = None
# Pydantic v2 konfiguráció az ORM (SQLAlchemy) támogatáshoz
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
# --- JÁRMŰ SÉMÁK (Asset) --- # --- JÁRMŰ SÉMÁK (Asset) ---
class AssetBase(BaseModel): class AssetBase(BaseModel):
"""Jármű példány alapadatai (egyedi azonosítók)."""
vin: str = Field(..., min_length=17, max_length=17) vin: str = Field(..., min_length=17, max_length=17)
license_plate: str license_plate: str
name: Optional[str] = None name: Optional[str] = None
year_of_manufacture: Optional[int] = None year_of_manufacture: Optional[int] = None
class AssetCreate(AssetBase): class AssetCreate(AssetBase):
# A létrehozáshoz kellenek a katalógus infók is """Séma új jármű felvételéhez."""
make: str make: str
model: str model: str
vehicle_class: Optional[str] = "land" vehicle_class: Optional[str] = "car"
fuel_type: Optional[str] = None fuel_type: Optional[str] = None
current_reading: Optional[int] = 0 current_reading: Optional[int] = 0
class AssetResponse(AssetBase): class AssetResponse(AssetBase):
"""
Teljes jármű válasz séma.
Ez a séma tartalmazza a 'catalog' objektumot, amiben a dúsított műszaki adatok vannak.
"""
id: UUID id: UUID
catalog_id: int catalog_id: int
is_verified: bool catalog: AssetCatalogResponse # Ez a pont kapcsolja össze a dúsított technikai adatokat
status: str status: str
is_verified: bool
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
# --- DIGITÁLIS IKER (Full Profile) --- # --- DIGITÁLIS IKER (Full Profile) ---
# Ez a séma felel a 9 pontos költség és a mélységi szerviz adatok átadásáért
class AssetFullProfile(BaseModel): class AssetFullProfile(BaseModel):
"""
Komplex jelentésekhez használt séma.
Összefogja az identitást, telemetriát, pénzügyeket és szerviztörténetet.
"""
identity: Dict[str, Any] identity: Dict[str, Any]
telemetry: Dict[str, Any] telemetry: Dict[str, Any]
financial_summary: Dict[str, Any] financial_summary: Dict[str, Any]

View File

@@ -32,12 +32,17 @@ class UserKYCComplete(BaseModel):
birth_date: date birth_date: date
mothers_last_name: str mothers_last_name: str
mothers_first_name: str mothers_first_name: str
# Bontott címmezők (B pont szerint)
address_zip: str address_zip: str
address_city: str address_city: str
address_street_name: str address_street_name: str
address_street_type: str address_street_type: str
address_house_number: str address_house_number: str
address_hrsz: Optional[str] = None address_stairwell: Optional[str] = None # Lépcsőház
address_floor: Optional[str] = None # Emelet
address_door: Optional[str] = None # Ajtó
address_hrsz: Optional[str] = None # Helyrajzi szám
identity_docs: Dict[str, DocumentDetail] identity_docs: Dict[str, DocumentDetail]
ice_contact: ICEContact ice_contact: ICEContact
preferred_currency: Optional[str] = Field("HUF", max_length=3) preferred_currency: Optional[str] = Field("HUF", max_length=3)

View File

@@ -0,0 +1,20 @@
from pydantic import BaseModel
from typing import Optional
class ServiceCreateInternal(BaseModel):
name: str
postal_code: str
city: str
street_name: str
street_type: str
house_number: str
stairwell: Optional[str] = None
floor: Optional[str] = None
door: Optional[str] = None
hrsz: Optional[str] = None
contact_phone: Optional[str] = None
email: Optional[str] = None
website: Optional[str] = None
source: str
external_id: Optional[str] = None

View File

@@ -112,25 +112,23 @@ class AuthService:
async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete): async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete):
""" """
Step 2: Atomi Tranzakció. Step 2: Atomi Tranzakció.
Itt dől el minden: Adatok rögzítése, Shadow Identity ellenőrzés, Módosított verzió: Meglévő biztonsági logika + Telephely (Branch) integráció.
Flotta és Wallet létrehozás, majd a fiók aktiválása.
""" """
try: try:
# 1. User és Person betöltése
stmt = select(User).options(joinedload(User.person)).where(User.id == user_id) stmt = select(User).options(joinedload(User.person)).where(User.id == user_id)
res = await db.execute(stmt) res = await db.execute(stmt)
user = res.scalar_one_or_none() user = res.scalar_one_or_none()
if not user: return None if not user: return None
# --- 1. BIZTONSÁG: User folder_slug generálása --- # --- BIZTONSÁG: Slug generálása ---
# Ha Google-lel jött vagy még nincs slugja, most kap egyet.
if not user.folder_slug: if not user.folder_slug:
user.folder_slug = generate_secure_slug(length=12) user.folder_slug = generate_secure_slug(length=12)
# Pénznem beállítása
if hasattr(kyc_in, 'preferred_currency') and kyc_in.preferred_currency: if hasattr(kyc_in, 'preferred_currency') and kyc_in.preferred_currency:
user.preferred_currency = kyc_in.preferred_currency user.preferred_currency = kyc_in.preferred_currency
# --- 2. Shadow Identity keresése (Már létezik-e ez a fizikai személy?) --- # --- SHADOW IDENTITY ELLENŐRZÉS ---
identity_stmt = select(Person).where(and_( identity_stmt = select(Person).where(and_(
Person.mothers_last_name == kyc_in.mothers_last_name, Person.mothers_last_name == kyc_in.mothers_last_name,
Person.mothers_first_name == kyc_in.mothers_first_name, Person.mothers_first_name == kyc_in.mothers_first_name,
@@ -140,15 +138,12 @@ class AuthService:
existing_person = (await db.execute(identity_stmt)).scalar_one_or_none() existing_person = (await db.execute(identity_stmt)).scalar_one_or_none()
if existing_person: if existing_person:
# Ha találtunk egyezést, összekötjük a User-t a meglévő Person-nel
user.person_id = existing_person.id user.person_id = existing_person.id
active_person = existing_person active_person = existing_person
logger.info(f"Shadow Identity linked: User {user_id} -> Person {existing_person.id}")
else: else:
# Ha nem, a saját (regisztrációkor létrehozott) Person-t töltjük fel
active_person = user.person active_person = user.person
# --- 3. Cím rögzítése GeoService segítségével --- # --- CÍM RÖGZÍTÉSE ---
addr_id = await GeoService.get_or_create_full_address( addr_id = await GeoService.get_or_create_full_address(
db, db,
zip_code=kyc_in.address_zip, zip_code=kyc_in.address_zip,
@@ -159,30 +154,26 @@ class AuthService:
parcel_id=kyc_in.address_hrsz parcel_id=kyc_in.address_hrsz
) )
# --- 4. Személyes adatok frissítése --- # --- SZEMÉLYES ADATOK FRISSÍTÉSE ---
active_person.mothers_last_name = kyc_in.mothers_last_name active_person.mothers_last_name = kyc_in.mothers_last_name
active_person.mothers_first_name = kyc_in.mothers_first_name active_person.mothers_first_name = kyc_in.mothers_first_name
active_person.birth_place = kyc_in.birth_place active_person.birth_place = kyc_in.birth_place
active_person.birth_date = kyc_in.birth_date active_person.birth_date = kyc_in.birth_date
active_person.phone = kyc_in.phone_number active_person.phone = kyc_in.phone_number
active_person.address_id = addr_id active_person.address_id = addr_id
# Dokumentumok és ICE kontakt mentése JSON-ként
active_person.identity_docs = jsonable_encoder(kyc_in.identity_docs) active_person.identity_docs = jsonable_encoder(kyc_in.identity_docs)
active_person.ice_contact = jsonable_encoder(kyc_in.ice_contact) active_person.ice_contact = jsonable_encoder(kyc_in.ice_contact)
# A Person most válik aktívvá
active_person.is_active = True active_person.is_active = True
# --- 5. EGYÉNI FLOTTA LÉTREHOZÁSA (A KYC szerves része) --- # --- EGYÉNI FLOTTA LÉTREHOZÁSA ---
# Itt generáljuk a flotta mappáját is (folder_slug)
new_org = Organization( new_org = Organization(
full_name=f"{active_person.last_name} {active_person.first_name} Egyéni Flotta", full_name=f"{active_person.last_name} {active_person.first_name} Egyéni Flotta",
name=f"{active_person.last_name} Flotta", name=f"{active_person.last_name} Flotta",
folder_slug=generate_secure_slug(length=12), # FLOTTA SLUG folder_slug=generate_secure_slug(length=12),
org_type=OrgType.individual, org_type=OrgType.individual,
owner_id=user.id, owner_id=user.id,
is_transferable=False, is_transferable=False, # Step 2: Individual flotta nem átruházható
is_ownership_transferable=False, # A te új meződ
is_active=True, is_active=True,
status="verified", status="verified",
language=user.preferred_language, language=user.preferred_language,
@@ -192,24 +183,36 @@ class AuthService:
db.add(new_org) db.add(new_org)
await db.flush() await db.flush()
# Flotta tagság (Owner) # --- ÚJ: MAIN BRANCH (KÖZPONTI TELEPHELY) LÉTREHOZÁSA ---
# Magánszemélynél a megadott cím lesz az első telephely is.
from app.models.address import Branch
new_branch = Branch(
organization_id=new_org.id,
address_id=addr_id,
name="Központ / Otthon",
is_main=True,
postal_code=kyc_in.address_zip,
city=kyc_in.address_city,
street_name=kyc_in.address_street_name,
street_type=kyc_in.address_street_type,
house_number=kyc_in.address_house_number,
hrsz=kyc_in.address_hrsz,
status="active"
)
db.add(new_branch)
await db.flush()
# --- TAGSÁG, WALLET, STATS ---
db.add(OrganizationMember( db.add(OrganizationMember(
organization_id=new_org.id, organization_id=new_org.id,
user_id=user.id, user_id=user.id,
role="owner", role="owner",
permissions={"can_add_asset": True, "can_view_costs": True, "is_admin": True} permissions={"can_add_asset": True, "can_view_costs": True, "is_admin": True}
)) ))
db.add(Wallet(user_id=user.id, currency=user.preferred_currency or "HUF"))
# --- 6. PÉNZTÁRCA ÉS GAMIFICATION LÉTREHOZÁSA ---
db.add(Wallet(
user_id=user.id,
coin_balance=0,
credit_balance=0,
currency=user.preferred_currency or "HUF"
))
db.add(UserStats(user_id=user.id, total_xp=0, current_level=1)) db.add(UserStats(user_id=user.id, total_xp=0, current_level=1))
# --- 7. AKTIVÁLÁS ÉS AUDIT --- # --- 7. AKTIVÁLÁS ÉS AUDIT (Ami az előzőből kimaradt) ---
user.is_active = True user.is_active = True
await security_service.log_event( await security_service.log_event(
@@ -223,7 +226,7 @@ class AuthService:
"status": "active", "status": "active",
"user_folder": user.folder_slug, "user_folder": user.folder_slug,
"organization_id": new_org.id, "organization_id": new_org.id,
"organization_folder": new_org.folder_slug, "branch_id": str(new_branch.id), # Új telephely az auditban
"wallet_created": True "wallet_created": True
} }
) )
@@ -231,6 +234,7 @@ class AuthService:
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
return user return user
except Exception as e: except Exception as e:
await db.rollback() await db.rollback()
logger.error(f"KYC Atomi Tranzakció Hiba: {str(e)}") logger.error(f"KYC Atomi Tranzakció Hiba: {str(e)}")

View File

@@ -45,22 +45,42 @@ class GeoService:
"""), {"n": street_type.lower()}) """), {"n": street_type.lower()})
# 4. Központi Address rekord rögzítése # 4. Központi Address rekord rögzítése
full_text = f"{zip_code} {city}, {street_name} {street_type} {house_number}" full_text = f"{zip_code} {city}, {street_name} {street_type} {house_number}."
addr_res = await db.execute(text(""" if stairwell: full_text += f" {stairwell}. lph,"
INSERT INTO data.addresses (postal_code_id, street_name, street_type, house_number, parcel_id, full_address_text) if floor: full_text += f" {floor}. em,"
VALUES (:zid, :sn, :st, :hn, :pid, :txt) if door: full_text += f" {door}. ajtó"
query = text("""
INSERT INTO data.addresses (
postal_code_id, street_name, street_type, house_number,
stairwell, floor, door, parcel_id, full_address_text
)
VALUES (
(SELECT id FROM data.geo_postal_codes WHERE zip_code = :z AND city = :c LIMIT 1),
:sn, :st, :hn, :sw, :fl, :dr, :pid, :txt
)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
RETURNING id RETURNING id
"""), { """)
"zid": zip_id, "sn": street_name, "st": street_type, "hn": house_number, "pid": parcel_id, "txt": full_text
}) params = {
addr_id = addr_res.scalar() "z": zip_code, "c": city, "sn": street_name, "st": street_type,
"hn": house_number, "sw": stairwell, "fl": floor, "dr": door,
"pid": parcel_id, "txt": full_text
}
res = await db.execute(query, params)
addr_id = res.scalar()
if not addr_id: if not addr_id:
# Ha már létezett, lekérjük az azonosítót # Ha már létezett ilyen részletes cím, lekérjük
addr_id = (await db.execute(text(""" addr_id = (await db.execute(text("""
SELECT id FROM data.addresses SELECT id FROM data.addresses
WHERE postal_code_id = :zid AND street_name = :sn AND street_type = :st AND house_number = :hn WHERE street_name = :sn AND house_number = :hn
"""), {"zid": zip_id, "sn": street_name, "st": street_type, "hn": house_number})).scalar() AND (stairwell IS NOT DISTINCT FROM :sw)
AND (floor IS NOT DISTINCT FROM :fl)
AND (door IS NOT DISTINCT FROM :dr)
LIMIT 1
"""), params)).scalar()
return addr_id return addr_id

View File

@@ -0,0 +1,61 @@
import asyncio
import httpx
import logging
from sqlalchemy import text
from app.db.session import SessionLocal
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("Smart-Seeder-v1.0.2")
async def seed_with_priority():
# RDW lekérdezés: Márka, Fő kategória és darabszám
# Olyan márkákat keresünk, amikből legalább 10 db van
URL = "https://opendata.rdw.nl/resource/m9d7-ebf2.json?$select=merk,voertuigsoort,count(*)%20as%20total&$group=merk,voertuigsoort&$having=total%20>=%2010"
logger.info("📥 Adatok lekérése az RDW-től prioritásos besoroláshoz...")
async with httpx.AsyncClient(timeout=120) as client:
try:
resp = await client.get(URL)
if resp.status_code != 200:
logger.error(f"❌ API hiba: {resp.status_code}")
return
raw_data = resp.json()
async with SessionLocal() as db:
for entry in raw_data:
make = entry.get("merk", "").upper()
v_kind = entry.get("voertuigsoort", "")
# --- PRIORITÁS LOGIKA ---
# 1. Személyautó (Personenauto) -> 'pending' (Azonnali feldolgozás)
# 2. Motor (Motorfiets) -> 'queued_motor'
# 3. Minden más -> 'queued_heavy'
status = 'queued_heavy'
if "Personenauto" in v_kind:
status = 'pending'
elif "Motorfiets" in v_kind:
status = 'queued_motor'
query = text("""
INSERT INTO data.catalog_discovery (make, model, vehicle_class, source, status)
VALUES (:make, 'ALL_VARIANTS', :v_class, 'smart_seeder_v2_1', :status)
ON CONFLICT (make, model, vehicle_class) DO UPDATE
SET status = EXCLUDED.status WHERE data.catalog_discovery.status = 'pending';
""")
await db.execute(query, {
"make": make,
"v_class": v_kind,
"status": status
})
await db.commit()
logger.info("✅ A Discovery lista feltöltve és prioritizálva (Autók az élen)!")
except Exception as e:
logger.error(f"❌ Hiba: {e}")
if __name__ == "__main__":
asyncio.run(seed_with_priority())

View File

@@ -2,178 +2,207 @@ import asyncio
import httpx import httpx
import logging import logging
import json import json
import re
import os import os
import datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import text
from sqlalchemy import select, func, or_, text
from app.db.session import SessionLocal from app.db.session import SessionLocal
from app.models.asset import AssetCatalog from app.models.asset import AssetCatalog
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("Robot1-Ghost-Commander-v1.1.9") logger = logging.getLogger("Robot-v1.0.13-Global-Hunter")
class CatalogScout: class CatalogMaster:
""" """
Robot 1.1.9: Environment Master. Master Hunter Robot v1.0.13 - Global Hunter Edition
- .env alapú hitelesítés (RDW App Token) - Holland (RDW), Brit (DVLA) és Amerikai (NHTSA) adatbázis integráció.
- Prioritás: RDW (EU) -> NHTSA (US) -> CarQuery (Ban-figyeléssel) - Ratio-Filter: Kiszűri a 0.19-es kW/kg arányszámokat.
- 2.5s lekérési frissítés a biztonságért - Multi-field Power Discovery: Minden lehetséges mezőből kinyeri a kW-ot.
- Dinamikus évjárat kezelés a duplikációk ellen.
""" """
CQ_URL = "https://www.carqueryapi.com/api/0.3/" # API Végpontok
NHTSA_BASE = "https://vpic.nhtsa.dot.gov/api/vehicles/GetModelsForMakeYear/make/" RDW_MAIN = "https://opendata.rdw.nl/resource/m9d7-ebf2.json"
RDW_URL = "https://opendata.rdw.nl/resource/ed7h-m8uz.json" RDW_FUEL = "https://opendata.rdw.nl/resource/8ys7-d773.json"
RDW_AXLE = "https://opendata.rdw.nl/resource/3huj-srit.json"
RDW_BODY = "https://opendata.rdw.nl/resource/vezc-m2t6.json"
UK_DVLA = "https://driver-vehicle-licensing.api.gov.uk/vehicle-enquiry/v1/vehicles"
US_NHTSA = "https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVinValuesBatch/"
# Adatok beolvasása környezeti változókból
RDW_TOKEN = os.getenv("RDW_APP_TOKEN") RDW_TOKEN = os.getenv("RDW_APP_TOKEN")
UK_API_KEY = os.getenv("UK_DVLA_API_KEY")
HEADERS = { HEADERS_RDW = {"X-App-Token": RDW_TOKEN} if RDW_TOKEN else {}
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", HEADERS_UK = {"x-api-key": UK_API_KEY, "Content-Type": "application/json"} if UK_API_KEY else {}
"Accept": "application/json",
"X-App-Token": RDW_TOKEN CATEGORY_MAP = {
"Personenauto": "car",
"Motorfiets": "motorcycle",
"Bedrijfsauto": "truck",
"Vrachtwagen": "truck",
"Opleggertrekker": "truck",
"Bus": "bus",
"Aanhangwagen": "trailer",
"Oplegger": "trailer",
"Landbouw- of bosbouwtrekker": "agricultural",
"camper": "camper"
} }
# BAN FIGYELŐ ÁLLAPOT @classmethod
cq_banned_until = None def clean_kw(cls, val):
"""Speciális kW tisztító: ignorálja az 1.0 alatti arányszámokat."""
# --- KATEGÓRIA DEFINÍCIÓK (Szigorúan az eredeti lista szerint) --- try:
MOTO_MAKES = ['ducati', 'ktm', 'triumph', 'aprilia', 'benelli', 'vespa', 'simson', 'mz', 'etz', 'jawa', 'husqvarna', 'gasgas', 'sherco'] if val is None: return None
MARINE_IDS = ['DF', 'DT', 'OUTBOARD', 'MARINE', 'JET SKI', 'SEA-DOO', 'WAVERUNNER', 'YACHT', 'BOAT'] f_val = float(str(val).replace(',', '.'))
AERIAL_IDS = ['CESSNA', 'PIPER', 'AIRBUS', 'BOEING', 'HELICOPTER', 'AIRCRAFT', 'BEECHCRAFT', 'EMBRAER', 'DRONE'] if 0 < f_val < 1.0: return None # Ez csak arányszám (kW/kg)
ATV_IDS = ['LT-', 'LTZ', 'LTR', 'KINGQUAD', 'QUAD', 'POLARIS', 'CAN-AM', 'MULE', 'RZR', 'ARCTIC CAT', 'UTV', 'SIDE-BY-SIDE'] v = int(f_val)
RACING_IDS = ['RM-Z', 'KX', 'CRF', 'YZ', 'SX-F', 'XC-W', 'RM125', 'RM250', 'CR125', 'CR250', 'MC450'] return v if v > 0 else None
MOTO_KEYWORDS = ['CBR', 'GSX', 'YZF', 'NINJA', 'Z1000', 'DR-Z', 'MT-0', 'V-STROM', 'ADVENTURE', 'SCRAMBLER', 'CBF', 'VFR', 'HAYABUSA'] except (ValueError, TypeError):
BUS_KEYWORDS = ['BUS', 'COACH', 'INTERCITY', 'SHUTTLE', 'TRANSIT'] return None
TRUCK_KEYWORDS = ['TRUCK', 'SEMI', 'TRACTOR', 'HAULER', 'ACTROS', 'MAN', 'SCANIA', 'IVECO', 'VOLVO FH', 'DAF', 'TGX', 'RENAULT T']
TRAILER_KEYWORDS = ['TRAILER', 'SEMITRAILER', 'PÓTKOCSI', 'UTÁNFUTÓ', 'SCHMITZ', 'KRONE', 'KÖGEL']
FALLBACK_BRANDS = ['Audi', 'BMW', 'Mercedes-Benz', 'Volkswagen', 'Toyota', 'Ford', 'Honda', 'Hyundai', 'Kia', 'Mazda', 'Nissan', 'Volvo', 'Skoda', 'Opel', 'Tesla', 'Lexus', 'Porsche', 'Dacia', 'Suzuki']
@classmethod @classmethod
def identify_class(cls, make: str, model: str) -> str: def clean_int(cls, val):
m_full = f"{str(make)} {str(model)}".upper() """Általános egész szám tisztító."""
if any(x in m_full for x in cls.AERIAL_IDS): return "aerial" try:
if any(x in m_full for x in cls.MARINE_IDS): return "marine" if val is None: return None
if any(x in m_full for x in cls.ATV_IDS): return "atv" return int(float(str(val).replace(',', '.')))
if any(x in m_full or str(make).lower() in cls.MOTO_MAKES for x in (cls.RACING_IDS + cls.MOTO_KEYWORDS)): except (ValueError, TypeError):
return "motorcycle" return None
if any(x in m_full for x in cls.BUS_KEYWORDS): return "bus"
if any(x in m_full for x in cls.TRUCK_KEYWORDS): return "truck"
if any(x in m_full for x in cls.TRAILER_KEYWORDS): return "trailer"
return "car"
@classmethod @classmethod
async def fetch_api(cls, url, params=None, is_cq=False): async def fetch_api(cls, url, params=None, headers=None, method="GET", json_data=None):
if is_cq and cls.cq_banned_until and datetime.datetime.now() < cls.cq_banned_until: """Univerzális API hívó sebességkorlátozással."""
return "SILENT_SKIP" async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
async with httpx.AsyncClient(headers=cls.HEADERS, follow_redirects=True) as client:
try: try:
# CarQuery: 5.0mp szünet (Hard Ban ellen), többi: 2.5mp (User kérése szerint) await asyncio.sleep(1.2) # Biztonsági késleltetés
await asyncio.sleep(5.0 if is_cq else 2.5) if method == "POST":
resp = await client.get(url, params=params, timeout=35) resp = await client.post(url, json=json_data, timeout=30)
else:
if resp.status_code == 403 or "denied" in resp.text.lower(): resp = await client.get(url, params=params, timeout=30)
logger.error("🚫 CarQuery BAN! 2 óra kényszerpihenő aktiválva.") return resp.json() if resp.status_code in [200, 201] else []
cls.cq_banned_until = datetime.datetime.now() + datetime.timedelta(hours=2)
return "DENIED"
if resp.status_code != 200: return None
content = resp.text.strip()
if is_cq:
match = re.search(r'(\{.*\}|\[.*\])', content, re.DOTALL)
if match: content = match.group(0)
return json.loads(content)
except Exception as e: except Exception as e:
logger.error(f"❌ API hiba: {e}") logger.error(f"❌ API Hiba ({url}): {e}")
return None return []
@classmethod @classmethod
async def is_model_processed(cls, db: AsyncSession, make: str, model: str, year: int): async def get_deep_tech(cls, plate, main_kw=None, vin=None):
stmt = select(AssetCatalog.id).where(AssetCatalog.make == make, AssetCatalog.model == model, AssetCatalog.year_from == year).limit(1) """Nemzetközi dúsítás: Holland -> Brit -> Amerikai sorrendben."""
result = await db.execute(stmt) res = {"kw": cls.clean_kw(main_kw), "fuel": "Unknown", "axles": None, "body": "Standard", "euro": None}
return result.scalars().first() is not None
# 1. HOLLAND (RDW) DÚSÍTÁS
fuel_data = await cls.fetch_api(cls.RDW_FUEL, {"kenteken": plate}, headers=cls.HEADERS_RDW)
if fuel_data:
f0 = fuel_data[0]
if not res["kw"]:
res["kw"] = cls.clean_kw(f0.get("nettomaximumvermogen") or f0.get("netto_maximum_vermogen"))
res["fuel"] = f0.get("brandstof_omschrijving", "Unknown")
res["euro"] = f0.get("uitlaatemissieniveau")
# 2. BRIT (DVLA) ELLENŐRZÉS (Ha van UK kulcs és még hiányzik adat)
if cls.UK_API_KEY and (not res["kw"] or not res["euro"]):
uk_data = await cls.fetch_api(cls.UK_DVLA, method="POST", json_data={"registrationNumber": plate}, headers=cls.HEADERS_UK)
if uk_data:
res["kw"] = res["kw"] or cls.clean_kw(uk_data.get("engineCapacity")) # Brit adatok finomítása
res["euro"] = res["euro"] or uk_data.get("euroStatus")
# 3. AMERIKAI (NHTSA) KUTATÁS (Ha van alvázszám)
if vin and len(vin) == 17:
us_data = await cls.fetch_api(cls.US_NHTSA, params={"format": "json", "data": vin})
if us_data and "Results" in us_data:
# Az amerikai adatbázisból kinyerjük a lóerőt (HP), ha a kW még mindig nincs meg
hp = us_data["Results"][0].get("EngineHP")
if hp and not res["kw"]:
res["kw"] = int(float(hp) * 0.7457) # HP -> kW konverzió
# RDW Extra adatok (Tengely, Karosszéria)
axle = await cls.fetch_api(cls.RDW_AXLE, {"kenteken": plate}, headers=cls.HEADERS_RDW)
if axle: res["axles"] = cls.clean_int(axle[0].get("aantal_assen"))
body = await cls.fetch_api(cls.RDW_BODY, {"kenteken": plate}, headers=cls.HEADERS_RDW)
if body: res["body"] = body[0].get("carrosserie_omschrijving", "Standard")
return res
@classmethod @classmethod
async def auto_heal(cls, db: AsyncSession, cq_active: bool): async def process_make(cls, db, task_id, make_name):
logger.info("🛠️ Auto-Heal: Hiányos rekordok dúsítása...") logger.info(f"🚀 >>> {make_name} GlobalHunter v1.0.13 INDUL...")
stmt = select(AssetCatalog).where(AssetCatalog.engine_variant == 'Standard', AssetCatalog.fuel_type == 'Unknown').limit(20) offset, limit, total_saved = 0, 1000, 0
results = await db.execute(stmt) unique_variants = {}
for r in results.scalars().all():
# 1. RDW javítás (Holland Open Data + Token) while True:
rdw = await cls.fetch_api(cls.RDW_URL, {"merk": r.make.upper(), "handelsbenaming": r.model.upper(), "$limit": 1}) params = {"merk": make_name.upper(), "$limit": limit, "$offset": offset}
if rdw and isinstance(rdw, list) and len(rdw) > 0: main_data = await cls.fetch_api(cls.RDW_MAIN, params, headers=cls.HEADERS_RDW)
item = rdw[0] if not main_data: break
r.fuel_type = item.get("brandstof_omschrijving", "Unknown")
r.factory_data.update({"hp": item.get("netto_maximum_vermogen"), "cc": item.get("cilinderinhoud"), "source": "heal_v1.9_rdw"}) for item in main_data:
plate = item.get("kenteken")
if not plate: continue
model = str(item.get("handelsbenaming", "Unknown")).upper()
ccm = cls.clean_int(item.get("cilinderinhoud"))
weight = cls.clean_int(item.get("massa_ledig_voertuig") or item.get("massa_rijklaar"))
kw_candidate = item.get("netto_maximum_vermogen") or item.get("vermogen_massarijklaar")
raw_date = item.get("datum_eerste_toelating")
prod_year = int(str(raw_date)[:4]) if raw_date else 2024
v_class = cls.CATEGORY_MAP.get(item.get("voertuigsoort"), "other")
if "kampeerwagen" in str(item.get("inrichting", "")).lower(): v_class = "camper"
# Variáns kulcs: Modell + CCM + Súly + kW + Év = Egyedi technikai ujjlenyomat
variant_key = f"{model}-{ccm}-{weight}-{v_class}-{kw_candidate}-{prod_year}"
if variant_key not in unique_variants:
unique_variants[variant_key] = {
"model": model, "ccm": ccm, "weight": weight, "v_class": v_class,
"plate": plate, "main_kw": kw_candidate, "prod_year": prod_year,
"vin": item.get("vin") # Ha az RDW-ben benne van a VIN
}
if len(main_data) < limit or offset > 90000: break
offset += limit
logger.info(f"📊 {len(unique_variants)} egyedi variáns kutatása indul...")
for key, v in unique_variants.items():
deep = await cls.get_deep_tech(v["plate"], main_kw=v["main_kw"], vin=v["vin"])
try:
db_item = AssetCatalog(
make=make_name.upper(), model=v["model"], vehicle_class=v["v_class"],
fuel_type=deep["fuel"], power_kw=deep["kw"], engine_capacity=v["ccm"],
max_weight_kg=v["weight"], axle_count=deep["axles"], body_type=deep["body"],
year_from=v["prod_year"], euro_class=deep["euro"],
factory_data={
"source": "GlobalHunter-v1.0.13",
"sample_plate": v["plate"],
"enriched_at": str(datetime.datetime.now())
}
)
db.add(db_item)
await db.commit()
total_saved += 1
if total_saved % 50 == 0: logger.info(f"{total_saved} variáns elmentve.")
except Exception:
await db.rollback()
continue continue
# 2. CQ javítás (Ha nem vagyunk kitiltva) await db.execute(text("UPDATE data.catalog_discovery SET status = 'processed' WHERE id = :id"), {"id": task_id})
if cq_active:
t_data = await cls.fetch_api(cls.CQ_URL, {"cmd": "getTrims", "make": r.make.lower(), "model": r.model, "year": r.year_from}, is_cq=True)
if t_data and t_data not in ["DENIED", "SILENT_SKIP"] and "Trims" in t_data:
t = t_data["Trims"][0]
r.engine_variant = t.get("model_trim") or "Standard"
r.factory_data.update({"hp": t.get("model_engine_power_ps"), "cc": t.get("model_engine_cc"), "source": "heal_v1.9_cq"})
await db.commit() await db.commit()
logger.info(f"🏁 {make_name} KÉSZ. {total_saved} rekord rögzítve.")
@classmethod @classmethod
async def run(cls): async def run(cls):
logger.info(f"🤖 Robot 1.9.2 indítása (RDW Token: {'Aktív' if cls.RDW_TOKEN else 'HIÁNYZIK!'})") logger.info("🤖 Robot 1.0.13 (Global Hunter) ONLINE")
while True:
for year in range(2026, 1989, -1):
logger.info(f"📅 --- CIKLUS: {year} ---")
cq_now_active = not (cls.cq_banned_until and datetime.datetime.now() < cls.cq_banned_until)
async with SessionLocal() as db: async with SessionLocal() as db:
await cls.auto_heal(db, cq_now_active) res = await db.execute(text("SELECT id, make FROM data.catalog_discovery WHERE status = 'pending' LIMIT 1"))
task = res.fetchone()
# 1. MÁRKALISTA (NHTSA + Fallback) if task:
makes_to_process = [] await cls.process_make(db, task[0], task[1])
for b in cls.FALLBACK_BRANDS: else:
makes_to_process.append({"id": b.lower(), "display": b}) logger.info("😴 Várólista üres. Alvás 60 mp...")
await asyncio.sleep(60)
for make in makes_to_process: await asyncio.sleep(1)
models_to_fetch = set()
# A: NHTSA (US)
n_data = await cls.fetch_api(f"{cls.NHTSA_BASE}{make['display']}/modelyear/{year}?format=json")
if n_data and n_data.get("Results"):
for r in n_data["Results"]: models_to_fetch.add(r["Model_Name"])
# B: RDW (Holland) - Tokennel védve
rdw_m = await cls.fetch_api(cls.RDW_URL, {"merk": make['display'].upper(), "$limit": 30})
if rdw_m and isinstance(rdw_m, list):
for r in rdw_m: models_to_fetch.add(r.get("handelsbenaming"))
async with SessionLocal() as db:
for model_name in models_to_fetch:
if not model_name or await cls.is_model_processed(db, make["display"], model_name, year):
continue
# C: CarQuery (Csak ha nincs ban)
found_trims = []
t_data = await cls.fetch_api(cls.CQ_URL, {"cmd": "getTrims", "make": make["id"], "model": model_name, "year": year}, is_cq=True)
if t_data and t_data not in ["DENIED", "SILENT_SKIP"] and "Trims" in t_data:
found_trims = t_data["Trims"]
if not found_trims:
found_trims = [{"model_trim": "Standard", "model_engine_fuel": "Unknown"}]
for t in found_trims:
db.add(AssetCatalog(
make=make["display"], model=model_name, year_from=year,
engine_variant=t.get("model_trim") or "Standard",
fuel_type=t.get("model_engine_fuel") or "Unknown",
vehicle_class=cls.identify_class(make["display"], model_name),
factory_data={
"hp": t.get("model_engine_power_ps"), "cc": t.get("model_engine_cc"),
"source": "ghost_v1.9.2", "sync_date": str(datetime.datetime.now())
}
))
await db.commit()
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(CatalogScout.run()) asyncio.run(CatalogMaster.run())

View File

@@ -1,282 +1,161 @@
import asyncio import asyncio
import httpx import httpx
import logging import logging
import uuid
import os import os
import sys from datetime import datetime, timezone
import csv
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, text from sqlalchemy import select, text
from sqlalchemy.orm import selectinload
from app.db.session import SessionLocal from app.db.session import SessionLocal
# Modellek importálása # Modellek - Az új v1.3 struktúra
from app.models.service import ServiceProfile, ExpertiseTag from app.models.service import ServiceStaging, DiscoveryParameter
from app.models.organization import Organization, OrganizationFinancials, OrgType, OrgUserRole, OrganizationMember
from app.models.identity import Person
from app.models.address import Address, GeoPostalCode
from geoalchemy2.elements import WKTElement
from datetime import datetime, timezone
# Naplózás beállítása logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logging.basicConfig(level=logging.INFO) logger = logging.getLogger("Robot-v1.3-ContinentalScout")
logger = logging.getLogger("Robot2-Dunakeszi-Detective")
class ServiceHunter: class ServiceHunter:
""" """
Robot 2.7.2: Dunakeszi Detective - Deep Model Integration. Robot v1.3.0: Continental Scout.
Logika: EU-szintű felderítő motor, Discovery tábla alapú vezérléssel.
1. Helyi CSV (Saját beküldés - Cím alapú Geocoding-al - 50 pont Trust)
2. OSM (Közösségi adat - 10 pont Trust)
3. Google (Adatpótlás/Fallback - 30 pont Trust)
""" """
OVERPASS_URL = "http://overpass-api.de/api/interpreter" OVERPASS_URL = "http://overpass-api.de/api/interpreter"
PLACES_NEW_URL = "https://places.googleapis.com/v1/places:searchNearby" PLACES_NEW_URL = "https://places.googleapis.com/v1/places:searchNearby"
GEOCODE_URL = "https://maps.googleapis.com/maps/api/geocode/json" GEOCODE_URL = "https://maps.googleapis.com/maps/api/geocode/json"
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
LOCAL_CSV_PATH = "/app/app/workers/local_services.csv"
@classmethod @classmethod
async def geocode_address(cls, address_text): async def get_coordinates(cls, city, country_code):
"""Cím szövegből GPS koordinátát és címkomponenseket csinál.""" """Város központjának lekérése a keresés indításához."""
if not cls.GOOGLE_API_KEY: params = {"address": f"{city}, {country_code}", "key": cls.GOOGLE_API_KEY}
logger.warning("⚠️ Google API kulcs hiányzik!") async with httpx.AsyncClient() as client:
return None resp = await client.get(cls.GEOCODE_URL, params=params)
if resp.status_code == 200:
params = {"address": address_text, "key": cls.GOOGLE_API_KEY} results = resp.json().get("results")
try: if results:
async with httpx.AsyncClient() as client: loc = results[0]["geometry"]["location"]
resp = await client.get(cls.GEOCODE_URL, params=params, timeout=10) return loc["lat"], loc["lng"]
if resp.status_code == 200: return None, None
data = resp.json()
if data.get("results"):
result = data["results"][0]
loc = result["geometry"]["location"]
# Címkomponensek kinyerése a kötelező mezőkhöz
components = result.get("address_components", [])
parsed = {"lat": loc["lat"], "lng": loc["lng"], "zip": "", "city": "", "street": "Ismeretlen", "type": "utca", "number": "1"}
for c in components:
types = c.get("types", [])
if "postal_code" in types: parsed["zip"] = c["long_name"]
if "locality" in types: parsed["city"] = c["long_name"]
if "route" in types: parsed["street"] = c["long_name"]
if "street_number" in types: parsed["number"] = c["long_name"]
logger.info(f"📍 Geocoding sikeres: {address_text}")
return parsed
else:
logger.error(f"❌ Geocoding hiba: {resp.status_code}")
except Exception as e:
logger.error(f"❌ Geocoding hiba: {e}")
return None
@classmethod @classmethod
async def get_google_place_details_new(cls, lat, lon): async def get_google_places(cls, lat, lon, keyword):
"""Google Places API (New) - Adatpótlás FieldMask használatával.""" """Google Places New API - Javított, 400-as hiba elleni védelemmel."""
if not cls.GOOGLE_API_KEY: if not cls.GOOGLE_API_KEY: return []
return None
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Goog-Api-Key": cls.GOOGLE_API_KEY, "X-Goog-Api-Key": cls.GOOGLE_API_KEY,
"X-Goog-FieldMask": "places.displayName,places.id,places.types,places.internationalPhoneNumber,places.websiteUri" "X-Goog-FieldMask": "places.displayName,places.id,places.types,places.internationalPhoneNumber,places.websiteUri,places.formattedAddress"
} }
# A 'keyword' a TextQuery-hez kellene, a SearchNearby-nél típusokat (includedTypes) használunk.
# EU szintű trükk: Ha nincs pontos típus, a 'car_repair' az alapértelmezett.
payload = { payload = {
"includedTypes": ["car_repair", "gas_station", "ev_charging_station", "car_wash", "motorcycle_repair"], "includedTypes": ["car_repair", "gas_station", "car_wash", "motorcycle_repair"],
"maxResultCount": 1, "maxResultCount": 20,
"locationRestriction": { "locationRestriction": {
"circle": { "circle": {
"center": {"latitude": lat, "longitude": lon}, "center": {"latitude": lat, "longitude": lon},
"radius": 40.0 "radius": 5000.0 # 5km körzet
} }
} }
} }
try: async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client: resp = await client.post(cls.PLACES_NEW_URL, json=payload, headers=headers)
resp = await client.post(cls.PLACES_NEW_URL, json=payload, headers=headers, timeout=10) if resp.status_code == 200:
if resp.status_code == 200: return resp.json().get("places", [])
places = resp.json().get("places", []) else:
if places: logger.error(f"❌ Google API hiba ({resp.status_code}): {resp.text}")
p = places[0] return []
return {
"name": p.get("displayName", {}).get("text"),
"google_id": p.get("id"),
"types": p.get("types", []),
"phone": p.get("internationalPhoneNumber"),
"website": p.get("websiteUri")
}
except Exception as e:
logger.error(f"❌ Google kiegészítő hívás hiba: {e}")
return None
@classmethod @classmethod
async def import_local_csv(cls, db: AsyncSession): async def save_to_staging(cls, db: AsyncSession, data: dict):
"""Manuális adatok betöltése CSV-ből.""" """Mentés a Staging táblába 9-mezős bontással."""
if not os.path.exists(cls.LOCAL_CSV_PATH): stmt = select(ServiceStaging).where(ServiceStaging.external_id == str(data['external_id']))
return if (await db.execute(stmt)).scalar_one_or_none(): return
try: new_entry = ServiceStaging(
with open(cls.LOCAL_CSV_PATH, mode='r', encoding='utf-8') as f: name=data['name'],
reader = csv.DictReader(f) source=data['source'],
for row in reader: external_id=str(data['external_id']),
geo_data = None # Itt történik a 9-mezős bontás (ha érkezik adat)
if row.get('cim'): postal_code=data.get('zip'),
geo_data = await cls.geocode_address(row['cim']) city=data.get('city'),
street_name=data.get('street'),
if geo_data: street_type=data.get('street_type', 'utca'),
element = { house_number=data.get('number'),
"tags": { full_address=data.get('full_address'),
"name": row['nev'], "phone": row.get('telefon'), contact_phone=data.get('phone'),
"website": row.get('web'), "amenity": row.get('tipus', 'car_repair'), website=data.get('website'),
"addr:full": row.get('cim'), raw_data=data.get('raw', {}),
"addr:city": geo_data["city"], "addr:zip": geo_data["zip"], status="pending",
"addr:street": geo_data["street"], "addr:type": geo_data["type"], trust_score=data.get('trust', 10)
"addr:number": geo_data["number"]
},
"lat": geo_data["lat"], "lon": geo_data["lng"]
}
await cls.save_service_deep(db, element, source="local_manual")
logger.info("✅ Helyi CSV adatok feldolgozva.")
except Exception as e:
logger.error(f"❌ CSV feldolgozási hiba: {e}")
@classmethod
async def get_or_create_person(cls, db: AsyncSession, name: str) -> Person:
"""Ghost Person kezelése."""
names = name.split(' ', 1)
last_name = names[0]
first_name = names[1] if len(names) > 1 else "Ismeretlen"
stmt = select(Person).where(Person.last_name == last_name, Person.first_name == first_name)
result = await db.execute(stmt); person = result.scalar_one_or_none()
if not person:
person = Person(last_name=last_name, first_name=first_name, is_ghost=True, is_active=False)
db.add(person); await db.flush()
return person
@classmethod
async def enrich_financials(cls, db: AsyncSession, org_id: int):
"""Pénzügyi rekord inicializálása."""
financial = OrganizationFinancials(
organization_id=org_id, year=datetime.now(timezone.utc).year - 1, source="bot_discovery"
) )
db.add(financial) db.add(new_entry)
@classmethod
async def save_service_deep(cls, db: AsyncSession, element: dict, source="osm"):
"""Mély mentés a modelled specifikus mezőneveivel és kötelező értékeivel."""
tags = element.get("tags", {})
lat, lon = element.get("lat"), element.get("lon")
if not lat or not lon: return
osm_name = tags.get("name") or tags.get("brand") or tags.get("operator")
google_data = None
if not osm_name or osm_name.lower() in ['aprilia', 'bosch', 'shell', 'mol', 'omv', 'ismeretlen']:
google_data = await cls.get_google_place_details_new(lat, lon)
final_name = (google_data["name"] if google_data else osm_name) or "Ismeretlen Szolgáltató"
stmt = select(Organization).where(Organization.full_name == final_name)
result = await db.execute(stmt); org = result.scalar_one_or_none()
if not org:
# 1. Address létrehozása (a kötelező mezőket kitöltjük az átadott tags-ből vagy alapértékkel)
new_addr = Address(
latitude=lat,
longitude=lon,
full_address_text=tags.get("addr:full") or f"2120 Dunakeszi, {tags.get('addr:street', 'Ismeretlen')} {tags.get('addr:housenumber', '1')}",
street_name=tags.get("addr:street") or "Ismeretlen",
street_type=tags.get("addr:type") or "utca",
house_number=tags.get("addr:number") or tags.get("addr:housenumber") or "1"
)
db.add(new_addr); await db.flush()
# 2. Organization létrehozása (a modelled alapján ezek a mezők itt vannak)
org = Organization(
full_name=final_name,
name=final_name[:50],
org_type=OrgType.service,
address_id=new_addr.id,
address_city=tags.get("addr:city") or "Dunakeszi",
address_zip=tags.get("addr:zip") or "2120",
address_street_name=new_addr.street_name,
address_street_type=new_addr.street_type,
address_house_number=new_addr.house_number
)
db.add(org); await db.flush()
# 3. Service Profile
trust = 50 if source == "local_manual" else (30 if google_data else 10)
spec = {"brands": [], "types": google_data["types"] if google_data else [], "osm_tags": tags}
if tags.get("brand"): spec["brands"].append(tags.get("brand"))
profile = ServiceProfile(
organization_id=org.id,
location=WKTElement(f'POINT({lon} {lat})', srid=4326),
status="ghost",
trust_score=trust,
google_place_id=google_data["google_id"] if google_data else None,
specialization_tags=spec,
website=google_data["website"] if google_data else tags.get("website"),
contact_phone=google_data["phone"] if google_data else tags.get("phone")
)
db.add(profile)
# 4. Tulajdonos rögzítése
owner_name = tags.get("operator") or tags.get("contact:person")
if owner_name and len(owner_name) > 3:
person = await cls.get_or_create_person(db, owner_name)
db.add(OrganizationMember(
organization_id=org.id,
person_id=person.id,
role=OrgUserRole.OWNER,
is_verified=False
))
await cls.enrich_financials(db, org.id)
await db.flush()
logger.info(f"✨ [{source.upper()}] Mentve: {final_name} (Bizalom: {trust})")
@classmethod @classmethod
async def run(cls): async def run(cls):
logger.info("🤖 Robot 2.7.2: Dunakeszi Detective indítása...") logger.info("🤖 Robot v1.3.0: Continental Scout elindult...")
# Kapcsolódási védelem
connected = False
while not connected:
try:
async with SessionLocal() as db:
await db.execute(text("SELECT 1"))
connected = True
except Exception as e:
logger.warning(f"⏳ Várakozás a hálózatra (shared-postgres host?): {e}")
await asyncio.sleep(5)
while True: while True:
async with SessionLocal() as db: async with SessionLocal() as db:
try: try:
await db.execute(text("SET search_path TO data, public")) await db.execute(text("SET search_path TO data, public"))
# 1. Beküldött CSV feldolgozása (Geocoding-al) # 1. Paraméterek lekérése a táblából
await cls.import_local_csv(db) stmt = select(DiscoveryParameter).where(DiscoveryParameter.is_active == True)
await db.commit() tasks = (await db.execute(stmt)).scalars().all()
for task in tasks:
logger.info(f"🔎 Felderítés: {task.city} ({task.country_code}) -> {task.keyword}")
# Koordináták beszerzése a kereséshez
lat, lon = await cls.get_coordinates(task.city, task.country_code)
if not lat: continue
# --- GOOGLE FÁZIS ---
google_places = await cls.get_google_places(lat, lon, task.keyword)
for p in google_places:
await cls.save_to_staging(db, {
"external_id": p.get('id'),
"name": p.get('displayName', {}).get('text'),
"full_address": p.get('formattedAddress'),
"phone": p.get('internationalPhoneNumber'),
"website": p.get('websiteUri'),
"source": "google",
"raw": p,
"trust": 30
})
# --- OSM FÁZIS (EU kompatibilis lekérdezés) ---
osm_query = f"""[out:json][timeout:60];
(nwr["amenity"~"car_repair|fuel"](around:5000, {lat}, {lon}););
out center;"""
async with httpx.AsyncClient() as client:
resp = await client.post(cls.OVERPASS_URL, data={"data": osm_query})
if resp.status_code == 200:
for el in resp.json().get("elements", []):
t = el.get("tags", {})
await cls.save_to_staging(db, {
"external_id": f"osm_{el['id']}",
"name": t.get('name', 'Ismeretlen szerviz'),
"city": t.get('addr:city', task.city),
"zip": t.get('addr:postcode'),
"street": t.get('addr:street'),
"number": t.get('addr:housenumber'),
"source": "osm",
"raw": el,
"trust": 15
})
task.last_run_at = datetime.now(timezone.utc)
await db.commit()
logger.info(f"{task.city} felderítve.")
# 2. OSM Szkennelés
query = """[out:json][timeout:120];area["name"="Dunakeszi"]->.city;(nwr["shop"~"car_repair|motorcycle_repair|tyres|car_parts|motorcycle"](area.city);nwr["amenity"~"car_repair|vehicle_inspection|motorcycle_repair|fuel|charging_station|car_wash"](area.city);nwr["amenity"~"car_repair|fuel|charging_station"](around:5000, 47.63, 19.13););out center;"""
async with httpx.AsyncClient() as client:
resp = await client.post(cls.OVERPASS_URL, data={"data": query}, timeout=120)
if resp.status_code == 200:
elements = resp.json().get("elements", [])
for el in elements:
await cls.save_service_deep(db, el, source="osm")
await db.commit()
except Exception as e: except Exception as e:
logger.error(f"❌ Futáshiba: {e}") logger.error(f"💥 Kritikus hiba a ciklusban: {e}")
logger.info("😴 Scan kész, 24 óra pihenő...") logger.info("😴 Minden aktív feladat kész. Alvás 1 órán át...")
await asyncio.sleep(86400) await asyncio.sleep(3600)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(ServiceHunter.run()) asyncio.run(ServiceHunter.run())

View File

@@ -0,0 +1,282 @@
import asyncio
import httpx
import logging
import uuid
import os
import sys
import csv
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, text
from sqlalchemy.orm import selectinload
from app.db.session import SessionLocal
# Modellek importálása
from app.models.service import ServiceProfile, ExpertiseTag
from app.models.organization import Organization, OrganizationFinancials, OrgType, OrgUserRole, OrganizationMember
from app.models.identity import Person
from app.models.address import Address, GeoPostalCode
from geoalchemy2.elements import WKTElement
from datetime import datetime, timezone
# Naplózás beállítása
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("Robot2-Dunakeszi-Detective")
class ServiceHunter:
"""
Robot 2.7.2: Dunakeszi Detective - Deep Model Integration.
Logika:
1. Helyi CSV (Saját beküldés - Cím alapú Geocoding-al - 50 pont Trust)
2. OSM (Közösségi adat - 10 pont Trust)
3. Google (Adatpótlás/Fallback - 30 pont Trust)
"""
OVERPASS_URL = "http://overpass-api.de/api/interpreter"
PLACES_NEW_URL = "https://places.googleapis.com/v1/places:searchNearby"
GEOCODE_URL = "https://maps.googleapis.com/maps/api/geocode/json"
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
LOCAL_CSV_PATH = "/app/app/workers/local_services.csv"
@classmethod
async def geocode_address(cls, address_text):
"""Cím szövegből GPS koordinátát és címkomponenseket csinál."""
if not cls.GOOGLE_API_KEY:
logger.warning("⚠️ Google API kulcs hiányzik!")
return None
params = {"address": address_text, "key": cls.GOOGLE_API_KEY}
try:
async with httpx.AsyncClient() as client:
resp = await client.get(cls.GEOCODE_URL, params=params, timeout=10)
if resp.status_code == 200:
data = resp.json()
if data.get("results"):
result = data["results"][0]
loc = result["geometry"]["location"]
# Címkomponensek kinyerése a kötelező mezőkhöz
components = result.get("address_components", [])
parsed = {"lat": loc["lat"], "lng": loc["lng"], "zip": "", "city": "", "street": "Ismeretlen", "type": "utca", "number": "1"}
for c in components:
types = c.get("types", [])
if "postal_code" in types: parsed["zip"] = c["long_name"]
if "locality" in types: parsed["city"] = c["long_name"]
if "route" in types: parsed["street"] = c["long_name"]
if "street_number" in types: parsed["number"] = c["long_name"]
logger.info(f"📍 Geocoding sikeres: {address_text}")
return parsed
else:
logger.error(f"❌ Geocoding hiba: {resp.status_code}")
except Exception as e:
logger.error(f"❌ Geocoding hiba: {e}")
return None
@classmethod
async def get_google_place_details_new(cls, lat, lon):
"""Google Places API (New) - Adatpótlás FieldMask használatával."""
if not cls.GOOGLE_API_KEY:
return None
headers = {
"Content-Type": "application/json",
"X-Goog-Api-Key": cls.GOOGLE_API_KEY,
"X-Goog-FieldMask": "places.displayName,places.id,places.types,places.internationalPhoneNumber,places.websiteUri"
}
payload = {
"includedTypes": ["car_repair", "gas_station", "ev_charging_station", "car_wash", "motorcycle_repair"],
"maxResultCount": 1,
"locationRestriction": {
"circle": {
"center": {"latitude": lat, "longitude": lon},
"radius": 40.0
}
}
}
try:
async with httpx.AsyncClient() as client:
resp = await client.post(cls.PLACES_NEW_URL, json=payload, headers=headers, timeout=10)
if resp.status_code == 200:
places = resp.json().get("places", [])
if places:
p = places[0]
return {
"name": p.get("displayName", {}).get("text"),
"google_id": p.get("id"),
"types": p.get("types", []),
"phone": p.get("internationalPhoneNumber"),
"website": p.get("websiteUri")
}
except Exception as e:
logger.error(f"❌ Google kiegészítő hívás hiba: {e}")
return None
@classmethod
async def import_local_csv(cls, db: AsyncSession):
"""Manuális adatok betöltése CSV-ből."""
if not os.path.exists(cls.LOCAL_CSV_PATH):
return
try:
with open(cls.LOCAL_CSV_PATH, mode='r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
geo_data = None
if row.get('cim'):
geo_data = await cls.geocode_address(row['cim'])
if geo_data:
element = {
"tags": {
"name": row['nev'], "phone": row.get('telefon'),
"website": row.get('web'), "amenity": row.get('tipus', 'car_repair'),
"addr:full": row.get('cim'),
"addr:city": geo_data["city"], "addr:zip": geo_data["zip"],
"addr:street": geo_data["street"], "addr:type": geo_data["type"],
"addr:number": geo_data["number"]
},
"lat": geo_data["lat"], "lon": geo_data["lng"]
}
await cls.save_service_deep(db, element, source="local_manual")
logger.info("✅ Helyi CSV adatok feldolgozva.")
except Exception as e:
logger.error(f"❌ CSV feldolgozási hiba: {e}")
@classmethod
async def get_or_create_person(cls, db: AsyncSession, name: str) -> Person:
"""Ghost Person kezelése."""
names = name.split(' ', 1)
last_name = names[0]
first_name = names[1] if len(names) > 1 else "Ismeretlen"
stmt = select(Person).where(Person.last_name == last_name, Person.first_name == first_name)
result = await db.execute(stmt); person = result.scalar_one_or_none()
if not person:
person = Person(last_name=last_name, first_name=first_name, is_ghost=True, is_active=False)
db.add(person); await db.flush()
return person
@classmethod
async def enrich_financials(cls, db: AsyncSession, org_id: int):
"""Pénzügyi rekord inicializálása."""
financial = OrganizationFinancials(
organization_id=org_id, year=datetime.now(timezone.utc).year - 1, source="bot_discovery"
)
db.add(financial)
@classmethod
async def save_service_deep(cls, db: AsyncSession, element: dict, source="osm"):
"""Mély mentés a modelled specifikus mezőneveivel és kötelező értékeivel."""
tags = element.get("tags", {})
lat, lon = element.get("lat"), element.get("lon")
if not lat or not lon: return
osm_name = tags.get("name") or tags.get("brand") or tags.get("operator")
google_data = None
if not osm_name or osm_name.lower() in ['aprilia', 'bosch', 'shell', 'mol', 'omv', 'ismeretlen']:
google_data = await cls.get_google_place_details_new(lat, lon)
final_name = (google_data["name"] if google_data else osm_name) or "Ismeretlen Szolgáltató"
stmt = select(Organization).where(Organization.full_name == final_name)
result = await db.execute(stmt); org = result.scalar_one_or_none()
if not org:
# 1. Address létrehozása (a kötelező mezőket kitöltjük az átadott tags-ből vagy alapértékkel)
new_addr = Address(
latitude=lat,
longitude=lon,
full_address_text=tags.get("addr:full") or f"2120 Dunakeszi, {tags.get('addr:street', 'Ismeretlen')} {tags.get('addr:housenumber', '1')}",
street_name=tags.get("addr:street") or "Ismeretlen",
street_type=tags.get("addr:type") or "utca",
house_number=tags.get("addr:number") or tags.get("addr:housenumber") or "1"
)
db.add(new_addr); await db.flush()
# 2. Organization létrehozása (a modelled alapján ezek a mezők itt vannak)
org = Organization(
full_name=final_name,
name=final_name[:50],
org_type=OrgType.service,
address_id=new_addr.id,
address_city=tags.get("addr:city") or "Dunakeszi",
address_zip=tags.get("addr:zip") or "2120",
address_street_name=new_addr.street_name,
address_street_type=new_addr.street_type,
address_house_number=new_addr.house_number
)
db.add(org); await db.flush()
# 3. Service Profile
trust = 50 if source == "local_manual" else (30 if google_data else 10)
spec = {"brands": [], "types": google_data["types"] if google_data else [], "osm_tags": tags}
if tags.get("brand"): spec["brands"].append(tags.get("brand"))
profile = ServiceProfile(
organization_id=org.id,
location=WKTElement(f'POINT({lon} {lat})', srid=4326),
status="ghost",
trust_score=trust,
google_place_id=google_data["google_id"] if google_data else None,
specialization_tags=spec,
website=google_data["website"] if google_data else tags.get("website"),
contact_phone=google_data["phone"] if google_data else tags.get("phone")
)
db.add(profile)
# 4. Tulajdonos rögzítése
owner_name = tags.get("operator") or tags.get("contact:person")
if owner_name and len(owner_name) > 3:
person = await cls.get_or_create_person(db, owner_name)
db.add(OrganizationMember(
organization_id=org.id,
person_id=person.id,
role=OrgUserRole.OWNER,
is_verified=False
))
await cls.enrich_financials(db, org.id)
await db.flush()
logger.info(f"✨ [{source.upper()}] Mentve: {final_name} (Bizalom: {trust})")
@classmethod
async def run(cls):
logger.info("🤖 Robot 2.7.2: Dunakeszi Detective indítása...")
# Kapcsolódási védelem
connected = False
while not connected:
try:
async with SessionLocal() as db:
await db.execute(text("SELECT 1"))
connected = True
except Exception as e:
logger.warning(f"⏳ Várakozás a hálózatra (shared-postgres host?): {e}")
await asyncio.sleep(5)
while True:
async with SessionLocal() as db:
try:
await db.execute(text("SET search_path TO data, public"))
# 1. Beküldött CSV feldolgozása (Geocoding-al)
await cls.import_local_csv(db)
await db.commit()
# 2. OSM Szkennelés
query = """[out:json][timeout:120];area["name"="Dunakeszi"]->.city;(nwr["shop"~"car_repair|motorcycle_repair|tyres|car_parts|motorcycle"](area.city);nwr["amenity"~"car_repair|vehicle_inspection|motorcycle_repair|fuel|charging_station|car_wash"](area.city);nwr["amenity"~"car_repair|fuel|charging_station"](around:5000, 47.63, 19.13););out center;"""
async with httpx.AsyncClient() as client:
resp = await client.post(cls.OVERPASS_URL, data={"data": query}, timeout=120)
if resp.status_code == 200:
elements = resp.json().get("elements", [])
for el in elements:
await cls.save_service_deep(db, el, source="osm")
await db.commit()
except Exception as e:
logger.error(f"❌ Futáshiba: {e}")
logger.info("😴 Scan kész, 24 óra pihenő...")
await asyncio.sleep(86400)
if __name__ == "__main__":
asyncio.run(ServiceHunter.run())

View File

@@ -0,0 +1,125 @@
import asyncio
import httpx
import logging
import os
import datetime
from sqlalchemy import text
from app.db.session import SessionLocal
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("Robot-v1.0.4-Master-Enricher")
class TechEnricher:
"""
Master Enricher v1.0.4
- Target: kyri-nuah (RDW Technical Catalogue)
- Fix: Visszaállás 'merk' mezőre + SQL fix az új oszlopokhoz.
"""
API_URL = "https://opendata.rdw.nl/resource/kyri-nuah.json"
RDW_TOKEN = os.getenv("RDW_APP_TOKEN")
HEADERS = {"X-App-Token": RDW_TOKEN} if RDW_TOKEN else {}
@classmethod
async def fetch_tech_data(cls, make, model):
# Tisztítás: Ha a modell névben benne van a márka, levágjuk
clean_model = str(model).upper().replace(str(make).upper(), "").strip()
# Ha a modellnév csak szám vagy túl rövid, az RDW nem fogja szeretni
if len(clean_model) < 2:
return None
# PRÓBA 1: A 'merk' mezővel (Ez a leggyakoribb)
params = {
"merk": make.upper(),
"handelsbenaming": clean_model,
"$limit": 1
}
async with httpx.AsyncClient(headers=cls.HEADERS) as client:
try:
await asyncio.sleep(1.1)
resp = await client.get(cls.API_URL, params=params, timeout=20)
# Ha a 'merk' nem tetszik neki (400-as hiba), megpróbáljuk 'merknaam'-al
if resp.status_code == 400:
params = {"merknaam": make.upper(), "handelsbenaming": clean_model, "$limit": 1}
resp = await client.get(cls.TECH_API_URL, params=params, timeout=20)
if resp.status_code == 200:
data = resp.json()
return data[0] if data else None
return None
except Exception as e:
logger.error(f"❌ API Hiba: {e}")
return None
@classmethod
async def run(cls):
logger.info("🚀 Master Enricher v1.0.4 - Új oszlopok töltése indul...")
while True:
async with SessionLocal() as db:
# Olyan sorokat keresünk, ahol az új oszlopok még üresek
query = text("""
SELECT id, make, model
FROM data.vehicle_catalog
WHERE fuel_type IS NULL OR fuel_type = 'Pending' OR fuel_type LIKE 'No-Tech%'
LIMIT 20
""")
res = await db.execute(query)
tasks = res.fetchall()
if not tasks:
logger.info("😴 Minden adat kész. Alvás 5 perc...")
await asyncio.sleep(300)
continue
for t_id, make, model in tasks:
logger.info(f"🧪 Gazdagítás: {make} | {model}")
tech = await cls.fetch_tech_data(make, model)
if tech:
# RDW mezők kinyerése
kw = tech.get("netto_maximum_vermogen_kw")
ccm = tech.get("cilinderinhoud")
weight = tech.get("technisch_toelaatbare_maximum_massa")
axles = tech.get("aantal_assen")
euro = tech.get("milieuklasse_eg_goedkeuring_licht")
fuel = tech.get("brandstof_omschrijving_brandstof_stam", "Standard")
# Biztonságos konverzió
def clean_num(v):
try: return int(float(v)) if v else None
except: return None
update_query = text("""
UPDATE data.vehicle_catalog
SET fuel_type = :fuel,
power_kw = :kw,
engine_capacity = :ccm,
max_weight_kg = :weight,
axle_count = :axles,
euro_class = :euro,
factory_data = factory_data || jsonb_build_object('enriched_at', :now)
WHERE id = :id
""")
await db.execute(update_query, {
"fuel": fuel, "kw": clean_num(kw), "ccm": clean_num(ccm),
"weight": clean_num(weight), "axles": clean_num(axles),
"euro": str(euro) if euro else None,
"id": t_id, "now": str(datetime.datetime.now())
})
await db.commit()
logger.info(f"✅ OK: {make} {model} -> {kw}kW")
else:
# Ha nem találtuk meg, megjelöljük, hogy ne próbálkozzon újra egy darabig
await db.execute(text("UPDATE data.vehicle_catalog SET fuel_type = 'No-Tech-V4' WHERE id = :id"), {"id": t_id})
await db.commit()
await asyncio.sleep(0.5)
if __name__ == "__main__":
asyncio.run(TechEnricher.run())

View File

@@ -39,7 +39,7 @@ def do_run_migrations(connection):
target_metadata=target_metadata, target_metadata=target_metadata,
include_schemas=True, include_schemas=True,
include_object=include_object, include_object=include_object,
version_table_schema='public' version_table_schema='data'
) )
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()

View File

@@ -0,0 +1,252 @@
"""update_staging_address_structure
Revision ID: 25d1658ccf1d
Revises: d0f9ed93b59f
Create Date: 2026-02-15 19:37:31.160172
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '25d1658ccf1d'
down_revision: Union[str, Sequence[str], None] = 'd0f9ed93b59f'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey')
op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_assignments_branch_id_fkey'), 'asset_assignments', type_='foreignkey')
op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey')
op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey')
op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_assignments', 'branches', ['branch_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey')
op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey')
op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey')
op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey')
op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey')
op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey')
op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey')
op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey')
op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey')
op.drop_constraint(op.f('assets_current_organization_id_fkey'), 'assets', type_='foreignkey')
op.create_foreign_key(None, 'assets', 'organizations', ['current_organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey')
op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('branches_organization_id_fkey'), 'branches', type_='foreignkey')
op.drop_constraint(op.f('branches_address_id_fkey'), 'branches', type_='foreignkey')
op.create_foreign_key(None, 'branches', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'branches', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey')
op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey')
op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey')
op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey')
op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey')
op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('organization_financials_organization_id_fkey'), 'organization_financials', type_='foreignkey')
op.create_foreign_key(None, 'organization_financials', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.alter_column('organization_members', 'role',
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
existing_nullable=True)
op.drop_constraint(op.f('organization_members_person_id_fkey'), 'organization_members', type_='foreignkey')
op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey')
op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey')
op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organization_members', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.alter_column('organizations', 'org_type',
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
existing_nullable=True)
op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey')
op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey')
op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('pending_actions_requester_id_fkey'), 'pending_actions', type_='foreignkey')
op.drop_constraint(op.f('pending_actions_approver_id_fkey'), 'pending_actions', type_='foreignkey')
op.create_foreign_key(None, 'pending_actions', 'users', ['approver_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'pending_actions', 'users', ['requester_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey')
op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey')
op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey')
op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_expertises_service_id_fkey'), 'service_expertises', type_='foreignkey')
op.drop_constraint(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', type_='foreignkey')
op.create_foreign_key(None, 'service_expertises', 'service_profiles', ['service_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_profiles_organization_id_fkey'), 'service_profiles', type_='foreignkey')
op.create_foreign_key(None, 'service_profiles', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey')
op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data')
op.add_column('service_staging', sa.Column('street_name', sa.String(length=150), nullable=True))
op.add_column('service_staging', sa.Column('street_type', sa.String(length=50), nullable=True))
op.add_column('service_staging', sa.Column('stairwell', sa.String(length=20), nullable=True))
op.add_column('service_staging', sa.Column('floor', sa.String(length=20), nullable=True))
op.add_column('service_staging', sa.Column('door', sa.String(length=20), nullable=True))
op.add_column('service_staging', sa.Column('hrsz', sa.String(length=50), nullable=True))
op.alter_column('service_staging', 'house_number',
existing_type=sa.VARCHAR(length=50),
type_=sa.String(length=20),
existing_nullable=True)
op.drop_column('service_staging', 'street')
op.drop_constraint(op.f('social_accounts_user_id_fkey'), 'social_accounts', type_='foreignkey')
op.create_foreign_key(None, 'social_accounts', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey')
op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey')
op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey')
op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey')
op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey')
op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey')
op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'])
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id'])
op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id'])
op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'social_accounts', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('social_accounts_user_id_fkey'), 'social_accounts', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.add_column('service_staging', sa.Column('street', sa.VARCHAR(length=255), autoincrement=False, nullable=True))
op.alter_column('service_staging', 'house_number',
existing_type=sa.String(length=20),
type_=sa.VARCHAR(length=50),
existing_nullable=True)
op.drop_column('service_staging', 'hrsz')
op.drop_column('service_staging', 'door')
op.drop_column('service_staging', 'floor')
op.drop_column('service_staging', 'stairwell')
op.drop_column('service_staging', 'street_type')
op.drop_column('service_staging', 'street_name')
op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id'])
op.drop_constraint(None, 'service_profiles', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_profiles_organization_id_fkey'), 'service_profiles', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'])
op.create_foreign_key(op.f('service_expertises_service_id_fkey'), 'service_expertises', 'service_profiles', ['service_id'], ['id'])
op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id'])
op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'persons', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id'])
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id'])
op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id'])
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id'])
op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id'])
op.alter_column('organizations', 'org_type',
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
existing_nullable=True)
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('organization_members_person_id_fkey'), 'organization_members', 'persons', ['person_id'], ['id'])
op.alter_column('organization_members', 'role',
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
existing_nullable=True)
op.drop_constraint(None, 'organization_financials', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organization_financials_organization_id_fkey'), 'organization_financials', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id'])
op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'])
op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'])
op.drop_constraint(None, 'documents', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id'])
op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id'])
op.drop_constraint(None, 'branches', schema='data', type_='foreignkey')
op.drop_constraint(None, 'branches', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('branches_address_id_fkey'), 'branches', 'addresses', ['address_id'], ['id'])
op.create_foreign_key(op.f('branches_organization_id_fkey'), 'branches', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('assets_current_organization_id_fkey'), 'assets', 'organizations', ['current_organization_id'], ['id'])
op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id'])
op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id'])
op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id'])
op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id'])
op.create_foreign_key(op.f('asset_assignments_branch_id_fkey'), 'asset_assignments', 'branches', ['branch_id'], ['id'])
op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'])
# ### end Alembic commands ###

View File

@@ -0,0 +1,27 @@
"""add_axles_and_body_type
Revision ID: 33c4f2235667
Revises: 75e3a57f9c14
Create Date: 2026-02-15 03:28:23.315925
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '33c4f2235667'
down_revision: Union[str, Sequence[str], None] = '75e3a57f9c14'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# op.add_column('vehicle_catalog', sa.Column('axle_count', sa.Integer(), nullable=True), schema='data')
op.add_column('vehicle_catalog', sa.Column('body_type', sa.String(100), nullable=True), schema='data')
def downgrade() -> None:
# op.drop_column('vehicle_catalog', 'axle_count', schema='data')
op.drop_column('vehicle_catalog', 'body_type', schema='data')

View File

@@ -0,0 +1,42 @@
"""enrich_catalog_technical_schema
Revision ID: 75e3a57f9c14
Revises: d229cc6bc347
Create Date: 2026-02-15 02:45:50.855386
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '75e3a57f9c14'
down_revision: Union[str, Sequence[str], None] = 'd229cc6bc347'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 1. Oszlopok tényleges hozzáadása (NEM idézőjelben!)
op.add_column('vehicle_catalog', sa.Column('power_kw', sa.Integer(), nullable=True), schema='data')
op.add_column('vehicle_catalog', sa.Column('engine_capacity', sa.Integer(), nullable=True), schema='data')
op.add_column('vehicle_catalog', sa.Column('max_weight_kg', sa.Integer(), nullable=True), schema='data')
op.add_column('vehicle_catalog', sa.Column('axle_count', sa.Integer(), nullable=True), schema='data')
op.add_column('vehicle_catalog', sa.Column('euro_class', sa.String(20), nullable=True), schema='data')
# 2. Indexek létrehozása (most már létező oszlopokon)
op.create_index('ix_vehicle_catalog_power', 'vehicle_catalog', ['power_kw'], schema='data')
op.create_index('ix_vehicle_catalog_capacity', 'vehicle_catalog', ['engine_capacity'], schema='data')
def downgrade() -> None:
# Oszlopok és indexek eltávolítása (fordított sorrendben érdemes)
op.drop_index('ix_vehicle_catalog_power', table_name='vehicle_catalog', schema='data')
op.drop_index('ix_vehicle_catalog_capacity', table_name='vehicle_catalog', schema='data')
op.drop_column('vehicle_catalog', 'power_kw', schema='data')
op.drop_column('vehicle_catalog', 'engine_capacity', schema='data')
op.drop_column('vehicle_catalog', 'max_weight_kg', schema='data')
op.drop_column('vehicle_catalog', 'axle_count', schema='data')
op.drop_column('vehicle_catalog', 'euro_class', schema='data')

View File

@@ -0,0 +1,230 @@
"""add_discovery_parameters_table
Revision ID: 8188636edd27
Revises: 25d1658ccf1d
Create Date: 2026-02-15 19:52:59.375620
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '8188636edd27'
down_revision: Union[str, Sequence[str], None] = '25d1658ccf1d'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey')
op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey')
op.drop_constraint(op.f('asset_assignments_branch_id_fkey'), 'asset_assignments', type_='foreignkey')
op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey')
op.create_foreign_key(None, 'asset_assignments', 'branches', ['branch_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey')
op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey')
op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey')
op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey')
op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey')
op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey')
op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey')
op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey')
op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('assets_current_organization_id_fkey'), 'assets', type_='foreignkey')
op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey')
op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'assets', 'organizations', ['current_organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey')
op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('branches_organization_id_fkey'), 'branches', type_='foreignkey')
op.drop_constraint(op.f('branches_address_id_fkey'), 'branches', type_='foreignkey')
op.create_foreign_key(None, 'branches', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'branches', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey')
op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey')
op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey')
op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey')
op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey')
op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('organization_financials_organization_id_fkey'), 'organization_financials', type_='foreignkey')
op.create_foreign_key(None, 'organization_financials', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.alter_column('organization_members', 'role',
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
existing_nullable=True)
op.drop_constraint(op.f('organization_members_person_id_fkey'), 'organization_members', type_='foreignkey')
op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey')
op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey')
op.create_foreign_key(None, 'organization_members', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.alter_column('organizations', 'org_type',
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
existing_nullable=True)
op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey')
op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey')
op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('pending_actions_approver_id_fkey'), 'pending_actions', type_='foreignkey')
op.drop_constraint(op.f('pending_actions_requester_id_fkey'), 'pending_actions', type_='foreignkey')
op.create_foreign_key(None, 'pending_actions', 'users', ['requester_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'pending_actions', 'users', ['approver_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey')
op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey')
op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey')
op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_expertises_service_id_fkey'), 'service_expertises', type_='foreignkey')
op.drop_constraint(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', type_='foreignkey')
op.create_foreign_key(None, 'service_expertises', 'service_profiles', ['service_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_profiles_organization_id_fkey'), 'service_profiles', type_='foreignkey')
op.create_foreign_key(None, 'service_profiles', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey')
op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('social_accounts_user_id_fkey'), 'social_accounts', type_='foreignkey')
op.create_foreign_key(None, 'social_accounts', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey')
op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey')
op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey')
op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey')
op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey')
op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey')
op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'])
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id'])
op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id'])
op.drop_constraint(None, 'social_accounts', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('social_accounts_user_id_fkey'), 'social_accounts', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id'])
op.drop_constraint(None, 'service_profiles', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_profiles_organization_id_fkey'), 'service_profiles', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'])
op.create_foreign_key(op.f('service_expertises_service_id_fkey'), 'service_expertises', 'service_profiles', ['service_id'], ['id'])
op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id'])
op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'persons', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id'])
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id'])
op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id'])
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id'])
op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id'])
op.alter_column('organizations', 'org_type',
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
existing_nullable=True)
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('organization_members_person_id_fkey'), 'organization_members', 'persons', ['person_id'], ['id'])
op.alter_column('organization_members', 'role',
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
existing_nullable=True)
op.drop_constraint(None, 'organization_financials', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organization_financials_organization_id_fkey'), 'organization_financials', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'])
op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id'])
op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'])
op.drop_constraint(None, 'documents', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id'])
op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id'])
op.drop_constraint(None, 'branches', schema='data', type_='foreignkey')
op.drop_constraint(None, 'branches', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('branches_address_id_fkey'), 'branches', 'addresses', ['address_id'], ['id'])
op.create_foreign_key(op.f('branches_organization_id_fkey'), 'branches', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id'])
op.create_foreign_key(op.f('assets_current_organization_id_fkey'), 'assets', 'organizations', ['current_organization_id'], ['id'])
op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id'])
op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id'])
op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id'])
op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id'])
op.create_foreign_key(op.f('asset_assignments_branch_id_fkey'), 'asset_assignments', 'branches', ['branch_id'], ['id'])
op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'])
# ### end Alembic commands ###

View File

@@ -0,0 +1,288 @@
"""upgrade_identity_and_audit_v1_6
Revision ID: b803fe324ebd
Revises: 8188636edd27
Create Date: 2026-02-15 23:49:00.074592
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = 'b803fe324ebd'
down_revision: Union[str, Sequence[str], None] = '8188636edd27'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('org_sales_assignments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('organization_id', sa.Integer(), nullable=True),
sa.Column('agent_user_id', sa.Integer(), nullable=True),
sa.Column('assigned_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['agent_user_id'], ['data.users.id'], ),
sa.ForeignKeyConstraint(['organization_id'], ['data.organizations.id'], ),
sa.PrimaryKeyConstraint('id'),
schema='data'
)
op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey')
op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey')
op.drop_constraint(op.f('asset_assignments_branch_id_fkey'), 'asset_assignments', type_='foreignkey')
op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey')
op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_assignments', 'branches', ['branch_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey')
op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey')
op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey')
op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey')
op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey')
op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey')
op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey')
op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey')
op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('assets_current_organization_id_fkey'), 'assets', type_='foreignkey')
op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey')
op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'assets', 'organizations', ['current_organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey')
op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('branches_organization_id_fkey'), 'branches', type_='foreignkey')
op.drop_constraint(op.f('branches_address_id_fkey'), 'branches', type_='foreignkey')
op.create_foreign_key(None, 'branches', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'branches', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey')
op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey')
op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey')
op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey')
op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey')
op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('organization_financials_organization_id_fkey'), 'organization_financials', type_='foreignkey')
op.create_foreign_key(None, 'organization_financials', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.alter_column('organization_members', 'role',
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
existing_nullable=True)
op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey')
op.drop_constraint(op.f('organization_members_person_id_fkey'), 'organization_members', type_='foreignkey')
op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey')
op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organization_members', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.alter_column('organizations', 'org_type',
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
existing_nullable=True)
op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey')
op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey')
op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('pending_actions_requester_id_fkey'), 'pending_actions', type_='foreignkey')
op.drop_constraint(op.f('pending_actions_approver_id_fkey'), 'pending_actions', type_='foreignkey')
op.create_foreign_key(None, 'pending_actions', 'users', ['approver_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'pending_actions', 'users', ['requester_id'], ['id'], source_schema='data', referent_schema='data')
op.add_column('persons', sa.Column('identity_hash', sa.String(length=64), nullable=True))
op.add_column('persons', sa.Column('lifetime_xp', sa.BigInteger(), server_default=sa.text('0'), nullable=True))
op.add_column('persons', sa.Column('penalty_points', sa.Integer(), server_default=sa.text('0'), nullable=True))
op.add_column('persons', sa.Column('social_reputation', sa.Numeric(precision=3, scale=2), server_default=sa.text('1.00'), nullable=True))
op.add_column('persons', sa.Column('is_sales_agent', sa.Boolean(), server_default=sa.text('false'), nullable=True))
op.create_index(op.f('ix_data_persons_identity_hash'), 'persons', ['identity_hash'], unique=True, schema='data')
op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey')
op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey')
op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey')
op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', type_='foreignkey')
op.drop_constraint(op.f('service_expertises_service_id_fkey'), 'service_expertises', type_='foreignkey')
op.create_foreign_key(None, 'service_expertises', 'service_profiles', ['service_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_profiles_organization_id_fkey'), 'service_profiles', type_='foreignkey')
op.create_foreign_key(None, 'service_profiles', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey')
op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('social_accounts_user_id_fkey'), 'social_accounts', type_='foreignkey')
op.create_foreign_key(None, 'social_accounts', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey')
op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey')
op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey')
op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.add_column('users', sa.Column('subscription_plan', sa.String(length=30), server_default=sa.text("'FREE'"), nullable=True))
op.add_column('users', sa.Column('subscription_expires_at', sa.DateTime(timezone=True), nullable=True))
op.add_column('users', sa.Column('is_vip', sa.Boolean(), server_default=sa.text('false'), nullable=True))
op.add_column('users', sa.Column('referral_code', sa.String(length=20), nullable=True))
op.add_column('users', sa.Column('referred_by_id', sa.Integer(), nullable=True))
op.add_column('users', sa.Column('current_sales_agent_id', sa.Integer(), nullable=True))
op.create_unique_constraint(None, 'users', ['referral_code'], schema='data')
op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey')
op.create_foreign_key(None, 'users', 'users', ['referred_by_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'users', 'users', ['current_sales_agent_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_column('users', 'two_factor_secret')
op.drop_column('users', 'refresh_token_hash')
op.drop_column('users', 'two_factor_enabled')
op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey')
op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
op.add_column('wallets', sa.Column('earned_credits', sa.Numeric(precision=18, scale=4), server_default=sa.text('0'), nullable=True))
op.add_column('wallets', sa.Column('purchased_credits', sa.Numeric(precision=18, scale=4), server_default=sa.text('0'), nullable=True))
op.add_column('wallets', sa.Column('service_coins', sa.Numeric(precision=18, scale=4), server_default=sa.text('0'), nullable=True))
op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey')
op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_column('wallets', 'coin_balance')
op.drop_column('wallets', 'credit_balance')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('wallets', sa.Column('credit_balance', sa.NUMERIC(precision=18, scale=2), autoincrement=False, nullable=True))
op.add_column('wallets', sa.Column('coin_balance', sa.NUMERIC(precision=18, scale=2), autoincrement=False, nullable=True))
op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id'])
op.drop_column('wallets', 'service_coins')
op.drop_column('wallets', 'purchased_credits')
op.drop_column('wallets', 'earned_credits')
op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'])
op.add_column('users', sa.Column('two_factor_enabled', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('refresh_token_hash', sa.VARCHAR(length=255), autoincrement=False, nullable=True))
op.add_column('users', sa.Column('two_factor_secret', sa.VARCHAR(length=100), autoincrement=False, nullable=True))
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id'])
op.drop_constraint(None, 'users', schema='data', type_='unique')
op.drop_column('users', 'current_sales_agent_id')
op.drop_column('users', 'referred_by_id')
op.drop_column('users', 'referral_code')
op.drop_column('users', 'is_vip')
op.drop_column('users', 'subscription_expires_at')
op.drop_column('users', 'subscription_plan')
op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id'])
op.drop_constraint(None, 'social_accounts', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('social_accounts_user_id_fkey'), 'social_accounts', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id'])
op.drop_constraint(None, 'service_profiles', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_profiles_organization_id_fkey'), 'service_profiles', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_expertises_service_id_fkey'), 'service_expertises', 'service_profiles', ['service_id'], ['id'])
op.create_foreign_key(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'])
op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id'])
op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'persons', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id'])
op.drop_index(op.f('ix_data_persons_identity_hash'), table_name='persons', schema='data')
op.drop_column('persons', 'is_sales_agent')
op.drop_column('persons', 'social_reputation')
op.drop_column('persons', 'penalty_points')
op.drop_column('persons', 'lifetime_xp')
op.drop_column('persons', 'identity_hash')
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id'])
op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id'])
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id'])
op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id'])
op.alter_column('organizations', 'org_type',
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
existing_nullable=True)
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('organization_members_person_id_fkey'), 'organization_members', 'persons', ['person_id'], ['id'])
op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id'])
op.alter_column('organization_members', 'role',
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
existing_nullable=True)
op.drop_constraint(None, 'organization_financials', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organization_financials_organization_id_fkey'), 'organization_financials', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id'])
op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'])
op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'])
op.drop_constraint(None, 'documents', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id'])
op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id'])
op.drop_constraint(None, 'branches', schema='data', type_='foreignkey')
op.drop_constraint(None, 'branches', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('branches_address_id_fkey'), 'branches', 'addresses', ['address_id'], ['id'])
op.create_foreign_key(op.f('branches_organization_id_fkey'), 'branches', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id'])
op.create_foreign_key(op.f('assets_current_organization_id_fkey'), 'assets', 'organizations', ['current_organization_id'], ['id'])
op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id'])
op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id'])
op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id'])
op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('asset_assignments_branch_id_fkey'), 'asset_assignments', 'branches', ['branch_id'], ['id'])
op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'])
op.drop_table('org_sales_assignments', schema='data')
# ### end Alembic commands ###

View File

@@ -0,0 +1,270 @@
"""v1.3_branch_system_and_fleet_scaling
Revision ID: d0f9ed93b59f
Revises: 33c4f2235667
Create Date: 2026-02-15 18:53:12.791636
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = 'd0f9ed93b59f'
down_revision: Union[str, Sequence[str], None] = '33c4f2235667'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('branches',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('organization_id', sa.Integer(), nullable=False),
sa.Column('address_id', sa.UUID(), nullable=True),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('is_main', sa.Boolean(), nullable=True),
sa.Column('postal_code', sa.String(length=10), nullable=True),
sa.Column('city', sa.String(length=100), nullable=True),
sa.Column('street_name', sa.String(length=150), nullable=True),
sa.Column('street_type', sa.String(length=50), nullable=True),
sa.Column('house_number', sa.String(length=20), nullable=True),
sa.Column('stairwell', sa.String(length=20), nullable=True),
sa.Column('floor', sa.String(length=20), nullable=True),
sa.Column('door', sa.String(length=20), nullable=True),
sa.Column('hrsz', sa.String(length=50), nullable=True),
sa.Column('opening_hours', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=True),
sa.Column('branch_rating', sa.Float(), nullable=True),
sa.Column('status', sa.String(length=30), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['address_id'], ['data.addresses.id'], ),
sa.ForeignKeyConstraint(['organization_id'], ['data.organizations.id'], ),
sa.PrimaryKeyConstraint('id'),
schema='data'
)
op.create_index(op.f('ix_data_branches_city'), 'branches', ['city'], unique=False, schema='data')
op.create_index(op.f('ix_data_branches_postal_code'), 'branches', ['postal_code'], unique=False, schema='data')
op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey')
op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
op.add_column('asset_assignments', sa.Column('branch_id', sa.UUID(), nullable=True))
op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey')
op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey')
op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_assignments', 'branches', ['branch_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey')
op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey')
op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey')
op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey')
op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey')
op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey')
op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey')
op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey')
op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('assets_current_organization_id_fkey'), 'assets', type_='foreignkey')
op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey')
op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'assets', 'organizations', ['current_organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey')
op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey')
op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey')
op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey')
op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey')
op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey')
op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('organization_financials_organization_id_fkey'), 'organization_financials', type_='foreignkey')
op.create_foreign_key(None, 'organization_financials', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.alter_column('organization_members', 'role',
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
existing_nullable=True)
op.drop_constraint(op.f('organization_members_person_id_fkey'), 'organization_members', type_='foreignkey')
op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey')
op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey')
op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organization_members', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.add_column('organizations', sa.Column('subscription_plan', sa.String(length=30), server_default=sa.text("'FREE'"), nullable=True))
op.add_column('organizations', sa.Column('base_asset_limit', sa.Integer(), server_default=sa.text('1'), nullable=True))
op.add_column('organizations', sa.Column('purchased_extra_slots', sa.Integer(), server_default=sa.text('0'), nullable=True))
op.add_column('organizations', sa.Column('is_ownership_transferable', sa.Boolean(), server_default=sa.text('true'), nullable=True))
op.alter_column('organizations', 'org_type',
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
existing_nullable=True)
op.create_index(op.f('ix_data_organizations_subscription_plan'), 'organizations', ['subscription_plan'], unique=False, schema='data')
op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey')
op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey')
op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('pending_actions_requester_id_fkey'), 'pending_actions', type_='foreignkey')
op.drop_constraint(op.f('pending_actions_approver_id_fkey'), 'pending_actions', type_='foreignkey')
op.create_foreign_key(None, 'pending_actions', 'users', ['approver_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'pending_actions', 'users', ['requester_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey')
op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey')
op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey')
op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', type_='foreignkey')
op.drop_constraint(op.f('service_expertises_service_id_fkey'), 'service_expertises', type_='foreignkey')
op.create_foreign_key(None, 'service_expertises', 'service_profiles', ['service_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_profiles_organization_id_fkey'), 'service_profiles', type_='foreignkey')
op.create_foreign_key(None, 'service_profiles', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey')
op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('social_accounts_user_id_fkey'), 'social_accounts', type_='foreignkey')
op.create_foreign_key(None, 'social_accounts', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey')
op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey')
op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey')
op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey')
op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_index(op.f('ix_vehicle_catalog_capacity'), table_name='vehicle_catalog')
op.drop_index(op.f('ix_vehicle_catalog_power'), table_name='vehicle_catalog')
op.create_index(op.f('ix_data_vehicle_catalog_engine_capacity'), 'vehicle_catalog', ['engine_capacity'], unique=False, schema='data')
op.create_index(op.f('ix_data_vehicle_catalog_power_kw'), 'vehicle_catalog', ['power_kw'], unique=False, schema='data')
op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey')
op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey')
op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'])
op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id'])
op.drop_index(op.f('ix_data_vehicle_catalog_power_kw'), table_name='vehicle_catalog', schema='data')
op.drop_index(op.f('ix_data_vehicle_catalog_engine_capacity'), table_name='vehicle_catalog', schema='data')
op.create_index(op.f('ix_vehicle_catalog_power'), 'vehicle_catalog', ['power_kw'], unique=False)
op.create_index(op.f('ix_vehicle_catalog_capacity'), 'vehicle_catalog', ['engine_capacity'], unique=False)
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id'])
op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id'])
op.drop_constraint(None, 'social_accounts', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('social_accounts_user_id_fkey'), 'social_accounts', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id'])
op.drop_constraint(None, 'service_profiles', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_profiles_organization_id_fkey'), 'service_profiles', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_expertises_service_id_fkey'), 'service_expertises', 'service_profiles', ['service_id'], ['id'])
op.create_foreign_key(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'])
op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id'])
op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'persons', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id'])
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id'])
op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id'])
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id'])
op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id'])
op.drop_index(op.f('ix_data_organizations_subscription_plan'), table_name='organizations', schema='data')
op.alter_column('organizations', 'org_type',
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
existing_nullable=True)
op.drop_column('organizations', 'is_ownership_transferable')
op.drop_column('organizations', 'purchased_extra_slots')
op.drop_column('organizations', 'base_asset_limit')
op.drop_column('organizations', 'subscription_plan')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('organization_members_person_id_fkey'), 'organization_members', 'persons', ['person_id'], ['id'])
op.alter_column('organization_members', 'role',
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
existing_nullable=True)
op.drop_constraint(None, 'organization_financials', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organization_financials_organization_id_fkey'), 'organization_financials', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'])
op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id'])
op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'])
op.drop_constraint(None, 'documents', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id'])
op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id'])
op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id'])
op.create_foreign_key(op.f('assets_current_organization_id_fkey'), 'assets', 'organizations', ['current_organization_id'], ['id'])
op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id'])
op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id'])
op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id'])
op.drop_column('asset_assignments', 'branch_id')
op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'])
op.drop_index(op.f('ix_data_branches_postal_code'), table_name='branches', schema='data')
op.drop_index(op.f('ix_data_branches_city'), table_name='branches', schema='data')
op.drop_table('branches', schema='data')
# ### end Alembic commands ###

View File

@@ -0,0 +1,243 @@
"""add_catalog_discovery_table
Revision ID: d229cc6bc347
Revises: 92616f34cdd3
Create Date: 2026-02-14 16:02:19.895343
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = 'd229cc6bc347'
down_revision: Union[str, Sequence[str], None] = '92616f34cdd3'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('catalog_discovery',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('make', sa.String(length=100), nullable=False),
sa.Column('model', sa.String(length=100), nullable=False),
sa.Column('vehicle_class', sa.String(length=50), nullable=True),
sa.Column('source', sa.String(length=50), nullable=True),
sa.Column('status', sa.String(length=20), server_default=sa.text("'pending'"), nullable=True),
sa.Column('attempts', sa.Integer(), nullable=True),
sa.Column('last_attempt', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('make', 'model', 'vehicle_class', name='_make_model_class_uc'),
schema='data'
)
op.create_index(op.f('ix_data_catalog_discovery_id'), 'catalog_discovery', ['id'], unique=False, schema='data')
op.create_index(op.f('ix_data_catalog_discovery_make'), 'catalog_discovery', ['make'], unique=False, schema='data')
op.create_index(op.f('ix_data_catalog_discovery_model'), 'catalog_discovery', ['model'], unique=False, schema='data')
op.create_index(op.f('ix_data_catalog_discovery_status'), 'catalog_discovery', ['status'], unique=False, schema='data')
op.create_index(op.f('ix_data_catalog_discovery_vehicle_class'), 'catalog_discovery', ['vehicle_class'], unique=False, schema='data')
op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey')
op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey')
op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey')
op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey')
op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey')
op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey')
op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey')
op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey')
op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey')
op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey')
op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey')
op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey')
op.drop_constraint(op.f('assets_current_organization_id_fkey'), 'assets', type_='foreignkey')
op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'assets', 'organizations', ['current_organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey')
op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey')
op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey')
op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey')
op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey')
op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey')
op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('organization_financials_organization_id_fkey'), 'organization_financials', type_='foreignkey')
op.create_foreign_key(None, 'organization_financials', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.alter_column('organization_members', 'role',
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
existing_nullable=True)
op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey')
op.drop_constraint(op.f('organization_members_person_id_fkey'), 'organization_members', type_='foreignkey')
op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey')
op.create_foreign_key(None, 'organization_members', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.alter_column('organizations', 'org_type',
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
existing_nullable=True)
op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey')
op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey')
op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('pending_actions_approver_id_fkey'), 'pending_actions', type_='foreignkey')
op.drop_constraint(op.f('pending_actions_requester_id_fkey'), 'pending_actions', type_='foreignkey')
op.create_foreign_key(None, 'pending_actions', 'users', ['approver_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'pending_actions', 'users', ['requester_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey')
op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey')
op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey')
op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_expertises_service_id_fkey'), 'service_expertises', type_='foreignkey')
op.drop_constraint(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', type_='foreignkey')
op.create_foreign_key(None, 'service_expertises', 'service_profiles', ['service_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_profiles_organization_id_fkey'), 'service_profiles', type_='foreignkey')
op.create_foreign_key(None, 'service_profiles', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey')
op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('social_accounts_user_id_fkey'), 'social_accounts', type_='foreignkey')
op.create_foreign_key(None, 'social_accounts', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey')
op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey')
op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey')
op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey')
op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey')
op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey')
op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'])
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id'])
op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id'])
op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'social_accounts', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('social_accounts_user_id_fkey'), 'social_accounts', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id'])
op.drop_constraint(None, 'service_profiles', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_profiles_organization_id_fkey'), 'service_profiles', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'])
op.create_foreign_key(op.f('service_expertises_service_id_fkey'), 'service_expertises', 'service_profiles', ['service_id'], ['id'])
op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id'])
op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'persons', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id'])
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id'])
op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id'])
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id'])
op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id'])
op.alter_column('organizations', 'org_type',
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
existing_nullable=True)
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('organization_members_person_id_fkey'), 'organization_members', 'persons', ['person_id'], ['id'])
op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id'])
op.alter_column('organization_members', 'role',
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
existing_nullable=True)
op.drop_constraint(None, 'organization_financials', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organization_financials_organization_id_fkey'), 'organization_financials', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id'])
op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'])
op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'])
op.drop_constraint(None, 'documents', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id'])
op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id'])
op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('assets_current_organization_id_fkey'), 'assets', 'organizations', ['current_organization_id'], ['id'])
op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id'])
op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id'])
op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id'])
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'])
op.drop_index(op.f('ix_data_catalog_discovery_vehicle_class'), table_name='catalog_discovery', schema='data')
op.drop_index(op.f('ix_data_catalog_discovery_status'), table_name='catalog_discovery', schema='data')
op.drop_index(op.f('ix_data_catalog_discovery_model'), table_name='catalog_discovery', schema='data')
op.drop_index(op.f('ix_data_catalog_discovery_make'), table_name='catalog_discovery', schema='data')
op.drop_index(op.f('ix_data_catalog_discovery_id'), table_name='catalog_discovery', schema='data')
op.drop_table('catalog_discovery', schema='data')
# ### end Alembic commands ###

View File

@@ -0,0 +1,302 @@
"""full_ecosystem_upgrade_v1_6
Revision ID: e78ce92243ed
Revises: b803fe324ebd
Create Date: 2026-02-16 00:10:37.974994
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = 'e78ce92243ed'
down_revision: Union[str, Sequence[str], None] = 'b803fe324ebd'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('financial_ledger',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('person_id', sa.BigInteger(), nullable=True),
sa.Column('amount', sa.Numeric(precision=18, scale=4), nullable=False),
sa.Column('currency', sa.String(length=10), nullable=True),
sa.Column('transaction_type', sa.String(length=50), nullable=True),
sa.Column('related_agent_id', sa.Integer(), nullable=True),
sa.Column('details', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['person_id'], ['data.persons.id'], ),
sa.ForeignKeyConstraint(['related_agent_id'], ['data.users.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ),
sa.PrimaryKeyConstraint('id'),
schema='data'
)
op.create_table('operational_logs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('action', sa.String(length=100), nullable=False),
sa.Column('resource_type', sa.String(length=50), nullable=True),
sa.Column('resource_id', sa.String(length=100), nullable=True),
sa.Column('details', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True),
sa.Column('ip_address', sa.String(length=45), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id'),
schema='data'
)
op.create_index(op.f('ix_data_operational_logs_id'), 'operational_logs', ['id'], unique=False, schema='data')
op.create_table('security_audit_logs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('action', sa.String(length=50), nullable=True),
sa.Column('actor_id', sa.Integer(), nullable=True),
sa.Column('target_id', sa.Integer(), nullable=True),
sa.Column('confirmed_by_id', sa.Integer(), nullable=True),
sa.Column('is_critical', sa.Boolean(), nullable=True),
sa.Column('payload_before', sa.JSON(), nullable=True),
sa.Column('payload_after', sa.JSON(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['actor_id'], ['data.users.id'], ),
sa.ForeignKeyConstraint(['confirmed_by_id'], ['data.users.id'], ),
sa.ForeignKeyConstraint(['target_id'], ['data.users.id'], ),
sa.PrimaryKeyConstraint('id'),
schema='data'
)
op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey')
op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey')
op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey')
op.drop_constraint(op.f('asset_assignments_branch_id_fkey'), 'asset_assignments', type_='foreignkey')
op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_assignments', 'branches', ['branch_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey')
op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey')
op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey')
op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey')
op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey')
op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey')
op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey')
op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey')
op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey')
op.drop_constraint(op.f('assets_current_organization_id_fkey'), 'assets', type_='foreignkey')
op.create_foreign_key(None, 'assets', 'organizations', ['current_organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey')
op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('branches_address_id_fkey'), 'branches', type_='foreignkey')
op.drop_constraint(op.f('branches_organization_id_fkey'), 'branches', type_='foreignkey')
op.create_foreign_key(None, 'branches', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'branches', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey')
op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey')
op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey')
op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('org_sales_assignments_organization_id_fkey'), 'org_sales_assignments', type_='foreignkey')
op.drop_constraint(op.f('org_sales_assignments_agent_user_id_fkey'), 'org_sales_assignments', type_='foreignkey')
op.create_foreign_key(None, 'org_sales_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'org_sales_assignments', 'users', ['agent_user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey')
op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey')
op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('organization_financials_organization_id_fkey'), 'organization_financials', type_='foreignkey')
op.create_foreign_key(None, 'organization_financials', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.alter_column('organization_members', 'role',
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
existing_nullable=True)
op.drop_constraint(op.f('organization_members_person_id_fkey'), 'organization_members', type_='foreignkey')
op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey')
op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey')
op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organization_members', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
op.alter_column('organizations', 'org_type',
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
existing_nullable=True)
op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey')
op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey')
op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('pending_actions_requester_id_fkey'), 'pending_actions', type_='foreignkey')
op.drop_constraint(op.f('pending_actions_approver_id_fkey'), 'pending_actions', type_='foreignkey')
op.create_foreign_key(None, 'pending_actions', 'users', ['requester_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'pending_actions', 'users', ['approver_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey')
op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey')
op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey')
op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', type_='foreignkey')
op.drop_constraint(op.f('service_expertises_service_id_fkey'), 'service_expertises', type_='foreignkey')
op.create_foreign_key(None, 'service_expertises', 'service_profiles', ['service_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_profiles_organization_id_fkey'), 'service_profiles', type_='foreignkey')
op.create_foreign_key(None, 'service_profiles', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey')
op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('social_accounts_user_id_fkey'), 'social_accounts', type_='foreignkey')
op.create_foreign_key(None, 'social_accounts', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
op.add_column('system_parameters', sa.Column('category', sa.String(), server_default='general', nullable=True))
op.add_column('system_parameters', sa.Column('last_modified_by', sa.String(), nullable=True))
op.create_index(op.f('ix_data_system_parameters_category'), 'system_parameters', ['category'], unique=False, schema='data')
op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey')
op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey')
op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey')
op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey')
op.drop_constraint(op.f('users_current_sales_agent_id_fkey'), 'users', type_='foreignkey')
op.drop_constraint(op.f('users_referred_by_id_fkey'), 'users', type_='foreignkey')
op.create_foreign_key(None, 'users', 'users', ['referred_by_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'users', 'users', ['current_sales_agent_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data')
op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey')
op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey')
op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'])
op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('users_referred_by_id_fkey'), 'users', 'users', ['referred_by_id'], ['id'])
op.create_foreign_key(op.f('users_current_sales_agent_id_fkey'), 'users', 'users', ['current_sales_agent_id'], ['id'])
op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id'])
op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id'])
op.drop_index(op.f('ix_data_system_parameters_category'), table_name='system_parameters', schema='data')
op.drop_column('system_parameters', 'last_modified_by')
op.drop_column('system_parameters', 'category')
op.drop_constraint(None, 'social_accounts', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('social_accounts_user_id_fkey'), 'social_accounts', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id'])
op.drop_constraint(None, 'service_profiles', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_profiles_organization_id_fkey'), 'service_profiles', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('service_expertises_service_id_fkey'), 'service_expertises', 'service_profiles', ['service_id'], ['id'])
op.create_foreign_key(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'])
op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id'])
op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'persons', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id'])
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id'])
op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id'])
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id'])
op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id'])
op.alter_column('organizations', 'org_type',
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
existing_nullable=True)
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id'])
op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('organization_members_person_id_fkey'), 'organization_members', 'persons', ['person_id'], ['id'])
op.alter_column('organization_members', 'role',
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
existing_nullable=True)
op.drop_constraint(None, 'organization_financials', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('organization_financials_organization_id_fkey'), 'organization_financials', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'])
op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id'])
op.drop_constraint(None, 'org_sales_assignments', schema='data', type_='foreignkey')
op.drop_constraint(None, 'org_sales_assignments', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('org_sales_assignments_agent_user_id_fkey'), 'org_sales_assignments', 'users', ['agent_user_id'], ['id'])
op.create_foreign_key(op.f('org_sales_assignments_organization_id_fkey'), 'org_sales_assignments', 'organizations', ['organization_id'], ['id'])
op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'])
op.drop_constraint(None, 'documents', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id'])
op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id'])
op.drop_constraint(None, 'branches', schema='data', type_='foreignkey')
op.drop_constraint(None, 'branches', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('branches_organization_id_fkey'), 'branches', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('branches_address_id_fkey'), 'branches', 'addresses', ['address_id'], ['id'])
op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('assets_current_organization_id_fkey'), 'assets', 'organizations', ['current_organization_id'], ['id'])
op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id'])
op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id'])
op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id'])
op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id'])
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('asset_assignments_branch_id_fkey'), 'asset_assignments', 'branches', ['branch_id'], ['id'])
op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id'])
op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id'])
op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey')
op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'])
op.drop_table('security_audit_logs', schema='data')
op.drop_index(op.f('ix_data_operational_logs_id'), table_name='operational_logs', schema='data')
op.drop_table('operational_logs', schema='data')
op.drop_table('financial_ledger', schema='data')
# ### end Alembic commands ###

32
backend/seed_discovery.py Normal file
View File

@@ -0,0 +1,32 @@
import asyncio
import httpx
from sqlalchemy import text
from app.db.session import SessionLocal
async def seed():
print("🚀 RDW Márka-felfedezés indul...")
url = "https://opendata.rdw.nl/resource/m9d7-ebf2.json?$select=distinct%20merk&$limit=50000"
async with httpx.AsyncClient() as client:
resp = await client.get(url, timeout=60)
if resp.status_code != 200:
print(f"❌ Hiba: {resp.status_code}")
return
makes = resp.json()
print(f"📦 {len(makes)} márkát találtam. Mentés...")
async with SessionLocal() as db:
for item in makes:
m = item['merk'].upper()
# ON CONFLICT: Ha már benne van (pl. n8n betette), ne legyen hiba
await db.execute(text("""
INSERT INTO data.catalog_discovery (make, model, source, status)
VALUES (:m, 'ALL', 'global_seed', 'pending')
ON CONFLICT DO NOTHING
"""), {"m": m})
await db.commit()
print("✅ Kész! A discovery tábla felöltve az összes EU-s márkával.")
if __name__ == "__main__":
asyncio.run(seed())

80
docker-compose.yml Executable file → Normal file
View File

@@ -1,5 +1,5 @@
services: services:
# 1. MIGRÁCIÓ (Adatbázis szerkezet frissítése) # 1. MIGRÁCIÓ
migrate: migrate:
build: build:
context: ./backend context: ./backend
@@ -7,7 +7,7 @@ services:
container_name: service_finder_migrate container_name: service_finder_migrate
env_file: .env env_file: .env
volumes: volumes:
- ./backend:/app # Ez tartalmazza az alembic.ini-t és a migrations mappát is! - ./backend:/app
environment: environment:
PYTHONPATH: /app PYTHONPATH: /app
DATABASE_URL: ${MIGRATION_DATABASE_URL} DATABASE_URL: ${MIGRATION_DATABASE_URL}
@@ -15,7 +15,7 @@ services:
networks: networks:
- default - default
- shared_db_net - shared_db_net
restart: "no" restart: "no" # Ez így helyes, lefut és megáll.
# 2. BACKEND API # 2. BACKEND API
service_finder_api: service_finder_api:
@@ -24,20 +24,14 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: service_finder_api container_name: service_finder_api
env_file: .env env_file: .env
volumes:
- ./backend:/app
- /mnt/nas/app_data:/mnt/nas/app_data # Központi NAS elérés
- ./static_previews:/app/static/previews # Lokális SSD gyorsítótár
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips="*"
ports: ports:
- "8000:8000" - "8000:8000"
volumes:
- ./backend:/app
- /mnt/nas/app_data:/mnt/nas/app_data
- ./static_previews:/app/static/previews
environment: environment:
PYTHONPATH: /app PYTHONPATH: /app
DATABASE_URL: ${DATABASE_URL}
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}
MINIO_ENDPOINT: ${MINIO_ENDPOINT}
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
depends_on: depends_on:
migrate: migrate:
condition: service_completed_successfully condition: service_completed_successfully
@@ -50,25 +44,19 @@ services:
- shared_db_net - shared_db_net
restart: unless-stopped restart: unless-stopped
# 3. MINIO (NAS-ra ment) # 3. MINIO
minio: minio:
image: minio/minio image: minio/minio
container_name: service_finder_minio container_name: service_finder_minio
env_file: .env env_file: .env
command: server /data --console-address ":9001" command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes: volumes:
- /mnt/nas/app_data/minio_data:/data - /mnt/nas/app_data/minio_data:/data
ports:
- "9000:9000"
- "9001:9001"
networks: networks:
- default - default
restart: unless-stopped restart: unless-stopped
# 4. REDIS (Lokális cache) # 4. REDIS
redis: redis:
image: redis:alpine image: redis:alpine
container_name: service_finder_redis container_name: service_finder_redis
@@ -79,15 +67,13 @@ services:
restart: unless-stopped restart: unless-stopped
# 5. FRONTEND # 5. FRONTEND
service_finder_frontend: service_frontend: # Rövidített szerviznév
build: build:
context: ./frontend context: ./frontend
container_name: service_finder_frontend container_name: service_finder_frontend
env_file: .env env_file: .env
ports: ports:
- "3001:80" - "3001:80"
environment:
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}
networks: networks:
- default - default
depends_on: depends_on:
@@ -95,15 +81,14 @@ services:
condition: service_started condition: service_started
restart: unless-stopped restart: unless-stopped
# Katalógus felderítő robot # 6. KATALÓGUS ROBOT (A mi kis felfedezőnk)
catalog_robot: catalog_robot:
build: ./backend build: ./backend
container_name: service_finder_robot_catalog container_name: service_finder_robot_catalog
command: python -m app.workers.catalog_robot command: python -m app.workers.catalog_robot
volumes: volumes:
- ./backend:/app - ./backend:/app
env_file: env_file: .env
- .env
depends_on: depends_on:
migrate: migrate:
condition: service_completed_successfully condition: service_completed_successfully
@@ -112,23 +97,14 @@ services:
- shared_db_net - shared_db_net
restart: always restart: always
# Szerviz vadász robot (Robot 2.7) # 7. SERVICE HUNTER
service_hunter: service_hunter:
build: ./backend build: ./backend
container_name: service_finder_robot_hunter container_name: service_finder_robot_hunter
command: python -m app.workers.service_hunter command: python -m app.workers.service_hunter
volumes: volumes:
- ./backend:/app - ./backend:/app
- ./backend/app/workers/local_services.csv:/app/app/workers/local_services.csv env_file: .env
environment:
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
# JAVÍTVA: shared-postgres lett a gépnév a 'db' helyett!
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@shared-postgres:5432/${POSTGRES_DB}
env_file:
- .env
dns:
- 8.8.8.8
- 1.1.1.1
depends_on: depends_on:
migrate: migrate:
condition: service_completed_successfully condition: service_completed_successfully
@@ -137,7 +113,7 @@ services:
- shared_db_net - shared_db_net
restart: always restart: always
# --- ÚJ: n8n AUTOMATIZÁCIÓ --- # 8. n8n AUTOMATIZÁCIÓ
n8n: n8n:
image: n8nio/n8n:latest image: n8nio/n8n:latest
container_name: service_finder_n8n container_name: service_finder_n8n
@@ -147,13 +123,8 @@ services:
env_file: .env env_file: .env
environment: environment:
- N8N_HOST=0.0.0.0 - N8N_HOST=0.0.0.0
- N8N_PORT=5678
- N8N_PROTOCOL=http
- N8N_SECURE_COOKIE=false # <--- EZ JAVÍTJA A BELÉPÉSI HIBÁT!
- DB_TYPE=postgresdb - DB_TYPE=postgresdb
- DB_POSTGRESDB_DATABASE=n8n_internal
- DB_POSTGRESDB_HOST=n8n_db - DB_POSTGRESDB_HOST=n8n_db
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_USER=n8n_admin - DB_POSTGRESDB_USER=n8n_admin
- DB_POSTGRESDB_PASSWORD=${N8N_DB_PASSWORD} - DB_POSTGRESDB_PASSWORD=${N8N_DB_PASSWORD}
volumes: volumes:
@@ -164,7 +135,6 @@ services:
depends_on: depends_on:
- n8n_db - n8n_db
# n8n belső meta-adatbázisa
n8n_db: n8n_db:
image: postgres:15-alpine image: postgres:15-alpine
container_name: service_finder_n8n_db container_name: service_finder_n8n_db
@@ -178,18 +148,32 @@ services:
networks: networks:
- default - default
# Browserless - A robot "szeme" (Központi 3005-ös porton) # 9. BROWSERLESS
browserless: browserless:
image: browserless/chrome:latest image: browserless/chrome:latest
container_name: service_finder_browserless container_name: service_finder_browserless
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3005:3000" - "3005:3000"
environment:
- MAX_CONCURRENT_SESSIONS=10
networks: networks:
- default - default
# 10. Technikai adatok dúsítása (kW, ccm, üzemanyag)
enricher_robot:
build: ./backend
container_name: service_finder_robot_enricher
command: python -m app.workers.technical_enricher
volumes:
- ./backend:/app
env_file: .env
depends_on:
migrate:
condition: service_completed_successfully
networks:
- default
- shared_db_net
restart: always
networks: networks:
default: default:
driver: bridge driver: bridge

198
docker-compose_1.yml Executable file
View File

@@ -0,0 +1,198 @@
services:
# 1. MIGRÁCIÓ (Adatbázis szerkezet frissítése)
migrate:
build:
context: ./backend
dockerfile: Dockerfile
container_name: service_finder_migrate
env_file: .env
volumes:
- ./backend:/app # Ez tartalmazza az alembic.ini-t és a migrations mappát is!
environment:
PYTHONPATH: /app
DATABASE_URL: ${MIGRATION_DATABASE_URL}
command: ["bash", "-lc", "alembic upgrade head"]
networks:
- default
- shared_db_net
restart: "no"
# 2. BACKEND API
service_finder_api:
build:
context: ./backend
dockerfile: Dockerfile
container_name: service_finder_api
env_file: .env
volumes:
- ./backend:/app
- /mnt/nas/app_data:/mnt/nas/app_data # Központi NAS elérés
- ./static_previews:/app/static/previews # Lokális SSD gyorsítótár
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips="*"
ports:
- "8000:8000"
environment:
PYTHONPATH: /app
DATABASE_URL: ${DATABASE_URL}
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}
MINIO_ENDPOINT: ${MINIO_ENDPOINT}
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
depends_on:
migrate:
condition: service_completed_successfully
minio:
condition: service_started
redis:
condition: service_started
networks:
- default
- shared_db_net
restart: unless-stopped
# 3. MINIO (NAS-ra ment)
minio:
image: minio/minio
container_name: service_finder_minio
env_file: .env
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- /mnt/nas/app_data/minio_data:/data
ports:
- "9000:9000"
- "9001:9001"
networks:
- default
restart: unless-stopped
# 4. REDIS (Lokális cache)
redis:
image: redis:alpine
container_name: service_finder_redis
volumes:
- /mnt/nas/app_data/redis_data:/data
networks:
- default
restart: unless-stopped
# 5. FRONTEND
service_finder_frontend:
build:
context: ./frontend
container_name: service_finder_frontend
env_file: .env
ports:
- "3001:80"
environment:
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}
networks:
- default
depends_on:
service_finder_api:
condition: service_started
restart: unless-stopped
# Katalógus felderítő robot
catalog_robot:
build: ./backend
image: service_finder-catalog_robot
container_name: service_finder_robot_catalog
command: python -m app.workers.catalog_robot
volumes:
- ./backend:/app
env_file:
- .env
depends_on:
migrate:
condition: service_completed_successfully
networks:
- default
- shared_db_net
restart: always
# Szerviz vadász robot (Robot 2.7)
service_hunter:
build: ./backend
container_name: service_finder_robot_hunter
command: python -m app.workers.service_hunter
volumes:
- ./backend:/app
- ./backend/app/workers/local_services.csv:/app/app/workers/local_services.csv
environment:
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
# JAVÍTVA: shared-postgres lett a gépnév a 'db' helyett!
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@shared-postgres:5432/${POSTGRES_DB}
env_file:
- .env
dns:
- 8.8.8.8
- 1.1.1.1
depends_on:
migrate:
condition: service_completed_successfully
networks:
- default
- shared_db_net
restart: always
# --- ÚJ: n8n AUTOMATIZÁCIÓ ---
n8n:
image: n8nio/n8n:latest
container_name: service_finder_n8n
restart: unless-stopped
ports:
- "5678:5678"
env_file: .env
environment:
- N8N_HOST=0.0.0.0
- N8N_PORT=5678
- N8N_PROTOCOL=http
- N8N_SECURE_COOKIE=false # <--- EZ JAVÍTJA A BELÉPÉSI HIBÁT!
- DB_TYPE=postgresdb
- DB_POSTGRESDB_DATABASE=n8n_internal
- DB_POSTGRESDB_HOST=n8n_db
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_USER=n8n_admin
- DB_POSTGRESDB_PASSWORD=${N8N_DB_PASSWORD}
volumes:
- ./n8n/data:/home/node/.n8n
networks:
- default
- shared_db_net
depends_on:
- n8n_db
# n8n belső meta-adatbázisa
n8n_db:
image: postgres:15-alpine
container_name: service_finder_n8n_db
restart: unless-stopped
environment:
- POSTGRES_USER=n8n_admin
- POSTGRES_PASSWORD=${N8N_DB_PASSWORD}
- POSTGRES_DB=n8n_internal
volumes:
- ./n8n/db_data:/var/lib/postgresql/data
networks:
- default
# Browserless - A robot "szeme" (Központi 3005-ös porton)
browserless:
image: browserless/chrome:latest
container_name: service_finder_browserless
restart: unless-stopped
ports:
- "3005:3000"
environment:
- MAX_CONCURRENT_SESSIONS=10
networks:
- default
networks:
default:
driver: bridge
shared_db_net:
external: true

View File

@@ -133,3 +133,34 @@ A rendszer támogatja a "Ghost Person" (Árnyék személy) entitásokat.
- **Ghost Person:** Olyan `data.persons` rekord, amelyet a Robot 2 hozott létre nyilvános adatok (pl. cégjegyzék) alapján. - **Ghost Person:** Olyan `data.persons` rekord, amelyet a Robot 2 hozott létre nyilvános adatok (pl. cégjegyzék) alapján.
- **Identity Linkage:** Regisztrációkor a `AuthService.complete_kyc` kötelezően ellenőrzi a meglévő Ghost rekordokat (Adószám/Név egyezés). - **Identity Linkage:** Regisztrációkor a `AuthService.complete_kyc` kötelezően ellenőrzi a meglévő Ghost rekordokat (Adószám/Név egyezés).
- **Merge Action:** Találat esetén a rendszer összefűzi a technikai User fiókot a Ghost Person rekorddal, aktiválja a jogosultságokat, és megszünteti a Ghost státuszt. - **Merge Action:** Találat esetén a rendszer összefűzi a technikai User fiókot a Ghost Person rekorddal, aktiválja a jogosultságokat, és megszünteti a Ghost státuszt.
## 2. The Dual Entity Model (Person vs. User)
A rendszer alapja a természetes személy (**Person**) és a felhasználói fiók (**User**) szigorú szétválasztása az adatbiztonság és az üzleti folytonosság érdekében.
### 2.1 Person (A DNS - "Az Örök Személy")
A `persons` tábla rekordja soha nem törlődik teljesen (GDPR esetén anonimizálódik), így biztosítva a rendszer memóriáját.
* **Identity Hash:** Egyedi SHA256 lenyomat (`normalized(name + mother + birth_place + birth_date)`), amely megakadályozza a multi-account visszaéléseket és felismeri a visszatérő felhasználókat.
* **Örök Adatok:**
* `lifetime_xp`: A valaha szerzett összes tapasztalati pont.
* `penalty_points`: A büntetési szint (0-3). Ez nem nullázódik új regisztrációval!
* `social_reputation`: A közösségi megbízhatósági index (1.00 = 100%).
* `is_sales_agent`: Jogosult-e jutalékra.
### 2.2 User (A Kulcs - "A Munkamenet")
A `users` tábla a belépési pont. Törölhető, eldobható, újraregisztrálható.
* **Kapcsolat:** Minden User egyetlen Person-höz tartozik (`person_id`).
* **Időkorlátos Jogok:**
* `subscription_plan`: FREE / PREMIUM / VIP.
* `subscription_expires_at`: A prémium funkciók lejárata.
* **Sales Kapcsolat:**
* `referral_code`: Saját meghívó kód.
* `current_sales_agent_id`: Ki kapja a "Farming" jutalékot ez után a felhasználó után.
### 2.3 Jogosultsági Szintek (Scope-Based RBAC)
A jogosultság nem csak szerepkör (Role), hanem hatókör (Scope) alapú:
1. **Global:** Superadmin.
2. **Country:** Országos Admin (pl. HU).
3. **Region:** Régiós Admin (pl. Pest megye).
4. **Entity:** Szerviz Tulajdonos (saját cég).
5. **Individual:** Átlagfelhasználó (saját adatok).

View File

@@ -202,3 +202,12 @@ A rendszer az adatintegritás és a sebesség érdekében hibrid modellt haszná
- **data.organization_financials:** Éves gazdasági adatok (árbevétel, profit, létszám) tárolása historikus elemzéshez. - **data.organization_financials:** Éves gazdasági adatok (árbevétel, profit, létszám) tárolása historikus elemzéshez.
- **data.service_profiles.specialization_tags:** JSONB mező a szigorú szakmai szűréshez (pl. márkák, specifikus javítási típusok). - **data.service_profiles.specialization_tags:** JSONB mező a szigorú szakmai szűréshez (pl. márkák, specifikus javítási típusok).
- **data.service_profiles.google_place_id:** Külső validációs kulcs a Google Places API-hoz. - **data.service_profiles.google_place_id:** Külső validációs kulcs a Google Places API-hoz.
### Identity & Economy Module (v1.6+)
* **`data.persons`**: Természetes személyek, Identity Hash, Örök XP/Büntetés.
* **`data.users`**: Login fiókok, Előfizetési idő, Sales kapcsolatok.
* **`data.wallets`**: 3-as osztású egyenleg (`earned`, `purchased`, `coins`).
* **`data.financial_ledger`**: Pénzügyi tranzakciók főkönyve.
* **`data.security_audit_logs`**: Biztonsági események és 4-szem jóváhagyások.
* **`data.org_sales_assignments`**: Cég-Üzletkötő kapcsolat (Farming jog).

View File

@@ -114,3 +114,23 @@ A meghívók érvényessége a típustól függ:
### 5.3 Biztonság ### 5.3 Biztonság
* A meghívó link tartalmaz egy aláírt JWT tokent, amely rögzíti a `target_org_id`-t (melyik flottába hívjuk) és a `role`-t (pl. sofőr). * A meghívó link tartalmaz egy aláírt JWT tokent, amely rögzíti a `target_org_id`-t (melyik flottába hívjuk) és a `role`-t (pl. sofőr).
* A kód felhasználása után a link érvénytelenné válik (One-time use). * A kód felhasználása után a link érvénytelenné válik (One-time use).
## 1. Háromlépcsős Onboarding (v1.5)
### 1.1 Step 1: Lite Registration
- Technikai `User` létrehozása (inaktív). Email ellenőrzés indítása.
### 1.2 Step 2: Individual Setup (Privát Identitás)
- **Cél:** A természetes személy (`Person`) és privát szférájának rögzítése.
- **Művelet:** - `Person` rögzítése/frissítése.
- Privát `Organization` létrehozása (`org_type='individual'`, `is_ownership_transferable=False`).
- **Központi Telephely (Main Branch)** létrehozása a lakcím alapján.
- Privát Flotta és Wallet inicializálása.
### 1.3 Step 3: Business Setup (Céges Identitás)
- **Cél:** Államilag nyilvántartott gazdasági egység rögzítése.
- **Művelet:**
- Adószám bekérése + VIES/Cégjegyzék ellenőrzés.
- Új, különálló `Organization` létrehozása (`org_type='business'`, `is_ownership_transferable=True`).
- Székhely rögzítése mint **Main Branch**.
- Opcionális további telephelyek rögzítése.

View File

@@ -111,3 +111,30 @@ A rendszer támogatja a dinamikus árazást a kozmetikai elemeknél is.
### 5.3 Bővíthetőség ### 5.3 Bővíthetőség
Új elem hozzáadásához **nem kell kódot módosítani**, csak a `shop_catalog` JSON-t kell frissíteni az Admin felületen. A kliens alkalmazás (App/Web) dinamikusan tölti be a kínálatot ebből a JSON-ből. Új elem hozzáadásához **nem kell kódot módosítani**, csak a `shop_catalog` JSON-t kell frissíteni az Admin felületen. A kliens alkalmazás (App/Web) dinamikusan tölti be a kínálatot ebből a JSON-ből.
## 3. The Triple Wallet System (3-as Pénztárca)
A `wallets` tábla három elkülönített alszámlát kezel a transzparencia érdekében:
| Alszámla | Kód | Forrás | Felhasználás | Átváltható? |
| :--- | :--- | :--- | :--- | :--- |
| **Earned Credits** | `earned_credits` | Munka (validálás), Referral, Jutalék | Prémium funkciók, Szolgáltatás vásárlás | IGEN |
| **Purchased Credits** | `purchased_credits` | Bankkártyás feltöltés (Stripe) | Prémium funkciók, Szolgáltatás vásárlás | IGEN |
| **Service Coins** | `service_coins` | B2B Csomagok, Partneri jóváírás | **Kizárólag** Hirdetés, Kiemelés, Szponzoráció | **NEM** |
## 4. Sales Commission Model (Hunting & Farming)
Az üzletkötők ösztönzése két fázisban történik:
### 4.1 Hunting (Vadász) Jutalék
* **Esemény:** Új fizető ügyfél behozatala (első tranzakció).
* **Mérték:** 10% (Alapértelmezett `system_parameter`).
* **Jóváírás:** Azonnal, `earned_credits` formájában.
### 4.2 Farming (Gazda) Jutalék
* **Esemény:** Meglévő ügyfél havidíj megújítása.
* **Mérték:** 5% (Alapértelmezett `system_parameter`).
* **Átruházhatóság:** A jutalékot nem a User, hanem az `OrganizationSalesAssignment` tábla aktív rekordja határozza meg. Ha az üzletkötő kilép, a portfóliója (és a Farming joga) átruházható egy másik ügynökre.
### 4.3 Financial Ledger (Pénzügyi Napló)
Minden tranzakció (Vásárlás, Jutalék jóváírás, Költés) bekerül a `financial_ledger` táblába, amely megmásíthatatlan (Append-only) és tartalmazza a `related_agent_id`-t a visszakövethetőségért.

View File

@@ -366,3 +366,119 @@ A rendszer most már képes egyetlen KYC folyamat alatt aktiválni a felhasznál
### 🔜 Következő Lépések ### 🔜 Következő Lépések
- Gamification és Moderátori felület (Admin UI) tervezése az adatok tisztítására. - Gamification és Moderátori felület (Admin UI) tervezése az adatok tisztítására.
- Logikai szabályrendszer (Business Rules) véglegesítése a "Robot vs. Ember" adatkonfliktusokra. - Logikai szabályrendszer (Business Rules) véglegesítése a "Robot vs. Ember" adatkonfliktusokra.
# Changelog - Service Finder Project
**Dátum:** 2026-02-15
**Verzió:** Backend v1.9.8 / Robot v1.0.7 (Deep Hunter)
**Fókusz:** Adatbázis séma bővítése, RDW API integráció stabilizálása, Multi-vehicle támogatás.
## 🏛️ Adatbázis és Architektúra (Alembic & SQLAlchemy)
### Hozzáadva
- **Új Migráció (`enrich_catalog_technical_schema`):**
- `power_kw` (Integer, Indexed): Teljesítmény tárolása.
- `engine_capacity` (Integer, Indexed): Hengerűrtartalom (ccm).
- `max_weight_kg` (Integer): Megengedett legnagyobb össztömeg.
- `euro_class` (String): Környezetvédelmi besorolás.
- **Új Migráció (`add_axles_and_body_type`):**
- `axle_count` (Integer): Tengelyek száma (Teherautókhoz/Kamionokhoz).
- `body_type` (String): Felépítmény (pl. Sedan, Box, Camper).
- **Modell Frissítés (`asset.py`):**
- Az `AssetCatalog` osztály szinkronba hozva az új DB sémával.
- `UniqueConstraint` és indexek optimalizálása a gyors kereséshez.
### Javítva
- **Alembic Syntax Error:** Javítva a `ddef` elírás a migrációs fájlban.
- **Column Duplication:** Javítva az `axle_count` duplikált létrehozási kísérlete a második migrációban.
## 🤖 Robot / Worker (Data Ingestion)
### Módosítva
- **Robot Upgrade (v1.0.2 -> v1.0.7 Deep Hunter):**
- **License Plate Bridge (Rendszám-híd):** Új stratégia az API 400-as hibák megkerülésére. A robot mostantól:
1. Lekéri az alapadatokat (`m9d7-ebf2`).
2. Kivesz egy minta rendszámot.
3. Ezzel a rendszámmal lekérdezi a `FUEL`, `AXLE` és `BODY` táblákat.
- **Pagination (Lapozás):** `$offset` támogatás beépítése, így a robot képes 50.000+ rekordos márkákat is végigolvasni.
- **Camper Detection:** Automatikus lakóautó (`camper`) kategória felismerés a "kampeerwagen" kulcsszó alapján.
- **Category Mapping:** Angol nyelvű kategóriák (Car, Truck, Motorcycle, Agricultural) kényszerítése.
### Javítva
- **RDW API 400 Bad Request:** Megoldva a `merk` vs `merknaam` paraméterek eltérésének kezelésével (átállás a fő táblára).
- **AttributeError:** Javítva a hibás `TECH_API_URL` hivatkozás.
## 💾 Adat (Seeding & SQL)
- **Grand Seeder v2:**
- SQL szkript létrehozva a világmárkák (Toyota, BMW, Scania, John Deere, stb.) tömeges betöltésére.
- `model` mező feltöltése `'ALL'` értékkel a `NOT NULL` kényszer miatt.
- Státuszok visszaállítása `pending`-re a teljes újradolgozáshoz.
# CHANGELOG - 2026.02.16 (Architectural Overhaul: Identity & Economy Engine)
## 🏆 Napi Összefoglaló
A mai napon alapjaiban strukturáltuk át az identitáskezelést (`Identity`), a jogosultsági rendszert (`RBAC`) és a gazdasági motort (`Economy`). Bevezetésre került a "Dual Entity" modell (Person vs. User), a 3-szintű Wallet rendszer, valamint a "Hunting & Farming" üzletkötői jutalékrendszer alapjai. A biztonságot a 4-szem elvű (Four-Eyes Principle) audit naplózás garantálja.
---
## 🏛️ 1. Architektúra és Logika (Master Book Updates)
### A. Identitás Filozófiája (The Dual Entity Rule)
* **Person (A DNS):** A természetes személy, aki "örök". Nem törlődik GDPR törléskor sem, csak anonimizálódik.
* Tárolja: `lifetime_xp` (életút pontok), `penalty_points` (büntetések 0-3 szint), `social_reputation`.
* **Identity Hash:** Egyedi SHA256 lenyomat (Kisbetűsített Anyja neve + Születési hely + Idő) a duplikációk és visszaélések ellen.
* **User (A Kulcs):** A belépési fiók. Bármikor törölhető/eldobható.
* Kapcsolódik a Person-höz.
* Tárolja: `subscription_plan`, `is_vip`, `session_data`.
### B. Gazdasági Modell (The Triple Wallet)
A pénztárcát (`Wallet`) három, szigorúan elkülönített alszámlára bontottuk:
1. **Earned Credits:** Munkával (validálás) és Referral jutalékból szerzett. (Beváltható Prémiumra).
2. **Purchased Credits:** Valódi pénzért vásárolt egyenleg. (Beváltható Prémiumra).
3. **Service Coins:** B2B egység. Kizárólag hirdetésre és kiemelésre fordítható. (NEM váltható Prémiumra).
### C. Üzletkötői Rendszer (Hunting & Farming)
* **Hunting (Vadász) Jutalék:** Egyszeri jutalék az első behozatalért (tervezett: 10%).
* **Farming (Gazda) Jutalék:** Folyamatos jutalék a havidíjakból (tervezett: 5%).
* **Átruházhatóság:** A Farming jog nem az üzletkötőhöz, hanem a Cég-Üzletkötő kapcsolathoz (`OrganizationSalesAssignment`) kötődik. Ha az üzletkötő kilép, a portfóliója (és a jutalék) átruházható másra.
### D. Biztonság (Audit & 4-Eyes)
* **Operational Log:** Napi üzemi események (pl. jármű rögzítés).
* **Financial Ledger:** Minden pénzmozgás (Kredit/Coin/HUF) központi főkönyve.
* **Security Audit Log:** Kiemelt biztonsági események (pl. VIP státusz adása).
* **4-szem elv:** Kritikusan érzékeny műveleteknél kötelező egy második admin jóváhagyása (`confirmed_by_id`).
---
## 🛠️ 2. Adatbázis és Modell Változások
### Új/Módosított Táblák (`data` séma)
| Tábla | Változás | Leírás |
| :--- | :--- | :--- |
| **persons** | **UPDATE** | Új mezők: `identity_hash`, `lifetime_xp`, `penalty_points`, `social_reputation`, `is_sales_agent`. |
| **users** | **UPDATE** | Új mezők: `subscription_expires_at`, `is_vip`, `referral_code`, `current_sales_agent_id`. |
| **wallets** | **REFACTOR** | Régi balance törölve. Új: `earned_credits`, `purchased_credits`, `service_coins`. |
| **org_sales_assignments** | **NEW** | Kapcsolótábla: Melyik cég után ki kapja épp a Farming jutalékot. |
| **financial_ledger** | **NEW** | Pénzügyi tranzakciók megmásíthatatlan naplója. |
| **security_audit_logs** | **NEW** | Adminisztrátori műveletek és 4-szem elv naplózása. |
| **operational_logs** | **NEW** | Általános rendszerhasználati napló. |
---
## 📂 3. Érintett Fájlok Listája (Checklist)
Kérlek, ellenőrizd, hogy ezek a fájlok a legfrissebb verziót tartalmazzák-e a mentésedben:
- [x] **`backend/app/models/identity.py`** (A teljes Person/User/Wallet logika alapja)
- [x] **`backend/app/models/audit.py`** (A Ledger és Security Log definíciók)
- [x] **`backend/app/models/organization.py`** (A SalesAssignment tábla hozzáadása)
- [x] **`backend/app/models/__init__.py`** (Az összes modell regisztrációja az Alembic számára)
- [x] **`backend/app/db/base.py`** (A metadata importok frissítése)
- [x] **`backend/app/core/validators.py`** (Az IdentityNormalizer és Hash generáló logika)
- [x] **`backend/migrations/versions/XXXX_full_ecosystem_upgrade_v1_6.py`** (A generált migrációs fájl)
---
## 🔮 4. Következő Lépések (Roadmap)
1. **Service Réteg Implementálása:** Megírni a logikát, ami ténylegesen számolja a 10/5%-os jutalékot és beírja a `FinancialLedger`-be.
2. **Admin UI:** Felületet készíteni a `system_parameters` (Jutalék szintek) állítására.
3. **Robot v1.8:** A "Ghost" szervizek bekötése az új `Person` logikába (automata `identity_hash` generálás a cégadatokból).

View File

@@ -130,3 +130,25 @@ A `rank: 100` szintű felhasználó (SuperAdmin) az egyetlen, aki:
- **Trigger:** Manuális (Gombnyomás a Dashboardon). - **Trigger:** Manuális (Gombnyomás a Dashboardon).
- **Action:** `data.translations` -> `static/locales/*.json`. - **Action:** `data.translations` -> `static/locales/*.json`.
- **Permission:** SuperAdmin ONLY. - **Permission:** SuperAdmin ONLY.
## 5. Security & Audit Logging
A rendszer két szinten naplózza az eseményeket:
### 5.1 Operational Log (Üzemi Napló)
* **Cél:** Hibakeresés, User Activity követés.
* **Tartalom:** Jármű rögzítés, Adatjavítás, Keresés.
* **Hozzáférés:** Moderátor szinttől felfelé.
### 5.2 Security Audit Log (Biztonsági Napló)
* **Cél:** Visszaélések megelőzése, Jogosultságok védelme.
* **Tartalom:** Rang emelés (Role Change), Kredit manuális jóváírása, VIP státusz adása, Admin belépés.
* **Hozzáférés:** Csak Superadmin és Country Admin (Szigorított).
### 5.3 The "Four-Eyes" Principle (4-Szem Elv)
Kritikus műveletek (pl. egy User `is_vip` státuszának kézi átállítása vagy `penalty_points` törlése) esetén a rendszer:
1. Rögzíti a kérést a `security_audit_logs`-ban.
2. A státusz "Pending" marad.
3. A változás **csak akkor lép életbe**, ha egy MÁSIK Adminisztrátor jóváhagyja azt (`confirmed_by_id` kitöltése).
4. Szuperadmin esetén a `is_critical` flag aktiválódik, és azonnali riasztás megy a többi adminnak.

View File

@@ -0,0 +1,24 @@
# 🏢 23_BRANCH_AND_LOCATION_SPEC (v1.0)
## 1. Telephely (Branch) Logika
A rendszer alapelve, hogy a jogi entitás (Organization) és a fizikai helyszín (Branch) elválik egymástól.
### 1.1 Struktúra
- **Organization:** Jogi egység (Adószám, név).
- **Branch (Telephely):** Konkrét fizikai pont, ahol a szolgáltatás zajlik vagy ahol a flotta állomásozik.
- **Main Branch:** Minden szervezetnek van legalább egy "Fő" telephelye (`is_main=True`).
### 1.2 Kapcsolatok
- **Szerviz:** Az értékelések és a nyitvatartás a `Branch`-hez kötődik.
- **Flotta:** A jármű hozzárendelés (`AssetAssignment`) opcionálisan tartalmaz egy `branch_id`-t, meghatározva a jármű fizikai helyét.
## 2. Részletes Címkezelés
A címeket atomizált formában tároljuk a `data.branches` és `data.addresses` táblákban:
- `postal_code`, `city`
- `street_name`, `street_type` (utca, út, tér)
- `house_number`, `stairwell`, `floor`, `door`
- `hrsz` (Helyrajzi szám külterületi vagy speciális telkekhez)
## 3. Életút Követés (Dual Twin)
- **Törlés:** A telephelyek "Soft Delete" (`is_deleted`) alá esnek.
- **Áthelyezés:** Ha egy telephely megszűnik, a hozzárendelt járművek automatikusan visszaállnak a Szervezet "Main Branch" helyszínére.

View File

@@ -217,3 +217,360 @@
{"__type":"$$EventMessageAudit","id":"ff14b040-c2d8-4f98-8d9a-68789d96ca9f","ts":"2026-02-14T09:13:10.495-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"eRVwBJfXw8ymc6aZ","workflowName":"01 - Dunakeszi Seed Hunter"}} {"__type":"$$EventMessageAudit","id":"ff14b040-c2d8-4f98-8d9a-68789d96ca9f","ts":"2026-02-14T09:13:10.495-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"eRVwBJfXw8ymc6aZ","workflowName":"01 - Dunakeszi Seed Hunter"}}
{"__type":"$$EventMessageAudit","id":"ad2b88bc-5aea-4a57-bc0d-f75b70237e9f","ts":"2026-02-14T09:13:22.220-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"eRVwBJfXw8ymc6aZ","workflowName":"01 - Dunakeszi Seed Hunter"}} {"__type":"$$EventMessageAudit","id":"ad2b88bc-5aea-4a57-bc0d-f75b70237e9f","ts":"2026-02-14T09:13:22.220-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"eRVwBJfXw8ymc6aZ","workflowName":"01 - Dunakeszi Seed Hunter"}}
{"__type":"$$EventMessageAudit","id":"f439a7c2-7aec-4bc5-8ef4-2891aa75072a","ts":"2026-02-14T09:13:32.643-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"eRVwBJfXw8ymc6aZ","workflowName":"01 - Dunakeszi Seed Hunter"}} {"__type":"$$EventMessageAudit","id":"f439a7c2-7aec-4bc5-8ef4-2891aa75072a","ts":"2026-02-14T09:13:32.643-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"eRVwBJfXw8ymc6aZ","workflowName":"01 - Dunakeszi Seed Hunter"}}
{"__type":"$$EventMessageAudit","id":"b03f51f4-38a5-4216-97fc-060228d357b3","ts":"2026-02-14T11:11:03.532-05:00","eventName":"n8n.audit.workflow.created","message":"n8n.audit.workflow.created","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"f7ee61b3-54e6-401b-aab5-81128d453f47","ts":"2026-02-14T11:15:31.558-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"f5d90ffb-677c-4278-9819-0168bf5abb56","ts":"2026-02-14T11:15:34.092-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"5f0b5c1d-2088-4f54-a925-e8760b0af90a","ts":"2026-02-14T11:15:34.851-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"21","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"d4b67302-8ed3-49b4-9dde-21f37bc60cf5","ts":"2026-02-14T11:15:34.851-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"21","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"f4480e67-7b51-4c8d-9088-b5a39d18875e","ts":"2026-02-14T11:15:34.852-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"21","nodeType":"n8n-nodes-base.manualTrigger","nodeName":"When clicking Execute workflow","nodeId":"d90cfc8e-4e86-4893-ae45-e57cf3cf1a98"}}
{"__type":"$$EventMessageNode","id":"ae82527c-6e80-49a8-915c-2119ff1843f7","ts":"2026-02-14T11:15:34.853-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"21","nodeType":"n8n-nodes-base.manualTrigger","nodeName":"When clicking Execute workflow","nodeId":"d90cfc8e-4e86-4893-ae45-e57cf3cf1a98"}}
{"__type":"$$EventMessageWorkflow","id":"eb367c68-079c-4ac7-a3c5-4404fce907f1","ts":"2026-02-14T11:15:34.855-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"21","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"e1136c57-8423-49ad-90b6-d19e35021ccb","ts":"2026-02-14T11:15:35.846-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"22","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"071583ee-734a-4cd4-9f6b-29b0d77f2eeb","ts":"2026-02-14T11:15:35.847-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"22","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"51e6cf75-0cf8-44ab-814e-695c6cb21a64","ts":"2026-02-14T11:15:35.847-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"22","nodeType":"n8n-nodes-base.manualTrigger","nodeName":"When clicking Execute workflow","nodeId":"d90cfc8e-4e86-4893-ae45-e57cf3cf1a98"}}
{"__type":"$$EventMessageNode","id":"cb797990-a264-4619-b61b-e986af3afec5","ts":"2026-02-14T11:15:35.848-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"22","nodeType":"n8n-nodes-base.manualTrigger","nodeName":"When clicking Execute workflow","nodeId":"d90cfc8e-4e86-4893-ae45-e57cf3cf1a98"}}
{"__type":"$$EventMessageWorkflow","id":"22876ee3-57dc-43a8-a942-16f16f6f41c0","ts":"2026-02-14T11:15:35.849-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"22","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"9836c88e-c2de-40bf-bccc-798adb53db35","ts":"2026-02-14T11:15:44.617-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"fd4fd0c9-743f-4d97-a12b-877662ec0e05","ts":"2026-02-14T11:16:02.910-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"3c46ffad-765e-4628-b363-a6871abca2a4","ts":"2026-02-14T11:16:09.335-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"636982ce-739a-427d-b156-3e4bc2df356f","ts":"2026-02-14T11:17:14.755-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"df7edb80-d61d-46c4-b957-0c36622b085b","ts":"2026-02-14T11:17:35.752-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"4351d203-e85a-4893-8017-c3fae8d65b6d","ts":"2026-02-14T11:18:13.197-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"751c7f6c-577d-47b1-8019-9e5c0edecf4d","ts":"2026-02-14T11:18:24.037-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"8d50c983-8b48-400d-99f5-7d953c11d3b7","ts":"2026-02-14T11:18:28.865-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"0f4b0ad4-afa7-4c20-b6e3-c638d5f7482a","ts":"2026-02-14T11:18:49.272-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"23","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"c93aa1a9-fc76-4d43-979b-471593317b4f","ts":"2026-02-14T11:18:49.273-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"23","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"8cb84cae-cae5-4eed-af29-a2f93948d4e3","ts":"2026-02-14T11:18:49.274-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"23","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"1ba901e3-a533-4170-ba71-69b06ce97ce1"}}
{"__type":"$$EventMessageNode","id":"00c076b2-b34f-4726-b30d-07a6092da31e","ts":"2026-02-14T11:18:49.346-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"23","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"1ba901e3-a533-4170-ba71-69b06ce97ce1"}}
{"__type":"$$EventMessageWorkflow","id":"3de894e1-07c3-4826-8044-244c2e5973c0","ts":"2026-02-14T11:18:49.348-05:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"23","success":false,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","lastNodeExecuted":"HTTP Request","errorNodeType":"n8n-nodes-base.httpRequest","errorMessage":"Forbidden - perhaps check your credentials?"}}
{"__type":"$$EventMessageWorkflow","id":"cc6b9419-63db-4a1f-8d9d-ad45d3ba889c","ts":"2026-02-14T11:19:50.235-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"24","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"71c8d27c-6120-4d9e-9e9c-34a0f323c4ad","ts":"2026-02-14T11:19:50.235-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"24","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"37203cad-1a6e-490a-8059-65b2241c4611","ts":"2026-02-14T11:19:50.236-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"24","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"1ba901e3-a533-4170-ba71-69b06ce97ce1"}}
{"__type":"$$EventMessageNode","id":"8d36ece3-aeb5-4db4-bb2e-ea1f28cb371b","ts":"2026-02-14T11:19:50.278-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"24","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"1ba901e3-a533-4170-ba71-69b06ce97ce1"}}
{"__type":"$$EventMessageWorkflow","id":"e73b3650-1367-4198-aa3b-4fc3d785fc7c","ts":"2026-02-14T11:19:50.280-05:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"24","success":false,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","lastNodeExecuted":"HTTP Request","errorNodeType":"n8n-nodes-base.httpRequest","errorMessage":"Forbidden - perhaps check your credentials?"}}
{"__type":"$$EventMessageAudit","id":"09a1987b-4a1c-4985-b3ca-98a6aa50576b","ts":"2026-02-14T11:21:31.809-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"c3e2d415-7933-4091-affa-9197e4ce6a6e","ts":"2026-02-14T11:21:41.829-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"84b18af9-32cd-43a7-b815-32bf0bfe72c6","ts":"2026-02-14T11:24:41.930-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"96b83cc9-4a78-43b4-8e8d-45949f3a62ec","ts":"2026-02-14T11:25:15.936-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"60d240e0-f9ef-4c05-b671-68436b2d9f81","ts":"2026-02-14T11:25:32.598-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"778d1495-3a54-490a-a126-5942f4ec03b7","ts":"2026-02-14T11:25:41.184-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"378803de-cb2c-4f30-bf13-da65d3f728f0","ts":"2026-02-14T11:25:44.477-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"6cff5779-6916-41b2-986f-7781b5410f2b","ts":"2026-02-14T11:25:49.355-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"d0eadcc3-7e3d-48b9-9c89-34b873c5a6df","ts":"2026-02-14T11:25:51.566-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"0bf6fe28-8624-4539-918a-e408d7901ab5","ts":"2026-02-14T11:25:55.564-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"c0caddc6-c61d-4966-b0a7-9936e414014c","ts":"2026-02-14T11:26:05.238-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"d17abd81-96cd-4f17-8c5e-0a3cf7a45aba","ts":"2026-02-14T11:26:06.904-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"853d2cf4-b6e0-4dd0-8677-dbf0adc2bb04","ts":"2026-02-14T11:26:12.924-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"d035437f-c1e2-4dc5-b7e2-de6d68a15e0f","ts":"2026-02-14T11:26:18.866-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"fd9be312-d7fe-4c8d-b0be-0a195406cff7","ts":"2026-02-14T11:26:20.628-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"ca9c208d-054b-4aff-ae11-14d6e7029136","ts":"2026-02-14T11:26:25.916-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"602190d4-f8d8-417c-9a17-1a72a8ba6941","ts":"2026-02-14T11:26:31.857-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"35d27eb7-44d1-4374-af6e-a35d45499000","ts":"2026-02-14T11:32:44.131-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"19b000a2-c46b-4286-809c-01f290ff0fd5","ts":"2026-02-14T11:32:49.816-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"9df0dd95-2cb1-439b-bbda-293eeb7204f6","ts":"2026-02-14T11:33:04.928-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"25","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"11bf7c33-35f8-4524-ab85-56d7f52631ef","ts":"2026-02-14T11:33:04.929-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"25","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"cad0e034-bde2-46f5-8485-34dbd6c33247","ts":"2026-02-14T11:33:04.930-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"25","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"1ba901e3-a533-4170-ba71-69b06ce97ce1"}}
{"__type":"$$EventMessageNode","id":"014c55f3-8c80-435a-b30f-d818bf6569bd","ts":"2026-02-14T11:33:09.112-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"25","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"1ba901e3-a533-4170-ba71-69b06ce97ce1"}}
{"__type":"$$EventMessageWorkflow","id":"3daf1134-6caa-4c70-af12-d99c0299c1c1","ts":"2026-02-14T11:33:09.116-05:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"25","success":false,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","lastNodeExecuted":"HTTP Request","errorNodeType":"n8n-nodes-base.httpRequest","errorMessage":"Forbidden - perhaps check your credentials?"}}
{"__type":"$$EventMessageAudit","id":"33a7ee00-3087-4727-b5bc-2275096be79d","ts":"2026-02-14T18:31:37.641-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"8819563e-6c91-4fb3-b829-61ebaaf07690","ts":"2026-02-14T18:31:39.572-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"eedce65c-74f0-4be6-a94b-e1669bf1e649","ts":"2026-02-14T18:35:27.417-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"05351c7d-25c2-41da-8369-3adccd8f9b99","ts":"2026-02-14T18:37:01.972-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"8f37e2cd-04ca-4197-a456-8146afe5a838","ts":"2026-02-14T18:37:17.638-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"d232bf5d-3820-474d-908a-930bd42b083d","ts":"2026-02-14T18:37:32.257-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"86bc1bad-9540-4a33-9f21-10aa62d20842","ts":"2026-02-14T18:37:34.508-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"690dd642-5b9a-4350-93de-d87a946c3550","ts":"2026-02-14T18:37:51.192-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"a6111065-dc46-40d0-8f85-e9dcf3a4434b","ts":"2026-02-14T18:37:53.849-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"0ee79dcc-e74e-4bc5-9a03-5cd12f267c97","ts":"2026-02-14T18:38:00.862-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"726d01ee-b17c-4f7a-806c-589aad5d28cc","ts":"2026-02-14T18:39:12.391-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"26","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"fe9907f3-c592-47f1-b386-0466e59e7837","ts":"2026-02-14T18:39:12.392-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"26","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"8c989f96-4e0b-4682-b52f-650652fb68b4","ts":"2026-02-14T18:39:12.393-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"26","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageNode","id":"4c9cae69-038e-4de3-810b-25a3449182aa","ts":"2026-02-14T18:39:12.419-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"26","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageWorkflow","id":"700c1cc8-24b1-4bc8-82fd-bcd3b0ebca29","ts":"2026-02-14T18:39:12.420-05:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"26","success":false,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","lastNodeExecuted":"HTTP Request","errorNodeType":"n8n-nodes-base.httpRequest","errorMessage":"The resource you are requesting could not be found"}}
{"__type":"$$EventMessageAudit","id":"48fb2e4b-ce51-4c8c-96c2-8fe9405eedaf","ts":"2026-02-14T18:39:38.937-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"0d11f1b7-12f1-40c4-a4ce-6def83cbe274","ts":"2026-02-14T18:39:39.949-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"27","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"56cabcab-f888-4bf7-8d33-d3376a68a137","ts":"2026-02-14T18:39:39.950-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"27","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"022b2038-a405-4185-bd66-801d8a49c545","ts":"2026-02-14T18:39:39.951-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"27","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageNode","id":"f3ccf75b-c99b-427c-b33f-edbc888e98a8","ts":"2026-02-14T18:39:39.960-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"27","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageWorkflow","id":"05dad2f5-cd61-4311-9501-bb591dd1c3da","ts":"2026-02-14T18:39:39.963-05:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"27","success":false,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","lastNodeExecuted":"HTTP Request","errorNodeType":"n8n-nodes-base.httpRequest","errorMessage":"The service refused the connection - perhaps it is offline"}}
{"__type":"$$EventMessageAudit","id":"3d1e556e-e177-4fcb-b9b2-f4c8d38df811","ts":"2026-02-14T18:41:03.693-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"bc35223e-9e31-4c55-b0b7-32212efe9118","ts":"2026-02-14T18:41:32.089-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"ee22062b-3f3d-481e-88f7-4546bdaf752c","ts":"2026-02-14T18:42:16.619-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"28","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"dc0c5ab8-e603-497f-a7f9-809bb31f0497","ts":"2026-02-14T18:42:16.620-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"28","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"62fecc92-aa19-4ce6-a590-26ce53d3fec3","ts":"2026-02-14T18:42:16.620-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"28","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageNode","id":"92b72036-ca0f-4e1a-8e7f-8b17dd1cd502","ts":"2026-02-14T18:42:16.667-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"28","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageWorkflow","id":"44a1ae4d-dd2c-48ed-b145-97cbad0a54c9","ts":"2026-02-14T18:42:16.670-05:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"28","success":false,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","lastNodeExecuted":"HTTP Request","errorNodeType":"n8n-nodes-base.httpRequest","errorMessage":"Bad request - please check your parameters"}}
{"__type":"$$EventMessageAudit","id":"c5e0c681-b96c-40d7-a7cd-1a7b2f89e00a","ts":"2026-02-14T18:45:25.350-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"4cb9f7a8-8bb9-438c-99b2-84dad891a158","ts":"2026-02-14T18:45:39.048-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"0345ee18-4791-4f37-9d83-1779604729e9","ts":"2026-02-14T18:45:44.576-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"29","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"a5029189-2c02-48f1-851d-f769da0420bd","ts":"2026-02-14T18:45:44.576-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"29","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"ee3bccb3-c669-4279-b6c1-036d4a79fa22","ts":"2026-02-14T18:45:44.577-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"29","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageNode","id":"6cedb5ca-ec3e-4503-be0b-f683cf7f18c0","ts":"2026-02-14T18:45:44.599-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"29","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageWorkflow","id":"d03f1e6b-40c0-4417-adee-46a01468d44b","ts":"2026-02-14T18:45:44.602-05:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"29","success":false,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","lastNodeExecuted":"HTTP Request","errorNodeType":"n8n-nodes-base.httpRequest","errorMessage":"Bad request - please check your parameters"}}
{"__type":"$$EventMessageAudit","id":"132a094e-3e46-4648-b1e0-c2d7757d5b53","ts":"2026-02-14T18:46:36.631-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"80bf7b06-b1e4-4731-ae95-f72bca141026","ts":"2026-02-14T18:46:40.347-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"1d7a157d-8f2b-4e8d-be32-f89c0fe01bc4","ts":"2026-02-14T18:46:41.323-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"30","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"c8520031-08d3-480c-8b21-dcfaacd312ef","ts":"2026-02-14T18:46:41.324-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"30","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"8e5b741b-8f14-42a7-b2a9-e9d441de62b7","ts":"2026-02-14T18:46:41.324-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"30","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageNode","id":"7413114e-ffd4-45c4-8b05-4deb0284d3b0","ts":"2026-02-14T18:47:12.089-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"30","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageWorkflow","id":"e6ce8afe-f67a-493e-823e-181928e1dec7","ts":"2026-02-14T18:47:12.092-05:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"30","success":false,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","lastNodeExecuted":"HTTP Request","errorNodeType":"n8n-nodes-base.httpRequest","errorMessage":"Bad request - please check your parameters"}}
{"__type":"$$EventMessageAudit","id":"81ea53d3-7bde-4fae-9deb-ea824da28727","ts":"2026-02-14T19:00:08.203-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"c00c2b31-eb02-42c2-a3a8-ba3682d3d038","ts":"2026-02-14T19:00:12.442-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"31","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"8f8adc88-e7d8-4dd8-bb5c-fa5ec1361ca8","ts":"2026-02-14T19:00:12.443-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"31","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"1face4d0-da30-495a-8fce-2173e016d75e","ts":"2026-02-14T19:00:12.443-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"31","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageNode","id":"1a586fff-2c6a-4b18-b31b-50fc4096f956","ts":"2026-02-14T19:00:12.467-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"31","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageWorkflow","id":"37e758a8-9542-4c42-8b86-b51843ffb4a9","ts":"2026-02-14T19:00:12.472-05:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"31","success":false,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","lastNodeExecuted":"HTTP Request","errorNodeType":"n8n-nodes-base.httpRequest","errorMessage":"Bad request - please check your parameters"}}
{"__type":"$$EventMessageAudit","id":"57e9abc4-6b9c-475c-9554-8b96571f0bda","ts":"2026-02-14T19:00:54.321-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"3e9823e8-e4bd-499e-953d-40291aa6970d","ts":"2026-02-14T19:04:46.227-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"32","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"c335fbc1-bdfc-4348-9d0b-18872ce098aa","ts":"2026-02-14T19:04:46.227-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"32","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"ec2439a8-c627-409d-908c-c3b75ca5769b","ts":"2026-02-14T19:04:46.228-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"32","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageNode","id":"5a1d7ac2-f960-4b0d-b8c2-7335e2eb26f3","ts":"2026-02-14T19:04:46.248-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"32","nodeType":"n8n-nodes-base.httpRequest","nodeName":"HTTP Request","nodeId":"19cb0235-1a2f-4905-95d8-4d37d8d8b608"}}
{"__type":"$$EventMessageWorkflow","id":"7cbf5ae3-5641-420d-883e-58d9768a3709","ts":"2026-02-14T19:04:46.250-05:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"32","success":false,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","lastNodeExecuted":"HTTP Request","errorNodeType":"n8n-nodes-base.httpRequest","errorMessage":"Bad request - please check your parameters"}}
{"__type":"$$EventMessageAudit","id":"858d8f5f-c0ef-4206-be14-14eea6bf26fc","ts":"2026-02-14T19:09:16.916-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"71fee88b-630f-4ee3-9b39-ac98364d4f2c","ts":"2026-02-14T19:11:45.977-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"f41df978-9b9b-4f61-ac3c-af2c9fa9e808","ts":"2026-02-14T19:12:50.462-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"4077f1a8-8a52-4d75-a202-ffc6e0683297","ts":"2026-02-14T19:13:19.708-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"28e3e612-7f33-4cf8-9ae3-72056b85ac34","ts":"2026-02-14T19:14:17.502-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"33","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"1e8f228c-645e-41ff-8f71-0f906b0ec95f","ts":"2026-02-14T19:14:17.502-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"33","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"4c8ec78d-f736-4ee3-9b6a-6be3544f3cb2","ts":"2026-02-14T19:14:17.503-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"33","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"cf12fd03-f52d-4321-a3e6-b838c2ad41b6","ts":"2026-02-14T19:14:17.511-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"ZNqhdM67","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"33"}}
{"__type":"$$EventMessageRunner","id":"e8c5ed50-b8ef-4a47-944d-86a5db75dcc7","ts":"2026-02-14T19:14:17.603-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"ZNqhdM67","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"33"}}
{"__type":"$$EventMessageNode","id":"05840ac7-b274-4fed-a04d-a10e4b3e4a27","ts":"2026-02-14T19:14:17.605-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"33","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageWorkflow","id":"b5cf7974-f861-4daf-a7e1-3aaedeeac17e","ts":"2026-02-14T19:14:17.612-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"33","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"18ca4a3a-8b35-484e-bffe-e55c53bb3391","ts":"2026-02-14T19:15:45.719-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"14dc22a9-d0bf-452e-b158-f9142d3f8e5d","ts":"2026-02-14T19:16:54.698-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"9317dfb5-03fe-4654-9eab-1e09ddd81c48","ts":"2026-02-14T19:17:11.870-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"824bbd95-f082-4fd8-9543-64f98f707a60","ts":"2026-02-14T19:17:25.474-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"d960dd2d-c06e-416b-b742-d70f19ffb929","ts":"2026-02-14T19:17:34.346-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"20240e56-8ed7-4e08-834c-13f443d8c137","ts":"2026-02-14T19:17:39.405-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"34","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"2b62dfc4-6f48-48f5-9c12-2075b2e714d0","ts":"2026-02-14T19:17:39.406-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"34","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"34007a36-dea2-4170-a6eb-3bcef2f979a5","ts":"2026-02-14T19:17:39.407-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"34","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"8217a0b8-cd7d-456c-b942-50860bb04533","ts":"2026-02-14T19:17:39.411-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"GF0uPgG1","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"34"}}
{"__type":"$$EventMessageRunner","id":"7095603d-0ad8-4414-a77e-1991b23a6d91","ts":"2026-02-14T19:17:39.469-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"GF0uPgG1","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"34"}}
{"__type":"$$EventMessageNode","id":"96f789e4-9c32-4396-b4a5-d6def55bfae2","ts":"2026-02-14T19:17:39.469-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"34","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"2c5f98a7-9bc6-426b-a870-e63d6029fe32","ts":"2026-02-14T19:17:39.475-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"34","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"f030adfc-8822-45d6-852c-d8b95ed5edfa","ts":"2026-02-14T19:17:39.549-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"34","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageWorkflow","id":"9c8b0ca9-454e-48cf-a58e-45f358d8b4c5","ts":"2026-02-14T19:17:39.550-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"34","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"8ddafa75-05e8-4bc2-966b-1f6c2a48d601","ts":"2026-02-14T19:19:25.577-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"c2b41941-44b2-4fd5-9b4a-242f14cd0328","ts":"2026-02-14T19:19:27.572-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"2c14d7c4-07c4-4519-89cc-347918f045a0","ts":"2026-02-14T19:20:30.609-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"6462c413-941d-4f47-894e-5e39b4e5ffc0","ts":"2026-02-14T19:20:42.613-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"d0af5296-0bdd-4001-9fca-ddd9d23b09c0","ts":"2026-02-14T19:20:56.942-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"35","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"1411cc3b-497d-4d1e-a1ac-7c0bf769873f","ts":"2026-02-14T19:20:56.943-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"35","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"fb7961de-f6c0-493f-9d9b-cb40add56730","ts":"2026-02-14T19:20:56.944-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"35","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"dd663b80-ab35-4fc1-ac97-bc5e91794176","ts":"2026-02-14T19:20:56.947-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"zOXMLRsd","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"35"}}
{"__type":"$$EventMessageRunner","id":"14af5a9b-ef65-486a-8e59-6aad16018c57","ts":"2026-02-14T19:20:57.003-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"zOXMLRsd","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"35"}}
{"__type":"$$EventMessageNode","id":"c0274d29-e2e5-43ec-93df-1716bb671133","ts":"2026-02-14T19:20:57.003-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"35","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"ffd8ab53-de72-4e04-a49d-6dc86efa6c32","ts":"2026-02-14T19:20:57.009-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"35","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"76584705-2a62-47f1-8860-26382d394eed","ts":"2026-02-14T19:20:57.064-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"35","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageWorkflow","id":"b6fe000b-ee40-4986-b081-d330ec89eea2","ts":"2026-02-14T19:20:57.066-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"35","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"02fbcd73-91af-47c4-bf24-d8ece5be0e0c","ts":"2026-02-14T19:22:00.354-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"aea81adc-3814-4e67-8ca4-fc2ca6eb1751","ts":"2026-02-14T19:22:17.335-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"c3889e7a-0098-4e81-8c6f-2d3a0430768c","ts":"2026-02-14T19:22:17.576-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"36","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"16d955d5-bd1c-4318-bfc0-ca94c081800a","ts":"2026-02-14T19:22:17.577-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"36","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"223baff6-e2ea-495b-9d8a-66806bccab30","ts":"2026-02-14T19:22:17.578-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"36","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"3ee02e49-afae-4a67-b22a-aab021b64f7a","ts":"2026-02-14T19:22:17.580-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"lKGVrE-q","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"36"}}
{"__type":"$$EventMessageRunner","id":"9d36ccfe-1b92-4264-97fe-01ffd55dd172","ts":"2026-02-14T19:22:17.632-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"lKGVrE-q","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"36"}}
{"__type":"$$EventMessageNode","id":"7e476f2d-d016-4616-96c2-9f05ae34dca7","ts":"2026-02-14T19:22:17.633-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"36","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"746f54ad-201a-4c68-8761-e186f5bf095b","ts":"2026-02-14T19:22:17.639-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"36","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"57a1bcaf-7c62-436b-896e-649df6d099c0","ts":"2026-02-14T19:22:17.682-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"36","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"75bd4a30-d1df-4f3d-b669-d9f764cc39b0","ts":"2026-02-14T19:22:17.682-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"36","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"de283afc-35db-4db5-9a17-3c81f70f9793","ts":"2026-02-14T19:22:17.684-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"36","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageWorkflow","id":"a8b51fa2-d381-4a34-95ba-b62b6ad8a5a2","ts":"2026-02-14T19:22:17.684-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"36","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"c298ce44-d72b-45a2-b674-71c23c2edf6f","ts":"2026-02-14T19:22:53.435-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"0a66e6ea-b1c1-48a5-9b79-94fc9eade352","ts":"2026-02-14T19:22:59.354-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"37","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"872f9421-3665-4b6e-be23-6803f948d715","ts":"2026-02-14T19:22:59.355-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"37","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"ee66f833-9eaf-4fea-918e-d30e4b5d26d1","ts":"2026-02-14T19:22:59.356-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"37","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"ed71f99e-5a3d-4679-a6aa-352264d752c7","ts":"2026-02-14T19:22:59.359-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"dPQ6-CQE","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"37"}}
{"__type":"$$EventMessageRunner","id":"7f71a6c8-f100-4996-966a-ef400c96fef3","ts":"2026-02-14T19:22:59.406-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"dPQ6-CQE","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"37"}}
{"__type":"$$EventMessageNode","id":"d0c65143-1052-4310-8ecd-88b6b377229e","ts":"2026-02-14T19:22:59.407-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"37","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"d066fbd9-057f-40bd-92bb-bc2e8b6d9e97","ts":"2026-02-14T19:22:59.411-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"37","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"b17788e8-6197-4274-8b08-cb74b389cc83","ts":"2026-02-14T19:22:59.450-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"37","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"6a1cb3c6-21ce-4314-82f7-9eb1866783ab","ts":"2026-02-14T19:22:59.451-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"37","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"9e62d0de-1cf9-4fce-b97c-04741fc20859","ts":"2026-02-14T19:22:59.451-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"37","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageWorkflow","id":"0a01a889-d517-4346-8a04-66d69361c8d4","ts":"2026-02-14T19:22:59.452-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"37","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"93d4e103-9501-4d28-83a8-6637604a47bc","ts":"2026-02-14T19:23:08.053-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"f80dc75b-7aad-4e94-a4f8-348a879794d3","ts":"2026-02-14T19:23:08.245-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"38","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"c7d624ac-5f87-477d-8c68-77472231092f","ts":"2026-02-14T19:23:08.245-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"38","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"ddc3cc6b-5ad5-4ff4-a53b-7839eacd5c92","ts":"2026-02-14T19:23:08.246-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"38","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"78d8367c-6755-43ba-a28b-7657b0676dfe","ts":"2026-02-14T19:23:08.248-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"nukUxiSj","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"38"}}
{"__type":"$$EventMessageRunner","id":"101bd4c0-be93-4e90-bd66-5b46eadfcdfd","ts":"2026-02-14T19:23:08.307-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"nukUxiSj","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"38"}}
{"__type":"$$EventMessageNode","id":"8fa9896a-be5a-4678-840b-8497807c4a29","ts":"2026-02-14T19:23:08.307-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"38","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"51d745f0-9076-4e51-91c7-0c06032a34d7","ts":"2026-02-14T19:23:08.313-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"38","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"fe36bf49-98fa-45c0-992f-05f55e1d4394","ts":"2026-02-14T19:23:08.352-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"38","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"6441dc1f-d008-45f3-bcdc-9b1e808fd954","ts":"2026-02-14T19:23:08.353-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"38","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"295bd675-4a0d-4cb6-b2ad-0f6b0216950a","ts":"2026-02-14T19:23:08.354-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"38","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageWorkflow","id":"7aafb097-3f6a-43cf-a6a8-9d51aeb26979","ts":"2026-02-14T19:23:08.355-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"38","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"24ba491b-e346-47a6-9d95-7584a5133d26","ts":"2026-02-14T19:23:15.007-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"3f0f6cde-1c82-46a1-bcef-57827a0a764c","ts":"2026-02-14T19:23:17.758-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"cdbc3c61-ecd8-43de-8b72-8ad51da22fb3","ts":"2026-02-14T19:23:21.919-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"39","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"78440af1-c937-4f0b-bd2b-5737883a2899","ts":"2026-02-14T19:23:21.920-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"39","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"33705212-ff58-483a-b273-363c7e4bffca","ts":"2026-02-14T19:23:21.921-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"39","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"ae6e7c4b-53a8-4e6e-8825-2c50b8e848aa","ts":"2026-02-14T19:23:21.924-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"hb5XzItq","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"39"}}
{"__type":"$$EventMessageRunner","id":"4ebc8d27-2279-4264-a3c6-0059c249b7c8","ts":"2026-02-14T19:23:21.975-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"hb5XzItq","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"39"}}
{"__type":"$$EventMessageNode","id":"7174c264-5019-437d-9654-5505e01cedf8","ts":"2026-02-14T19:23:21.976-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"39","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"49a0cad3-33ac-4b3f-811c-58df24916542","ts":"2026-02-14T19:23:21.981-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"39","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"bd42d021-b453-47b6-b924-b6d8dca4d244","ts":"2026-02-14T19:23:22.019-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"39","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"a252efd0-43e5-4fb3-876d-9a30403616e5","ts":"2026-02-14T19:23:22.020-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"39","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"960b612a-cb9b-4eca-a247-74f02aa72484","ts":"2026-02-14T19:23:22.020-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"39","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageWorkflow","id":"b2e19719-fae3-4881-a8c3-3dc6f90078b0","ts":"2026-02-14T19:23:22.022-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"39","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"815de0f4-64c0-4e14-93b9-70937acec49c","ts":"2026-02-14T19:23:22.370-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"bc2956b5-b275-49f4-96ee-40443957a926","ts":"2026-02-14T19:24:00.518-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"8bcaf347-33b2-427e-ab99-c6b88df882ae","ts":"2026-02-14T19:24:48.923-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"355d6391-1902-499f-9b7a-7382044a7b34","ts":"2026-02-14T19:24:50.810-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"ddab95bf-fa53-46e2-8434-7c6f92e4b86d","ts":"2026-02-14T19:25:01.433-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"a8fb95f7-7c63-4d6f-a327-a83d94fad5a1","ts":"2026-02-14T19:25:01.743-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"40","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"cbbe81bf-5796-4243-8ace-16bfbfd07e23","ts":"2026-02-14T19:25:01.744-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"40","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"517e33a7-ce32-4226-847d-79db0ba8783b","ts":"2026-02-14T19:25:01.745-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"40","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"4795ea85-c9eb-4d95-9327-793a0a5c98fd","ts":"2026-02-14T19:25:01.748-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"A0etMNPy","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"40"}}
{"__type":"$$EventMessageRunner","id":"3e9a11aa-c2af-4bf9-ab06-4a05d03e83f8","ts":"2026-02-14T19:25:01.800-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"A0etMNPy","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"40"}}
{"__type":"$$EventMessageNode","id":"f70d0374-b06d-4214-8cc1-d68ca5962d63","ts":"2026-02-14T19:25:01.801-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"40","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"492d70d1-2b77-485e-be2c-02564aefaefa","ts":"2026-02-14T19:25:01.806-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"40","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"bc845d1f-0525-4138-93f3-3107dc7ba8d5","ts":"2026-02-14T19:25:01.846-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"40","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageWorkflow","id":"86b024cf-0fd2-4eb3-9749-41064fffb394","ts":"2026-02-14T19:25:01.847-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"40","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"66d33457-7859-4035-aff5-6d75c43cfb03","ts":"2026-02-14T19:25:06.406-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"d86357d8-bc0a-49fd-bdf7-0ca0c2bd2d85","ts":"2026-02-14T19:25:08.886-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"41","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"29a9992a-4b7d-47a6-9af9-1c5c68b16f8d","ts":"2026-02-14T19:25:08.887-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"41","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"3255d0e6-f1d6-485f-99ef-85f4fb01b5cf","ts":"2026-02-14T19:25:08.888-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"41","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"e7ff66f4-5f6f-4335-8dee-c07aacbce539","ts":"2026-02-14T19:25:08.892-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"eyClpGUr","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"41"}}
{"__type":"$$EventMessageRunner","id":"e26731d5-f2b9-4640-bc63-e0974e02aa0d","ts":"2026-02-14T19:25:08.937-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"eyClpGUr","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"41"}}
{"__type":"$$EventMessageNode","id":"d0718e41-edf4-411c-a511-c64e4255c44a","ts":"2026-02-14T19:25:08.938-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"41","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"55e6a449-88fb-4978-a5be-0665091351ce","ts":"2026-02-14T19:25:08.944-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"41","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"9d25a223-38c8-49dd-85c6-191575090b70","ts":"2026-02-14T19:25:08.973-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"41","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"dc87a504-86a9-4c3a-992b-f193d9a9e38e","ts":"2026-02-14T19:25:08.974-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"41","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"4a7f02a5-7dc9-4909-8825-4b569d79ac34","ts":"2026-02-14T19:25:08.974-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"41","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageWorkflow","id":"dd28111a-ae5a-43cd-a94e-4baa5a1160af","ts":"2026-02-14T19:25:08.975-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"41","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"1c72db37-7cdc-4ed3-8aca-9cd932a99f6f","ts":"2026-02-14T19:27:05.491-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"3b591a4a-3c3d-4e01-8663-bff7cf36db57","ts":"2026-02-14T19:27:39.078-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"af76b173-b732-4ef8-9daa-7d02a275df32","ts":"2026-02-14T19:27:43.308-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"42","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"8bd55b0d-fcf0-4d2f-b977-a03e4fd1e650","ts":"2026-02-14T19:27:43.309-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"42","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"b42a4247-2b3d-4f42-93b1-b8641f0eb5e4","ts":"2026-02-14T19:27:43.310-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"42","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"ac38e18c-05f3-4a1e-918a-c5417558553b","ts":"2026-02-14T19:27:43.313-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"ptZotiS7","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"42"}}
{"__type":"$$EventMessageRunner","id":"2523767d-d19a-486c-9f6a-2f9ccd278e1f","ts":"2026-02-14T19:27:43.363-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"ptZotiS7","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"42"}}
{"__type":"$$EventMessageNode","id":"a09af45a-6cc7-4477-b576-286e0738f59a","ts":"2026-02-14T19:27:43.364-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"42","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"0b73059a-5838-4698-9683-888f8a8417e6","ts":"2026-02-14T19:27:43.370-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"42","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"20b1f59d-cf15-4706-af9f-dd80d5be1923","ts":"2026-02-14T19:27:43.405-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"42","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"0caef885-dfb0-4e22-8c5f-cbc79cb66fdd","ts":"2026-02-14T19:27:43.406-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"42","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"b76576c7-c6c7-49aa-abf1-c67c76838a55","ts":"2026-02-14T19:27:43.407-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"42","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"4ac9682b-35d1-490f-a84d-0588dbcb5deb","ts":"2026-02-14T19:27:43.407-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"42","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageRunner","id":"f884bd2c-116d-478d-b24b-856cfae01b8e","ts":"2026-02-14T19:27:43.408-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"mjnoQD3U","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"42"}}
{"__type":"$$EventMessageNode","id":"0472b27e-f7eb-42f9-9736-07f137f9b78e","ts":"2026-02-14T19:27:43.435-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"42","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageWorkflow","id":"ea286ebe-73ae-40f0-9918-9faca88f9edb","ts":"2026-02-14T19:27:43.437-05:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"42","success":false,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","lastNodeExecuted":"Code in JavaScript1","errorMessage":"item is not defined [line 2]"}}
{"__type":"$$EventMessageAudit","id":"f1bcc4e2-e43b-46a7-95fd-7ff1cfcaeda8","ts":"2026-02-14T19:29:07.457-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"4db656ec-fc9e-4836-b54d-faa872e4332d","ts":"2026-02-14T19:29:07.935-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"43","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"d16ac747-5f76-4bfd-85dd-e68fe25bf073","ts":"2026-02-14T19:29:07.935-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"43","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"d7d26429-e64a-46c7-a489-be21816fb13a","ts":"2026-02-14T19:29:07.937-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"43","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"c9100d04-1fe2-4d2b-a35b-ee37cbf65102","ts":"2026-02-14T19:29:07.939-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"u9QPBtvo","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"43"}}
{"__type":"$$EventMessageRunner","id":"0110dd92-0307-406a-b131-42b34386ff32","ts":"2026-02-14T19:29:07.995-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"u9QPBtvo","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"43"}}
{"__type":"$$EventMessageNode","id":"d4050257-81bb-41aa-8f6f-193473bad858","ts":"2026-02-14T19:29:07.995-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"43","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"68151441-eb01-4911-8efe-c1f9015d3ff1","ts":"2026-02-14T19:29:08.001-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"43","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"5391c200-4cd3-4997-a6a7-96897715f1c2","ts":"2026-02-14T19:29:08.040-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"43","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"03f20266-20fd-4601-858b-ea03a9c07ab8","ts":"2026-02-14T19:29:08.041-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"43","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"244a5637-818e-4890-8c2f-df0f52da9cb7","ts":"2026-02-14T19:29:08.042-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"43","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"6fc2d269-9365-4a9f-9de7-cb37b1493743","ts":"2026-02-14T19:29:08.043-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"43","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageRunner","id":"ce24de19-85ab-49ff-8c43-9ede9dce7c05","ts":"2026-02-14T19:29:08.044-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"ePHzT-N3","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"43"}}
{"__type":"$$EventMessageRunner","id":"9e289a7a-e4a5-43de-86e9-5efeb195592f","ts":"2026-02-14T19:29:08.054-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"ePHzT-N3","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"43"}}
{"__type":"$$EventMessageNode","id":"bc519240-c055-4de1-9225-de3717459e35","ts":"2026-02-14T19:29:08.055-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"43","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageWorkflow","id":"d491c9e8-d7ac-4691-9db3-a84359f9add5","ts":"2026-02-14T19:29:08.056-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"43","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"947ab9ed-5655-4601-9fe0-4c61fa89e228","ts":"2026-02-14T19:30:00.692-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"44","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"ed9274ba-ce85-4fa0-8c20-39771ec404df","ts":"2026-02-14T19:30:00.692-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"44","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"88d8c2dc-99b8-43f3-b35a-a18febdba092","ts":"2026-02-14T19:30:00.693-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"44","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"478886b2-0275-4641-9561-1b72fb8280a3","ts":"2026-02-14T19:30:00.695-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"aNOkY1kM","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"44"}}
{"__type":"$$EventMessageRunner","id":"e6cd758b-db0c-4f5e-bfca-a5bec18b6bbc","ts":"2026-02-14T19:30:00.746-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"aNOkY1kM","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"44"}}
{"__type":"$$EventMessageNode","id":"70f63f0c-e91a-469a-8e8f-dd96c1ae1170","ts":"2026-02-14T19:30:00.747-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"44","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"524881cc-7296-4468-a9f6-c2353a41a324","ts":"2026-02-14T19:30:00.753-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"44","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"640bef7b-2269-40d9-a273-5966a27284fd","ts":"2026-02-14T19:30:00.786-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"44","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"a4f6cf77-9d64-4be5-ab35-c2249c57edc5","ts":"2026-02-14T19:30:00.787-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"44","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"bb5e322b-32a4-44ea-853c-fb0ba443c14d","ts":"2026-02-14T19:30:00.787-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"44","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"524c81ea-91d3-4a0f-9c74-88e36e7d951c","ts":"2026-02-14T19:30:00.788-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"44","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageRunner","id":"e580273a-eccc-4216-a414-7d28bdf9706e","ts":"2026-02-14T19:30:00.789-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"ZU1z_J3D","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"44"}}
{"__type":"$$EventMessageRunner","id":"a84db632-bf60-4329-909a-bc4b8d933813","ts":"2026-02-14T19:30:00.806-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"ZU1z_J3D","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"44"}}
{"__type":"$$EventMessageNode","id":"f981aed6-f748-43b5-87cd-161795645db7","ts":"2026-02-14T19:30:00.806-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"44","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageWorkflow","id":"53b8d2bb-1fb2-47d7-8f24-256da88a2f03","ts":"2026-02-14T19:30:00.807-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"44","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"8f72b6cf-2577-449a-a839-273a62670c50","ts":"2026-02-14T19:31:28.463-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"45","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"da087b62-593f-4b12-bff1-20016766ec68","ts":"2026-02-14T19:31:28.464-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"45","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"b3c44f86-923b-4b3d-97f7-59f5c94355c9","ts":"2026-02-14T19:31:28.465-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"45","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"c24d54e0-7cb7-4446-ba2f-48c00edf4413","ts":"2026-02-14T19:31:28.467-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"k-76NVt4","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"45"}}
{"__type":"$$EventMessageRunner","id":"d9ec34cd-b0ff-4faf-a2eb-830ec07ff68b","ts":"2026-02-14T19:31:28.514-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"k-76NVt4","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"45"}}
{"__type":"$$EventMessageNode","id":"ae7fa026-e449-4377-ae1a-5f7c58bc428b","ts":"2026-02-14T19:31:28.515-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"45","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"a85b7b11-7b44-407c-a4ef-51f55ade6abc","ts":"2026-02-14T19:31:28.521-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"45","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"8f6a0d31-1b13-44b2-851c-c0948c7b5fd8","ts":"2026-02-14T19:31:28.557-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"45","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"68c1599a-a9f6-4700-9c01-a0123f90f922","ts":"2026-02-14T19:31:28.558-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"45","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"9a060c42-228f-4ef2-8f8d-abefc352e501","ts":"2026-02-14T19:31:28.561-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"45","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"d5787cda-0c96-426e-900a-685b7ec5ad08","ts":"2026-02-14T19:31:28.561-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"45","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageRunner","id":"7133b1a0-1c8b-4e2b-b92e-3c0dca5ee586","ts":"2026-02-14T19:31:28.562-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"vy40OpXM","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"45"}}
{"__type":"$$EventMessageRunner","id":"6c1ff876-18df-4fdd-81b4-e7a8a6c4e08b","ts":"2026-02-14T19:31:28.578-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"vy40OpXM","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"45"}}
{"__type":"$$EventMessageNode","id":"8a50d35f-d1d0-41e1-8b23-9d14dd488218","ts":"2026-02-14T19:31:28.578-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"45","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageWorkflow","id":"665a45ac-57d4-4402-8874-bf65416cbb6c","ts":"2026-02-14T19:31:28.579-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"45","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"29071081-ba9a-4bc5-88d1-3a90a7e9334b","ts":"2026-02-14T19:31:29.212-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"646a23a2-cef3-4057-b8a4-8048575660a0","ts":"2026-02-14T19:35:08.748-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"9d179d52-c0c4-43f3-a5be-37221ae0f091","ts":"2026-02-14T19:35:08.898-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"46","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"ff977fef-b6f1-4319-8063-ede1431c90ef","ts":"2026-02-14T19:35:08.899-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"46","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"34c0ff62-c17c-49e7-8b2c-d68fcc622101","ts":"2026-02-14T19:35:08.900-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"46","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"e94f9203-fee5-4aaf-a5a0-00167cdb095e","ts":"2026-02-14T19:35:08.903-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"i4zWqeIN","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"46"}}
{"__type":"$$EventMessageRunner","id":"bb0e2e99-3498-4ed6-b949-8cd9f09575b1","ts":"2026-02-14T19:35:08.953-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"i4zWqeIN","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"46"}}
{"__type":"$$EventMessageNode","id":"1d693c3c-b973-443a-9612-e34fe2c7615f","ts":"2026-02-14T19:35:08.953-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"46","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"98000f5a-581c-4fba-8f12-1ab3fef78492","ts":"2026-02-14T19:35:08.958-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"46","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"e8d72597-307e-4857-b2f1-231489e1be5d","ts":"2026-02-14T19:35:08.995-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"46","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"78b15d82-830c-4cc6-8dae-75fec96ae7f5","ts":"2026-02-14T19:35:08.996-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"46","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"456217e2-847b-45d7-8376-988077cfef65","ts":"2026-02-14T19:35:08.996-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"46","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"5249826b-5e54-499b-87ef-99d9b44ef6f0","ts":"2026-02-14T19:35:08.997-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"46","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageRunner","id":"73b3b670-6b34-4e63-980c-d5217c849b0e","ts":"2026-02-14T19:35:09.002-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"4qb7CrXF","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"46"}}
{"__type":"$$EventMessageRunner","id":"90e68362-6785-4de1-b9f4-b66c4fa696d3","ts":"2026-02-14T19:35:09.077-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"4qb7CrXF","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"46"}}
{"__type":"$$EventMessageNode","id":"4d21f751-b74f-415f-ae9e-78df1bc1ebac","ts":"2026-02-14T19:35:09.078-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"46","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageWorkflow","id":"b44613b4-2cfa-4a2b-b974-40b920cc03bd","ts":"2026-02-14T19:35:09.082-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"46","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"126cc823-d958-4119-bd94-d334f651f183","ts":"2026-02-14T19:36:20.863-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"671ef520-5053-40ed-9455-a13d22c952f4","ts":"2026-02-14T19:36:25.669-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"226571d0-a943-4024-8a77-bb94d66faadc","ts":"2026-02-14T19:37:08.386-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"cdb9579c-4cd0-4467-9730-7aaa67423e61","ts":"2026-02-14T19:37:14.414-05:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"97f284ca-74d6-41da-b193-3f30c5a64dfe","ts":"2026-02-14T19:37:48.390-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"47","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"00288d57-3c5c-4d87-91ab-91804fb858df","ts":"2026-02-14T19:37:48.390-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"47","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"cdf82349-62ea-44d5-8af7-3855c9994bcc","ts":"2026-02-14T19:37:48.391-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"47","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"be26c409-bb18-41c6-8881-766451a6ec53","ts":"2026-02-14T19:37:48.393-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"O1361oJS","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"47"}}
{"__type":"$$EventMessageRunner","id":"ac5cd0c6-9d51-4241-9e2f-c2d12bf31128","ts":"2026-02-14T19:37:48.442-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"O1361oJS","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"47"}}
{"__type":"$$EventMessageNode","id":"366c37c1-ac1d-4110-91ac-9389bee3ebe0","ts":"2026-02-14T19:37:48.442-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"47","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"1c37ed7f-0118-487f-a1f6-3ce7d6ce1ca1","ts":"2026-02-14T19:37:48.448-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"47","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"80a251da-f2f8-4879-9e51-4f392a9bc73e","ts":"2026-02-14T19:37:48.486-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"47","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"268c771f-39a6-4c2c-bbeb-20ba62b66a63","ts":"2026-02-14T19:37:48.487-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"47","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"3e2fd2a5-bb51-40bb-8c2c-4b5d82f18a31","ts":"2026-02-14T19:37:48.488-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"47","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"7acb6106-223d-4c36-8ec4-833e60dd9769","ts":"2026-02-14T19:37:48.488-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"47","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageRunner","id":"bc2caefb-fb5c-4700-81b3-e1e7bab48c41","ts":"2026-02-14T19:37:48.489-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"G_oanKfN","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"47"}}
{"__type":"$$EventMessageRunner","id":"8c74ab0b-78af-4d35-b5cd-2282d1aa3874","ts":"2026-02-14T19:37:48.558-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"G_oanKfN","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"47"}}
{"__type":"$$EventMessageNode","id":"354bec36-df6f-4a8d-a62a-eea1f62e0e5b","ts":"2026-02-14T19:37:48.559-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"47","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageNode","id":"b87c3eda-ac7c-48a9-8247-fadc1230a458","ts":"2026-02-14T19:37:48.561-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"47","nodeType":"n8n-nodes-base.postgres","nodeName":"Execute a SQL query","nodeId":"2d92f41f-baea-47ac-9340-54b40ad343a6"}}
{"__type":"$$EventMessageNode","id":"02b81ec5-8027-4a65-bdd4-18750dc8a1cc","ts":"2026-02-14T19:37:48.684-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"47","nodeType":"n8n-nodes-base.postgres","nodeName":"Execute a SQL query","nodeId":"2d92f41f-baea-47ac-9340-54b40ad343a6"}}
{"__type":"$$EventMessageWorkflow","id":"990d53ac-8cc6-48b8-b2b7-5ce0da736501","ts":"2026-02-14T19:37:48.686-05:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"47","success":false,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","lastNodeExecuted":"Execute a SQL query","errorNodeType":"n8n-nodes-base.postgres","errorMessage":"permission denied for table catalog_discovery"}}
{"__type":"$$EventMessageWorkflow","id":"f93ba421-f604-448a-ad1c-e79758e92b86","ts":"2026-02-14T19:40:00.381-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"48","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"3d4cb2a2-8983-4dc3-86de-5371ff056c42","ts":"2026-02-14T19:40:00.381-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"48","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"043ae25e-7d8d-4e68-8ff6-8995dd1c2671","ts":"2026-02-14T19:40:00.382-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"48","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"c656c676-2121-4cb4-bf96-eb6e5d8f79b1","ts":"2026-02-14T19:40:00.384-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"hv6uWpW3","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"48"}}
{"__type":"$$EventMessageRunner","id":"675a5d55-8a72-4b2f-9bff-2e19f96222e5","ts":"2026-02-14T19:40:00.428-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"hv6uWpW3","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"48"}}
{"__type":"$$EventMessageNode","id":"01a9cf62-4fa1-43ad-8186-91e2a962c6e8","ts":"2026-02-14T19:40:00.428-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"48","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"df9e7132-c40a-4fa9-9347-5a3c0fa09eb3","ts":"2026-02-14T19:40:00.434-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"48","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"5fce15d1-ba5e-4f2b-b3b0-09ed241372af","ts":"2026-02-14T19:40:00.470-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"48","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"75edd6f4-6424-4201-ac21-44d4512078f2","ts":"2026-02-14T19:40:00.470-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"48","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"fc5b52c4-03e1-436d-a384-fe37b3dec44b","ts":"2026-02-14T19:40:00.472-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"48","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"eda6d295-d4c4-457a-a317-1c375549433f","ts":"2026-02-14T19:40:00.473-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"48","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageRunner","id":"d01c4667-a4af-4a29-a7c1-08ebcced3313","ts":"2026-02-14T19:40:00.474-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"xhOSvWvS","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"48"}}
{"__type":"$$EventMessageRunner","id":"a2e5c94f-caec-4f3d-abfa-6377cad3cad9","ts":"2026-02-14T19:40:00.531-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"xhOSvWvS","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"48"}}
{"__type":"$$EventMessageNode","id":"10b60953-0422-49db-b82a-c6a3ef2dd452","ts":"2026-02-14T19:40:00.532-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"48","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageNode","id":"e773e4cb-58ef-428e-b1ae-98c641726fbc","ts":"2026-02-14T19:40:00.534-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"48","nodeType":"n8n-nodes-base.postgres","nodeName":"Execute a SQL query","nodeId":"2d92f41f-baea-47ac-9340-54b40ad343a6"}}
{"__type":"$$EventMessageNode","id":"7692b2bf-811f-4811-9a8f-7e7cd57d758f","ts":"2026-02-14T19:40:00.664-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"48","nodeType":"n8n-nodes-base.postgres","nodeName":"Execute a SQL query","nodeId":"2d92f41f-baea-47ac-9340-54b40ad343a6"}}
{"__type":"$$EventMessageWorkflow","id":"60152d8b-2d17-4695-a31f-b1ed659afc5a","ts":"2026-02-14T19:40:00.668-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"48","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageWorkflow","id":"14339d78-0184-45ee-b954-bee60af83d19","ts":"2026-02-14T19:40:57.346-05:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"49","workflowId":"g3yuktNVnNC6OqUt","isManual":false,"workflowName":"Market Discovery Pipeline"}}
{"__type":"$$EventMessageAudit","id":"39ec9840-29c1-4d41-ab82-c81aea3c9c82","ts":"2026-02-14T19:40:57.346-05:00","eventName":"n8n.audit.workflow.executed","message":"n8n.audit.workflow.executed","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","_email":"kincses@gmail.com","_firstName":"Zsolt","_lastName":"Gyongyossy","globalRole":"global:owner","workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"49","source":"user-manual"}}
{"__type":"$$EventMessageNode","id":"2c91cee5-7733-473a-9b33-063bec8a8cf4","ts":"2026-02-14T19:40:57.347-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"49","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageRunner","id":"733dcdfb-949b-4dd5-b11b-b8760572927e","ts":"2026-02-14T19:40:57.349-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"zNrKb4XJ","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"49"}}
{"__type":"$$EventMessageRunner","id":"176fb88d-c371-4e7b-b803-39726430732e","ts":"2026-02-14T19:40:57.396-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"zNrKb4XJ","nodeId":"396b714c-a809-4623-974c-06173c445c64","workflowId":"g3yuktNVnNC6OqUt","executionId":"49"}}
{"__type":"$$EventMessageNode","id":"507318b3-d7b9-486f-becb-c572442d3544","ts":"2026-02-14T19:40:57.396-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"49","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript","nodeId":"396b714c-a809-4623-974c-06173c445c64"}}
{"__type":"$$EventMessageNode","id":"06df90a8-dc85-47e6-a2d9-7cdb5601f8db","ts":"2026-02-14T19:40:57.403-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"49","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"51f1cfae-4773-4851-91f5-22b97acc84cb","ts":"2026-02-14T19:40:57.440-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"49","nodeType":"n8n-nodes-base.html","nodeName":"HTML","nodeId":"12a5aa30-9500-4808-9d81-83682ba1a990"}}
{"__type":"$$EventMessageNode","id":"106ad46e-7d82-45f6-bfec-56015e099336","ts":"2026-02-14T19:40:57.441-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"49","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"9ae3bf81-ce6f-4f30-b141-84a081a6efa8","ts":"2026-02-14T19:40:57.441-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"49","nodeType":"n8n-nodes-base.splitOut","nodeName":"Split Out","nodeId":"8e0871e2-af94-4a6d-9b44-76e732b51a15"}}
{"__type":"$$EventMessageNode","id":"2ee88d59-be92-4297-91be-d1f32328c31f","ts":"2026-02-14T19:40:57.442-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"49","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageRunner","id":"d19926bc-9bd6-4402-a8f2-050ed54b099b","ts":"2026-02-14T19:40:57.443-05:00","eventName":"n8n.runner.task.requested","message":"n8n.runner.task.requested","payload":{"taskId":"5wxIWF7i","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"49"}}
{"__type":"$$EventMessageRunner","id":"6d21e237-e8d1-4d70-85c3-bae5f25dc585","ts":"2026-02-14T19:40:57.512-05:00","eventName":"n8n.runner.response.received","message":"n8n.runner.response.received","payload":{"taskId":"5wxIWF7i","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408","workflowId":"g3yuktNVnNC6OqUt","executionId":"49"}}
{"__type":"$$EventMessageNode","id":"f1863211-22d8-4d12-9a71-eca6fcd55804","ts":"2026-02-14T19:40:57.513-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"49","nodeType":"n8n-nodes-base.code","nodeName":"Code in JavaScript1","nodeId":"78726a29-919c-451d-a4a2-e9a76aa46408"}}
{"__type":"$$EventMessageNode","id":"4dca0bde-fb2a-4a40-96c3-a29dab684206","ts":"2026-02-14T19:40:57.514-05:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"49","nodeType":"n8n-nodes-base.postgres","nodeName":"Execute a SQL query","nodeId":"2d92f41f-baea-47ac-9340-54b40ad343a6"}}
{"__type":"$$EventMessageNode","id":"2e82f24c-37a6-4f79-87f4-3be1a0cdb2ed","ts":"2026-02-14T19:40:57.630-05:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline","executionId":"49","nodeType":"n8n-nodes-base.postgres","nodeName":"Execute a SQL query","nodeId":"2d92f41f-baea-47ac-9340-54b40ad343a6"}}
{"__type":"$$EventMessageWorkflow","id":"655184c6-e578-42d7-8c8a-df700ef9feca","ts":"2026-02-14T19:40:57.633-05:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"07cbc1c9-f562-4c77-88d0-c0c8faff3e82","executionId":"49","success":true,"isManual":true,"workflowId":"g3yuktNVnNC6OqUt","workflowName":"Market Discovery Pipeline"}}

12
seed_discovery.py Normal file
View File

@@ -0,0 +1,12 @@
# seed_discovery.py
async def seed():
# Az RDW-től lekérjük az ÖSSZES egyedi márkát
url = "https://opendata.rdw.nl/resource/m9d7-ebf2.json?$select=distinct%20merk&$limit=50000"
async with httpx.AsyncClient() as client:
resp = await client.get(url)
makes = resp.json()
async with SessionLocal() as db:
for item in makes:
m = item['merk'].upper()
await db.execute(text("INSERT INTO data.catalog_discovery (make, model, source, status) VALUES (:m, 'ALL', 'global_seed', 'pending') ON CONFLICT DO NOTHING"), {"m": m})
await db.commit()