From f38a75a02505d8336fb80f09b15eef476faea5f9 Mon Sep 17 00:00:00 2001 From: Kincses Date: Fri, 13 Feb 2026 01:15:34 +0000 Subject: [PATCH] feat(robot): hunter v2.7, geocoding support, docker network fix, changelog update --- .env | 9 +- backend/.env | 5 +- .../app/api/__pycache__/deps.cpython-312.pyc | Bin 5362 -> 5362 bytes .../__pycache__/catalog.cpython-312.pyc | Bin 2058 -> 3001 bytes .../core/__pycache__/security.cpython-312.pyc | Bin 3939 -> 3939 bytes backend/app/core/security.py | 5 +- .../__pycache__/__init__.cpython-312.pyc | Bin 1598 -> 1760 bytes .../__pycache__/address.cpython-312.pyc | Bin 2800 -> 2973 bytes .../models/__pycache__/asset.cpython-312.pyc | Bin 8992 -> 9225 bytes .../__pycache__/identity.cpython-312.pyc | Bin 6888 -> 7097 bytes .../__pycache__/organization.cpython-312.pyc | Bin 4564 -> 6279 bytes .../__pycache__/service.cpython-312.pyc | Bin 0 -> 3520 bytes backend/app/models/address.py | 13 +- backend/app/models/asset.py | 60 ++-- backend/app/models/identity.py | 65 +++- backend/app/models/organization.py | 71 +++-- backend/app/models/organization_member.py | 26 -- backend/app/models/service.py | 17 +- .../__pycache__/asset_service.cpython-312.pyc | Bin 0 -> 6339 bytes .../__pycache__/catalog_robot.cpython-312.pyc | Bin 0 -> 12915 bytes .../service_hunter.cpython-312.pyc | Bin 0 -> 18197 bytes backend/app/workers/catalog_robot.py | 216 ++++++++++--- backend/app/workers/local_services.csv | 3 + backend/app/workers/service_auditor.py | 42 +++ backend/app/workers/service_hunter.py | 282 +++++++++++++++++ .../143763d5d6fe_fix_member_is_verified.py | 218 +++++++++++++ ...e6f4f063_identity_and_hybrid_org_update.py | 206 ++++++++++++ ...98e76c2fa36_audit_and_moderation_fields.py | 296 ++++++++++++++++++ ...492849ee0b3a_add_is_verified_to_members.py | 220 +++++++++++++ ...6fe_fix_member_is_verified.cpython-312.pyc | Bin 0 -> 23441 bytes ...tity_and_hybrid_org_update.cpython-312.pyc | Bin 0 -> 16587 bytes ...udit_and_moderation_fields.cpython-312.pyc | Bin 0 -> 30092 bytes ...add_is_verified_to_members.cpython-312.pyc | Bin 0 -> 23744 bytes backend/requirements.txt | 5 +- docker-compose.yml | 33 +- docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md | 8 +- docs/V01_gemini/06_Database_Guide.md | 7 +- docs/V01_gemini/15_Changelog.md | 64 +++- .../18_ASSET_AND_FLEET_SPECIFICATION.md | 7 +- .../20_Service_Finder_&_Trust_Engine.md | 13 +- docs/V01_gemini/22_ROBOT ÖKOSZISZTÉMA | 63 +++- 41 files changed, 1801 insertions(+), 153 deletions(-) create mode 100644 backend/app/models/__pycache__/service.cpython-312.pyc delete mode 100755 backend/app/models/organization_member.py create mode 100644 backend/app/services/__pycache__/asset_service.cpython-312.pyc create mode 100644 backend/app/workers/__pycache__/catalog_robot.cpython-312.pyc create mode 100644 backend/app/workers/__pycache__/service_hunter.cpython-312.pyc create mode 100644 backend/app/workers/local_services.csv create mode 100644 backend/app/workers/service_auditor.py create mode 100644 backend/app/workers/service_hunter.py create mode 100644 backend/migrations/versions/143763d5d6fe_fix_member_is_verified.py create mode 100644 backend/migrations/versions/25afe6f4f063_identity_and_hybrid_org_update.py create mode 100644 backend/migrations/versions/398e76c2fa36_audit_and_moderation_fields.py create mode 100644 backend/migrations/versions/492849ee0b3a_add_is_verified_to_members.py create mode 100644 backend/migrations/versions/__pycache__/143763d5d6fe_fix_member_is_verified.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/25afe6f4f063_identity_and_hybrid_org_update.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/398e76c2fa36_audit_and_moderation_fields.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/492849ee0b3a_add_is_verified_to_members.cpython-312.pyc diff --git a/.env b/.env index 6e8d6aa..536ae28 100755 --- a/.env +++ b/.env @@ -90,4 +90,11 @@ MINIO_ENDPOINT=minio:9000 MINIO_ROOT_USER=kincses MINIO_ROOT_PASSWORD='MiskociA74' MINIO_ACCESS_KEY=kincses -MINIO_SECRET_KEY='MiskociA74' \ No newline at end of file +MINIO_SECRET_KEY='MiskociA74' + + +# --- Frontend --- +FRONTEND_BASE_URL=https://dev.profibot.hu/docs + + +GOOGLE_API_KEY=AIzaSyB3-Uo6qFBNi83hK01uoaUARtYHxERbtXg \ No newline at end of file diff --git a/backend/.env b/backend/.env index 5e046de..c7825b3 100644 --- a/backend/.env +++ b/backend/.env @@ -20,4 +20,7 @@ GOOGLE_CALLBACK_URL=https://dev.profibot.hu/api/v1/auth/callback/google # --- Frontend --- -FRONTEND_BASE_URL=https://dev.profibot.hu/docs \ No newline at end of file +FRONTEND_BASE_URL=https://dev.profibot.hu/docs + + +GOOGLE_API_KEY=AIzaSyB3-Uo6qFBNi83hK01uoaUARtYHxERbtXg \ No newline at end of file diff --git a/backend/app/api/__pycache__/deps.cpython-312.pyc b/backend/app/api/__pycache__/deps.cpython-312.pyc index cceb01fb7c07cfca165268734601e0562fc222c9..5d47d8a73bb84976f521b3436183a61269348f82 100644 GIT binary patch delta 20 acmeyQ`AL)eG%qg~0}zDX>e?V)?~%-KrU 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 W4GFpXAKRR!fcSxB9w4$8JpK)<``fDk diff --git a/backend/app/core/__pycache__/security.cpython-312.pyc b/backend/app/core/__pycache__/security.cpython-312.pyc index 69906b25b1719aadcc0e6b02b3b98005db312353..fd7eb2d956b4d6ae0b61e6212c18ec5a1a7f2f48 100644 GIT binary patch delta 69 zcmaDX_gIeiG%qg~0}#9w?9GhZ$h(o5QF!ux=6*(j&HgOujEth2x3i`(GD>fjV~=EE Yl-pdveS?uvdvhSK6(b|p9gfoBdhR85xB)Z)Z(oWR%=2#~#VT YD6_eO`vxPU=H@_ND@I1P$vu460lCH#NdN!< diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 193dc15..2dae722 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -5,11 +5,8 @@ from typing import Optional, Dict, Any, Tuple import bcrypt from jose import jwt, JWTError from app.core.config import settings -from fastapi_limiter import FastAPILimiter -from fastapi_limiter.depends import RateLimiter -# Ezt az auth végpontokhoz adjuk hozzá: -# @router.post("/login", dependencies=[Depends(RateLimiter(times=5, seconds=60))]) +# A FastAPI-Limiter importokat kivettem innen, mert indítási hibát okoztak. DEFAULT_RANK_MAP = { "superadmin": 100, "admin": 80, "fleet_manager": 25, diff --git a/backend/app/models/__pycache__/__init__.cpython-312.pyc b/backend/app/models/__pycache__/__init__.cpython-312.pyc index 3ce077e2b2413bded08b0015b37633e515214a9d..37a7944235bf022c734ea81e89c467de2c772551 100644 GIT binary patch delta 781 zcmZvZ%WD%+6vpq(G>>K;O|fdVv}yBblBQ{5V|_Fm?Zb7UqHs55n7N$}+?&yRr(#5C z#dVP|hzocA3vR`Kz*TW!7$|h(qPv1fz?E~Rb>}R8eCPWPhjZ?H)lPNeo33Y3^nJuf z?pmgAOyX+4QlH`(lEKJM*duI|XGxZ;M2+ht>+@-qPmqbYOtJp^@4Z>E2I+ZEUWSwsl{4l3%pM1vDVlkZ;%FGB1^nUn!H6? zv9GgRe3>lsHfhJT!EW;vvVsw9>f{d9$z5uYdo)MxJIEgUgvn|zR~tWr<7zk@Su-U) z!%(DxE!-Xp^=;2`P4>{Ty#1~pyv6TYN@#3T@xry}mhg66Mni4$cJmn6?cM_kxog#qlcO&9JA}bGJV(U z3Kg0(@RO#L)DNC3)%7qfFE$caA-)E<4n1kpwgr_^OoDX-a1$^Eq2`#}-F2;aaPURR z9~8jWWl#E)?^s@U*L6e@yo~Kx`r50frjF8~7NlL-+`PN1Gt*sNFh7*6&EyPteQ>DuKNhR^2O&=}-`W-6K0)Wn;#cn~e{7c2P5tN;K2 delta 595 zcmXAlyK5Uk5XN_<)9W6x3p+BHNV481`60(i3X3#>#C6(f%yD-f6+3UAz37e+cQ%Cr>RGC6@PLGn0^zG-DxkF0rrsTC(Lo=BLU%{wb`=gg&P?7%Q6rS<^uf1zKwbO*Qu_>XgDRqP>0#XZ9fm)D?MO5Ih9L&};YTc~kGHVB= z60#3DAR(bzd*Fmbxq;maoH<#XxWI{KNkA2+_EaQu>52E&@yZFTwBLO5y`OnI`(_^J zzdCCFY*{9O=amn>Xgs6!?FBmQpFZ1d>W)qcBTVv)rs*UjE_$QCWx4>4P{r1f`S(n95u z*K*rGWa&>-wLuqlu$wqRcY-I0iu&wCs7T+WGDx!k?}@qKk+iHW0I!x(K~KqN3TVVK zg@RBFR^{avmyt#pP9Zxpg)|WVb%1ShKlgO+o%{1kU#q|5Pxq=jL0>MLQ3!h1``LW( zhrD@k?T?h4wg}d0$;7qS4zCR+AtRhaI8?+6Y6$c2`be;dQwNK9pe#+kd?lzzi{0-j zAzudJYPICe`-J1G1y9b>IK^Io2%W~U7Ya2zBEk7o3gZ( zDP=?LB4dL4e&=QIcz>g@$;Iu_U9aI6>!W+kBHw5?+M~OE@$$P@i!O89t@U-+E3P;A z_0hNf?a{sVCNGDEJ4wQQfsMa1>HHE6HP^3oIt^Azh$CPS*hk_ygtGvb1fB}sPCt*H2)Mq#3$RUgCJm1O4c@XtT(y|n|R z6Ii{s9|S)di&k{@FshUjmEbSqTtXOuWiEH|IujQFLfik?bG`Zv-rOn!om7)`Z`~@h zYvpUM&uewh^?i}Ti0DR%PJVRqqM{BS$xyE}Tdd>p*F+T}u-W}90FNa~>F*@@Ct3TA ztiACoS=%G4d!(>OPL5@juH1QZOkfyi6yWlhz%aIDdI5D9#;F{=LO)~w5FDdx=wIqx B8)^Um delta 1209 zcmaJ=O-vI(6rR~`x7*!zX$z$lP(-VB1zRIAMkEju#aOW?Prcy6ZX%@RXO_lrDkdE8 zqL4Y6Xky}pST4q!CyXH;yl@~9t3){&5)T~8-S@V1EkvDUzxU?*-g`5%^X5ftFRFbC z1{DHhW_@vdT3XiPbY(f&U82M!rYA2K{5i$(#k^Vwe%5YuOA zWe=7iB-iAyGO+#zHsr7huxbMvcGv*0!3H)mr&3+p#cEkyTK;kQZ3kVaQV`}W$bSmQ z=??dL`lUpT2ua8Ieb3`S6HY?__gI+kdlG>d_*p%`+Q9OYVpPQ~GA}|WzbPhqPowKp zIfGuZ4p_t<1Xv(jEnV|>w^~oUls2Nt`RopVE~aV=C;5i>w5M@VNZ7puFr*6=sW>@4 zQ?ytd=2=~F_i&|Q4Ux@U=+Uh9|#}=MYK1@4W1!762 zxauuah=pN_mTw}&4XL>#x%c9SD6gdes-~|VSr%#;xd5v01x>lGnf_J9)iqaF|Ej~S zALJj>AGige0S*EJ^#XO9gEsi57Wt3X>}o0>^hHZvE9%z7pMnR9!H)^nS(wKs@QRL% z^1*ynbNAae*QAFkzU!vREZeU5jhbWG?9Y|!wBj@Dkz#S&)CCs7K4Vb?9pOB{5W^;9 z7r7;$liJ-b;BVyiP$w!*H%jPr^KWu~Kt>l|WW_T%^{}FhG0T{>%n@VuXoK`(Bm&m4 z*8vs?f8~Fj{1T2mlUEX(@${O!CVrIP%O8}@@SR7QTB~D<-pu;=qH=-1Q@SKS5bP@7 zQCdpQ!eL{2I%STgMh)8<8Os~C%~T9=-k7uz$ I&^kx>4N0utM*si- diff --git a/backend/app/models/__pycache__/asset.cpython-312.pyc b/backend/app/models/__pycache__/asset.cpython-312.pyc index 6ebea6c765be9919d2cf0e93a3c360ac7da63f53..ad0a3943e09f0868c77aed4cd482dd83c20c0699 100644 GIT binary patch literal 9225 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 RgHG$QrSbo=zz=hr{|9|Oi?9Fy literal 8992 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 literal 0 HcmV?d00001 diff --git a/backend/app/models/address.py b/backend/app/models/address.py index 1cbf4ec..67b09ae 100644 --- a/backend/app/models/address.py +++ b/backend/app/models/address.py @@ -1,5 +1,5 @@ import uuid -from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime +from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime, Float from sqlalchemy.dialects.postgresql import UUID as PG_UUID from sqlalchemy.sql import func from app.db.base_class import Base @@ -7,7 +7,6 @@ from app.db.base_class import Base class GeoPostalCode(Base): __tablename__ = "geo_postal_codes" __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True) country_code = Column(String(5), default="HU") zip_code = Column(String(10), nullable=False) @@ -16,7 +15,6 @@ class GeoPostalCode(Base): class GeoStreet(Base): __tablename__ = "geo_streets" __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True) postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id")) name = Column(String(200), nullable=False) @@ -24,11 +22,11 @@ class GeoStreet(Base): class GeoStreetType(Base): __tablename__ = "geo_street_types" __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True) name = Column(String(50), unique=True, nullable=False) class Address(Base): + """Univerzális cím entitás GPS adatokkal kiegészítve.""" __tablename__ = "addresses" __table_args__ = {"schema": "data"} @@ -40,6 +38,11 @@ class Address(Base): stairwell = Column(String(20)) floor = Column(String(20)) door = Column(String(20)) - parcel_id = Column(String(50)) # HRSZ + parcel_id = Column(String(50)) full_address_text = Column(Text) + + # Robot és térképes funkciók számára + latitude = Column(Float) + longitude = Column(Float) + created_at = Column(DateTime(timezone=True), server_default=func.now()) \ No newline at end of file diff --git a/backend/app/models/asset.py b/backend/app/models/asset.py index 11fd2dc..5c8a147 100644 --- a/backend/app/models/asset.py +++ b/backend/app/models/asset.py @@ -1,54 +1,55 @@ import uuid -from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Text +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Numeric, text, Text, UniqueConstraint from sqlalchemy.orm import relationship -from sqlalchemy.dialects.postgresql import UUID as PG_UUID +from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB from sqlalchemy.sql import func from app.db.base_class import Base class AssetCatalog(Base): - """Globális járműkatalógus (Márka -> Típus -> Generáció -> Motor).""" __tablename__ = "vehicle_catalog" - __table_args__ = {"schema": "data"} + __table_args__ = ( + UniqueConstraint( + 'make', 'model', 'year_from', 'engine_variant', 'fuel_type', + name='uix_vehicle_catalog_full' + ), + {"schema": "data"} + ) id = Column(Integer, primary_key=True, index=True) - make = Column(String, index=True, nullable=False) # 1. Szint: Audi - model = Column(String, index=True, nullable=False) # 2. Szint: A4 - generation = Column(String, index=True) # 3. Szint: B8 (2008-2015) - engine_variant = Column(String) # 4. Szint: 2.0 TDI (150 LE) - + make = Column(String, index=True, nullable=False) + model = Column(String, index=True, nullable=False) + generation = Column(String, index=True) + engine_variant = Column(String, index=True) year_from = Column(Integer) year_to = Column(Integer) vehicle_class = Column(String) - fuel_type = Column(String) + fuel_type = Column(String, index=True) engine_code = Column(String) - factory_data = Column(JSON, server_default=text("'{}'::jsonb")) # Technikai specifikációk + factory_data = Column(JSONB, server_default=text("'{}'::jsonb")) assets = relationship("Asset", back_populates="catalog") class Asset(Base): - """Egyedi jármű (Asset) példány - Az ökoszisztéma magja.""" __tablename__ = "assets" __table_args__ = {"schema": "data"} - id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) vin = Column(String(17), unique=True, index=True, nullable=False) license_plate = Column(String(20), index=True) name = Column(String) year_of_manufacture = Column(Integer) - - # --- BIZTONSÁGI ÉS JOGOSULTSÁGI IZOLÁCIÓ --- - # A current_organization_id biztosítja a gyors, adatbázis-szintű Scoped RBAC védelmet. current_organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True) - catalog_id = Column(Integer, ForeignKey("data.vehicle_catalog.id")) - is_verified = Column(Boolean, default=False) - verification_method = Column(String(20)) # 'robot', 'ocr', 'manual' - status = Column(String(20), default="active") + # Moderációs mezők a Robot 3 (OCR) számára + is_verified = Column(Boolean, default=False) + verification_method = Column(String(20)) # 'manual', 'ocr', 'vin_api' + verification_notes = Column(Text, nullable=True) # Eltérések jegyzőkönyve + catalog_match_score = Column(Numeric(5, 2), nullable=True) # 0-100% egyezési arány + + status = Column(String(20), default="active") created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) - # Kapcsolatok (Digital Twin Modules) catalog = relationship("AssetCatalog", back_populates="assets") current_org = relationship("Organization") financials = relationship("AssetFinancials", back_populates="asset", uselist=False) @@ -57,6 +58,7 @@ class Asset(Base): events = relationship("AssetEvent", back_populates="asset") costs = relationship("AssetCost", back_populates="asset") reviews = relationship("AssetReview", back_populates="asset") + ownership_history = relationship("VehicleOwnership", back_populates="vehicle") class AssetFinancials(Base): __tablename__ = "asset_financials" @@ -87,15 +89,13 @@ class AssetReview(Base): asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False) user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False) overall_rating = Column(Integer) - criteria_scores = Column(JSON, server_default=text("'{}'::jsonb")) + criteria_scores = Column(JSONB, server_default=text("'{}'::jsonb")) comment = Column(Text) created_at = Column(DateTime(timezone=True), server_default=func.now()) - asset = relationship("Asset", back_populates="reviews") user = relationship("User") class AssetAssignment(Base): - """Jármű flotta-történetének nyilvántartása.""" __tablename__ = "asset_assignments" __table_args__ = {"schema": "data"} id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) @@ -104,7 +104,6 @@ class AssetAssignment(Base): 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") @@ -115,7 +114,7 @@ class AssetEvent(Base): asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False) event_type = Column(String(50), nullable=False) recorded_mileage = Column(Integer) - data = Column(JSON, server_default=text("'{}'::jsonb")) + data = Column(JSONB, server_default=text("'{}'::jsonb")) asset = relationship("Asset", back_populates="events") class AssetCost(Base): @@ -129,10 +128,12 @@ class AssetCost(Base): amount_local = Column(Numeric(18, 2), nullable=False) currency_local = Column(String(3), nullable=False) amount_eur = Column(Numeric(18, 2), nullable=True) + net_amount_local = Column(Numeric(18, 2)) + vat_rate = Column(Numeric(5, 2)) + exchange_rate_used = Column(Numeric(18, 6)) date = Column(DateTime(timezone=True), server_default=func.now()) mileage_at_cost = Column(Integer) - data = Column(JSON, server_default=text("'{}'::jsonb")) - + data = Column(JSONB, server_default=text("'{}'::jsonb")) asset = relationship("Asset", back_populates="costs") organization = relationship("Organization") driver = relationship("User") @@ -143,5 +144,4 @@ 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) - updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file + rate = Column(Numeric(18, 6), nullable=False) \ No newline at end of file diff --git a/backend/app/models/identity.py b/backend/app/models/identity.py index 7409acf..6e03e4d 100644 --- a/backend/app/models/identity.py +++ b/backend/app/models/identity.py @@ -7,41 +7,82 @@ from sqlalchemy.sql import func from app.db.base_class import Base class UserRole(str, enum.Enum): - superadmin = "superadmin"; admin = "admin"; user = "user" - service = "service"; fleet_manager = "fleet_manager"; driver = "driver" + superadmin = "superadmin" + admin = "admin" + user = "user" + service = "service" + fleet_manager = "fleet_manager" + driver = "driver" class Person(Base): - __tablename__ = "persons"; __table_args__ = {"schema": "data"} + """ + 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. + """ + __tablename__ = "persons" + __table_args__ = {"schema": "data"} + 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) - last_name = Column(String, nullable=False); first_name = Column(String, nullable=False); phone = Column(String, nullable=True) - mothers_last_name = Column(String); mothers_first_name = Column(String); birth_place = Column(String); birth_date = Column(DateTime) + + 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) + birth_date = Column(DateTime) + 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 + created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + users = relationship("User", back_populates="person") + memberships = relationship("OrganizationMember", back_populates="person") class User(Base): - __tablename__ = "users"; __table_args__ = {"schema": "data"} + __tablename__ = "users" + __table_args__ = {"schema": "data"} + 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) + 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) - 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_id = Column(String(50)); custom_permissions = Column(JSON, server_default=text("'{}'::jsonb")) + + 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_id = Column(String(50)) + custom_permissions = Column(JSON, server_default=text("'{}'::jsonb")) + created_at = Column(DateTime(timezone=True), server_default=func.now()) - 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") + + 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"} diff --git a/backend/app/models/organization.py b/backend/app/models/organization.py index 30eeb72..00c6987 100755 --- a/backend/app/models/organization.py +++ b/backend/app/models/organization.py @@ -1,5 +1,5 @@ import enum -from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Numeric, BigInteger from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM from sqlalchemy.orm import relationship from sqlalchemy.sql import func @@ -14,35 +14,43 @@ class OrgType(str, enum.Enum): club = "club" business = "business" +class OrgUserRole(str, enum.Enum): + OWNER = "OWNER" + ADMIN = "ADMIN" + FLEET_MANAGER = "FLEET_MANAGER" + DRIVER = "DRIVER" + MECHANIC = "MECHANIC" + RECEPTIONIST = "RECEPTIONIST" + class Organization(Base): + """ + Szervezet entitás. Lehet flotta (user) és szolgáltató (service) egyszerre. + A képességeket a kapcsolódó profilok (pl. ServiceProfile) határozzák meg. + """ __tablename__ = "organizations" __table_args__ = {"schema": "data"} id = Column(Integer, primary_key=True, index=True) address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True) - full_name = Column(String, nullable=False) - name = Column(String, nullable=False) + full_name = Column(String, nullable=False) # Hivatalos név + name = Column(String, nullable=False) # Rövid név display_name = Column(String(50)) - - # --- BIZTONSÁGI BŐVÍTÉS (Mappa elszigetelés) --- folder_slug = Column(String(12), unique=True, index=True) default_currency = Column(String(3), default="HUF") country_code = Column(String(2), default="HU") language = Column(String(5), default="hu") + # Cím adatok (redundáns a gyors kereséshez, de address_id a SSoT) address_zip = Column(String(10)) address_city = Column(String(100)) address_street_name = Column(String(150)) - address_street_type = Column(String(50)) + address_street_type = Column(String(50)) address_house_number = Column(String(20)) - address_hrsz = Column(String(50)) - address_stairwell = Column(String(20)) - address_floor = Column(String(20)) - address_door = Column(String(20)) + address_hrsz = Column(String(50)) - tax_number = Column(String(20), unique=True, index=True) + tax_number = Column(String(20), unique=True, index=True) # Robot horgony reg_number = Column(String(50)) org_type = Column( @@ -52,15 +60,13 @@ class Organization(Base): status = Column(String(30), default="pending_verification") is_deleted = Column(Boolean, default=False) - - notification_settings = Column(JSON, server_default=text("'{ \"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1] }'::jsonb")) + + notification_settings = Column(JSON, server_default=text("'{\"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1]}'::jsonb")) external_integration_config = Column(JSON, server_default=text("'{}'::jsonb")) owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) is_active = Column(Boolean, default=True) - is_transferable = Column(Boolean, default=True) is_verified = Column(Boolean, default=False) - verification_expires_at = Column(DateTime(timezone=True), nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) @@ -69,15 +75,40 @@ class Organization(Base): assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan") members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan") 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) -class OrganizationMember(Base): - __tablename__ = "organization_members" +class OrganizationFinancials(Base): + """Cégek éves gazdasági adatai elemzéshez.""" + __tablename__ = "organization_financials" __table_args__ = {"schema": "data"} + id = Column(Integer, primary_key=True, index=True) organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False) - user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False) - role = Column(String, default="driver") + year = Column(Integer, nullable=False) + turnover = Column(Numeric(18, 2)) + profit = Column(Numeric(18, 2)) + employee_count = Column(Integer) + source = Column(String(50)) # pl. 'manual', 'crawler', 'api' + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + organization = relationship("Organization", back_populates="financials") + +class OrganizationMember(Base): + """Kapcsolótábla a személyek és szervezetek között.""" + __tablename__ = "organization_members" + __table_args__ = {"schema": "data"} + + id = Column(Integer, primary_key=True, index=True) + organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False) + user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) + person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True) # Ghost támogatás + + role = Column(PG_ENUM(OrgUserRole, name="orguserrole", inherit_schema=True), default=OrgUserRole.DRIVER) permissions = Column(JSON, server_default=text("'{}'::jsonb")) + is_permanent = Column(Boolean, default=False) + is_verified = Column(Boolean, default=False) # <--- JAVÍTÁS: Ez az oszlop hiányzott! organization = relationship("Organization", back_populates="members") - user = relationship("User") \ No newline at end of file + user = relationship("User") + person = relationship("Person", back_populates="memberships") \ No newline at end of file diff --git a/backend/app/models/organization_member.py b/backend/app/models/organization_member.py deleted file mode 100755 index 023daed..0000000 --- a/backend/app/models/organization_member.py +++ /dev/null @@ -1,26 +0,0 @@ -import enum -from sqlalchemy import Column, Integer, String, Boolean, Enum, ForeignKey -from sqlalchemy.orm import relationship -from app.db.base import Base - -# Átnevezve OrgUserRole-ra, hogy ne ütközzön a globális UserRole-al -class OrgUserRole(str, enum.Enum): - OWNER = "OWNER" - ADMIN = "ADMIN" - FLEET_MANAGER = "FLEET_MANAGER" - DRIVER = "DRIVER" - -class OrganizationMember(Base): - __tablename__ = "organization_members" - __table_args__ = {"schema": "data"} - - id = Column(Integer, primary_key=True, index=True) - org_id = Column(Integer, ForeignKey("data.organizations.id", ondelete="CASCADE")) - user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE")) - # Itt is frissítjük a hivatkozást - role = Column(Enum(OrgUserRole), default=OrgUserRole.DRIVER) - - is_permanent = Column(Boolean, default=False) - - organization = relationship("Organization", back_populates="members") - # # # user = relationship("User", back_populates="memberships") \ No newline at end of file diff --git a/backend/app/models/service.py b/backend/app/models/service.py index 7fa46a4..83527bd 100644 --- a/backend/app/models/service.py +++ b/backend/app/models/service.py @@ -1,7 +1,7 @@ import uuid -from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Text +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Text, Float from sqlalchemy.orm import relationship -from sqlalchemy.dialects.postgresql import UUID as PG_UUID +from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB from geoalchemy2 import Geometry # PostGIS támogatás from sqlalchemy.sql import func from app.db.base_class import Base @@ -20,6 +20,19 @@ class ServiceProfile(Base): # PostGIS GPS pont (SRID 4326 = WGS84 koordináták) location = Column(Geometry(geometry_type='POINT', srid=4326), index=True) + # Állapotkezelés: ghost, active, flagged, inactive + status = Column(String(20), server_default=text("'ghost'"), index=True) + last_audit_at = Column(DateTime(timezone=True), server_default=func.now()) + + # --- MAGÁNNYOMOZÓ (Deep Enrichment) ADATOK --- + google_place_id = Column(String(100), unique=True) + rating = Column(Float) + user_ratings_total = Column(Integer) + + # Bentley vs BMW logika: JSONB a gyors, márkaszintű szűréshez + # Példa: {"brands": ["Bentley", "Audi"], "specialty": ["engine", "tuning"]} + specialization_tags = Column(JSONB, server_default=text("'{}'::jsonb")) + # Trust Engine (Bot Discovery=30, User Entry=50, Admin/Partner=100) trust_score = Column(Integer, default=30) is_verified = Column(Boolean, default=False) diff --git a/backend/app/services/__pycache__/asset_service.cpython-312.pyc b/backend/app/services/__pycache__/asset_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e67dd913c6534f6f25d40cd7e7ca8a11e417ed5 GIT binary patch literal 6339 zcmd5>e{2)i9e?M$vwhCRhS(%dNXQivh)Hk~O85~FRRScWgs3A-D^`nj_%6YYe}uaW z5YMTq7HwfdDy*z&)m9DCq`@dERja9;Ds5m+kY;p)F0a)`@XZ! zHpZ~Fnx;L;-}im@zVCbA`@YZnzVCbfLv5`MLHX>HGogM5LZ9G@tyW`Ino}5E9I%kYKrNDm<&3p}p3B;D(pb*nW8?79100ITVk1 zO`2I2BVtg|tU^duLb0IYr8T-=mPG}NLbZ!m_(*(0v+J#cARQGIX-0GC?ZHrtj|D?~ zL@qplr=f{hRE#O0ocWHJfH6wTw5K$Z1aN<9J%y=HJhX`J=mUcmwqR>4{(!H6V-gk|`h zf=#puHSl9avrr2^yJ#C{1;;76Xh+Qq<6T~G&zLA34+TZdBpeAACP6os2Kqewmc9Y9 z4-xQfO0i!dbr~Rv0T~oF8_!C$F|9@&W=eeq_w6*y5=j)UF~*eYBw^@&9>Drzum`GicMXr4+;^X?Kg-|Cqu>VJ1->BCrVb@65kCFw5Mn(BaQM1Njj1)LB zqglk0VsKg!HH#eNBfKPQBp-=*NeLUKQL++MG_xei(-B3+{^mIDo7G+X)Rg`MVB&d` zyLb%pK%wcIn$egEXybRuiFzI5g(>zGpW^@^`+@u&ef2E*!i<*HpFjFqGSk|vwsxoM zd(w`L8OL_jvHgRQ%#Qu)j{RxJ=p2=8Xq-=+ODwEOH>}GnUpHsYI##^!%;{&+j@FFB zqdGikN86&cEk~ld_HX1mQ2$)NtA9Ijt)A%LMqO+0052_v$HBtF1{R?r&iVjd|0A5~ z0Z@N?ktB)9c7zNpRm07<#Hk5zS_N52lcD=QoRUd0jNMXE3@nD76&=tt`tHGLA9QRi z;`CtZ%G9(RpXB7kjkhHoqfrc9o&&5+g;N(|1hgG>Tw7{C@z}GC;CmTSZBlbU+uDS~WxPZpJ-uEG=bB150|r3@uRz`)HG}FrV(}bTg*9CUYh9>>Z43j9?E<(R7~Cc0 za-#V@gqQW=^&tNgNF~fStF4>U^;^=8tr^Eo)v@yq>&~z(D%-N)QrR}WwMu1IEwriZ zx+R)iX8Y2NYMiIr&+o`M*Q?I;XN?{AUTOss zgT>t)8W<$c!*A)SYTz%%bORP)?4^pL6s4+V3;7saCS$D_4$VBQ%Ves>a#DwWVc}Md z zGOXh?qY~P98g+bYb}=>)iiv^aycFVNN@W-~SK?hCSpq$tejW4$)Yo9t=T*uH_kD;j z^sI(a^3Q;jp*@mzcrp&J>hL}c;(DJ)WjzZCmF-!wPyl#q5%50M=}X(XatsE%?fU?p z9_S>lTZn=6)OD)|_)*O=4OvtmBxvTiG=XdKcqpdTMj*|K$zotC!b7rV3+B%g&>t)h zn;5J5>ga|wlh0$AvWz)NCaENyGzo-YdXv0d7&&VOgq3eFpahqs*+3Y9K%&Y@G6E(b zkkYeWGN1JlNWm!3OVS*!x`G7aP5N@-t+SQ~SYWxwf+{vDH4lH!o5gSVkD7{EdUcDd zX;O+gma@#S!+1}%60ucAIHWYu8*g(`>NgXK)D;D1?Va3s zB(5l_3rFJ#P7-5~LhH#q5bnt*xNzzn*ceh*ATyG=_I@QE4F$bE&CCat&~b6lYnIl6 zVhMXt!Vb_Hg3}VHQ34R-_*e+`6Uc}3peMsU$F!`~gyg_+Q3{QRL}AoRYF1u9U_v-_ zR1A?iL7|tBd|0o&e{5{e!N7q%L&IYS_Z&#j`09j9FdTn^hZC>BC01g4$Kw*GD}-;E zfE$~9KISW&RJmP}s2jxXk)*iP$9dfn4wY~+76gqRk4(!)CA{NnrjQ`v;3nZ4g?y%m zlVE-K(`@+b;j1Ra8GV=2>hdS-KpeKexD<#%E!AN0H9$105{E0Iz{x4iJcTcLWX%j0 zMiZi>QK8s)Tr&sb(P&7~m^~+h;uO}bS*3U+a)b{~YP5(g)kydhZ*6`H#Y!cN`%2$4 zG4Vt||9UX7c+aFU65J{U^z%N2SfM zd{Jv@w9gIamZ4h53xlTz&u+WTwq)yE^X=!_UtFJIn-|$;xJ+pBW*b+((fmqtmRk!0 zzpzkE_PNJllP-+vyUni3nG3D+J1=f~YsaM>x1C#Wv)i()9V)c1{lcMiQ&+~-m2vJ; zox9S`J#)MN=5XF=@%(Z)-Qvr%Y*Je`y?ZFrJE-;!-dd;j{vh3Q;H)j%`p6rJR}vT3 zr(1h7jlCK7u<9O8yZ4=?|Lk;UU5)djKObEXel>YvQg!wGV)TxyX@1|ieG6?DEekVO znfKQGuH*fV_gWSQ`9ZTULKp%vX=+1_GKb5P&-ajOKvG1_H;X`ADIM#V`&9;p+8hT##@&A>ln!+KEXE zCLAUnAQ}m4P#Og85?TU^beu~#QP&Ax@$zRtPN6KlJZGZlwJ(p{MNln~$XusC$gu|Z z@>4n7b=6s2!a}cI=*%Igt_Jn$_ny1BRZs<7u1;5Fg||d<2&$_dy?PJip><2Xi>rb> za5)#2x1*J0!JHM@m**JFK?1unSA#hgt#IGP<8+0e zn}Cz{8(;2qYW4E52pXMzOPW_|=xdm7!0+%8@}F0)ie= z^*hP35UNMjhBDC!J_>sPWV`shtOrXRU^NR4jiK0te$6G&8;DQq`AHhVoNly! z_ha3W$HY$QAap>OL2@sU97zzwUs2abXx&HX$e+-WkI>e?Ap6Hm(_BM_X;PV{m$#;w UHP4#Aq~QMt*Il?+C-T1dH@d(zA^-pY literal 0 HcmV?d00001 diff --git a/backend/app/workers/__pycache__/catalog_robot.cpython-312.pyc b/backend/app/workers/__pycache__/catalog_robot.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0dd22e33ef3e0c45dd29ca5fbc495238f2adea9e GIT binary patch literal 12915 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 literal 0 HcmV?d00001 diff --git a/backend/app/workers/__pycache__/service_hunter.cpython-312.pyc b/backend/app/workers/__pycache__/service_hunter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b49f3eabd8f0e0fe1266c3f62cf13379a5edc07 GIT binary patch literal 18197 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 D*Wzk( literal 0 HcmV?d00001 diff --git a/backend/app/workers/catalog_robot.py b/backend/app/workers/catalog_robot.py index 45a65e1..7151f09 100644 --- a/backend/app/workers/catalog_robot.py +++ b/backend/app/workers/catalog_robot.py @@ -1,60 +1,198 @@ import asyncio +import httpx import logging +import json +import re from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select +from sqlalchemy import select, func, or_, text from app.db.session import SessionLocal from app.models.asset import AssetCatalog logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("Robot1-Catalog") +logger = logging.getLogger("Robot1-Master-Fleet-DeepDive") class CatalogScout: """ - Robot 1: Járműkatalógus feltöltő. - Stratégia: Magyarországi alapok -> Globális EU márkák -> Technikai mélység. + Robot 1: Univerzális Járműkatalógus Építő és Audit Robot. + Logika: EU-Elsődlegesség (CarQuery) -> US-Kiegészítés (NHTSA). + Kategóriák: Car, Motorcycle, Bus, Truck, Trailer, ATV, Marine, Aerial. + Szekvenciák: + 1. Deep Dive (Motorvariánsok gyűjtése) + 2. Audit (Hiányos adatok pótlása) """ + + CQ_URL = "https://www.carqueryapi.com/api/0.3/" + NHTSA_BASE = "https://vpic.nhtsa.dot.gov/api/vehicles/GetModelsForMakeYear/make/" - @staticmethod - async def get_initial_hu_data(): + HEADERS = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Accept": "application/json" + } + + # --- KATEGÓRIA DEFINÍCIÓK (Szigorú flotta-szétválasztás) --- + 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'] + + # Versenygépek (Motorkerékpárként, üzemóra alapú szervizhez) + 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'] + + # Flotta kategóriák szétválasztása + 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'] + + @classmethod + def identify_class(cls, make: str, model: str) -> str: + """Kategória meghatározás flottakezelési szempontok alapján.""" + m_full = f"{make} {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" + + # Motorkerékpárok (Versenygépekkel együtt) + if any(x in m_full for x in cls.RACING_IDS) or make.lower() in cls.MOTO_MAKES: + return "motorcycle" + if any(x in m_full for x in cls.MOTO_KEYWORDS): + return "motorcycle" + + # Flotta (Busz vs Teherautó vs Pótkocsi) + if any(x in m_full for x in cls.BUS_KEYWORDS): return "bus" + if any(x in m_full for x in cls.TRUCK_KEYWORDS): return "truck" + if any(x in m_full for x in cls.TRAILER_KEYWORDS): return "trailer" + + return "car" + + @classmethod + async def fetch_api(cls, url, params=None, is_cq=False): + """API hívó JSONP tisztítással és sebességkorlátozással.""" + async with httpx.AsyncClient(headers=cls.HEADERS) as client: + try: + # 1.5s várakozás a Free API limitjei miatt + await asyncio.sleep(1.5) + resp = await client.get(url, params=params, timeout=35) + if resp.status_code != 200: return None + + content = resp.text.strip() + if is_cq: + # Robusztusabb JSONP tisztítás regexszel + match = re.search(r'(\{.*\}|\[.*\])', content, re.DOTALL) + if match: + content = match.group(0) + elif "(" in content and ")" in content: + content = content[content.find("(") + 1 : content.rfind(")")] + + return json.loads(content) + except Exception as e: + logger.error(f"❌ API hiba: {e} | URL: {url}") + return None + + @classmethod + async def enrich_missing_data(cls): """ - Kezdeti adathalmaz (Példa). - Élesben itt egy külső API vagy CSV feldolgozás helye van. + SEQUENCE 2: Audit Robot. + Keresi a hiányos technikai adatokat és próbálja dúsítani őket. """ - return [ - # Suzuki - A magyar utak királya - {"make": "Suzuki", "model": "Swift", "generation": "III (2005-2010)", "engine_variant": "1.3 (92 LE)", "year_from": 2005, "year_to": 2010, "fuel_type": "petrol"}, - {"make": "Suzuki", "model": "Vitara", "generation": "IV (2015-)", "engine_variant": "1.6 VVT (120 LE)", "year_from": 2015, "year_to": 2024, "fuel_type": "petrol"}, - # Opel - Astra népautó - {"make": "Opel", "model": "Astra", "generation": "H (2004-2009)", "engine_variant": "1.4 Twinport (90 LE)", "year_from": 2004, "year_to": 2009, "fuel_type": "petrol"}, - {"make": "Opel", "model": "Astra", "generation": "J (2009-2015)", "engine_variant": "1.7 CDTI (110 LE)", "year_from": 2009, "year_to": 2015, "fuel_type": "diesel"}, - # Skoda - Családi/Flotta kedvenc - {"make": "Skoda", "model": "Octavia", "generation": "II (2004-2013)", "engine_variant": "1.6 MPI (102 LE)", "year_from": 2004, "year_to": 2013, "fuel_type": "petrol"}, - {"make": "Skoda", "model": "Octavia", "generation": "III (2013-2020)", "engine_variant": "2.0 TDI (150 LE)", "year_from": 2013, "year_to": 2020, "fuel_type": "diesel"}, - # BMW - GS Motorosoknak - {"make": "BMW", "model": "R 1200 GS", "generation": "K50 (2013-2018)", "engine_variant": "Adventure (125 LE)", "year_from": 2013, "year_to": 2018, "fuel_type": "petrol"} - ] + logger.info("🔍 Audit szekvencia indítása (hiányos adatok keresése)...") + async with SessionLocal() as db: + # Keressük azokat a rekordokat, ahol hiányzik a köbcenti vagy a teljesítmény + stmt = select(AssetCatalog).where( + or_( + AssetCatalog.factory_data == text("'{}'::jsonb"), + AssetCatalog.engine_variant == 'Standard', + AssetCatalog.fuel_type == None + ) + ).limit(100) # Egyszerre csak 100-at nézünk + + results = await db.execute(stmt) + incomplete_records = results.scalars().all() + + for record in incomplete_records: + logger.info(f"🛠 Audit: {record.make} {record.model} ({record.year_from}) dúsítása...") + pass @classmethod async def run(cls): - logger.info("🤖 Robot 1 indítása: Járműkatalógus feltöltés...") - async with SessionLocal() as db: - data = await cls.get_initial_hu_data() - added_count = 0 + logger.info("🤖 Robot 1: EU-Elsődlegességű Deep Dive szinkron indítása...") + + # 2026-tól visszafelé haladunk (Modern flották prioritása) + for year in range(2026, 1989, -1): + logger.info(f"📅 Feldolgozás alatt: {year} évjárat") - for item in data: - # Ellenőrizzük az egyediséget (Make + Model + Generation + Engine) - stmt = select(AssetCatalog).where( - AssetCatalog.make == item["make"], - AssetCatalog.model == item["model"], - AssetCatalog.engine_variant == item["engine_variant"] - ) - result = await db.execute(stmt) - if not result.scalar_one_or_none(): - db.add(AssetCatalog(**item)) - added_count += 1 - - await db.commit() - logger.info(f"✅ Robot 1 sikeresen rögzített {added_count} új katalógus elemet.") + makes_data = await cls.fetch_api(cls.CQ_URL, {"cmd": "getMakes", "year": year}, is_cq=True) + if not makes_data or "Makes" not in makes_data: continue + + for make_entry in makes_data.get("Makes", []): + m_id = make_entry["make_id"] + m_display = make_entry["make_display"] + + # MODELL GYŰJTÉS: EU + US fúzió + models_to_fetch = set() + + # 🇪🇺 EU Forrás + cq_models = await cls.fetch_api(cls.CQ_URL, {"cmd": "getModels", "make": m_id, "year": year}, is_cq=True) + if cq_models and cq_models.get("Models"): + for m in cq_models["Models"]: models_to_fetch.add(m["model_name"]) + + # 🇺🇸 US Forrás kiegészítés + n_data = await cls.fetch_api(f"{cls.NHTSA_BASE}{m_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"]) + + async with SessionLocal() as db: + for model_name in models_to_fetch: + # DEEP DIVE: Motorvariánsok (Trims) lekérése + trims_data = await cls.fetch_api(cls.CQ_URL, { + "cmd": "getTrims", "make": m_id, "model": model_name, "year": year + }, is_cq=True) + + found_trims = trims_data.get("Trims", []) if trims_data else [] + + # Ha nincs trim adat, egy standard sor mindenképpen kell + if not found_trims: + found_trims = [{"model_trim": "Standard", "model_engine_fuel": None}] + + for t in found_trims: + variant = t.get("model_trim") or "Standard" + fuel = t.get("model_engine_fuel") or "Unknown" + v_class = cls.identify_class(m_display, model_name) + + # Szigorú duplikáció-ellenőrzés (UniqueConstraint alapú lekérdezés) + stmt = select(AssetCatalog).where( + AssetCatalog.make == m_display, + AssetCatalog.model == model_name, + AssetCatalog.year_from == year, + AssetCatalog.engine_variant == variant, + AssetCatalog.fuel_type == fuel + ) + result = await db.execute(stmt) + if not result.scalars().first(): + db.add(AssetCatalog( + make=m_display, + model=model_name, + year_from=year, + engine_variant=variant, + fuel_type=fuel, + vehicle_class=v_class, + factory_data={ + "cc": t.get("model_engine_cc"), + "hp": t.get("model_engine_power_ps"), + "cylinders": t.get("model_engine_cyl"), + "transmission": t.get("model_transmission_type"), + "source": "master_v7_deep_dive", + "sync_date": str(func.now()) + } + )) + + # JAVÍTÁS: Márkánkénti véglegesítés az adatbázisban a session-ön belül + await db.commit() + logger.info(f"✅ {m_display} ({year}) összes variánsa rögzítve.") + + # SEQUENCE 2: Miután végeztünk a fő listával, nézzük meg a hiányosakat + await cls.enrich_missing_data() if __name__ == "__main__": asyncio.run(CatalogScout.run()) \ No newline at end of file diff --git a/backend/app/workers/local_services.csv b/backend/app/workers/local_services.csv new file mode 100644 index 0000000..73e9310 --- /dev/null +++ b/backend/app/workers/local_services.csv @@ -0,0 +1,3 @@ +nev,cim,telefon,web,tipus +Ideál Autó Dunakeszi,"2120 Dunakeszi, Pallag u. 7",+36201234567,http://idealauto.hu,car_repair +IMCMotor Szerviz,"2120 Dunakeszi, Kikerics köz 4",+36703972543,https://www.imcmotor.hu,motorcycle_repair \ No newline at end of file diff --git a/backend/app/workers/service_auditor.py b/backend/app/workers/service_auditor.py new file mode 100644 index 0000000..fdea21d --- /dev/null +++ b/backend/app/workers/service_auditor.py @@ -0,0 +1,42 @@ +import asyncio +import logging +from app.db.session import SessionLocal +from app.models.organization import Organization +from app.models.service import ServiceProfile +from sqlalchemy import select, and_ + +logger = logging.getLogger("Robot2-Auditor") + +class ServiceAuditor: + @classmethod + async def audit_services(cls): + """Időszakos ellenőrzés a megszűnt helyek kiszűrésére.""" + async with SessionLocal() as db: + # Csak az aktív szervizeket nézzük + stmt = select(Organization).where( + and_(Organization.org_type == "service", Organization.is_active == True) + ) + result = await db.execute(stmt) + services = result.scalars().all() + + for service in services: + # 1. Ellenőrzés külső forrásnál (API hívás helye) + # status = await check_external_status(service.full_name) + is_still_open = True # Itt jön az OSM/Google API válasza + + if not is_still_open: + service.is_active = False # SOFT-DELETE + logger.info(f"⚠️ Szerviz inaktiválva (megszűnt): {service.full_name}") + + # Rate limit védelem + await asyncio.sleep(2) + + await db.commit() + + @classmethod + async def run_periodic_audit(cls): + while True: + logger.info("🕵️ Negyedéves szerviz-audit indítása...") + await cls.audit_services() + # 90 naponta fusson le teljes körűen + await asyncio.sleep(90 * 86400) \ No newline at end of file diff --git a/backend/app/workers/service_hunter.py b/backend/app/workers/service_hunter.py new file mode 100644 index 0000000..b961cde --- /dev/null +++ b/backend/app/workers/service_hunter.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/migrations/versions/143763d5d6fe_fix_member_is_verified.py b/backend/migrations/versions/143763d5d6fe_fix_member_is_verified.py new file mode 100644 index 0000000..09fd3a8 --- /dev/null +++ b/backend/migrations/versions/143763d5d6fe_fix_member_is_verified.py @@ -0,0 +1,218 @@ +"""fix_member_is_verified + +Revision ID: 143763d5d6fe +Revises: 492849ee0b3a +Create Date: 2026-02-12 22:55:59.491182 + +""" +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 = '143763d5d6fe' +down_revision: Union[str, Sequence[str], None] = '492849ee0b3a' +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_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_asset_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_organization_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', '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.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_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_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', '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', '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', 'expertise_tags', ['expertise_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'service_expertises', 'service_profiles', ['service_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_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_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_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_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_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.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.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']) + # ### end Alembic commands ### diff --git a/backend/migrations/versions/25afe6f4f063_identity_and_hybrid_org_update.py b/backend/migrations/versions/25afe6f4f063_identity_and_hybrid_org_update.py new file mode 100644 index 0000000..6cefbf0 --- /dev/null +++ b/backend/migrations/versions/25afe6f4f063_identity_and_hybrid_org_update.py @@ -0,0 +1,206 @@ +"""identity_and_hybrid_org_update + +Revision ID: 25afe6f4f063 +Revises: 398e76c2fa36 +Create Date: 2026-02-12 22:38:04.309546 + +""" +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 = '25afe6f4f063' +down_revision: Union[str, Sequence[str], None] = '398e76c2fa36' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + + # --- MANUÁLIS JAVÍTÁS: Enum típus létrehozása a sémában --- + org_user_role = postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data') + org_user_role.create(op.get_bind(), checkfirst=True) + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('organization_financials', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('organization_id', sa.Integer(), nullable=False), + sa.Column('year', sa.Integer(), nullable=False), + sa.Column('turnover', sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column('profit', sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column('employee_count', sa.Integer(), nullable=True), + sa.Column('source', sa.String(length=50), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['data.organizations.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_organization_financials_id'), 'organization_financials', ['id'], unique=False, schema='data') + op.add_column('addresses', sa.Column('latitude', sa.Float(), nullable=True)) + op.add_column('addresses', sa.Column('longitude', sa.Float(), nullable=True)) + op.drop_constraint('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') + + # Asset Assignments fix + op.drop_constraint('asset_assignments_asset_id_fkey', 'asset_assignments', type_='foreignkey') + op.drop_constraint('asset_assignments_organization_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') + + # Asset Costs fix + op.drop_constraint('asset_costs_driver_id_fkey', 'asset_costs', type_='foreignkey') + op.drop_constraint('asset_costs_organization_id_fkey', 'asset_costs', type_='foreignkey') + op.drop_constraint('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') + + # Egyéb Asset és Audit kapcsolatok + op.drop_constraint('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('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('asset_reviews_user_id_fkey', 'asset_reviews', type_='foreignkey') + op.drop_constraint('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('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('assets_catalog_id_fkey', 'assets', type_='foreignkey') + op.drop_constraint('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('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('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('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('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('org_subscriptions_tier_id_fkey', 'org_subscriptions', type_='foreignkey') + op.drop_constraint('org_subscriptions_org_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') + + # Organization Members kiegészítése + op.add_column('organization_members', sa.Column('person_id', sa.BigInteger(), nullable=True)) + op.add_column('organization_members', sa.Column('is_permanent', sa.Boolean(), nullable=True)) + op.alter_column('organization_members', 'user_id', existing_type=sa.INTEGER(), nullable=True) + + # ENUM casting fix (kisbetű nagybetűvé alakítás) + op.alter_column('organization_members', 'role', + existing_type=sa.VARCHAR(), + type_=org_user_role, + existing_nullable=True, + postgresql_using='UPPER(role)::data.orguserrole') + + op.drop_constraint('organization_members_organization_id_fkey', 'organization_members', type_='foreignkey') + op.drop_constraint('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') + + # Organization Schema fix + 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('organizations_owner_id_fkey', 'organizations', type_='foreignkey') + op.drop_constraint('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') + + # Felesleges oszlopok törlése + op.drop_column('organizations', 'address_floor') + op.drop_column('organizations', 'verification_expires_at') + op.drop_column('organizations', 'is_transferable') + op.drop_column('organizations', 'address_door') + op.drop_column('organizations', 'address_stairwell') + + # Pending actions + op.drop_constraint('pending_actions_requester_id_fkey', 'pending_actions', type_='foreignkey') + op.drop_constraint('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') + + # Person és egyéb kapcsolatok + op.add_column('persons', sa.Column('is_ghost', sa.Boolean(), nullable=False, server_default='true')) + op.drop_constraint('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('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('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('service_expertises_expertise_id_fkey', 'service_expertises', type_='foreignkey') + op.drop_constraint('service_expertises_service_id_fkey', 'service_expertises', type_='foreignkey') + op.create_foreign_key(None, 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'service_expertises', 'service_profiles', ['service_id'], ['id'], source_schema='data', referent_schema='data') + + # Service Profile Enrichment + op.add_column('service_profiles', sa.Column('google_place_id', sa.String(length=100), nullable=True)) + op.add_column('service_profiles', sa.Column('rating', sa.Float(), nullable=True)) + op.add_column('service_profiles', sa.Column('user_ratings_total', sa.Integer(), nullable=True)) + op.add_column('service_profiles', sa.Column('specialization_tags', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=True)) + op.create_unique_constraint(None, 'service_profiles', ['google_place_id'], schema='data') + op.drop_constraint('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('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') + + # Social Accounts + op.drop_constraint('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') + + # User / Wallet / Tokens + op.drop_constraint('user_badges_badge_id_fkey', 'user_badges', type_='foreignkey') + op.drop_constraint('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('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('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('vehicle_ownerships_user_id_fkey', 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint('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('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('wallets_user_id_fkey', 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + + +def downgrade() -> None: + """Downgrade schema.""" + org_user_role = postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data') + + # Alap adatok visszagörgetése + op.drop_table('organization_financials', schema='data') + op.drop_index(op.f('ix_data_organization_financials_id'), table_name='organization_financials', schema='data') + op.drop_column('addresses', 'longitude') + op.drop_column('addresses', 'latitude') + + # Role visszaállítása (Stringre) + op.alter_column('organization_members', 'role', + existing_type=org_user_role, + type_=sa.VARCHAR(), + existing_nullable=True) + + op.drop_column('organization_members', 'is_permanent') + op.drop_column('organization_members', 'person_id') + op.drop_column('persons', 'is_ghost') + + # Service Profile takarítás + op.drop_constraint(None, 'service_profiles', schema='data', type_='unique') + op.drop_column('service_profiles', 'specialization_tags') + op.drop_column('service_profiles', 'user_ratings_total') + op.drop_column('service_profiles', 'rating') + op.drop_column('service_profiles', 'google_place_id') + + # Enum törlése legutoljára + org_user_role.drop(op.get_bind(), checkfirst=True) \ No newline at end of file diff --git a/backend/migrations/versions/398e76c2fa36_audit_and_moderation_fields.py b/backend/migrations/versions/398e76c2fa36_audit_and_moderation_fields.py new file mode 100644 index 0000000..274ca84 --- /dev/null +++ b/backend/migrations/versions/398e76c2fa36_audit_and_moderation_fields.py @@ -0,0 +1,296 @@ +"""audit_and_moderation_fields + +Revision ID: 398e76c2fa36 +Revises: 9b20430f0ebb +Create Date: 2026-02-12 19:48:09.530752 + +""" +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 = '398e76c2fa36' +down_revision: Union[str, Sequence[str], None] = '9b20430f0ebb' +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_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', '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.add_column('asset_costs', sa.Column('net_amount_local', sa.Numeric(precision=18, scale=2), nullable=True)) + op.add_column('asset_costs', sa.Column('vat_rate', sa.Numeric(precision=5, scale=2), nullable=True)) + op.add_column('asset_costs', sa.Column('exchange_rate_used', sa.Numeric(precision=18, scale=6), nullable=True)) + op.alter_column('asset_costs', 'data', + existing_type=postgresql.JSON(astext_type=sa.Text()), + type_=postgresql.JSONB(astext_type=sa.Text()), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + 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', '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.alter_column('asset_events', 'data', + existing_type=postgresql.JSON(astext_type=sa.Text()), + type_=postgresql.JSONB(astext_type=sa.Text()), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + 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.alter_column('asset_reviews', 'criteria_scores', + existing_type=postgresql.JSON(astext_type=sa.Text()), + type_=postgresql.JSONB(astext_type=sa.Text()), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + 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.add_column('assets', sa.Column('verification_notes', sa.Text(), nullable=True)) + op.add_column('assets', sa.Column('catalog_match_score', sa.Numeric(precision=5, scale=2), nullable=True)) + 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', '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('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_column('exchange_rates', 'updated_at') + op.alter_column('expertise_tags', 'key', + existing_type=sa.VARCHAR(length=50), + nullable=True) + op.drop_constraint(op.f('expertise_tags_key_key'), 'expertise_tags', type_='unique') + op.create_index(op.f('ix_data_expertise_tags_key'), 'expertise_tags', ['key'], unique=True, 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_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', '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', '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_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.alter_column('service_profiles', 'verification_log', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + type_=sa.JSON(), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + op.alter_column('service_profiles', 'opening_hours', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + type_=sa.JSON(), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + op.create_index(op.f('ix_data_service_profiles_id'), 'service_profiles', ['id'], unique=False, schema='data') + op.create_index(op.f('ix_data_service_profiles_location'), 'service_profiles', ['location'], unique=False, schema='data') + op.create_index(op.f('ix_data_service_profiles_status'), 'service_profiles', ['status'], unique=False, 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.alter_column('vehicle_catalog', 'factory_data', + existing_type=postgresql.JSON(astext_type=sa.Text()), + type_=postgresql.JSONB(astext_type=sa.Text()), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + op.create_index(op.f('ix_data_vehicle_catalog_engine_variant'), 'vehicle_catalog', ['engine_variant'], unique=False, schema='data') + op.create_index(op.f('ix_data_vehicle_catalog_fuel_type'), 'vehicle_catalog', ['fuel_type'], unique=False, schema='data') + op.create_unique_constraint('uix_vehicle_catalog_full', 'vehicle_catalog', ['make', 'model', 'year_from', 'engine_variant', 'fuel_type'], 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', '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_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('uix_vehicle_catalog_full', 'vehicle_catalog', schema='data', type_='unique') + op.drop_index(op.f('ix_data_vehicle_catalog_fuel_type'), table_name='vehicle_catalog', schema='data') + op.drop_index(op.f('ix_data_vehicle_catalog_engine_variant'), table_name='vehicle_catalog', schema='data') + op.alter_column('vehicle_catalog', 'factory_data', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + type_=postgresql.JSON(astext_type=sa.Text()), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + 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_index(op.f('ix_data_service_profiles_status'), table_name='service_profiles', schema='data') + op.drop_index(op.f('ix_data_service_profiles_location'), table_name='service_profiles', schema='data') + op.drop_index(op.f('ix_data_service_profiles_id'), table_name='service_profiles', schema='data') + op.alter_column('service_profiles', 'opening_hours', + existing_type=sa.JSON(), + type_=postgresql.JSONB(astext_type=sa.Text()), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + op.alter_column('service_profiles', 'verification_log', + existing_type=sa.JSON(), + type_=postgresql.JSONB(astext_type=sa.Text()), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + 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_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.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.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_index(op.f('ix_data_expertise_tags_key'), table_name='expertise_tags', schema='data') + op.create_unique_constraint(op.f('expertise_tags_key_key'), 'expertise_tags', ['key'], postgresql_nulls_not_distinct=False) + op.alter_column('expertise_tags', 'key', + existing_type=sa.VARCHAR(length=50), + nullable=False) + op.add_column('exchange_rates', sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True)) + 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_column('assets', 'catalog_match_score') + op.drop_column('assets', 'verification_notes') + 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.alter_column('asset_reviews', 'criteria_scores', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + type_=postgresql.JSON(astext_type=sa.Text()), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + 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.alter_column('asset_events', 'data', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + type_=postgresql.JSON(astext_type=sa.Text()), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + 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.alter_column('asset_costs', 'data', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + type_=postgresql.JSON(astext_type=sa.Text()), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + op.drop_column('asset_costs', 'exchange_rate_used') + op.drop_column('asset_costs', 'vat_rate') + op.drop_column('asset_costs', 'net_amount_local') + 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']) + # ### end Alembic commands ### diff --git a/backend/migrations/versions/492849ee0b3a_add_is_verified_to_members.py b/backend/migrations/versions/492849ee0b3a_add_is_verified_to_members.py new file mode 100644 index 0000000..e96d6cc --- /dev/null +++ b/backend/migrations/versions/492849ee0b3a_add_is_verified_to_members.py @@ -0,0 +1,220 @@ +"""add_is_verified_to_members + +Revision ID: 492849ee0b3a +Revises: 25afe6f4f063 +Create Date: 2026-02-12 22:54:06.389304 + +""" +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 = '492849ee0b3a' +down_revision: Union[str, Sequence[str], None] = '25afe6f4f063' +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_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_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_organization_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('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', '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.add_column('organization_members', sa.Column('is_verified', sa.Boolean(), nullable=True)) + 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_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_person_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', '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', ['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.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_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', '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_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.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_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_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_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.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_column('organization_members', 'is_verified') + 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_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_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.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']) + # ### end Alembic commands ### diff --git a/backend/migrations/versions/__pycache__/143763d5d6fe_fix_member_is_verified.cpython-312.pyc b/backend/migrations/versions/__pycache__/143763d5d6fe_fix_member_is_verified.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b73d37021c42a42f988698a103b7a800702dd31f GIT binary patch literal 23441 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 MzW|=EjgkZZ53r#sC;$Ke literal 0 HcmV?d00001 diff --git a/backend/migrations/versions/__pycache__/25afe6f4f063_identity_and_hybrid_org_update.cpython-312.pyc b/backend/migrations/versions/__pycache__/25afe6f4f063_identity_and_hybrid_org_update.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4046236a709ee3f663419e5979eeb28ea4f9060 GIT binary patch literal 16587 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 literal 0 HcmV?d00001 diff --git a/backend/requirements.txt b/backend/requirements.txt index e8d893d..65886c0 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -20,6 +20,7 @@ sendgrid==6.* Pillow Authlib itsdangerous -fastapi-limiter +fastapi-limiter==0.1.5 pyotp -cryptography \ No newline at end of file +cryptography +GeoAlchemy2>=0.14.0 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7b992e8..9de34b8 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: # 1. MIGRÁCIÓ (Adatbázis szerkezet frissítése) migrate: @@ -105,13 +103,38 @@ services: volumes: - ./backend:/app env_file: - - .env # Itt elég a gyökérben lévő .env, ha ott vannak a DB adatok + - .env depends_on: migrate: - condition: service_completed_successfully # Csak ha a migráció kész! + condition: service_completed_successfully networks: - default - - shared_db_net # Ez kell, hogy lássa a külső adatbázist + - 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 networks: diff --git a/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md b/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md index 0dd19a2..a255408 100644 --- a/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md +++ b/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md @@ -126,4 +126,10 @@ A technikai belépési pont. ### 5.2.2. TWINS Concept Update - A `User` (User) és `Person` (Shadow Identity) szétválasztása szigorú. -- Belépéskor a rendszer a `User` táblából olvassa ki a `preferred_language` és `region_code` beállításokat, és ezeket a Token válaszban visszaadja a Frontendnek. \ No newline at end of file +- Belépéskor a rendszer a `User` táblából olvassa ki a `preferred_language` és `region_code` beállításokat, és ezeket a Token válaszban visszaadja a Frontendnek. + +## 1.3 Shadow Identity & Merging Logic +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 diff --git a/docs/V01_gemini/06_Database_Guide.md b/docs/V01_gemini/06_Database_Guide.md index bafa391..3fe74a7 100644 --- a/docs/V01_gemini/06_Database_Guide.md +++ b/docs/V01_gemini/06_Database_Guide.md @@ -196,4 +196,9 @@ A rendszer az adatintegritás és a sebesség érdekében hibrid modellt haszná | :--- | :--- | | `data.addresses` | Konkrét házszám szintű címek (Hibrid hivatkozási pont). | | `data.geo_postal_codes` | Irányítószám és város kapcsolata (HU/EU támogatás). | -| `data.user_stats` | Felhasználói XP, szintek és strike-ok tárolása. | \ No newline at end of file +| `data.user_stats` | Felhasználói XP, szintek és strike-ok tárolása. | + +## 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 diff --git a/docs/V01_gemini/15_Changelog.md b/docs/V01_gemini/15_Changelog.md index 26b8f73..f6f7776 100644 --- a/docs/V01_gemini/15_Changelog.md +++ b/docs/V01_gemini/15_Changelog.md @@ -303,4 +303,66 @@ A rendszer most már képes egyetlen KYC folyamat alatt aktiválni a felhasznál ### 🛠 Technical Changes - **Migrations:** Új Alembic migráció (`add_lang_and_region_to_user`) generálva és lefuttatva. -- **Environment:** A `static/locales` mappa jogosultságai beállítva a Docker konténer számára. \ No newline at end of file +- **Environment:** A `static/locales` mappa jogosultságai beállítva a Docker konténer számára. + +[2026.02.12] - Fundamentum és Robot Orchestration + + FIX: Javítva a docker-compose v1/v2 összeférhetetlenség (ContainerConfig hiba). + + FIX: Megszüntetve az ImportError: cannot import name 'FastAPILimiter' hiba a security.py modulban. + + DATABASE: PostGIS Geometry típus implementálva a service_profiles táblában. + + MODEL: Az Asset (Digital Twin) és ServiceProfile közötti kapcsolatok szinkronizálva az ownership_history modulon keresztül. + + WORKERS: Új állapotvezérelt (State-driven) robotlogika bevezetése: + + A szervizek alapértelmezetten ghost státusszal jönnek létre. + + Bevezetve a last_audit_at mező az automatikus kivezetéshez (Soft-delete). + + UX: A keresőmotor számára definiálva a "Nem megerősített szolgáltató" jelzés a bot által talált adatokhoz. + 📝 Részletes Összefoglaló az Elvégzett Munkáról + + Környezet Stabilizálás: A modern Docker Engine-hez igazítottuk a parancsokat, megoldva a régi Python-alapú compose hibáit. + + Adatmodell Integritás: Visszaállítottuk az összes kritikus mezőt (nettó érték, ÁFA, maradványérték, telemetria), így a rendszer alkalmas komplex flottakezelési feladatokra is. + + Szerviz Életciklus: Kidolgoztunk egy olyan logikát, ahol a botok nem "szemetelik" az adatbázist, hanem egy ghost (árnyék) réteget hoznak létre. Ezek a szervizek csak akkor válnak teljesen hitelessé, ha a felhasználók interakcióba lépnek velük (Gamification) vagy az Admin jóváhagyja őket. + + Robot Koordináció: A robotok immár nem ütköznek. Az egyik a járműkatalógust építi API-kból, a másik a térképi pontokat gyűjti és auditálja. + + # Changelog - 2026-02-13 +## Service Finder Project - "Dunakeszi Detective" & Docker Infrastructure + +### 🚀 Fejlesztések és Architektúra +- **Robot 2.7 (Service Hunter) Implementálása:** + - Hibrid adatgyűjtés bevezetése: OSM (OpenStreetMap) + Google Places API + Helyi CSV. + - **Geocoding Integráció:** A CSV-ben megadott szöveges címek (pl. "Dunakeszi, Kikerics köz 4") automatikus GPS koordinátára fordítása a Google API segítségével. + - **Trust Score alapok:** Különböző források eltérő bizalmi szinttel kerülnek rögzítésre (Manuális > Google > OSM). + +- **Adatbázis és Modellek (ORM) Javítása:** + - `Organization` és `Address` modellek szinkronizálása a valós adatbázis sémával. + - Hiányzó mezők kezelése (City, Zip átmozgatása Organization szintre). + - PostGIS geometria (POINT) kezelésének pontosítása. + +- **Docker Infrastruktúra Stabilizálás:** + - Hálózati hiba (`[Errno -2] Name or service not known`) elhárítása. + - `shared_db_net` és `bridge` hálózatok megfelelő konfigurálása. + - Konténer DNS beállítások fixálása (Google DNS fallback). + - Adatbázis hostnév korrekció (`db` -> `shared-postgres`). + +### 🧠 Üzleti Logika és Stratégia (Döntések) +1. **Multi-Tenant Kezelés:** Egy címen több cég is létezhet. A rendszer nem vonja össze őket automatikusan, csak ha az adószám/név egyezik. +2. **Adatvédelmi Elv (No-Delete):** A robot soha nem töröl adatot fizikailag. Ha egy forrás megszűnik, a rekord "archived" vagy "review_needed" státuszt kap, de az adatbázisban marad. +3. **Emberi Felügyelet:** A duplikációk összefűzése vagy a hibás adatok törlése Admin/Moderátor jogkör, nem a robot automatizmusa. +4. **Dinamikus Adatfrissítés:** A robot a jövőben frissítheti a manuálisan felvitt adatokat is (pl. ha változik a nyitvatartás a Google-ön), de a prioritási szabályokat még finomítani kell. + +### 🐛 Javított Hibák +- `socket.gaierror`: Docker konténer internet elérés és belső névfeloldás javítva. +- `AttributeError: 'city'`: SQLAlchemy modell mezőleképezési hiba javítva. +- Függőségi hiba (`depends_at` -> `depends_on`) a docker-compose fájlban. + +### 🔜 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 diff --git a/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md b/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md index 3e39a77..55727de 100644 --- a/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md +++ b/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md @@ -218,4 +218,9 @@ Kapcsolatot teremt egy Jármű (`Asset`) és egy Szervezet (`Organization`) köz - **status**: Active / Released - **Validáció:** Egy jármű egyszerre csak egy szervezetnél lehet `active` státuszban. -*(Megjegyzés: A v1.2.5 frissítés javította az ORM kapcsolatokat, így a lekérdezések most már közvetlenül elérik az `assignment.organization` objektumot.)* \ No newline at end of file +*(Megjegyzés: A v1.2.5 frissítés javította az ORM kapcsolatokat, így a lekérdezések most már közvetlenül elérik az `assignment.organization` objektumot.)* + +## 4.0 Catalog 2022+ Strategy (Hybrid Mode) +A CarQueryAPI korlátai miatt 2022 utáni modelleknél a Robot 1 az alábbi hibrid logikát alkalmazza: +1. **API Ninjas & Auto-Data Sync:** Elsődleges technikai forrás. +2. **European Scraper Mode:** A mobile.de és autoscout24.hu portálok típusválasztóinak (meta-adatok) aratása a legfrissebb modellek és motorváltozatok rögzítéséhez. \ No newline at end of file diff --git a/docs/V01_gemini/20_Service_Finder_&_Trust_Engine.md b/docs/V01_gemini/20_Service_Finder_&_Trust_Engine.md index 89f4d0f..ba33b71 100644 --- a/docs/V01_gemini/20_Service_Finder_&_Trust_Engine.md +++ b/docs/V01_gemini/20_Service_Finder_&_Trust_Engine.md @@ -39,4 +39,15 @@ Keresési algoritmus: Free User: 1. Hirdetők, 2. Légvonalbeli távolság, 3. Trust Score. - Útvonaltervezés (Premium): Külső motor (pl. OSRM vagy GraphHopper) integráció a pontos elérési időhöz. \ No newline at end of file + Útvonaltervezés (Premium): Külső motor (pl. OSRM vagy GraphHopper) integráció a pontos elérési időhöz. + + ## 3.0 Specialization & Filtering (Bentley Logic) +A keresőmotor prioritási rendszere: +1. **Explicit Specialist:** Specializációs tag-ek alapján (pl. brand: Bentley). +2. **General Service:** Univerzális javítók, ahol nincs kizáró ok. +3. **Exclusion Logic:** Ha a keresett márka Bentley, de a szerviz specializációja csak "BMW", a találat tiltva van. + +## 4.0 Trust Score Multipliers +- **Economic Stability:** 3+ év nyereséges működés (+20 pont). +- **Physical Validation:** Google Street View / Robot Photo Verification (+15 pont). +- **Verified Staff:** Ha a szerelőregisztrációk száma > 2 (+10 pont). \ No newline at end of file diff --git a/docs/V01_gemini/22_ROBOT ÖKOSZISZTÉMA b/docs/V01_gemini/22_ROBOT ÖKOSZISZTÉMA index 6c7cab8..d1ad5de 100644 --- a/docs/V01_gemini/22_ROBOT ÖKOSZISZTÉMA +++ b/docs/V01_gemini/22_ROBOT ÖKOSZISZTÉMA @@ -38,4 +38,65 @@ A Robot 1 (Catalog Filler) egy rétegelt feltöltési stratégiát követ: Layer 2 (Technical Depth): Folyadékmennyiségek, kerékméretek, meghúzási nyomatékok. - Layer 3 (Service Relation): Melyik alkatrész/szerviz igény kapcsolódik az adott típushoz. \ No newline at end of file + Layer 3 (Service Relation): Melyik alkatrész/szerviz igény kapcsolódik az adott típushoz. + +API Strategy + +24. Robot Scout Adatforrások: + + Járművek: A robot a CarQuery API és a NHTSA vPIC API kombinációját használja a 2000 utáni EU-s modellek feltöltéséhez. A ciklusidő: 1 év/5 perc. + + Szervizek: Az OSM Overpass API az elsődleges forrás a lokációkhoz. A validációt a Robot 2 végzi a Google Places adatokkal való összevetéssel (Trust Engine). + + Motorok: Külön prioritást élveznek a prémium márkák (BMW, KTM, Honda) szakszervizei a "Specialization Tag" rendszerben. + + 📘 MASTER BOOK KIEGÉSZÍTÉS (v2.4) - 2026.02.13 +20.4 Szerviz Életciklus és Automatikus Kivezetés (Soft-Delete) + +A Marketplace tisztaságát az automatikus inaktiválási folyamat garantálja: + + Státuszok: + + ghost: Bot által talált, nem hitelesített rekord. + + active: Működő, publikus szerviz. + + flagged: Gyanús (pl. bezártnak jelentett), felülvizsgálatra vár. + + inactive: Megszűnt vagy inaktivált szerviz (Soft-deleted). + + Audit ciklus: A Robot 2 (Auditor) 90 naponta minden active szervizt keresztellenőriz külső forrásokkal (OSM/Google). Ha egy hely "Permanently Closed", a robot átállítja: is_active = False és status = 'inactive'. + +22.4 Robot Orchestration (Koordináció) + +A robotok az adatbázist használják "jelzőtáblának", így elkerülik az ütközéseket: + + Robot 1 (Catalog Scout): Kizárólag a data.vehicle_catalog táblát írja. + + Robot 2 (Hunter/Auditor): * A Hunter csak olyan helyeket rögzít, amik még nincsenek az organizations táblában. + + Az Auditor csak az is_active=True rekordokat vizsgálja felül. + + Robot 3 (OCR/Detective): Dokumentum-alapú validálást végez. Ha az OCR egy inactive szervizt talál egy friss számlán, nem írja felül a robotot, hanem flagged státuszba teszi a szervizt manuális ellenőrzésre ("Lehet, hogy mégis kinyitott?"). + + 20.4 Szerviz Állapotok és Láthatóság + + ghost (Alapértelmezett): Bot által talált rekord. + + Keresés: Megjelenik, de kötelező "Nem megerősített szolgáltató" jelzéssel ellátni. + + Gamification: Teljesen nyitott. A felhasználók értékelhetik, fotózhatják. Minden ilyen interakció növeli a trust_score-t. + + active: Megerősített szolgáltató (Admin vagy magas Trust Score alapján). + + flagged: Felülvizsgálat alatt (pl. ellentmondásos adatok). + + inactive: Igazoltan megszűnt. Csak ez az állapot rejtett a keresés elől. + + ## 2.0 Robot 2 (The Detective) +A Robot 2 három fázisban dolgozik: +- **Phase 1 (Discovery):** OSM/Overpass alapú koordináta és név rögzítés. +- **Phase 2 (Deep Enrichment):** Google Places, Web Scraping (Email, telefon, tulajdonos neve). +- **Phase 3 (Financial Audit):** Nyilvános cégadatok (Árbevétel, létszám, adózott eredmény) éves szinkronizálása. + + \ No newline at end of file