From d574d3297db422057a451c085919942d3645f1f2 Mon Sep 17 00:00:00 2001 From: Kincses Date: Mon, 16 Feb 2026 00:42:49 +0000 Subject: [PATCH] feat: v1.7 overhaul - identity hash, triple wallet, financial ledger, and security audit system --- .env | 2 + .gitignore | 4 + N8N/data/config | 3 - N8N/data/n8nEventLog.log | 0 N8N/data/nodes/package.json | 5 - .../__pycache__/assets.cpython-312.pyc | Bin 6522 -> 6166 bytes .../__pycache__/auth.cpython-312.pyc | Bin 10197 -> 10232 bytes backend/app/api/v1/endpoints/assets.py | 38 +- backend/app/api/v1/endpoints/auth.py | 2 +- backend/app/core/validators.py | 31 +- .../app/db/__pycache__/base.cpython-312.pyc | Bin 1379 -> 1542 bytes backend/app/db/base.py | 15 +- backend/app/models/__init__.py | 25 +- .../__pycache__/__init__.cpython-312.pyc | Bin 1790 -> 1978 bytes .../__pycache__/address.cpython-312.pyc | Bin 2973 -> 4997 bytes .../models/__pycache__/asset.cpython-312.pyc | Bin 9225 -> 10807 bytes .../__pycache__/identity.cpython-312.pyc | Bin 7097 -> 7896 bytes .../__pycache__/organization.cpython-312.pyc | Bin 6279 -> 7361 bytes .../__pycache__/service.cpython-312.pyc | Bin 4987 -> 5825 bytes backend/app/models/address.py | 50 ++- backend/app/models/asset.py | 41 +- backend/app/models/audit.py | 54 ++- backend/app/models/identity.py | 71 ++-- backend/app/models/organization.py | 23 +- backend/app/models/service.py | 29 +- backend/app/models/system.py | 4 + .../schemas/__pycache__/auth.cpython-312.pyc | Bin 3784 -> 3950 bytes backend/app/schemas/asset.py | 29 +- backend/app/schemas/auth.py | 7 +- backend/app/schemas/service.py | 20 + .../__pycache__/auth_service.cpython-312.pyc | Bin 17552 -> 17897 bytes .../__pycache__/geo_service.cpython-312.pyc | Bin 4029 -> 4667 bytes backend/app/services/auth_service.py | 64 ++-- backend/app/services/geo_service.py | 42 ++- .../__pycache__/catalog_robot.cpython-312.pyc | Bin 14980 -> 13102 bytes .../service_hunter.cpython-312.pyc | Bin 18197 -> 10403 bytes backend/app/workers/brand_seeder.py | 61 +++ backend/app/workers/catalog_robot.py | 313 ++++++++------- backend/app/workers/service_hunter.py | 339 ++++++----------- backend/app/workers/service_hunter_old.py | 282 ++++++++++++++ backend/app/workers/technical_enricher.py | 125 ++++++ .../__pycache__/env.cpython-312.pyc | Bin 3226 -> 3223 bytes backend/migrations/env.py | 2 +- ...8ccf1d_update_staging_address_structure.py | 252 +++++++++++++ .../33c4f2235667_add_axles_and_body_type.py | 27 ++ ...57f9c14_enrich_catalog_technical_schema.py | 42 +++ ...636edd27_add_discovery_parameters_table.py | 230 +++++++++++ ...e324ebd_upgrade_identity_and_audit_v1_6.py | 288 ++++++++++++++ ...9f_v1_3_branch_system_and_fleet_scaling.py | 270 +++++++++++++ ...229cc6bc347_add_catalog_discovery_table.py | 243 ++++++++++++ ...78ce92243ed_full_ecosystem_upgrade_v1_6.py | 302 +++++++++++++++ backend/seed_discovery.py | 32 ++ docker-compose.yml | 80 ++-- docker-compose_1.yml | 198 ++++++++++ docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md | 33 +- docs/V01_gemini/06_Database_Guide.md | 11 +- .../07_REGISTRATION_INVITATION_AND_API.md | 22 +- .../10_Billing_Credits_Subscriptions.md | 29 +- docs/V01_gemini/15_Changelog.md | 118 +++++- .../19_ADMIN_AND_PERMISSIONS_SPEC.md | 24 +- .../V01_gemini/23_BRANCH_AND_LOCATION_SPEC.md | 24 ++ n8n/data/n8nEventLog.log | 357 ++++++++++++++++++ seed_discovery.py | 12 + 63 files changed, 3710 insertions(+), 565 deletions(-) create mode 100644 .gitignore delete mode 100644 N8N/data/config delete mode 100644 N8N/data/n8nEventLog.log delete mode 100644 N8N/data/nodes/package.json create mode 100644 backend/app/schemas/service.py create mode 100644 backend/app/workers/brand_seeder.py create mode 100644 backend/app/workers/service_hunter_old.py create mode 100644 backend/app/workers/technical_enricher.py create mode 100644 backend/migrations/versions/25d1658ccf1d_update_staging_address_structure.py create mode 100644 backend/migrations/versions/33c4f2235667_add_axles_and_body_type.py create mode 100644 backend/migrations/versions/75e3a57f9c14_enrich_catalog_technical_schema.py create mode 100644 backend/migrations/versions/8188636edd27_add_discovery_parameters_table.py create mode 100644 backend/migrations/versions/b803fe324ebd_upgrade_identity_and_audit_v1_6.py create mode 100644 backend/migrations/versions/d0f9ed93b59f_v1_3_branch_system_and_fleet_scaling.py create mode 100644 backend/migrations/versions/d229cc6bc347_add_catalog_discovery_table.py create mode 100644 backend/migrations/versions/e78ce92243ed_full_ecosystem_upgrade_v1_6.py create mode 100644 backend/seed_discovery.py mode change 100755 => 100644 docker-compose.yml create mode 100755 docker-compose_1.yml create mode 100644 docs/V01_gemini/23_BRANCH_AND_LOCATION_SPEC.md create mode 100644 seed_discovery.py diff --git a/.env b/.env index 9d78a65..a7ef284 100755 --- a/.env +++ b/.env @@ -1,3 +1,5 @@ +COMPOSE_PROJECT_NAME=service_finder + # --- ADATBÁZIS KAPCSOLAT (Központi) --- # Itt a 'shared-postgres' nevet használjuk, ami a központi konténer neve APP_DB_HOST=shared-postgres diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67f601c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +n8n/db_data/ +N8N/db_data/ +*.pyc +__pycache__/ diff --git a/N8N/data/config b/N8N/data/config deleted file mode 100644 index f334cc5..0000000 --- a/N8N/data/config +++ /dev/null @@ -1,3 +0,0 @@ -{ - "encryptionKey": "54T2Q0sTamS0GDOb8vyTtOQXBJxq3d0/" -} \ No newline at end of file diff --git a/N8N/data/n8nEventLog.log b/N8N/data/n8nEventLog.log deleted file mode 100644 index e69de29..0000000 diff --git a/N8N/data/nodes/package.json b/N8N/data/nodes/package.json deleted file mode 100644 index 9e96546..0000000 --- a/N8N/data/nodes/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "installed-nodes", - "private": true, - "dependencies": {} -} \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc index 0f8d0775f9480b8cb675a9eef1d140afb99d2a3d..c52fd9093aff7d997ecdf260e834d9e628b60878 100644 GIT binary patch delta 1727 zcmah}YiJx*6rP#QzLM-Sn{0NQG&jxGWYf(OTU!$?LS91ESQ|~X0SUJ|H`&?OgnMU` zY!DJ3U_%0Jm?AA$q@ZAF`=c!QLllflK@`D|KRP0vTI{y6Z2pkG}x1ki$fpl0sl!@r?i9 zb7Pg~$H4RH|Hvai4iabTrAIZlnsl$!d2YWbLDITggF|kviWJ?ahR~xm9NW+PUU$M( zp1TWT^nm_{RCYu~!!^Jd(Kjq-YORp`+ilCQ0WE(m0bFim2PXQrXdjGOsP1XXGqCS zsWe-U0#H zR~9st2T0)c>qYx3&vyYEfWsJ&--W-I zgojOn)3cgiaouyemm62}7w4Dfuk>wrIyRk=drt3i@N&!hFg=n1*@}M zD+HSK(~jsNhfwcZdZFwQ+>veO2cK^ZbPxK)^_V#5ty%Ah4El@Pj;j_C>6hz2tZM^6 zC8Z91$Q4sBqvkE#XVf?C-aFUhE7jXtdYq)BQotZ@hqP;Y_m9% zCoEcDT_(3Z%pul<$}~ogalsRu9OqIoVOXF?h7)Hs{Wn*8u#-zY5PlN#kxf>thKmQ? z1<@AdcCE#0o}gIM54`@lH@t{&UQ@GJQ_`>@BjaNzt>$J4U4R{dgyPC2Q4kh<<^rHo z!zimT04I{P1sp^zm1*z?#IQp>bW)LDR33s!)LtpW!DBXXeH|qUmC9yHW>XC3$b3S- z;B&ub&Xqga87_xAa%9fL8X{pfknsISh?*7p5|r>3?iEQ#0WuDU6@Zw#Q1%x8^!*~$ z@kfL5_ko_#e+slNR?UqIBb=BR-jf|5aE3jJ8P+sP$Kdww?c+dmEEbJqm=&&7$u;b? zAG9P(G+Ll%0h5NqKGF+~>*A5((@h=X2_Ni8oF$SZi&@+(hBg7nuu(Oy5lWxpmzweM zQ^S<+I(?3lVf~F@OE<6A^m;?s0#>Lr#&I&M%J~d=hNeIUa}d}~{mbC-*MAj`{Ot6v z99XiIZGz-@H~eP!!sy36OW{qi?}1HdZo4w8Fa8ih-?cx!89MsF-YELMcl$3Y%Szb- z{C!8DY=u7($RSuk`uFle-qc-*>f?t?&m6XJgsUIS03O0>^C? Ru`F?6x84$(s8Jx(e*o%avyA`% delta 1972 zcmah~YiJx*6rS1HnayT)H;;W~(`_eq6a>$mZPT@F^um1i z+;h)&?z!ijyZheeyIb7fy4{r&j04<^^8KtkM1MZpezng=A?_kk!#LiE8e;<`xg2FT zVo(#Zjo3nqGE&PRwY@kM!Z~uLL;19`zE{Qxp^JW-bX!4|L@sX=D7tt zKl_+Cyr=;=V}~Byx#Yjd=+$)VvGvXgpp9#{sE~tYXSJ_$i@HZ|v2{J~5AqW#^W2f8 zBlY_4wxWKEImo*p>Nnt9-E7M}Z5L=;jMHz_co}=lcW(~do9oCNw8seJp%JV9kG(M2 zn|eI2;UPz`)SB+=UVSV_W)!v7FA&!q_~kA&8UKs97*AXF)t}bPU+EiayzF?*N7--DV&WG zM#OH~6XG~B*+fc4anr74(+HEmBdlptWRsatlBPSZNGPeIXgV%x$h0TL1Tq~GIVUw5 zslhzh<0=4?JvoLj!kdWfNToF;l~ggwDQ>e=NdJgydZgOEqzH#dk-?z;7=lTBYFMZc~v-_LwrQJuD_+!(91+QOEIU-XWRpmP~c6zJ; zQ}%SW5NcQsb>%}{g=+s?o<-q-tM zxcS5V7l!7CuI%|@%ToBjZ71uiyv_42u1N7LSL7*A%U#tA@xDILIT)aCbkT!8`;G3s zOvhj#_nG4yLvPZL@E`N-U{bnpL}y$rQ+;Ip1c1pvLQGCfr%_V2vW&M8buF5d#>C_( ziee~GG@~LJ@38FEOv2z)Lp{<;lsjsDmj7hDQMO%;RuSO1k$h~3L{ui)e^N!IQl#^ zes02;H!fpiUhT0wDeux0b*g3nBGa!|d73vs3=yW(E<+@62c!f?75O>k4p>GUIH=#M z+R$$GsdN9NgsfII85MesE2W9>?l)X-b-2q)K#hbE$?$_5;9;;MpPB`*8nIZHJ6Zj! zZR$nh;V6M)045JDtwl3wcy|bDVLh4}vRhVevg{;aamimunFGg|sFK%!kAwc-Pl6a) z)nqgjS6NsJ0t+?3z537I5c@JHQu^QCujxqcOJ6hHz~NLzLm2NR#KQoleR%kpK}=p1 zKBR99ggeQdFd;5dtAG@?5>U|(KNOib+Jj#K9psWyujo=>ThlMj+L;a0T#=(}6>rtQ zR{!?ULd$}BW#7W&V$XE_61`K;2YuZ^nk#r}&nTyrA_MYDg|EoM3@mT(4+luA3}xF@ yv=LpO{%2rjM}Y2MV}QKk4i{ONf#LKQ9Ym@iTqj6`tI$OoQEt&c4Ia0XUi=4EfZ&`s4k+)Trv1IaOS@p>~W$T3mf#PnNhc{;{BxaXnmK|Q0 zGdWLgF1!6jZifpj4x5GK{Wvro8E^1Ne_-ZebYy(MA@YG0#Q4C*AT0ig9mM?1%)lh~ MfeFZ*+^L=q0B@Zl;{X5v diff --git a/backend/app/api/v1/endpoints/assets.py b/backend/app/api/v1/endpoints/assets.py index 3a98065..133ae3e 100644 --- a/backend/app/api/v1/endpoints/assets.py +++ b/backend/app/api/v1/endpoints/assets.py @@ -11,35 +11,35 @@ from app.models.asset import Asset, AssetCost, AssetTelemetry from app.models.identity import User from app.services.cost_service import cost_service 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() -# --- 1. MODUL: IDENTITÁS (Alapadatok) --- -@router.get("/{asset_id}", response_model=Dict[str, Any]) +# --- 1. MODUL: IDENTITÁS (Alapadatok & Technikai katalógus) --- +@router.get("/{asset_id}", response_model=AssetResponse) async def get_asset_identity( asset_id: uuid.UUID, db: AsyncSession = Depends(get_db), 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() if not asset: raise HTTPException(status_code=404, detail="Jármű nem található") - return { - "id": asset.id, - "vin": asset.vin, - "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', {}) - } - } + # Közvetlenül az objektumot adjuk vissza, a Pydantic AssetResponse + # modellje fogja formázni a kimenetet a dúsított adatokkal együtt. + return asset # --- 2. MODUL: PÉNZÜGY (Költségek) --- @router.get("/{asset_id}/costs", response_model=Dict[str, Any]) @@ -89,11 +89,7 @@ async def create_asset_cost( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): - """ - Ú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 + """Új költség rögzítése automatikus EUR konverzióval.""" if cost_in.asset_id != asset_id: raise HTTPException(status_code=400, detail="Asset ID mismatch") diff --git a/backend/app/api/v1/endpoints/auth.py b/backend/app/api/v1/endpoints/auth.py index 8861f96..9da6cfc 100644 --- a/backend/app/api/v1/endpoints/auth.py +++ b/backend/app/api/v1/endpoints/auth.py @@ -153,7 +153,7 @@ async def complete_kyc( ): """ 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) if not user: diff --git a/backend/app/core/validators.py b/backend/app/core/validators.py index f59bf8f..628e679 100644 --- a/backend/app/core/validators.py +++ b/backend/app/core/validators.py @@ -1,3 +1,5 @@ +import hashlib +import unicodedata import re class VINValidator: @@ -44,4 +46,31 @@ class VINValidator: "country": countries.get(vin[0], "Ismeretlen"), "year_code": vin[9], # Modellév kódja "wmi": vin[0:3] # World Manufacturer Identifier - } \ No newline at end of file + } + +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() \ No newline at end of file diff --git a/backend/app/db/__pycache__/base.cpython-312.pyc b/backend/app/db/__pycache__/base.cpython-312.pyc index e14e879cac5a9f8d7c9f5884e7680d153496f650..d2e5944483f5afb1b91a18d0b0ec16253e9af92a 100644 GIT binary patch delta 822 zcmZXR&rcIk5Xax^7U-{SOACdzltRmoZo##Hm}oo*2e}Y1;h>pBlMTC;jc;1=b~)4w zoV;m*`+s=xZ}8*^jh9VG4@MJjCMI(A&Bj9$H+i4Te7`gEc4xn(UT5@Ax}IX{`S|Re z`{PnC$MXELwQ`OVFu|G4Y)vFE2}w*r3e%9r3}hlbE_BR77ITopJme#7OiW?{3Rr|9 zPQg^9jf-h4K?!GI24`Uw%TUG&RImzFoP#;6K`pAA5c61vIxfHhF2eaDHlTq`XyOtq z;W8{oMG3KjtFRiyNzpd2_t5|rm2xji-1RheUVKfLEJ zIlmiJw1;alq23`ZTBoNb_H9RarfaLIYM4waN78SSX+kEr&}uJq3bTZqLYN4g#f#P$5X9LOehEs;C=wRZpbjMgZZ$tjKW1sCxQUkTpCtL;^4Z4nGYn@UHAc>ne< z*MiUScIScexjNo+g?HUkOd}K z5E7L~CcXlE2NuR|k*W-cfvpmV4lLZ`$(H=nIrsdJCEs_MAG-0~Fm$G#Paod9pD&EM z_J>^ztv8&_**qpTF$qabK?-%yF%9YPJT5Y5fPq=ahJ8Y8VGeSbhddUbfJG=`2})Ro zGFG61Rj7u!4N=27)Ug2#Y(f)T&_WYTY(pD6&1v6@M`Gol!h^%x?#8 z`Gjl17vB47X)fotf{(F7YgL?{pWEoo9pT&V+&Npgi=|AeZJ8qIzGOQ17c&ntR2qs^ zF0wQAePs1bFdK{Sb$VpWTL-Cz0(2Tx|+*$ekn znlvs%#xGre;mM_J({d-cj@S8ipeI_7x|A$Ok~2>_C&D{*XR=4(J~1G6C>p&5-664)_YR=W`KT}-CIOjiE?`DJXr~HyVS+k={cC=;>gKvpm`|g_Ezhc8HX5VNU P*M2j_Ulq0G9ku))h1Z*p diff --git a/backend/app/db/base.py b/backend/app/db/base.py index b3b2d5f..6451d1d 100755 --- a/backend/app/db/base.py +++ b/backend/app/db/base.py @@ -1,10 +1,11 @@ # /opt/docker/dev/service_finder/backend/app/db/base.py from app.db.base_class import Base # noqa -# Közvetlen importok a fájlokból (Circular Import elkerülése) -from app.models.address import Address, GeoPostalCode, GeoStreet, GeoStreetType # noqa -from app.models.identity import User, Person, VerificationToken, Wallet # noqa -from app.models.organization import Organization, OrganizationMember # noqa +# Közvetlen importok (HOZZÁADVA az audit és sales modellek) +from app.models.address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Branch # noqa +from app.models.identity import User, Person, VerificationToken, Wallet # 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 Asset, AssetCatalog, AssetCost, AssetEvent, AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate @@ -12,11 +13,11 @@ from app.models.asset import ( # noqa from app.models.gamification import ( # noqa 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.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 SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty ) -from app.models.security import PendingAction # noqa <--- CSAK A BIZTONSÁG KEDVÉÉRT, HA EZ IS HIÁNYZOTT VOLNA \ No newline at end of file +from app.models.security import PendingAction # noqa \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index b03dbb0..f880981 100755 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,11 +1,12 @@ # /opt/docker/dev/service_finder/backend/app/models/__init__.py + from app.db.base_class import Base # Identitás és Jogosultság from .identity import User, Person, Wallet, UserRole, VerificationToken, SocialAccount -# Szervezeti struktúra -from .organization import Organization, OrganizationMember +# Szervezeti struktúra (HOZZÁADVA: OrganizationSalesAssignment) +from .organization import Organization, OrganizationMember, OrganizationSalesAssignment # Járművek és Eszközök (Digital Twin) from .asset import ( @@ -13,24 +14,25 @@ from .asset import ( AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate ) -# Szerviz és Szakértelem (ÚJ) +# Szerviz és Szakértelem from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging # Földrajzi adatok és Címek -from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType +from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Branch # Gamification és Economy from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger -# Rendszerkonfiguráció és Alapok -from .system_config import SystemParameter +# Rendszerkonfiguráció (HASZNÁLJUK a frissített system.py-t!) +from .system import SystemParameter from .document import Document from .translation import Translation # Üzleti logika és Előfizetés 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 .security import PendingAction @@ -42,16 +44,15 @@ ServiceRecord = AssetEvent __all__ = [ "Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount", - "Organization", "OrganizationMember", + "Organization", "OrganizationMember", "OrganizationSalesAssignment", "Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials", "AssetTelemetry", "AssetReview", "ExchangeRate", - "Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", - "PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger", + "Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch", + "Point_Rule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger", "SystemParameter", "Document", "Translation", "PendingAction", "SubscriptionTier", "OrganizationSubscription", "CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership", - # --- SZERVIZ MODUL (Tisztítva) --- + "SecurityAuditLog", "OperationalLog", "FinancialLedger", # <--- KRITIKUS! "ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", - # --- ALIASOK --- "Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord" ] \ No newline at end of file diff --git a/backend/app/models/__pycache__/__init__.cpython-312.pyc b/backend/app/models/__pycache__/__init__.cpython-312.pyc index 655b2d54fb8eaa29cc943a02a44f516da9a8f142..3dc9bf0731c1ee5640069b873dc70bd11af64710 100644 GIT binary patch delta 918 zcmZvZOHUI~6vyvvr_<6WkQBLl~aIp>~p=KlZlGxjzSe;DT4qWQFdar7wNz$7HSR5rS>8@e$CDeQqB?1f&h9yI!}ANny3 zX&it79E3p}f+6oLWL&^u7{&}_Fbi26fe{>qQOrRO$6(CsX*2RT4&z=NHYRWqCcQXf zOyM*%roAL;%wPcuI195_gd)ztoL7$-^H_osF2DjV!lGA;8y9g2mT(!Cy*y#8;3}+A z!s2j=zn578)>sEzDHE;zn8JF!Bj0%@oXAsG+E6TZMnxjK(tXQ0q-)ggyrRQtw{OcV zD;52TV(Zn)wqh`A-Lmv@1zE**KGK&o;52>3kSlMPN<}?z-uOPu``z|!rq)c|uCLcL z-QKL0-T2laGrc8>A@am^y}}1{o-wV=OsDBD&^hO|e}^WWkklV_!*{BB#ooVLGnkW? zvYR=6KXXi=D3BKz7nl&36etMH3QP%13(N>~33Lmj1bUpNv`jagchX+ejPZ&aP%Mkt z&R`%jVJ1Z(XfgA#uCjkdy+cLQOlDb5MCysV(XxX2LtXXMIaw*waAm7**$mB5(H+vN zYEAfM3JGq+=1y6L*X`+X!)n!J`$n~_tN)Y5FDG35OOqQs&@H=a*8iw>LzZWI%C7(R zXr6!1ED0j7idKZ zy+=QhZP9R>ekQj>pz#s)gs5Ui_LK~qlB_5n@W!$TuJKJ*zml0#k~x=2q`m2UlRLw) Mi0^68yumN<3+$Wi=l}o! delta 750 zcmZvZ&ubGw6vubcBpbWkG)0;wZIZT0+N5ckq(7{-6+}c4JcuAw-X(n>5-i~|EQd9hcm=Ca4fQx*!5Y*;ZSYmBLp{_J{N5Ti zpn*+jVhdWh4(nmU=BE|kE|$-$Xgbb?;;!WXXKMSA8WFPtVH)&Rz8pBWG!CW5zFdn#2TSOs1jBPHNq;P zPM9X72{VL@OviTFb@?FnSPxZdm>MMfiO>4AS@D!gpxvFdIQgQux<5w2X0rCm3JH@?7nq`PR|!h zRAcy})ARU6PntCtxB9|q^M2R0{x@=ye+@STcumdU}}5M6wbyNW8bvmd0bQ9@>Sa>u_y29x%+q67wtBMw(_mdn)OTR y)l;o-s+Gw;RQWE6)~QxHi)%(|sGlS+4~>(l{4jZvnjM-yqDdq2LKipHO@9F3QnXM2 diff --git a/backend/app/models/__pycache__/address.cpython-312.pyc b/backend/app/models/__pycache__/address.cpython-312.pyc index 3a1ac25909009a57c02932b194d41baf7a8bcd94..69e348710fcb5986b8c33104d2d698ca50c48702 100644 GIT binary patch literal 4997 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 delta 888 zcma))zi-n(6vyup$Bym%PJl)&q!FR5AtX>35Co7ADpV?wB2k7gSg!4gNOsbm)zxVaMckiC>@$$=KnfK|mM&P-0 z?^*ApGS1A=H{<2zh!Tf5aZmNNt}e9XrTla^Ep)=m_}Oljk{G!~9QlAaikp2M$25d= zbHYu6t4_Ij;cDROQ*J@HDR9$MZgF3yMrMN9+J?eq+A13~k~TNjZ{mA9?A!RhVFj)| z&L80~XhaP@h`q~ZAcP?l7{MK0m8<-I{G5DB_I4SYy~Bb{;S(0U#hb?D_7aLEiYF zfo+;oE#<$G^SMP#EBY#@=LM#unTn)Ap<&U@43cS4jL* z(N3||zkQC9Eq~yIo_me8A%emPHUK~3l+rIm|4KTaNJsrhI)~))A*t}I#iss~l;{Q( G*XB1n*R6>F diff --git a/backend/app/models/__pycache__/asset.cpython-312.pyc b/backend/app/models/__pycache__/asset.cpython-312.pyc index ad0a3943e09f0868c77aed4cd482dd83c20c0699..60e7beff984783d7316f1c70edbae2e44fbd1c98 100644 GIT binary patch delta 3792 zcmb7HU2Gf25xyfSilq1}{)v)hoh8K*ZC8$!#6j#NmSbD-k0d9yV%fA4_Qbo;Cs9Xv z?#A+%ykT7e#3#E|j(@ELaZ;1k^xL6fJ}RX@dd=#1_@P`K73khuVeuVxUE5 z_C(5NohBi0-|f!M&dtxx?C=N2Zx4IF@OWGt{JOk9QU-R+d7JowPrIJwn=RbSoD_SH zlbYr5Eh}Z` z$0_+MJe7AMQe6vFXq9&&ifc`>vr1?}f*b>EmKv$p8jQ8Tc$*ZG!i$lrimmx*yonyL zW||gw!={Q8vSHU!m*ol3ut}mW4lmH(T4URGfVPH1&1Ev;XhvS(2@=*r2ohbl_Ve56 zleX&vj>+t_Op@c%hEG=0iYg~l;-r{T^t|Dj5U(buWhH%CPmZSzmv}V;tJ$2Y8!kCV zvdL6N)HK66nw9cNJwGYa&uya?7f<)peKdIA&iNy=>G|QZZ&&g7T4Y;kb@x{_739uUQocDm2m#`n`#o$b*s;EeNzBdg{nfnO#;+`pT? z@7%-hp|_or;qM@!2VpP5lL-6h^R70&mtJzU^4sW)>o5E+`f9^>V~23D58*HZvkGC? z@*f+%&)eBboX&+~G~-$2&(c0`r0}r8%t~?uc0BMh?I9GhzNz+cAg?z9kU<0{T*c9D zfT4JZJXTX=z$_s)gkFT#5gY)9L)7S>y~_4sq|_*X7HJ44=q2B~rtW8%?*I1vqi_zl z`VQcC>_CMaUyyK9W-a>zLVwWqg7RAPMbB!vntyeWqbq7;hRD ze2if`AO^_sRhE&r=m&x0kF^UmCkVszpMeidyUw9q%QqV1d?5iVhQ~CF$Ww|ut$npw z{p2LDKk%?vXwYeR^(5QHTCTj&$h)C8JpxyDn!eT4qH#n=06kTd9yAKtQtp(fWy{o~!#ep?Xtkk{g z>3FB#tY0H6)4y!`vTz=R8*bC}^3*r+`Z_awD+^cxTtQ{gsYCJbw{fHAQ9q70@9$3jenv=?E-|!5P_fhH%gc$^^SnDk2Sisip zNUI(yvj8xw2`-pN2}eKVpokB}JHL$s9NXkZ9i|;rzKRTFa~YePdJ%N@Di7>NPNO7SrRs=4Rx|zvP#xP zTebD8M=CrHsjgBrB;Z-BuW~rQmcv!$a4;DK&{F|0UB;OQ|8U7>C7-h2Qql%Ww!Xe4$J_06}0KY0p&{sYl&4>qt6Ot^X z^Phb>rjM=sW?T@3Nd+27p-Y8YFfQv|LU&;@(<2;Fbdr@-;mA|*9wzCqa3(vN)dfXW zfvl)fE;A(x<1&#oMVOG&f;c0r{7TbiWKGDdEa>vgXLEusr!K3?xTpwIB2iX8RyEy} zW&~2E&Pl1F^e|}yt;rJ{1gk%8F}pu(hs`wYz)>f_jI(P}RwYGEcL5iA*6bFm&B#NE z8^+ZL!ZQFyC^;dH%gKqXBxlUNIGIb47F@4p%22!ztw{i*KADuVsbtb@7Fn4vlK^+t zA=G@xu4Qv1B@=9m2wnxynd!PbF$v`fZwkW=O}U=jq;1|C$U6>DAZ{G8e&`U6`Vd$H zdK*UwpTML20pLCEzQtPSxaZ))-NnJXPTy?({P0q!?A%{`X{{kJx3d(ayKmMl4VN1Z z6pyaC8|U_yI_SZh;U%f;J_t8PsQLQ-`SuSF+#D{4dZFF-1YelzFZI%stDd%EKTOTp zg1&TWb#LG2wz6js$i6WAYq(PO#o=E=Fm}Bg{xvl3TMhOW2k6CctmnRibH+-M`RL8P zclyqi9V35r24?r)aWvnnvqtRi44EdOA#s7cLACa``Pb>a_JgmmBFj2e*4D9xhh2;8 z7GTwm6&dD;N1bCho`=BZGV+UL1q4IZaaxH^b+mf~{)O46H#m509OdaRI(iTJ`OaBs ugM-&br-MH@8`|LDwGj&P?X$KG4qh9(?0hfog4c#W%-d)6e{t|)n*SHe&{>QC delta 2297 zcmb7F-ESL35WjUCJ2r2F08 z%)NNijbav5gI1JDV5TFoULz-F4#B(`CS~K7_60igu;2sAl zlwGjuAk&rr3X%m&I~pa+mXNoTifPadDK(d-p~{#CyKeHGr4RLx-z@3V0tk8m4gmB4 zJOfZiK{L<25Rb)99M_ESU9BE4jQc@f5Bj=AMb&HxWnNY@ay*@YPqdKxjla;uFI&(0 z4};|pz!3n(3m{lRws(+;X}Uusvhn%3pd>S zdtsI;v?e{=telXOau(;SqQxp~9|lMPqyeDO@beVP77Ci-QWB4Y2q;{jH@#jxrgJ$@ z*PC4UJ-BpK6{?yH>}mkO8&@&YQHq8iPn6T|YNBEMc; zgc@3t_&I9+mkmxre=Z~AOur)G5PSi;4^INH+FXPc)J8MLs9i+x@4Us+9SH z!n@ofS2r2xI88?P4F@)?zK^_L1s+<*-k#Xt+wa=f`GG4h>cb*Zy0g!S3Mc6%&8qjI zyeO;LU2Vt?R!x@5gLF$(^Rd2L02%aMgeLp(God;ooNaeT`dWv2U=r^npq4Vkivd*f zUhC$#ILVIHfxxVxFh@07qfTc&m3cB7z2;ZI>n(uG05B-ZtbF6JGJ$NnEp+OPwE_xY z&6)vU<)$D3`~Ob@cJT*KY-FyGvEVWi$&bOV!!`N1NsTm)HH(DFq%wBxrBTae@S;B zdS_P1Z6Rd!pl4UZ6^=eC2gwh@;2{e-xGGgR`l$FC(8#K*!qG>?#iP)wvBJ?urPqW8 MSHpjB^ubQYKjdD_;s5{u diff --git a/backend/app/models/__pycache__/identity.cpython-312.pyc b/backend/app/models/__pycache__/identity.cpython-312.pyc index ad0300b1e2864f706ec8b8ff4842f202f5e9a323..656f7ee312c7c102d14247b2a28361bf4a2b9b8c 100644 GIT binary patch literal 7896 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 delta 3529 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 delta 895 zcmaKrPfQb05XSc{wJqCr(|@!`p%kH%rL<556OnL0C17i@p%JYLf$o}=)D~VTh8P2c zgPIs4nF|*aZ-%r{%wD~C_NXzjXSsRNw4QylmTHK)$^PEVH{Z^i_mX{g?qNiHZ?Om* zd*sy9%tyzr*h=hg0uG|vAd+28l0%;V-lU$_ZShk|H_s_Oaz}}vpX18j68|eo`B_C@ zuXp~X-mBFOjDqZgRYS{#fTncGC(71Fm%h3t{GmyxvA3T&yN)`uJh*%va(0jZ_Y9%` zJ2y`?_6alRpxiC@$o_I~si}U3Ax#Y&QzM$%Crjo2#`mW+EqKg%MpFlvDwIQwuEQH) zX|Q6Rq3UO2iNJwy$-Yl2VnLzn3SE$uJB!8a8q|al7&2{=1RR*c)&!fVVkj1rY^Jal zmqyedrd6VEW-1k6-?9Z4#Zh=9ddaBzQcM$=v_64DYs|7eL%nE{7}C487In!MC1eg> z*&oMZOp|yzjrI7Ei9W#S8be%arEQpa5bcN*;wIu20k$x;RZrPT!n8A zhcJgBA_1cFE!*0sJE?wg-XLTa#$Aeeo0TSM1YJ>h?HXvijNudlho<)tI1f!i%@v&Z z_YCMUB6b+G%c2VznVKs`OrkGz_+dE<~&Nt6_bP=wlqw0X6Mz`;>#9M6;(96QX~O`QWa zD1SiANZ=|6N`+f6$v%9~U z{cG@tQQwbVuSbCE)Bcar3z3SiP5iAg^h<|Lm={Q3S0F*!dSu4{L5N={iZS&|@hiLd zkbmu%7JH9>cg)+xh2xi7qM|YgA?yW!m6(?&q>jK85;&JR5U%iDsV8y+hYJXc2+IcW zZ>2StK`!%7&o|--Z+PyDH~F&M(;35vFv4|&%fx?<^yshfS= zYiVASGyAyD(tIXo_VJSc95HZ?7Cc8&E$#WmgixcBxqz8?xJkg1`mHw+IfILy!L{=5 zyghOoD$wA)@jsN-Qo9ieCZe4rU>2$ct?U=9coT&rNw^lVGIalw z;UYvv%~`-*dgrPj@T{%R*;{OlwH<%q^NWiY;ZriS=aY|dbPM4Uz(bMU#_IH=KAC3QG?UStcQd&h(;dXX4W{kr%H2XHlSmT6sHU-5D5^W@W-^^&b9_56dhHcl z&}#^8gdoB+fbK~#n#@y@NajzQe+`2WaEEDk_^*Mv(quy&D=(hN!OCDY#9usEZOFsr z*%P(xLG-}B-&t1&KsD}c`yF+)AMQf!4+am^{o#6OpuBVvh*s`Z*ZEF8&|AK7qNtTC z)m}bbSGvn{&8#XPuFGAJ)2LzIJr1>32lN;|K)I>V^JTl&z}PRqBrK#)!DR`%H5-^cq_@k4|(|RVPbNLEtAw(6rPv00<5x| zfF;AGud}yc`~=OX+pfRytN{aT3|KafFo7_MaQdW0&WVr$(B1FS;&zS^T}kB%**q&I zQaM7I3pFXNC6cLp8rF)2D(P}2spS)_kUhgl3{xT$p=lccyTYUJaC!MrwEMvIQ)H;T zaw7Tn+~6i@xO~-e*P*i^Uj`@m!j(6x*ZA7uRKqu3o^LuTxF0)QZGD4_e_>`!(qh&3lL@@7MTO zF~~C>X+%L4`v14rX~y}10nO9cAiwPKU-vdv<}2E~Ay64d-7o{FaYC4at1=J(e5lJEmtcY)a04ia3VPNx!C?bqF0eI?+t`9agIP*F-n`|OT6*X08*=CcMron$ez;uJX=bUoc#pEW`+hN>&tx@?QC(+~ z@RGWnE@pFiHpR!~fyq%!Fo9@C$cQmO-L=4Iwm`K^ws5d`(r7|p(drTYTE3FdcEW~l zqLio!ayh!8@OZ`cX37x8N>epaDNk>lnc zLpf7Q{&ZOUJ^YDpQ!wivbErr8y#MoA^Vcw|*38~o&h?Iqi_2OuPsbU95Uf)DCchm> R%Kaos)^Gf>;AsA6e*xVy&58g3 diff --git a/backend/app/models/address.py b/backend/app/models/address.py index 67b09ae..5cf92e7 100644 --- a/backend/app/models/address.py +++ b/backend/app/models/address.py @@ -1,7 +1,9 @@ import uuid -from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime, Float -from sqlalchemy.dialects.postgresql import UUID as PG_UUID -from sqlalchemy.sql import func +# Hozzáadva: Boolean, text, func +from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime, Float, Boolean, text, 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 class GeoPostalCode(Base): @@ -45,4 +47,44 @@ class Address(Base): latitude = Column(Float) longitude = Column(Float) - created_at = Column(DateTime(timezone=True), server_default=func.now()) \ No newline at end of file + 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')") \ No newline at end of file diff --git a/backend/app/models/asset.py b/backend/app/models/asset.py index 5c8a147..907d1ec 100644 --- a/backend/app/models/asset.py +++ b/backend/app/models/asset.py @@ -24,6 +24,16 @@ class AssetCatalog(Base): year_to = Column(Integer) vehicle_class = Column(String) 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) 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) asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.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()) released_at = Column(DateTime(timezone=True), nullable=True) status = Column(String(30), default="active") + asset = relationship("Asset", back_populates="assignments") organization = relationship("Organization") + branch = relationship("Branch") # Új kapcsolat class AssetEvent(Base): __tablename__ = "asset_events" @@ -144,4 +160,27 @@ class ExchangeRate(Base): id = Column(Integer, primary_key=True) base_currency = Column(String(3), default="EUR") target_currency = Column(String(3), unique=True) - rate = Column(Numeric(18, 6), nullable=False) \ No newline at end of file + 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"} + ) \ No newline at end of file diff --git a/backend/app/models/audit.py b/backend/app/models/audit.py index eb0c53c..ac7d03c 100644 --- a/backend/app/models/audit.py +++ b/backend/app/models/audit.py @@ -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 app.db.base_class import Base -class AuditLog(Base): - __tablename__ = "audit_logs" - __table_args__ = {"schema": "data"} +class SecurityAuditLog(Base): + """ Kiemelt biztonsági események és a 4-szem elv. """ + __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) - action = Column(String(100), nullable=False) # pl. "LOGIN", "REGISTER", "DELETE_ASSET" - resource_type = Column(String(50)) # pl. "User", "Asset", "Organization" + action = Column(String(100), nullable=False) # pl. "ADD_VEHICLE", "UPDATE_COST" + resource_type = Column(String(50)) # pl. "Asset", "Expense" resource_id = Column(String(100)) details = Column(JSON, server_default=text("'{}'::jsonb")) ip_address = Column(String(45)) + 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()) \ No newline at end of file diff --git a/backend/app/models/identity.py b/backend/app/models/identity.py index 6e03e4d..ba92094 100644 --- a/backend/app/models/identity.py +++ b/backend/app/models/identity.py @@ -9,29 +9,34 @@ from app.db.base_class import Base class UserRole(str, enum.Enum): superadmin = "superadmin" admin = "admin" + region_admin = "region_admin" + country_admin = "country_admin" + moderator = "moderator" + sales_agent = "sales_agent" user = "user" - service = "service" + service_owner = "service_owner" fleet_manager = "fleet_manager" driver = "driver" class Person(Base): """ - Természetes személy identitása. - A bot által talált személyek is ide kerülnek (is_ghost=True). - Azonosítás: Név + Anyja neve + Születési adatok alapján. + Természetes személy identitása. A DNS szint. + Itt tároljuk az örök adatokat, amik nem vesznek el account törléskor. """ __tablename__ = "persons" - __table_args__ = {"schema": "data"} + __table_args__ = {"schema": "data", "extend_existing": True} id = Column(BigInteger, primary_key=True, index=True) 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) + # --- 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) first_name = Column(String, nullable=False) phone = Column(String, nullable=True) - # --- TERMÉSZETES AZONOSÍTÓK (Azonosításhoz, nem publikus) --- mothers_last_name = Column(String) mothers_first_name = Column(String) birth_place = Column(String) @@ -40,8 +45,14 @@ class Person(Base): identity_docs = Column(JSON, server_default=text("'{}'::jsonb")) ice_contact = Column(JSON, server_default=text("'{}'::jsonb")) - is_active = Column(Boolean, default=False, nullable=False) - is_ghost = Column(Boolean, default=True, nullable=False) # Bot találta = True, Regisztrált = False + # --- ÖRÖK ADATOK (Person szint) --- + 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()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) @@ -50,28 +61,39 @@ class Person(Base): memberships = relationship("OrganizationMember", back_populates="person") class User(Base): + """ + Login entitás. Bármikor törölhető (GDPR), de Person-höz kötött. + """ __tablename__ = "users" - __table_args__ = {"schema": "data"} + __table_args__ = {"schema": "data", "extend_existing": True} id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, index=True, nullable=False) hashed_password = Column(String, nullable=True) 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) - folder_slug = Column(String(12), unique=True, index=True) - refresh_token_hash = Column(String(255), nullable=True) - two_factor_secret = Column(String(100), nullable=True) - two_factor_enabled = Column(Boolean, default=False) + # --- ELŐFIZETÉS ÉS VIP (Időkorlátos logika) --- + subscription_plan = Column(String(30), server_default=text("'FREE'")) + 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") region_code = Column(String(5), server_default="HU") 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)) custom_permissions = Column(JSON, server_default=text("'{}'::jsonb")) @@ -79,18 +101,25 @@ class User(Base): person = relationship("Person", back_populates="users") 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") 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) 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") +# ... (VerificationToken és SocialAccount változatlan) ... + class VerificationToken(Base): __tablename__ = "verification_tokens"; __table_args__ = {"schema": "data"} id = Column(Integer, primary_key=True, index=True) diff --git a/backend/app/models/organization.py b/backend/app/models/organization.py index 00c6987..9e3c1cb 100755 --- a/backend/app/models/organization.py +++ b/backend/app/models/organization.py @@ -61,6 +61,11 @@ class Organization(Base): status = Column(String(30), default="pending_verification") 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")) external_integration_config = Column(JSON, server_default=text("'{}'::jsonb")) @@ -70,6 +75,10 @@ class Organization(Base): created_at = Column(DateTime(timezone=True), server_default=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 assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan") @@ -77,6 +86,7 @@ class Organization(Base): owner = relationship("User", back_populates="owned_organizations") financials = relationship("OrganizationFinancials", back_populates="organization", cascade="all, delete-orphan") service_profile = relationship("ServiceProfile", back_populates="organization", uselist=False) + branches = relationship("Branch", back_populates="organization", cascade="all, delete-orphan") class OrganizationFinancials(Base): """Cégek éves gazdasági adatai elemzéshez.""" @@ -111,4 +121,15 @@ class OrganizationMember(Base): organization = relationship("Organization", back_populates="members") user = relationship("User") - person = relationship("Person", back_populates="memberships") \ No newline at end of file + 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) \ No newline at end of file diff --git a/backend/app/models/service.py b/backend/app/models/service.py index e286b6a..bd82241 100644 --- a/backend/app/models/service.py +++ b/backend/app/models/service.py @@ -86,12 +86,17 @@ class ServiceStaging(Base): name = Column(String, nullable=False, index=True) # --- Strukturált cím adatok (A kérésedre bontva) --- - postal_code = Column(String(10), nullable=True, index=True) # Irányítószám - city = Column(String(100), nullable=True, index=True) # Település - street = Column(String(255), nullable=True) # Utca és közterület jellege (pl. Diófa utca) - house_number = Column(String(50), nullable=True) # Házszám, emelet, ajtó - full_address = Column(String, nullable=True) # Az eredeti, egybefüggő cím (ha van) - + postal_code = Column(String(10), index=True) + city = Column(String(100), index=True) + street_name = Column(String(150)) + street_type = Column(String(50)) # utca, út, tér... + 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 --- contact_phone = Column(String, nullable=True) email = Column(String, nullable=True) @@ -111,4 +116,14 @@ class ServiceStaging(Base): status = Column(String(20), server_default=text("'pending'"), index=True) trust_score = Column(Integer, default=0) - created_at = Column(DateTime(timezone=True), server_default=func.now()) \ No newline at end of file + 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)) \ No newline at end of file diff --git a/backend/app/models/system.py b/backend/app/models/system.py index d202a64..0ae42bc 100644 --- a/backend/app/models/system.py +++ b/backend/app/models/system.py @@ -7,7 +7,11 @@ class SystemParameter(Base): __table_args__ = {"schema": "data", "extend_existing": True} 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) is_active = Column(Boolean, default=True) 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()) \ No newline at end of file diff --git a/backend/app/schemas/__pycache__/auth.cpython-312.pyc b/backend/app/schemas/__pycache__/auth.cpython-312.pyc index ed6517b34c9e878266873265ececb96bea96982e..164aa027cdbf6e512ba7a6686180359b8d43ef4b 100644 GIT binary patch delta 583 zcmX>h`%aGUG%qg~0}vb$nv^NUv5`-kjfsVE@;@$}$?0rS^(ov@9I5Q7%#uJAsVphH zHb5FE#)%}x2NmN&65|JnrE;e-r}CsRLG=oN#dv|H2!dEl45@r6LMg&)L{>8axj+!b zk7R-*HnMLKPIXSm@VM1v+`T0e+cwpQVAaC*^ z_Hagl$$wd-q&R`*6j^`>HV|P6BCJ4!^<;Yv2}Z}sioC*;vpM(}7f-I@P-c8Ec^-$H zrMmV77SRb63z#R?US`q0p{(BEJHcXx^F-UrEb2Fu)EayyXv{F4sC$`34VCv`@+poD z3eF(oL4+|77qbJ2R)!ng!Y%$q@?e?C3T%>-H*;QLbO%|^ zh)}RC{valZaGY$%o6hJzc^PlK9@wQ|`@wdB)yRPqf*q;=WZmMh$<0qG%}KQ@ik@uD X7s4mZ=*&1F^9uur{-QH^DxWa`!g+~< delta 423 zcmaDScS4r$G%qg~0}yzB>dG`=-^i!U#&}|KEvwYzz1+f+)7hfjQaMtYQ#sR^B!P-j zSyFgyKr|PS#Rpru zC;w%Un!JF6Uz8mvRip_bR6qnPh_IV{nM0AWV)9oGWyTwmB{}8zl++r0CTPqso~V17 zMeWAq0?rKz4j`2vLJx?GLB_N)+~5{&@h_4GvWpxhJ8->~a{)2sKm=G&0mO2G5r&h! zxosJpCfBh@PF}=4fy)gf0V2#N+wdqbx`8A%m+>ewdi#Q8*g%9Vh>!shejvghNZex1 z%quAh0C59BL=cETv(pD81|n=GKjBSh^qK6#7cbxd5(K*jY_reg-F&i)5tDE7h4Aq) TIx|kl{K5dDzbH+%<~IfaT6I`V diff --git a/backend/app/schemas/asset.py b/backend/app/schemas/asset.py index c5ab1e5..25bc254 100644 --- a/backend/app/schemas/asset.py +++ b/backend/app/schemas/asset.py @@ -5,6 +5,7 @@ from datetime import datetime # --- KATALÓGUS SÉMÁK (Gyári adatok) --- class AssetCatalogBase(BaseModel): + """Alap katalógus adatok, amik a technikai dúsításból származnak.""" make: str model: str generation: Optional[str] = None @@ -13,39 +14,57 @@ class AssetCatalogBase(BaseModel): vehicle_class: Optional[str] = None fuel_type: Optional[str] = None engine_code: Optional[str] = None + + # --- ÚJ TECHNIKAI MEZŐK (Robot v1.0.8 Smart Hunter adatai) --- + power_kw: Optional[int] = None + 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 # A robot által gyűjtött adatok + factory_data: Optional[Dict[str, Any]] = None + # Pydantic v2 konfiguráció az ORM (SQLAlchemy) támogatáshoz model_config = ConfigDict(from_attributes=True) # --- JÁRMŰ SÉMÁK (Asset) --- class AssetBase(BaseModel): + """Jármű példány alapadatai (egyedi azonosítók).""" vin: str = Field(..., min_length=17, max_length=17) license_plate: str name: Optional[str] = None year_of_manufacture: Optional[int] = None class AssetCreate(AssetBase): - # A létrehozáshoz kellenek a katalógus infók is + """Séma új jármű felvételéhez.""" make: str model: str - vehicle_class: Optional[str] = "land" + vehicle_class: Optional[str] = "car" fuel_type: Optional[str] = None current_reading: Optional[int] = 0 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 catalog_id: int - is_verified: bool + catalog: AssetCatalogResponse # Ez a pont kapcsolja össze a dúsított technikai adatokat status: str + is_verified: bool model_config = ConfigDict(from_attributes=True) # --- 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): + """ + Komplex jelentésekhez használt séma. + Összefogja az identitást, telemetriát, pénzügyeket és szerviztörténetet. + """ identity: Dict[str, Any] telemetry: Dict[str, Any] financial_summary: Dict[str, Any] diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index ac72049..543375f 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -32,12 +32,17 @@ class UserKYCComplete(BaseModel): birth_date: date mothers_last_name: str mothers_first_name: str + # Bontott címmezők (B pont szerint) address_zip: str address_city: str address_street_name: str address_street_type: 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] ice_contact: ICEContact preferred_currency: Optional[str] = Field("HUF", max_length=3) diff --git a/backend/app/schemas/service.py b/backend/app/schemas/service.py new file mode 100644 index 0000000..fa8b362 --- /dev/null +++ b/backend/app/schemas/service.py @@ -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 \ No newline at end of file diff --git a/backend/app/services/__pycache__/auth_service.cpython-312.pyc b/backend/app/services/__pycache__/auth_service.cpython-312.pyc index 31580d8fd47efe6a7defd98ebf260c922da65d64..fa1072ba0c3215074a64e12fe9c7ca1b4acc369d 100644 GIT binary patch delta 2225 zcmaKseQXow8Nl!L_s5-`_>;38`{I1q4k7kPNLWTbVWA3Rl*;KuCS;kMmpB;P++E0; znU$ffqJwU*2WhQnD@?UQmsFy18~!WM42f<{S&Gwm9@?f-Y3#3BEt`m@s(asathJT4 zv+j4#_w&5(>$|hhqJNx5rav%@fx_3S`B%wM&l{!}tmvb{6{|yuY;fJt3Sp<)7$O1W znr?%isI6)$rmiWw@roB2vOmLb4anEhEJTof!{f80bQ)2nnzJNeNGqi)--9L zCgIdUSkby%*v-|9UvUo;cM!W9Yi?eHHGfT$cGskwZu&8Zv39iRwXStVj>% zSl0NRTJ-dHA+NE)yR6-4;0?TnH|EsSkIS}yv+e+mwI^z^|9vhn^K?}^Zpcy7WAd^e zcbU-lV87M{%WZl%;n7=o6K_tnAK7x;h$sb>Gv*9%(_z=^`8quO|K27GMMWZn|sH%?04HYd?vRK*4YM@UU1ASs&&3n zuUcWw%{7>K=fb9W<=12I%<7mQuDJ)aEbrpo(j;7Gy((6)2lDK0DVJ^8;=;ih%CR0nV9lLdoJCvN0GHG!sKbhoG znaSkgIJcRLCQ^y)R3h~h7aSJi>4~WjmrP5ENnt5Jf%75l?SR|fSwzF2Z>OouCdF80 zHk}Z}sbm(8`G%er$a#cNSyN4)smJP`rNy~yCM|JcZl5GgWzuC$d_qbdON5MNb4Hkq zr;~GWDVa&fl6=_^=XoI^idYs-;^eXx_c#(yrpv}`MwH^I*hGd;2v1>`Fh}qsf*grd zO$l0YPGGPM*L+<~!gIvcNAL~eH)a#Uk)$Z%PGU$QJD`u-+ZBMZTT2>&(E}Aen4#u1M>wd1Hs0Y-e6I6xHA-(NmxYG0_(!du@ z4&}#Y2e_JlYcDlM)*3rk8avOb&YMbH_#E>$t{FaWepPv|trxDeG#N^*ZKw7Y>86Jo zSZmp*XbVG2YhWkKI8CQ^S3DGB{^iKCBWHWg>?>JpzYd-YLfabc`6KQ5r;cK_{)zRK zxWHvY-cWM;OZH%Clc(hIuX#FFJRRU_Y2obdHG6OcJDk3UPTFNUHCl1iYW5asFT`4h zTj>qU`R{{dse#ke9#rWJyW!f73n8Svy=iSUK3+_RN*Mb`=~_w*B@%AIu>3DeDmoK z@jjkJV`22up-|LM-Q0qrjOu2G1IzEIi2WV)&Ind+Z9~yI)vfMfGgfY^QPirst@$>> z%CZ(k9jayBuof$K%qYsL?$qs4W96 zR|UEbCpY)j1g1;thgD@U}3Uu^b|Jm*5V;3c;TV zK7#Xu``d;TYTNU<3Pqk*C#ugI1E0$JXG%(AsVIo6GNJvTg_dF3hA7{le!^zbk9rtw!vp6rmjSAE(Y&?f?J) delta 2072 zcmaKtZA?>F7{~8v?=5|&dZ910&F@rZ z=l|U2oO^mt@BM>d-~dp6uTsegbU1Dt3_hwot3FHvN#Z1}6$3ZCq%BtpR_x*b)w2e8$z%fs@JWppJ~3%UaXAF8JUN<<@hCN78qR^sj7HdA*jcrt zju;U4I}t_wyAVNteI3!?h`tW=)mjK4oL#0;f^Xc+2 zU!*fC_5l|3DRwD(Ko9YjxQ*xmr^P+MR@8kN4i-6guNDM`9UBZYT$t$!Mp!PglxbiC zLUbF$kAAtecl5L{dMd_MFdI4pY_x}2jVgqo@HVOsbCIz@z8BdQjGUQ2n!-^*2rwwg zyc$3y0uvrRCGcElw0HD4D(nh`SSG-1363VVF@bGzA^7I#@i4LiFyCBR(OdZJIN@Mq z8^a-;m#KqS~35Q5Q&KgjYc z(0cGy2r0#Oj_(S_V#tW`B9yU_88@Zuhz2A6_CPog>EKf84xVF!LNQiyqiNh{t<~w7Lh#^6tRTN;jFLM4o<% zwj^oG9ZTJL{+4Cs0C`Kp%t#204$@^`xPL8nP8HWo6xR%pht$)|3j?Y<8tsRQLB)8{ zvh&U7-Iv#lxA>=8+9z7t$Jcd?o7f*UTv|dDm%(J&=M?H=Uj}!UTa+lb$i9E=Ev02f z3Qv}=gMXKo(>kS2y}vnKK&W!xZ#vL4P}jc>o?NujrI}A0s-4!Gj^!NAnJz4vHZ#+P z!YPAu!r)9A+-FwJ$f!K^3_+=K(@IniHP$xSw7q)D?wPQAV7IltqC-s3w%N3mV7#+w zy;P^3P5S_$Dx6JMs8#C6X@*c0J)TyU%>JIMhxe`S-J@u&_JLLa-fgh?T*MUz@Rg8P zDz!*oC2{^L)#yaQ*b?9?Bgbl2x=?U^G4NT*>-JRy3U1ng&raT~X!M|90y&lBgk6Vp zQUrWXGAY($Z3Mn*GHIf)F2;S6B>?H>WEnmstr?x+T61;}eWyu*}h{xPawFjB~KZ(Fz_U zZaCV+UCNq7s^V+biZ+RI$T-`$ON!#SKyz;~+ZfjV)ms#jEy8A^uJ zl^E3+^Mk(TGE3$!=yTNrOX6!+75TgiWNrbRgS*__pdvBjzDL@|QMaa6n}Oeq+kS(A z-*5Z^gcKP<+m2V`c<2MKUUD5PFRXIwCG!(l?p>n6x4=(fe2Z}o?)AO_ni6-t4}keA zoN33nhVdQ74UF3uKf^y8)-Ub@RNhDNG=a}&I_A!oy$>_-Ut&VCFfGDXdY2e>uKWSq xOVl;WMYdmXgUqJIFGc=0jC&Y65K_|RT~W3>%z607FtloWZX*Q@4++dn(Z8mzFL?j} diff --git a/backend/app/services/__pycache__/geo_service.cpython-312.pyc b/backend/app/services/__pycache__/geo_service.cpython-312.pyc index 11bfa2d3b83ecc548934c6f79678b577ab9c2c10..c4bb0106650d1881122bc15979957961a6af763d 100644 GIT binary patch delta 1642 zcmZ`(T}<0n6uwTJnAous5)w!X;TBp!SQ-QxElNiS5mZbq2$K9W%SJ+$JUnI>S`4O5M+R_tpkSOu=HG0mb)L=6_ zB(`YdYj*S%z%$p(vn&9}$_Vy9tExs;_NtZL2(%Ncs*x32wK5x4{RnyP3u4P*!tT_TM+(UB=&A?aY0uiiT8l+w|wQ59o)*6Q5bSgA!2}2rvi;ho|gn!7G&5?!)J#2wXL_b=dG+H(^5eY#Soklb2%vl zc>|}D(xf0J(mZ;f7sbSO=A2}V3KWF#$cyuH>Sek%o9uAM6Itlrt zk_^8i-g0`Kah8LX+Qptq z+fC0(bLXn7vm9J!9A#gHSUgbiRthVD&eht^vhQzaoemhUeFHGH?7T&91TKGty!qO9 zAFR0oi|KFk%R_7K#>$xo6zQVpt-2j}>Mx#HIB}_zuT1?~cjz8>Xx_Z$@?7j#=ukuR z)SnLT{if#YXD^@qlGFhec5VPFSgUiu?b~or0skhT8vL6aXl%KB_)4$t0BoJ^0?zt* zPL~PN+woX;nr&T=b-T&d@koyVNBEIWBbO)gTc`mawnSed7Kvyx`PoPhqPP7-w1vFA zuN(0@ED=3O-r)u)B$hoyw2fT$zKr-?nurF;yNn0(76Riz1KSJ?B2@on&k;T9IW9na zuFi48rGCOvzh-aH&6wPGd31|Gn&39+?A{`}6?lC*g%=SGhPG)9sB645xX`EDj1?Nt z+$X+|eGeHdH_X85dSJp-I);jp@E<*B)4cL~Vp5z;37HwS*U>Y6^y!+^hnZpOK}@T8 tC6nXxqHs(aKob9uW&B0zW`ZErLFYQy_XpVjJ76}+*9oHjA;4(3{|DM?pUnUO delta 877 zcmZ9L%WD%+6o>DfM<$tagijeDfS(kHp*XITd>4uNo z<>TC_=!#??-^xp_WOveri2Q$pq1@oeIbmZKYWAZpbZG(X71$-Pez5do_B76`lb?j` z3^TGsdc-_AC3dH)MeC?p(ua-JxuKJ#l3p|n%d~CXG0!+xq-b{Y2u*Fa%M=da7**Bg zrgO$2uch4?pOuBP*BQGE=G2*EOpLiqWXR;++PdKCsMbQ*OT`(5Amzy2I5yLo{YlDJE^CAZ`_%~ z=1lqY0?RBD8mZz>3#tU@&u%WJrML$=r0@l*p zl%MKpjI|6m%|xh2k}}m6U^W{EVvigeO1-RDShe Person {existing_person.id}") else: - # Ha nem, a saját (regisztrációkor létrehozott) Person-t töltjük fel 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( db, zip_code=kyc_in.address_zip, @@ -159,30 +154,26 @@ class AuthService: 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_first_name = kyc_in.mothers_first_name active_person.birth_place = kyc_in.birth_place active_person.birth_date = kyc_in.birth_date active_person.phone = kyc_in.phone_number 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.ice_contact = jsonable_encoder(kyc_in.ice_contact) - - # A Person most válik aktívvá active_person.is_active = True - # --- 5. EGYÉNI FLOTTA LÉTREHOZÁSA (A KYC szerves része) --- - # Itt generáljuk a flotta mappáját is (folder_slug) + # --- EGYÉNI FLOTTA LÉTREHOZÁSA --- new_org = Organization( full_name=f"{active_person.last_name} {active_person.first_name} Egyéni 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, 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, status="verified", language=user.preferred_language, @@ -192,24 +183,36 @@ class AuthService: db.add(new_org) 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( organization_id=new_org.id, user_id=user.id, role="owner", permissions={"can_add_asset": True, "can_view_costs": True, "is_admin": True} )) - - # --- 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(Wallet(user_id=user.id, currency=user.preferred_currency or "HUF")) 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 await security_service.log_event( @@ -223,7 +226,7 @@ class AuthService: "status": "active", "user_folder": user.folder_slug, "organization_id": new_org.id, - "organization_folder": new_org.folder_slug, + "branch_id": str(new_branch.id), # Új telephely az auditban "wallet_created": True } ) @@ -231,6 +234,7 @@ class AuthService: await db.commit() await db.refresh(user) return user + except Exception as e: await db.rollback() logger.error(f"KYC Atomi Tranzakció Hiba: {str(e)}") diff --git a/backend/app/services/geo_service.py b/backend/app/services/geo_service.py index f2a988e..957e885 100644 --- a/backend/app/services/geo_service.py +++ b/backend/app/services/geo_service.py @@ -45,22 +45,42 @@ class GeoService: """), {"n": street_type.lower()}) # 4. Központi Address rekord rögzítése - full_text = f"{zip_code} {city}, {street_name} {street_type} {house_number}" - addr_res = await db.execute(text(""" - INSERT INTO data.addresses (postal_code_id, street_name, street_type, house_number, parcel_id, full_address_text) - VALUES (:zid, :sn, :st, :hn, :pid, :txt) + full_text = f"{zip_code} {city}, {street_name} {street_type} {house_number}." + if stairwell: full_text += f" {stairwell}. lph," + if floor: full_text += f" {floor}. em," + 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 RETURNING id - """), { - "zid": zip_id, "sn": street_name, "st": street_type, "hn": house_number, "pid": parcel_id, "txt": full_text - }) - addr_id = addr_res.scalar() + """) + + params = { + "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: - # 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(""" SELECT id FROM data.addresses - WHERE postal_code_id = :zid AND street_name = :sn AND street_type = :st AND house_number = :hn - """), {"zid": zip_id, "sn": street_name, "st": street_type, "hn": house_number})).scalar() + WHERE street_name = :sn AND house_number = :hn + 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 \ No newline at end of file diff --git a/backend/app/workers/__pycache__/catalog_robot.cpython-312.pyc b/backend/app/workers/__pycache__/catalog_robot.cpython-312.pyc index 1b2569b19324058de96187bc786fc88e96a1d822..c9a2d28140aa9a63672d6a62b8ed9968340f0f08 100644 GIT binary patch literal 13102 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 literal 14980 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
  • qc-wWjnOCoP07Q<+rONNtO`aQkRwhOB(@ufrs*E*7Ff*c8^o zLxuFWU{Y+8L;79R57E5a1($`V-NPbz(6O&y)WU8Yl5V>s3n96%Z=i9ZHRmoOB2B6R zHK6HAZulN<2D0#dVf=X@@EPLxAF$j%UiEyy_w8P~i2e7@~<&Bt{x81V1H8 i;$+_W{eRc@H+`3KV)<2({Hkbv&F@Hnc8oMjBm7@^_NCnb literal 18197 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 D*Wzk( diff --git a/backend/app/workers/brand_seeder.py b/backend/app/workers/brand_seeder.py new file mode 100644 index 0000000..4daa356 --- /dev/null +++ b/backend/app/workers/brand_seeder.py @@ -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()) \ No newline at end of file diff --git a/backend/app/workers/catalog_robot.py b/backend/app/workers/catalog_robot.py index d86e232..96d9148 100644 --- a/backend/app/workers/catalog_robot.py +++ b/backend/app/workers/catalog_robot.py @@ -2,178 +2,207 @@ import asyncio import httpx import logging import json -import re import os import datetime -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select, func, or_, text +from sqlalchemy import text from app.db.session import SessionLocal from app.models.asset import AssetCatalog 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. - - .env alapú hitelesítés (RDW App Token) - - Prioritás: RDW (EU) -> NHTSA (US) -> CarQuery (Ban-figyeléssel) - - 2.5s lekérési frissítés a biztonságért + Master Hunter Robot v1.0.13 - Global Hunter Edition + - Holland (RDW), Brit (DVLA) és Amerikai (NHTSA) adatbázis integráció. + - Ratio-Filter: Kiszűri a 0.19-es kW/kg arányszámokat. + - 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/" - NHTSA_BASE = "https://vpic.nhtsa.dot.gov/api/vehicles/GetModelsForMakeYear/make/" - RDW_URL = "https://opendata.rdw.nl/resource/ed7h-m8uz.json" + # API Végpontok + RDW_MAIN = "https://opendata.rdw.nl/resource/m9d7-ebf2.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" - # Adatok beolvasása környezeti változókból + UK_DVLA = "https://driver-vehicle-licensing.api.gov.uk/vehicle-enquiry/v1/vehicles" + US_NHTSA = "https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVinValuesBatch/" + RDW_TOKEN = os.getenv("RDW_APP_TOKEN") + UK_API_KEY = os.getenv("UK_DVLA_API_KEY") - HEADERS = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", - "Accept": "application/json", - "X-App-Token": RDW_TOKEN + HEADERS_RDW = {"X-App-Token": RDW_TOKEN} if RDW_TOKEN else {} + HEADERS_UK = {"x-api-key": UK_API_KEY, "Content-Type": "application/json"} if UK_API_KEY else {} + + 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 - cq_banned_until = None - - # --- KATEGÓRIA DEFINÍCIÓK (Szigorúan az eredeti lista szerint) --- - MOTO_MAKES = ['ducati', 'ktm', 'triumph', 'aprilia', 'benelli', 'vespa', 'simson', 'mz', 'etz', 'jawa', 'husqvarna', 'gasgas', 'sherco'] - MARINE_IDS = ['DF', 'DT', 'OUTBOARD', 'MARINE', 'JET SKI', 'SEA-DOO', 'WAVERUNNER', 'YACHT', 'BOAT'] - AERIAL_IDS = ['CESSNA', 'PIPER', 'AIRBUS', 'BOEING', 'HELICOPTER', 'AIRCRAFT', 'BEECHCRAFT', 'EMBRAER', 'DRONE'] - ATV_IDS = ['LT-', 'LTZ', 'LTR', 'KINGQUAD', 'QUAD', 'POLARIS', 'CAN-AM', 'MULE', 'RZR', 'ARCTIC CAT', 'UTV', 'SIDE-BY-SIDE'] - RACING_IDS = ['RM-Z', 'KX', 'CRF', 'YZ', 'SX-F', 'XC-W', 'RM125', 'RM250', 'CR125', 'CR250', 'MC450'] - MOTO_KEYWORDS = ['CBR', 'GSX', 'YZF', 'NINJA', 'Z1000', 'DR-Z', 'MT-0', 'V-STROM', 'ADVENTURE', 'SCRAMBLER', 'CBF', 'VFR', 'HAYABUSA'] - BUS_KEYWORDS = ['BUS', 'COACH', 'INTERCITY', 'SHUTTLE', 'TRANSIT'] - 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 - def identify_class(cls, make: str, model: str) -> str: - m_full = f"{str(make)} {str(model)}".upper() - if any(x in m_full for x in cls.AERIAL_IDS): return "aerial" - if any(x in m_full for x in cls.MARINE_IDS): return "marine" - if any(x in m_full for x in cls.ATV_IDS): return "atv" - if any(x in m_full or str(make).lower() in cls.MOTO_MAKES for x in (cls.RACING_IDS + cls.MOTO_KEYWORDS)): - return "motorcycle" - 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" + def clean_kw(cls, val): + """Speciális kW tisztító: ignorálja az 1.0 alatti arányszámokat.""" + try: + if val is None: return None + f_val = float(str(val).replace(',', '.')) + if 0 < f_val < 1.0: return None # Ez csak arányszám (kW/kg) + v = int(f_val) + return v if v > 0 else None + except (ValueError, TypeError): + return None @classmethod - async def fetch_api(cls, url, params=None, is_cq=False): - if is_cq and cls.cq_banned_until and datetime.datetime.now() < cls.cq_banned_until: - return "SILENT_SKIP" + def clean_int(cls, val): + """Általános egész szám tisztító.""" + try: + if val is None: return None + return int(float(str(val).replace(',', '.'))) + except (ValueError, TypeError): + return None - async with httpx.AsyncClient(headers=cls.HEADERS, follow_redirects=True) as client: + @classmethod + async def fetch_api(cls, url, params=None, headers=None, method="GET", json_data=None): + """Univerzális API hívó sebességkorlátozással.""" + async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client: try: - # CarQuery: 5.0mp szünet (Hard Ban ellen), többi: 2.5mp (User kérése szerint) - await asyncio.sleep(5.0 if is_cq else 2.5) - resp = await client.get(url, params=params, timeout=35) - - if resp.status_code == 403 or "denied" in resp.text.lower(): - logger.error("🚫 CarQuery BAN! 2 óra kényszerpihenő aktiválva.") - 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) + await asyncio.sleep(1.2) # Biztonsági késleltetés + if method == "POST": + resp = await client.post(url, json=json_data, timeout=30) + else: + resp = await client.get(url, params=params, timeout=30) + return resp.json() if resp.status_code in [200, 201] else [] except Exception as e: - logger.error(f"❌ API hiba: {e}") - return None + logger.error(f"❌ API Hiba ({url}): {e}") + return [] @classmethod - async def is_model_processed(cls, db: AsyncSession, make: str, model: str, year: int): - stmt = select(AssetCatalog.id).where(AssetCatalog.make == make, AssetCatalog.model == model, AssetCatalog.year_from == year).limit(1) - result = await db.execute(stmt) - return result.scalars().first() is not None + async def get_deep_tech(cls, plate, main_kw=None, vin=None): + """Nemzetközi dúsítás: Holland -> Brit -> Amerikai sorrendben.""" + res = {"kw": cls.clean_kw(main_kw), "fuel": "Unknown", "axles": None, "body": "Standard", "euro": 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 - async def auto_heal(cls, db: AsyncSession, cq_active: bool): - logger.info("🛠️ Auto-Heal: Hiányos rekordok dúsítása...") - stmt = select(AssetCatalog).where(AssetCatalog.engine_variant == 'Standard', AssetCatalog.fuel_type == 'Unknown').limit(20) - results = await db.execute(stmt) - for r in results.scalars().all(): - # 1. RDW javítás (Holland Open Data + Token) - rdw = await cls.fetch_api(cls.RDW_URL, {"merk": r.make.upper(), "handelsbenaming": r.model.upper(), "$limit": 1}) - if rdw and isinstance(rdw, list) and len(rdw) > 0: - item = rdw[0] - 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"}) + async def process_make(cls, db, task_id, make_name): + logger.info(f"🚀 >>> {make_name} GlobalHunter v1.0.13 INDUL...") + offset, limit, total_saved = 0, 1000, 0 + unique_variants = {} + + while True: + params = {"merk": make_name.upper(), "$limit": limit, "$offset": offset} + main_data = await cls.fetch_api(cls.RDW_MAIN, params, headers=cls.HEADERS_RDW) + if not main_data: break + + 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 - - # 2. CQ javítás (Ha nem vagyunk kitiltva) - 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.execute(text("UPDATE data.catalog_discovery SET status = 'processed' WHERE id = :id"), {"id": task_id}) await db.commit() + logger.info(f"🏁 {make_name} KÉSZ. {total_saved} rekord rögzítve.") @classmethod 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!'})") - - 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) - + logger.info("🤖 Robot 1.0.13 (Global Hunter) ONLINE") + while True: async with SessionLocal() as db: - await cls.auto_heal(db, cq_now_active) - - # 1. MÁRKALISTA (NHTSA + Fallback) - makes_to_process = [] - for b in cls.FALLBACK_BRANDS: - makes_to_process.append({"id": b.lower(), "display": b}) - - for make in makes_to_process: - 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() + res = await db.execute(text("SELECT id, make FROM data.catalog_discovery WHERE status = 'pending' LIMIT 1")) + task = res.fetchone() + if task: + await cls.process_make(db, task[0], task[1]) + else: + logger.info("😴 Várólista üres. Alvás 60 mp...") + await asyncio.sleep(60) + await asyncio.sleep(1) if __name__ == "__main__": - asyncio.run(CatalogScout.run()) \ No newline at end of file + asyncio.run(CatalogMaster.run()) \ No newline at end of file diff --git a/backend/app/workers/service_hunter.py b/backend/app/workers/service_hunter.py index b961cde..db6c8fe 100644 --- a/backend/app/workers/service_hunter.py +++ b/backend/app/workers/service_hunter.py @@ -1,282 +1,161 @@ import asyncio import httpx import logging -import uuid import os -import sys -import csv +from datetime import datetime, timezone 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 +# Modellek - Az új v1.3 struktúra +from app.models.service import ServiceStaging, DiscoveryParameter -# Naplózás beállítása -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("Robot2-Dunakeszi-Detective") +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger("Robot-v1.3-ContinentalScout") 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) + Robot v1.3.0: Continental Scout. + EU-szintű felderítő motor, Discovery tábla alapú vezérléssel. """ 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 + async def get_coordinates(cls, city, country_code): + """Város központjának lekérése a keresés indításához.""" + params = {"address": f"{city}, {country_code}", "key": cls.GOOGLE_API_KEY} + async with httpx.AsyncClient() as client: + resp = await client.get(cls.GEOCODE_URL, params=params) + if resp.status_code == 200: + results = resp.json().get("results") + if results: + loc = results[0]["geometry"]["location"] + return loc["lat"], loc["lng"] + return None, 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 + async def get_google_places(cls, lat, lon, keyword): + """Google Places New API - Javított, 400-as hiba elleni védelemmel.""" + if not cls.GOOGLE_API_KEY: return [] 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" + "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 = { - "includedTypes": ["car_repair", "gas_station", "ev_charging_station", "car_wash", "motorcycle_repair"], - "maxResultCount": 1, + "includedTypes": ["car_repair", "gas_station", "car_wash", "motorcycle_repair"], + "maxResultCount": 20, "locationRestriction": { "circle": { "center": {"latitude": lat, "longitude": lon}, - "radius": 40.0 + "radius": 5000.0 # 5km körzet } } } - 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 + async with httpx.AsyncClient() as client: + resp = await client.post(cls.PLACES_NEW_URL, json=payload, headers=headers) + if resp.status_code == 200: + return resp.json().get("places", []) + else: + logger.error(f"❌ Google API hiba ({resp.status_code}): {resp.text}") + return [] @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 + async def save_to_staging(cls, db: AsyncSession, data: dict): + """Mentés a Staging táblába 9-mezős bontással.""" + stmt = select(ServiceStaging).where(ServiceStaging.external_id == str(data['external_id'])) + if (await db.execute(stmt)).scalar_one_or_none(): 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" + new_entry = ServiceStaging( + name=data['name'], + source=data['source'], + external_id=str(data['external_id']), + # Itt történik a 9-mezős bontás (ha érkezik adat) + postal_code=data.get('zip'), + city=data.get('city'), + street_name=data.get('street'), + street_type=data.get('street_type', 'utca'), + house_number=data.get('number'), + full_address=data.get('full_address'), + contact_phone=data.get('phone'), + website=data.get('website'), + raw_data=data.get('raw', {}), + status="pending", + trust_score=data.get('trust', 10) ) - 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})") + db.add(new_entry) @classmethod 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: 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() + # 1. Paraméterek lekérése a táblából + stmt = select(DiscoveryParameter).where(DiscoveryParameter.is_active == True) + 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.") + 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ő...") - await asyncio.sleep(86400) + logger.info("😴 Minden aktív feladat kész. Alvás 1 órán át...") + await asyncio.sleep(3600) if __name__ == "__main__": asyncio.run(ServiceHunter.run()) \ No newline at end of file diff --git a/backend/app/workers/service_hunter_old.py b/backend/app/workers/service_hunter_old.py new file mode 100644 index 0000000..b961cde --- /dev/null +++ b/backend/app/workers/service_hunter_old.py @@ -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()) \ No newline at end of file diff --git a/backend/app/workers/technical_enricher.py b/backend/app/workers/technical_enricher.py new file mode 100644 index 0000000..81f2f2b --- /dev/null +++ b/backend/app/workers/technical_enricher.py @@ -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()) \ No newline at end of file diff --git a/backend/migrations/__pycache__/env.cpython-312.pyc b/backend/migrations/__pycache__/env.cpython-312.pyc index 952453cb41d780cc0b25791bd1d184a614204581..6555ba8c05b1d99b1df121c3398f358bc4beb6c2 100644 GIT binary patch delta 169 zcmbOwIbD+XG%qg~0}x1RP0GyK$ScmuTEq?HPS#degB$Scmuaf_{>G$|)Dd9o&J9HYeK4pu!z@yXj+l^EqF z-(po|RNchp%>-ooaL53~Gda{5wKh-W;AaFXUC1fSC^>m6=XFM<$-P|mjuJp~i-bXh z9Ei{Y61Vs>i{tat(sDBMQsZ;;Q&Nkh!J_G@CGok5nR)T~1tppJc||fH4YHGYx#bz% KHtTX{F#-Tzr!AZS diff --git a/backend/migrations/env.py b/backend/migrations/env.py index 5e06b95..5b0a57a 100755 --- a/backend/migrations/env.py +++ b/backend/migrations/env.py @@ -39,7 +39,7 @@ def do_run_migrations(connection): target_metadata=target_metadata, include_schemas=True, include_object=include_object, - version_table_schema='public' + version_table_schema='data' ) with context.begin_transaction(): context.run_migrations() diff --git a/backend/migrations/versions/25d1658ccf1d_update_staging_address_structure.py b/backend/migrations/versions/25d1658ccf1d_update_staging_address_structure.py new file mode 100644 index 0000000..552c534 --- /dev/null +++ b/backend/migrations/versions/25d1658ccf1d_update_staging_address_structure.py @@ -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 ### diff --git a/backend/migrations/versions/33c4f2235667_add_axles_and_body_type.py b/backend/migrations/versions/33c4f2235667_add_axles_and_body_type.py new file mode 100644 index 0000000..ebf9acd --- /dev/null +++ b/backend/migrations/versions/33c4f2235667_add_axles_and_body_type.py @@ -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') diff --git a/backend/migrations/versions/75e3a57f9c14_enrich_catalog_technical_schema.py b/backend/migrations/versions/75e3a57f9c14_enrich_catalog_technical_schema.py new file mode 100644 index 0000000..6c7db8a --- /dev/null +++ b/backend/migrations/versions/75e3a57f9c14_enrich_catalog_technical_schema.py @@ -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') \ No newline at end of file diff --git a/backend/migrations/versions/8188636edd27_add_discovery_parameters_table.py b/backend/migrations/versions/8188636edd27_add_discovery_parameters_table.py new file mode 100644 index 0000000..f1bdc28 --- /dev/null +++ b/backend/migrations/versions/8188636edd27_add_discovery_parameters_table.py @@ -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 ### diff --git a/backend/migrations/versions/b803fe324ebd_upgrade_identity_and_audit_v1_6.py b/backend/migrations/versions/b803fe324ebd_upgrade_identity_and_audit_v1_6.py new file mode 100644 index 0000000..0f1f7bd --- /dev/null +++ b/backend/migrations/versions/b803fe324ebd_upgrade_identity_and_audit_v1_6.py @@ -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 ### diff --git a/backend/migrations/versions/d0f9ed93b59f_v1_3_branch_system_and_fleet_scaling.py b/backend/migrations/versions/d0f9ed93b59f_v1_3_branch_system_and_fleet_scaling.py new file mode 100644 index 0000000..cf20aa7 --- /dev/null +++ b/backend/migrations/versions/d0f9ed93b59f_v1_3_branch_system_and_fleet_scaling.py @@ -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 ### diff --git a/backend/migrations/versions/d229cc6bc347_add_catalog_discovery_table.py b/backend/migrations/versions/d229cc6bc347_add_catalog_discovery_table.py new file mode 100644 index 0000000..ad9805b --- /dev/null +++ b/backend/migrations/versions/d229cc6bc347_add_catalog_discovery_table.py @@ -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 ### diff --git a/backend/migrations/versions/e78ce92243ed_full_ecosystem_upgrade_v1_6.py b/backend/migrations/versions/e78ce92243ed_full_ecosystem_upgrade_v1_6.py new file mode 100644 index 0000000..04e5ea6 --- /dev/null +++ b/backend/migrations/versions/e78ce92243ed_full_ecosystem_upgrade_v1_6.py @@ -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 ### diff --git a/backend/seed_discovery.py b/backend/seed_discovery.py new file mode 100644 index 0000000..018eb28 --- /dev/null +++ b/backend/seed_discovery.py @@ -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()) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml old mode 100755 new mode 100644 index 860b421..83c53c4 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ services: - # 1. MIGRÁCIÓ (Adatbázis szerkezet frissítése) + # 1. MIGRÁCIÓ migrate: build: context: ./backend @@ -7,7 +7,7 @@ services: container_name: service_finder_migrate env_file: .env volumes: - - ./backend:/app # Ez tartalmazza az alembic.ini-t és a migrations mappát is! + - ./backend:/app environment: PYTHONPATH: /app DATABASE_URL: ${MIGRATION_DATABASE_URL} @@ -15,7 +15,7 @@ services: networks: - default - shared_db_net - restart: "no" + restart: "no" # Ez így helyes, lefut és megáll. # 2. BACKEND API service_finder_api: @@ -24,20 +24,14 @@ services: 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" + volumes: + - ./backend:/app + - /mnt/nas/app_data:/mnt/nas/app_data + - ./static_previews:/app/static/previews 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 @@ -50,25 +44,19 @@ services: - shared_db_net restart: unless-stopped - # 3. MINIO (NAS-ra ment) + # 3. MINIO 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) + # 4. REDIS redis: image: redis:alpine container_name: service_finder_redis @@ -79,15 +67,13 @@ services: restart: unless-stopped # 5. FRONTEND - service_finder_frontend: + service_frontend: # Rövidített szerviznév build: context: ./frontend container_name: service_finder_frontend env_file: .env ports: - "3001:80" - environment: - ALLOWED_ORIGINS: ${ALLOWED_ORIGINS} networks: - default depends_on: @@ -95,15 +81,14 @@ services: condition: service_started restart: unless-stopped - # Katalógus felderítő robot + # 6. KATALÓGUS ROBOT (A mi kis felfedezőnk) catalog_robot: build: ./backend container_name: service_finder_robot_catalog command: python -m app.workers.catalog_robot volumes: - ./backend:/app - env_file: - - .env + env_file: .env depends_on: migrate: condition: service_completed_successfully @@ -112,23 +97,14 @@ services: - shared_db_net restart: always - # Szerviz vadász robot (Robot 2.7) + # 7. SERVICE HUNTER 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 + env_file: .env depends_on: migrate: condition: service_completed_successfully @@ -137,7 +113,7 @@ services: - shared_db_net restart: always - # --- ÚJ: n8n AUTOMATIZÁCIÓ --- + # 8. n8n AUTOMATIZÁCIÓ n8n: image: n8nio/n8n:latest container_name: service_finder_n8n @@ -147,13 +123,8 @@ services: 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: @@ -164,7 +135,6 @@ services: depends_on: - n8n_db - # n8n belső meta-adatbázisa n8n_db: image: postgres:15-alpine container_name: service_finder_n8n_db @@ -178,18 +148,32 @@ services: networks: - default - # Browserless - A robot "szeme" (Központi 3005-ös porton) + # 9. BROWSERLESS browserless: image: browserless/chrome:latest container_name: service_finder_browserless restart: unless-stopped ports: - "3005:3000" - environment: - - MAX_CONCURRENT_SESSIONS=10 networks: - 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: default: driver: bridge diff --git a/docker-compose_1.yml b/docker-compose_1.yml new file mode 100755 index 0000000..f55e754 --- /dev/null +++ b/docker-compose_1.yml @@ -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 \ No newline at end of file diff --git a/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md b/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md index a255408..0e304fb 100644 --- a/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md +++ b/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md @@ -132,4 +132,35 @@ A technikai belépési pont. 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. - **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. \ No newline at end of file +- **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). \ No newline at end of file diff --git a/docs/V01_gemini/06_Database_Guide.md b/docs/V01_gemini/06_Database_Guide.md index 3fe74a7..ab6c0c0 100644 --- a/docs/V01_gemini/06_Database_Guide.md +++ b/docs/V01_gemini/06_Database_Guide.md @@ -201,4 +201,13 @@ A rendszer az adatintegritás és a sebesség érdekében hibrid modellt haszná ## 2.4 Financial & Enrichment Tables - **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.google_place_id:** Külső validációs kulcs a Google Places API-hoz. \ No newline at end of file +- **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). \ No newline at end of file diff --git a/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md b/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md index fcab3ec..1009d4d 100644 --- a/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md +++ b/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md @@ -113,4 +113,24 @@ A meghívók érvényessége a típustól függ: ### 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 kód felhasználása után a link érvénytelenné válik (One-time use). \ No newline at end of file +* 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. \ No newline at end of file diff --git a/docs/V01_gemini/10_Billing_Credits_Subscriptions.md b/docs/V01_gemini/10_Billing_Credits_Subscriptions.md index ddd81e5..58f9198 100644 --- a/docs/V01_gemini/10_Billing_Credits_Subscriptions.md +++ b/docs/V01_gemini/10_Billing_Credits_Subscriptions.md @@ -110,4 +110,31 @@ A rendszer támogatja a dinamikus árazást a kozmetikai elemeknél is. 4. **Equip:** Opcionálisan azonnali beállítás (pl. profilkép keret). ### 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. \ No newline at end of file +Ú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. \ No newline at end of file diff --git a/docs/V01_gemini/15_Changelog.md b/docs/V01_gemini/15_Changelog.md index f6f7776..edc5eb3 100644 --- a/docs/V01_gemini/15_Changelog.md +++ b/docs/V01_gemini/15_Changelog.md @@ -365,4 +365,120 @@ A rendszer most már képes egyetlen KYC folyamat alatt aktiválni a felhasznál ### 🔜 Következő Lépések - 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. \ No newline at end of file +- 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). \ No newline at end of file diff --git a/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md b/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md index cdc739c..0da4f18 100644 --- a/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md +++ b/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md @@ -129,4 +129,26 @@ A `rank: 100` szintű felhasználó (SuperAdmin) az egyetlen, aki: - `POST /admin/translations/sync`: - **Trigger:** Manuális (Gombnyomás a Dashboardon). - **Action:** `data.translations` -> `static/locales/*.json`. - - **Permission:** SuperAdmin ONLY. \ No newline at end of file + - **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. \ No newline at end of file diff --git a/docs/V01_gemini/23_BRANCH_AND_LOCATION_SPEC.md b/docs/V01_gemini/23_BRANCH_AND_LOCATION_SPEC.md new file mode 100644 index 0000000..c7d108c --- /dev/null +++ b/docs/V01_gemini/23_BRANCH_AND_LOCATION_SPEC.md @@ -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. \ No newline at end of file diff --git a/n8n/data/n8nEventLog.log b/n8n/data/n8nEventLog.log index e4100d7..a7300ed 100644 --- a/n8n/data/n8nEventLog.log +++ b/n8n/data/n8nEventLog.log @@ -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":"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":"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"}} diff --git a/seed_discovery.py b/seed_discovery.py new file mode 100644 index 0000000..2f3c558 --- /dev/null +++ b/seed_discovery.py @@ -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() \ No newline at end of file