diff --git a/backend/.env b/backend/.env index 1caad0d..5e046de 100644 --- a/backend/.env +++ b/backend/.env @@ -4,10 +4,20 @@ DATABASE_URL=postgresql+asyncpg://service_finder_app:JELSZAVAD@db:5432/service_f # Security SECRET_KEY=ide_generálj_egy_hosszú_véletlen_karaktersort +ALGORITHM=HS256 # Initial Admin (Ezt fogja a seed script használni) INITIAL_ADMIN_EMAIL=kincses@valami.hu INITIAL_ADMIN_PASSWORD=Kincs€s74 # Debug mód (opcionális) -DEBUG=True \ No newline at end of file +DEBUG=True + +# --- Google OAuth --- +GOOGLE_CLIENT_ID=575071309971-8icc0o61hiat9sioeuqin8k4tnvnssmd.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=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 +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 diff --git a/backend/app/__pycache__/main.cpython-312.pyc b/backend/app/__pycache__/main.cpython-312.pyc index de9f997..efe3ef5 100644 Binary files a/backend/app/__pycache__/main.cpython-312.pyc and b/backend/app/__pycache__/main.cpython-312.pyc differ diff --git a/backend/app/api/__pycache__/deps.cpython-312.pyc b/backend/app/api/__pycache__/deps.cpython-312.pyc index 8e23a62..cceb01f 100644 Binary files a/backend/app/api/__pycache__/deps.cpython-312.pyc and b/backend/app/api/__pycache__/deps.cpython-312.pyc differ diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index 9cab3b9..d1f0ed8 100755 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, Union import logging from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer @@ -6,26 +6,37 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.db.session import get_db -from app.core.security import decode_token, RANK_MAP -from app.models.identity import User +from app.core.security import decode_token, DEFAULT_RANK_MAP +from app.models.identity import User, UserRole +from app.core.config import settings logger = logging.getLogger(__name__) -reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") + +# Az OAuth2 folyamat a bejelentkezési végponton keresztül +reusable_oauth2 = OAuth2PasswordBearer( + tokenUrl=f"{settings.API_V1_STR}/auth/login" +) async def get_current_token_payload( token: str = Depends(reusable_oauth2) ) -> Dict[str, Any]: - if token == "dev_bypass_active": - return { + """ + JWT token visszafejtése és a típus (access) ellenőrzése. + """ + # Dev bypass (ha esetleg fejlesztéshez használtad korábban, itt a helye, + # de élesben a token validáció fut le) + if settings.DEBUG and token == "dev_bypass_active": + return { "sub": "1", "role": "superadmin", "rank": 100, "scope_level": "global", - "scope_id": "all" + "scope_id": "all", + "type": "access" } payload = decode_token(token) - if not payload: + if not payload or payload.get("type") != "access": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Érvénytelen vagy lejárt munkamenet." @@ -33,39 +44,93 @@ async def get_current_token_payload( return payload async def get_current_user( - db: AsyncSession = Depends(get_db), - payload: Dict[str, Any] = Depends(get_current_token_payload), + db: AsyncSession = Depends(get_db), + payload: Dict = Depends(get_current_token_payload) ) -> User: + """ + Lekéri a felhasználót a token 'sub' mezője alapján. + """ user_id = payload.get("sub") if not user_id: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token azonosítási hiba.") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token azonosítási hiba." + ) result = await db.execute(select(User).where(User.id == int(user_id))) user = result.scalar_one_or_none() - - if not user or user.is_deleted: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="A felhasználó nem található.") + if not user or user.is_deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="A felhasználó nem található." + ) return user async def get_current_active_user( current_user: User = Depends(get_current_user), ) -> User: - """Ellenőrzi, hogy a felhasználó aktív-e.""" + """ + Ellenőrzi, hogy a felhasználó aktív-e. + Ez elengedhetetlen az Admin felület és a védett végpontok számára. + """ if not current_user.is_active: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="A felhasználói fiók zárolva van vagy inaktív." + detail="A művelethez aktív profil és KYC azonosítás (Step 2) szükséges." ) return current_user -def check_min_rank(required_rank: int): - def rank_checker(payload: Dict[str, Any] = Depends(get_current_token_payload)): +async def check_resource_access( + resource_scope_id: Union[str, int], + current_user: User = Depends(get_current_user) +): + """ + Scoped RBAC: Megakadályozza, hogy egy felhasználó más valaki erőforrásaihoz nyúljon. + Kezeli az ID-t (int) és a Scope ID-t / Slug-ot (str) is. + """ + if current_user.role == UserRole.superadmin: + return True + + # Ha a usernek van beállított scope_id-ja (pl. egy flottához tartozik), + # akkor ellenőrizzük, hogy a kért erőforrás abba a scope-ba tartozik-e. + user_scope = current_user.scope_id + requested_scope = str(resource_scope_id) + + # 1. Saját erőforrás (saját ID) + if str(current_user.id) == requested_scope: + return True + + # 2. Scope alapú hozzáférés (pl. flotta tagja) + if user_scope and user_scope == requested_scope: + return True + + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Nincs jogosultsága ehhez az erőforráshoz." + ) + +def check_min_rank(role_key: str): + """ + Dinamikus Rank ellenőrzés. + Az adatbázisból (system_parameters) kéri le az elvárt szintet. + """ + async def rank_checker( + db: AsyncSession = Depends(get_db), + payload: Dict = Depends(get_current_token_payload) + ): + # A settings.get_db_setting-et használjuk a dinamikus lekéréshez + ranks = await settings.get_db_setting( + db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP + ) + + required_rank = ranks.get(role_key, 0) user_rank = payload.get("rank", 0) + if user_rank < required_rank: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"Nincs elegendő jogosultsága a művelethez. (Szükséges szint: {required_rank})" + detail=f"Alacsony jogosultsági szint. (Szükséges: {required_rank})" ) return True return rank_checker \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc index 0338035..eb00251 100644 Binary files a/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc and b/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc differ diff --git a/backend/app/api/v1/endpoints/auth.py b/backend/app/api/v1/endpoints/auth.py index 9109092..8861f96 100644 --- a/backend/app/api/v1/endpoints/auth.py +++ b/backend/app/api/v1/endpoints/auth.py @@ -1,11 +1,15 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, Request from fastapi.security import OAuth2PasswordRequestForm +from fastapi.responses import RedirectResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select +from authlib.integrations.starlette_client import OAuth from app.db.session import get_db from app.services.auth_service import AuthService -from app.core.security import create_access_token, RANK_MAP +from app.services.social_auth_service import SocialAuthService +from app.core.security import create_tokens, DEFAULT_RANK_MAP +from app.core.config import settings from app.schemas.auth import ( UserLiteRegister, Token, PasswordResetRequest, UserKYCComplete, PasswordResetConfirm @@ -15,57 +19,50 @@ from app.models.identity import User router = APIRouter() -@router.post("/register-lite", response_model=Token, status_code=status.HTTP_201_CREATED) -async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)): - """Step 1: Alapszintű regisztráció. Az új felhasználó alapértelmezetten 'user' (Rank 10).""" - stmt = select(User).where(User.email == user_in.email) - result = await db.execute(stmt) - if result.scalar_one_or_none(): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Ez az e-mail cím már regisztrálva van." - ) - - try: - user = await AuthService.register_lite(db, user_in) - - # Kezdeti token generálása - token_data = { - "sub": str(user.id), - "role": "user", - "rank": 10, - "scope_level": "individual", - "scope_id": str(user.id) - } - - token = create_access_token(data=token_data) - return { - "access_token": token, - "token_type": "bearer", - "is_active": user.is_active - } - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Sikertelen regisztráció: {str(e)}" - ) +# --- GOOGLE OAUTH KONFIGURÁCIÓ --- +oauth = OAuth() +oauth.register( + name='google', + client_id=settings.GOOGLE_CLIENT_ID, + client_secret=settings.GOOGLE_CLIENT_SECRET, + server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', + client_kwargs={'scope': 'openid email profile'} +) -@router.post("/login", response_model=Token) -async def login( - db: AsyncSession = Depends(get_db), - form_data: OAuth2PasswordRequestForm = Depends() -): - """Bejelentkezés és okos JWT generálása RBAC adatokkal.""" - user = await AuthService.authenticate(db, form_data.username, form_data.password) - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Hibás e-mail cím vagy jelszó." - ) - - # Szerepkör string kinyerése és rang meghatározása a RANK_MAP-ből +# --- SOCIAL AUTH ENDPOINTS --- + +@router.get("/login/google") +async def login_google(request: Request): + """ + Step 1: Átirányítás a Google bejelentkező oldalára. + """ + redirect_uri = settings.GOOGLE_CALLBACK_URL + return await oauth.google.authorize_redirect(request, redirect_uri) + +@router.get("/callback/google") +async def auth_google(request: Request, db: AsyncSession = Depends(get_db)): + """ + Step 2: Google visszahívás lekezelése + Dupla Token generálás. + """ + try: + token = await oauth.google.authorize_access_token(request) + user_info = token.get('userinfo') + except Exception: + raise HTTPException(status_code=400, detail="Google hitelesítési hiba.") + + if not user_info: + raise HTTPException(status_code=400, detail="Nincs adat a Google-től.") + + # Step 1: Technikai user létrehozása/keresése (inaktív, nincs mappa) + user = await SocialAuthService.get_or_create_social_user( + db, provider="google", social_id=user_info['sub'], email=user_info['email'], + first_name=user_info.get('given_name'), last_name=user_info.get('family_name') + ) + + # Dinamikus token generálás + ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP) role_name = user.role.value if hasattr(user.role, 'value') else str(user.role) - user_rank = RANK_MAP.get(role_name, 10) + user_rank = ranks.get(role_name, 10) token_data = { "sub": str(user.id), @@ -76,48 +73,104 @@ async def login( "region": user.region_code } - token = create_access_token(data=token_data) + access, refresh = create_tokens(data=token_data) + + # Visszatérés a frontendre mindkét tokennel + response_url = f"{settings.FRONTEND_BASE_URL}/auth/callback?access={access}&refresh={refresh}" + return RedirectResponse(url=response_url) + + +# --- STANDARD AUTH ENDPOINTS --- + +@router.post("/register-lite", response_model=Token, status_code=status.HTTP_201_CREATED) +async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)): + """Step 1: Manuális regisztráció (inaktív, nincs mappa).""" + stmt = select(User).where(User.email == user_in.email) + if (await db.execute(stmt)).scalar_one_or_none(): + raise HTTPException(status_code=400, detail="Email már regisztrálva.") + + user = await AuthService.register_lite(db, user_in) + + ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP) + role_name = user.role.value if hasattr(user.role, 'value') else str(user.role) + + token_data = { + "sub": str(user.id), + "role": role_name, + "rank": ranks.get(role_name, 10), + "scope_level": "individual", + "scope_id": str(user.id), + "region": user.region_code + } + + access, refresh = create_tokens(data=token_data) return { - "access_token": token, + "access_token": access, + "refresh_token": refresh, + "token_type": "bearer", + "is_active": user.is_active + } + +@router.post("/login", response_model=Token) +async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()): + """Hagyományos belépés + Dupla Token.""" + user = await AuthService.authenticate(db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=401, detail="Hibás adatok.") + + ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP) + role_name = user.role.value if hasattr(user.role, 'value') else str(user.role) + user_rank = ranks.get(role_name, 10) + + token_data = { + "sub": str(user.id), + "role": role_name, + "rank": user_rank, + "scope_level": user.scope_level or "individual", + "scope_id": user.scope_id or str(user.id), + "region": user.region_code + } + + access, refresh = create_tokens(data=token_data) + return { + "access_token": access, + "refresh_token": refresh, "token_type": "bearer", "is_active": user.is_active } @router.get("/verify-email") async def verify_email(token: str, db: AsyncSession = Depends(get_db)): - """E-mail megerősítése.""" - success = await AuthService.verify_email(db, token) - if not success: - raise HTTPException(status_code=400, detail="Érvénytelen vagy lejárt token.") - return {"message": "Email sikeresen megerősítve!"} + if not await AuthService.verify_email(db, token): + raise HTTPException(status_code=400, detail="Érvénytelen token.") + return {"message": "Email megerősítve!"} @router.post("/complete-kyc") async def complete_kyc( kyc_in: UserKYCComplete, - db: AsyncSession = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): - """Step 2: KYC adatok rögzítése és aktiválás.""" + """ + Step 2: KYC Aktiválás. + Itt használjuk a get_current_user-t (nem active), mert a user még inaktív. + """ user = await AuthService.complete_kyc(db, current_user.id, kyc_in) if not user: - raise HTTPException(status_code=404, detail="Felhasználó nem található.") - return {"status": "success", "message": "A profil aktiválva."} + raise HTTPException(status_code=404, detail="User nem található.") + return {"status": "success", "message": "Fiók aktiválva."} @router.post("/forgot-password") async def forgot_password(req: PasswordResetRequest, db: AsyncSession = Depends(get_db)): - """Elfelejtett jelszó folyamat.""" result = await AuthService.initiate_password_reset(db, req.email) if result == "cooldown": - raise HTTPException(status_code=429, detail="Kérjük várjon 2 percet.") - return {"message": "Amennyiben a cím létezik, a linket kiküldtük."} + raise HTTPException(status_code=429, detail="Túl sok kérés.") + return {"message": "Visszaállító link kiküldve."} @router.post("/reset-password") async def reset_password(req: PasswordResetConfirm, db: AsyncSession = Depends(get_db)): - """Új jelszó beállítása.""" if req.password != req.password_confirm: - raise HTTPException(status_code=400, detail="A jelszavak nem egyeznek.") - - success = await AuthService.reset_password(db, req.email, req.token, req.password) - if not success: - raise HTTPException(status_code=400, detail="Hiba a jelszó frissítésekor.") - return {"message": "A jelszó sikeresen frissítve!"} \ No newline at end of file + raise HTTPException(status_code=400, detail="Nem egyeznek a jelszavak.") + if not await AuthService.reset_password(db, req.email, req.token, req.password): + raise HTTPException(status_code=400, detail="Sikertelen frissítés.") + return {"message": "Jelszó frissítve!"} \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/catalog.py b/backend/app/api/v1/endpoints/catalog.py index 1bd059d..f4e92c3 100644 --- a/backend/app/api/v1/endpoints/catalog.py +++ b/backend/app/api/v1/endpoints/catalog.py @@ -1,37 +1,46 @@ -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select, or_ from app.db.session import get_db -from app.models import AssetCatalog +from app.services.asset_service import AssetService from typing import List router = APIRouter() -@router.get("/search") -async def search_catalog( - q: str = Query(..., min_length=2, description="Márka vagy típus keresése"), - category: str = None, - db: AsyncSession = Depends(get_db) -): - """Keresés a Robot által feltöltött katalógusban.""" - stmt = select(VehicleCatalog).where( - or_( - VehicleCatalog.brand.ilike(f"%{q}%"), - VehicleCatalog.model.ilike(f"%{q}%") - ) - ) - if category: - stmt = stmt.where(VehicleCatalog.category == category) - - result = await db.execute(stmt.limit(20)) - items = result.scalars().all() +@router.get("/makes", response_model=List[str]) +async def list_makes(db: AsyncSession = Depends(get_db)): + """1. Szint: Márkák listázása.""" + return await AssetService.get_makes(db) + +@router.get("/models", response_model=List[str]) +async def list_models(make: str, db: AsyncSession = Depends(get_db)): + """2. Szint: Típusok listázása egy adott márkához.""" + models = await AssetService.get_models(db, make) + if not models: + raise HTTPException(status_code=404, detail="Márka nem található vagy nincsenek típusok.") + return models + +@router.get("/generations", response_model=List[str]) +async def list_generations(make: str, model: str, db: AsyncSession = Depends(get_db)): + """3. Szint: Generációk/Évjáratok listázása.""" + generations = await AssetService.get_generations(db, make, model) + if not generations: + raise HTTPException(status_code=404, detail="Nincs generációs adat ehhez a típushoz.") + return generations + +@router.get("/engines") +async def list_engines(make: str, model: str, gen: str, db: AsyncSession = Depends(get_db)): + """4. Szint: Motorváltozatok és technikai specifikációk.""" + engines = await AssetService.get_engines(db, make, model, gen) + if not engines: + raise HTTPException(status_code=404, detail="Nincs motorváltozat adat.") + # Itt visszaküldjük a teljes katalógus objektumokat (ID, motorváltozat, specifikációk) return [ { - "id": i.id, - "full_name": f"{i.brand} {i.model}", - "category": i.category, - "status": i.verification_status, - "specs": i.factory_specs - } for i in items + "id": e.id, + "variant": e.engine_variant, + "engine_code": e.engine_code, + "fuel_type": e.fuel_type, + "factory_data": e.factory_data + } for e in engines ] \ No newline at end of file diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc index 0da2b50..3d05279 100644 Binary files a/backend/app/core/__pycache__/config.cpython-312.pyc and b/backend/app/core/__pycache__/config.cpython-312.pyc differ diff --git a/backend/app/core/__pycache__/security.cpython-312.pyc b/backend/app/core/__pycache__/security.cpython-312.pyc index 45a8a51..69906b2 100644 Binary files a/backend/app/core/__pycache__/security.cpython-312.pyc and b/backend/app/core/__pycache__/security.cpython-312.pyc differ diff --git a/backend/app/core/config.py b/backend/app/core/config.py index e19274e..37a864f 100755 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -6,21 +6,21 @@ from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession class Settings(BaseSettings): - # --- Paths (ÚJ SZEKCIÓ) --- - # Meghatározzuk a projekt gyökérmappáját és a statikus fájlok helyét + # --- Paths --- BASE_DIR: Path = Path(__file__).resolve().parent.parent.parent STATIC_DIR: str = os.path.join(str(BASE_DIR), "static") # --- General --- - PROJECT_NAME: str = "Traffic Ecosystem SuperApp" - VERSION: str = "1.0.0" + PROJECT_NAME: str = "Service Finder Ecosystem" + VERSION: str = "2.1.0" API_V1_STR: str = "/api/v1" DEBUG: bool = False # --- Security / JWT --- SECRET_KEY: str = "NOT_SET_DANGER" ALGORITHM: str = "HS256" - ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 nap + ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 + REFRESH_TOKEN_EXPIRE_DAYS: int = 7 # --- Initial Admin --- INITIAL_ADMIN_EMAIL: str = "admin@servicefinder.hu" @@ -42,21 +42,35 @@ class Settings(BaseSettings): SMTP_PASSWORD: Optional[str] = None # --- External URLs --- - FRONTEND_BASE_URL: str = "http://localhost:3000" + FRONTEND_BASE_URL: str = "https://dev.profibot.hu" - # --- Dinamikus Admin Motor --- + # --- Google OAuth --- + GOOGLE_CLIENT_ID: str = "" + GOOGLE_CLIENT_SECRET: str = "" + GOOGLE_CALLBACK_URL: str = "https://dev.profibot.hu/api/v1/auth/callback/google" + + # --- Brute-Force & Security --- + LOGIN_RATE_LIMIT_ANON: str = "5/minute" + AUTH_MIN_PASSWORD_LENGTH: int = 8 + + # --- Dinamikus Admin Motor (Javított) --- async def get_db_setting(self, db: AsyncSession, key_name: str, default: Any = None) -> Any: + """ + Lekér egy beállítást a data.system_parameters táblából. + Ha a tábla még nem létezik (migráció előtt), elkapja a hibát és default-ot ad. + """ try: - query = text("SELECT value_json FROM data.system_settings WHERE key_name = :key") + # A lekérdezés a system_parameters táblát és a 'key' mezőt használja + query = text("SELECT value FROM data.system_parameters WHERE key = :key") result = await db.execute(query, {"key": key_name}) row = result.fetchone() if row and row[0] is not None: return row[0] return default except Exception: + # Adatbázis hiba vagy hiányzó tábla esetén fallback az alapértelmezett értékre return default - # .env fájl konfigurációja model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 755cd18..193dc15 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -1,65 +1,48 @@ +import secrets +import string from datetime import datetime, timedelta, timezone -from typing import Optional, Dict, Any +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 -# Master Book 5.0: RBAC Rank Definition Matrix -# Ezek a szintek határozzák meg a hozzáférést a Middleware szintjén. -RANK_MAP = { - "superadmin": 100, - "country_admin": 80, - "region_admin": 60, - "moderator": 40, - "sales": 20, - "user": 10, - "service": 15, - "fleet_manager": 25, - "driver": 5 +# Ezt az auth végpontokhoz adjuk hozzá: +# @router.post("/login", dependencies=[Depends(RateLimiter(times=5, seconds=60))]) + +DEFAULT_RANK_MAP = { + "superadmin": 100, "admin": 80, "fleet_manager": 25, + "service": 15, "user": 10, "driver": 5 } +def generate_secure_slug(length: int = 12) -> str: + """Biztonságos kód generálása (pl. mappákhoz).""" + alphabet = string.ascii_lowercase + string.digits + return ''.join(secrets.choice(alphabet) for _ in range(length)) + def verify_password(plain_password: str, hashed_password: str) -> bool: - """Összehasonlítja a sima szöveges jelszót a hash-elt változattal.""" - if not hashed_password: - return False + if not hashed_password: return False try: - return bcrypt.checkpw( - plain_password.encode("utf-8"), - hashed_password.encode("utf-8") - ) - except Exception: - return False + return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8")) + except Exception: return False def get_password_hash(password: str) -> str: - """Létrehozza a jelszó hash-elt változatát.""" - salt = bcrypt.gensalt() - return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8") + return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") -def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: - """ - Létrehozza a JWT access tokent bővített RBAC adatokkal. - Várt kulcsok: sub (user_id), role, rank, scope_level, scope_id - """ +def create_tokens(data: Dict[str, Any], access_delta: Optional[timedelta] = None, refresh_delta: Optional[timedelta] = None) -> Tuple[str, str]: + """Access és Refresh token generálása.""" to_encode = data.copy() - if expires_delta: - expire = datetime.now(timezone.utc) + expires_delta - else: - expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + now = datetime.now(timezone.utc) + acc_min = access_delta if access_delta else timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + access_payload = {**to_encode, "exp": now + acc_min, "iat": now, "type": "access", "iss": "service-finder-auth"} + access_token = jwt.encode(access_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM) - # Rendszer szintű metaadatok hozzáadása - to_encode.update({ - "exp": expire, - "iat": datetime.now(timezone.utc), - "iss": "service-finder-auth" - }) - - encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) - return encoded_jwt + ref_days = refresh_delta if refresh_delta else timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) + refresh_payload = {"sub": str(to_encode.get("sub")), "exp": now + ref_days, "iat": now, "type": "refresh"} + refresh_token = jwt.encode(refresh_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + return access_token, refresh_token def decode_token(token: str) -> Optional[Dict[str, Any]]: - """JWT token visszafejtése és validálása.""" - try: - payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) - return payload - except JWTError: - return None \ No newline at end of file + try: return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + except JWTError: return None \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index f8d9b89..9e1718f 100755 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,7 +2,8 @@ import os from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles -from app.api.v1.api import api_router # Ez már tartalmaz mindent (auth, services, stb.) +from starlette.middleware.sessions import SessionMiddleware # ÚJ +from app.api.v1.api import api_router from app.core.config import settings os.makedirs("static/previews", exist_ok=True) @@ -14,12 +15,19 @@ app = FastAPI( docs_url="/docs" ) +# --- SESSION MIDDLEWARE (Google Authhoz kötelező) --- +app.add_middleware( + SessionMiddleware, + secret_key=settings.SECRET_KEY +) + app.add_middleware( CORSMiddleware, allow_origins=[ "http://192.168.100.10:3001", "http://localhost:3001", - "https://dev.profibot.hu" + "https://dev.profibot.hu", + "https://app.profibot.hu" ], allow_credentials=True, allow_methods=["*"], @@ -27,8 +35,6 @@ app.add_middleware( ) app.mount("/static", StaticFiles(directory="static"), name="static") - -# CSAK EZT AZ EGYET KELL BEKÖTNI: app.include_router(api_router, prefix="/api/v1") @app.get("/") @@ -36,5 +42,5 @@ async def root(): return { "status": "online", "message": "Service Finder Master System v2.0", - "features": ["Document Engine", "Asset Vault", "Org Onboarding", "Service Hunt"] + "features": ["Google Auth Enabled", "Asset Vault", "Org Onboarding"] } \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index aa41faa..da43d41 100755 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,34 +1,54 @@ # /opt/docker/dev/service_finder/backend/app/models/__init__.py from app.db.base_class import Base -from .identity import User, Person, Wallet, UserRole, VerificationToken +# Identitás és Jogosultság +from .identity import User, Person, Wallet, UserRole, VerificationToken, SocialAccount + +# Szervezeti struktúra from .organization import Organization, OrganizationMember + +# Járművek és Eszközök (Digital Twin) from .asset import ( Asset, AssetCatalog, AssetCost, AssetEvent, AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate ) + +# Szerviz és Szakértelem (ÚJ) +from .service import ServiceProfile, ExpertiseTag, ServiceExpertise + +# Földrajzi adatok és Címek from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType + +# Gamification és Economy from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger + +# Rendszerkonfiguráció és Alapok from .system_config import SystemParameter from .document import Document -from .translation import Translation # <--- HOZZÁADVA -from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty -from .history import AuditLog, VehicleOwnership -from .security import PendingAction # <--- HOZZÁADVA +from .translation import Translation -# Aliasok +# Üzleti logika és Előfizetés +from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty + +# Naplózás és Biztonság +from .history import AuditLog, VehicleOwnership +from .security import PendingAction + +# Aliasok a kényelmesebb fejlesztéshez Vehicle = Asset UserVehicle = Asset VehicleCatalog = AssetCatalog ServiceRecord = AssetEvent __all__ = [ - "Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", - "Organization", "OrganizationMember", "Asset", "AssetCatalog", "AssetCost", - "AssetEvent", "AssetFinancials", "AssetTelemetry", "AssetReview", "ExchangeRate", - "Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "PointRule", - "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger", - "SystemParameter", "Document", "Translation", "PendingAction", # <--- BŐVÍTVE + "Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount", + "Organization", "OrganizationMember", + "Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials", + "AssetTelemetry", "AssetReview", "ExchangeRate", + "ServiceProfile", "ExpertiseTag", "ServiceExpertise", # <--- HOZZÁADVA + "Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", + "PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger", + "SystemParameter", "Document", "Translation", "PendingAction", "SubscriptionTier", "OrganizationSubscription", "CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership", "Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord" diff --git a/backend/app/models/__pycache__/asset.cpython-312.pyc b/backend/app/models/__pycache__/asset.cpython-312.pyc index 5e69cee..6ebea6c 100644 Binary files a/backend/app/models/__pycache__/asset.cpython-312.pyc and b/backend/app/models/__pycache__/asset.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/identity.cpython-312.pyc b/backend/app/models/__pycache__/identity.cpython-312.pyc index 4e6e0e8..765bf72 100644 Binary files a/backend/app/models/__pycache__/identity.cpython-312.pyc and b/backend/app/models/__pycache__/identity.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/organization.cpython-312.pyc b/backend/app/models/__pycache__/organization.cpython-312.pyc index e92395d..7b5c6d5 100644 Binary files a/backend/app/models/__pycache__/organization.cpython-312.pyc and b/backend/app/models/__pycache__/organization.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/system_config.cpython-312.pyc b/backend/app/models/__pycache__/system_config.cpython-312.pyc index b868ff4..45ed76b 100644 Binary files a/backend/app/models/__pycache__/system_config.cpython-312.pyc and b/backend/app/models/__pycache__/system_config.cpython-312.pyc differ diff --git a/backend/app/models/asset.py b/backend/app/models/asset.py index 93b7dea..11fd2dc 100644 --- a/backend/app/models/asset.py +++ b/backend/app/models/asset.py @@ -6,42 +6,57 @@ 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"} + id = Column(Integer, primary_key=True, index=True) - make = Column(String, index=True, nullable=False) - model = Column(String, index=True, nullable=False) - generation = Column(String) + 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) + year_from = Column(Integer) year_to = Column(Integer) vehicle_class = Column(String) fuel_type = Column(String) engine_code = Column(String) - factory_data = Column(JSON, server_default=text("'{}'::jsonb")) + factory_data = Column(JSON, server_default=text("'{}'::jsonb")) # Technikai specifikációk + 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") + 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) telemetry = relationship("AssetTelemetry", back_populates="asset", uselist=False) assignments = relationship("AssetAssignment", back_populates="asset") 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" @@ -77,9 +92,10 @@ class AssetReview(Base): created_at = Column(DateTime(timezone=True), server_default=func.now()) asset = relationship("Asset", back_populates="reviews") - user = relationship("User") # <--- JAVÍTÁS: Hozzáadva + 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) @@ -90,7 +106,7 @@ class AssetAssignment(Base): status = Column(String(30), default="active") asset = relationship("Asset", back_populates="assignments") - organization = relationship("Organization") # <--- KRITIKUS JAVÍTÁS: Ez okozta a login hibát + organization = relationship("Organization") class AssetEvent(Base): __tablename__ = "asset_events" @@ -113,16 +129,13 @@ 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")) asset = relationship("Asset", back_populates="costs") - organization = relationship("Organization") # <--- JAVÍTÁS: Hozzáadva - driver = relationship("User") # <--- JAVÍTÁS: Hozzáadva + organization = relationship("Organization") + driver = relationship("User") class ExchangeRate(Base): __tablename__ = "exchange_rates" @@ -131,5 +144,4 @@ class ExchangeRate(Base): base_currency = Column(String(3), default="EUR") target_currency = Column(String(3), unique=True) rate = Column(Numeric(18, 6), nullable=False) - rate_date = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file diff --git a/backend/app/models/audit.py b/backend/app/models/audit.py new file mode 100644 index 0000000..eb0c53c --- /dev/null +++ b/backend/app/models/audit.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey, text +from sqlalchemy.sql import func +from app.db.base_class import Base + +class AuditLog(Base): + __tablename__ = "audit_logs" + __table_args__ = {"schema": "data"} + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("data.users.id", ondelete="SET NULL"), nullable=True) + action = Column(String(100), nullable=False) # pl. "LOGIN", "REGISTER", "DELETE_ASSET" + resource_type = Column(String(50)) # pl. "User", "Asset", "Organization" + resource_id = Column(String(100)) + details = Column(JSON, server_default=text("'{}'::jsonb")) + ip_address = Column(String(45)) + created_at = Column(DateTime(timezone=True), server_default=func.now()) \ No newline at end of file diff --git a/backend/app/models/identity.py b/backend/app/models/identity.py index a6f2ac4..7409acf 100644 --- a/backend/app/models/identity.py +++ b/backend/app/models/identity.py @@ -1,85 +1,68 @@ import uuid import enum -from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger, UniqueConstraint from sqlalchemy.orm import relationship from sqlalchemy.dialects.postgresql import UUID as PG_UUID 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"} - + __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) - + 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) identity_docs = Column(JSON, server_default=text("'{}'::jsonb")) + ice_contact = Column(JSON, server_default=text("'{}'::jsonb")) is_active = Column(Boolean, default=False, nullable=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) - users = relationship("User", 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) - - # ÚJ MEZŐK HOZZÁADVA: - preferred_language = Column(String(5), server_default="hu") - region_code = Column(String(5), server_default="HU") - - # RBAC & SCOPE - scope_level = Column(String(30), server_default="individual") - scope_id = Column(String(50)) - custom_permissions = Column(JSON, server_default=text("'{}'::jsonb")) - + 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")) 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") - -# A Wallet és VerificationToken osztályok maradnak változatlanok... class Wallet(Base): - __tablename__ = "wallets" - __table_args__ = {"schema": "data"} + __tablename__ = "wallets"; __table_args__ = {"schema": "data"} id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("data.users.id"), unique=True) - coin_balance = Column(Numeric(18, 2), default=0.00) - credit_balance = Column(Numeric(18, 2), default=0.00) - currency = Column(String(3), default="HUF") + coin_balance = Column(Numeric(18, 2), default=0.00); credit_balance = Column(Numeric(18, 2), default=0.00); currency = Column(String(3), default="HUF") user = relationship("User", back_populates="wallet") class VerificationToken(Base): - __tablename__ = "verification_tokens" - __table_args__ = {"schema": "data"} + __tablename__ = "verification_tokens"; __table_args__ = {"schema": "data"} id = Column(Integer, primary_key=True, index=True) token = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False) user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False) - token_type = Column(String(20), nullable=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - expires_at = Column(DateTime(timezone=True), nullable=False) - is_used = Column(Boolean, default=False) \ No newline at end of file + token_type = Column(String(20), nullable=False); created_at = Column(DateTime(timezone=True), server_default=func.now()) + expires_at = Column(DateTime(timezone=True), nullable=False); is_used = Column(Boolean, default=False) + +class SocialAccount(Base): + __tablename__ = "social_accounts" + __table_args__ = (UniqueConstraint('provider', 'social_id', name='uix_social_provider_id'), {"schema": "data"}) + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False) + provider = Column(String(50), nullable=False); social_id = Column(String(255), nullable=False, index=True); email = Column(String(255), nullable=False) + extra_data = Column(JSON, server_default=text("'{}'::jsonb")); created_at = Column(DateTime(timezone=True), server_default=func.now()) + user = relationship("User", back_populates="social_accounts") \ No newline at end of file diff --git a/backend/app/models/organization.py b/backend/app/models/organization.py index 6279661..30eeb72 100755 --- a/backend/app/models/organization.py +++ b/backend/app/models/organization.py @@ -1,5 +1,4 @@ import enum -import uuid from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM from sqlalchemy.orm import relationship @@ -25,6 +24,9 @@ class Organization(Base): full_name = Column(String, nullable=False) name = Column(String, nullable=False) 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") @@ -63,7 +65,7 @@ class Organization(Base): created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) - # String alapú hivatkozás a körkörös import ellen + # Kapcsolatok 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") @@ -75,8 +77,7 @@ class OrganizationMember(Base): 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") - permissions = Column(JSON, server_default=text("'{}'::jsonb")) organization = relationship("Organization", back_populates="members") - user = relationship("User") # Egyszerűsített string hivatkozás \ No newline at end of file + user = relationship("User") \ No newline at end of file diff --git a/backend/app/models/service.py b/backend/app/models/service.py new file mode 100644 index 0000000..7fa46a4 --- /dev/null +++ b/backend/app/models/service.py @@ -0,0 +1,59 @@ +import uuid +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Text +from sqlalchemy.orm import relationship +from sqlalchemy.dialects.postgresql import UUID as PG_UUID +from geoalchemy2 import Geometry # PostGIS támogatás +from sqlalchemy.sql import func +from app.db.base_class import Base + +class ServiceProfile(Base): + """ + Szerviz szolgáltató kiterjesztett adatai. + Egy Organization-höz (org_type='service') kapcsolódik. + """ + __tablename__ = "service_profiles" + __table_args__ = {"schema": "data"} + + id = Column(Integer, primary_key=True, index=True) + organization_id = Column(Integer, ForeignKey("data.organizations.id"), unique=True) + + # PostGIS GPS pont (SRID 4326 = WGS84 koordináták) + location = Column(Geometry(geometry_type='POINT', srid=4326), index=True) + + # Trust Engine (Bot Discovery=30, User Entry=50, Admin/Partner=100) + trust_score = Column(Integer, default=30) + is_verified = Column(Boolean, default=False) + verification_log = Column(JSON, server_default=text("'{}'::jsonb")) + + opening_hours = Column(JSON, server_default=text("'{}'::jsonb")) + contact_phone = Column(String) + website = Column(String) + bio = Column(Text) + + # Kapcsolatok + organization = relationship("Organization") + expertises = relationship("ServiceExpertise", back_populates="service") + +class ExpertiseTag(Base): + """Szakmai szempontok taxonómiája.""" + __tablename__ = "expertise_tags" + __table_args__ = {"schema": "data"} + + id = Column(Integer, primary_key=True) + key = Column(String(50), unique=True, index=True) # pl. 'bmw_gs_specialist' + name_hu = Column(String(100)) + category = Column(String(30)) # 'repair', 'fuel', 'food', 'emergency' + +class ServiceExpertise(Base): + """Kapcsolótábla a szerviz és a szakterület között.""" + __tablename__ = "service_expertises" + __table_args__ = {"schema": "data"} + + service_id = Column(Integer, ForeignKey("data.service_profiles.id"), primary_key=True) + expertise_id = Column(Integer, ForeignKey("data.expertise_tags.id"), primary_key=True) + + # Validációs szint (0-100% - Mennyire hiteles ez a szakértelem) + validation_level = Column(Integer, default=0) + + service = relationship("ServiceProfile", back_populates="expertises") + expertise = relationship("ExpertiseTag") \ No newline at end of file diff --git a/backend/app/models/system_config.py b/backend/app/models/system_config.py index 9a1be7c..dbfa219 100644 --- a/backend/app/models/system_config.py +++ b/backend/app/models/system_config.py @@ -1,16 +1,18 @@ -from sqlalchemy import Column, String, JSON, Integer, Boolean, DateTime, func +from sqlalchemy import Column, String, JSON, Boolean, DateTime, Integer, text +from sqlalchemy.sql import func from app.db.base_class import Base class SystemParameter(Base): """ - Globális rendszerbeállítások (A meglévő data.system_parameters tábla alapján). + Rendszerszintű dinamikus paraméterek tárolása. + Szinkronban az admin.py és config.py elvárásaival. """ __tablename__ = "system_parameters" __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True, index=True) - key = Column(String(50), unique=True, index=True, nullable=False) - value = Column(JSON, nullable=False) + # Az admin.py 'key' mezőt vár, nem 'key_name'-et! + key = Column(String(50), primary_key=True, index=True) + value = Column(JSON, server_default=text("'{}'::jsonb"), nullable=False) + description = Column(String(255), nullable=True) is_active = Column(Boolean, default=True) - description = Column(String, nullable=True) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) \ No newline at end of file diff --git a/backend/app/services/__pycache__/auth_service.cpython-312.pyc b/backend/app/services/__pycache__/auth_service.cpython-312.pyc index 2ed4e66..31580d8 100644 Binary files a/backend/app/services/__pycache__/auth_service.cpython-312.pyc and b/backend/app/services/__pycache__/auth_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/social_auth_service.cpython-312.pyc b/backend/app/services/__pycache__/social_auth_service.cpython-312.pyc new file mode 100644 index 0000000..a81518e Binary files /dev/null and b/backend/app/services/__pycache__/social_auth_service.cpython-312.pyc differ diff --git a/backend/app/services/asset_service.py b/backend/app/services/asset_service.py index ac1b2af..1a9039a 100644 --- a/backend/app/services/asset_service.py +++ b/backend/app/services/asset_service.py @@ -1,35 +1,99 @@ from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select -from app.models.asset import Asset, AssetTelemetry, AssetFinancials +from sqlalchemy import select, distinct +from app.models.asset import Asset, AssetCatalog, AssetTelemetry, AssetFinancials, AssetAssignment from app.models.gamification import UserStats, PointRule -import uuid +import logging -async def create_new_vehicle(db: AsyncSession, user_id: int, vin: str, license_plate: str): - # 1. Alap Asset létrehozása - new_asset = Asset( - vin=vin, - license_plate=license_plate, - name=f"Teszt Autó ({license_plate})" - ) - db.add(new_asset) - await db.flush() # Hogy legyen ID-ja +logger = logging.getLogger(__name__) - # 2. Modulok inicializálása (Digital Twin alapozás) - db.add(AssetTelemetry(asset_id=new_asset.id, current_mileage=0)) - db.add(AssetFinancials(asset_id=new_asset.id)) +class AssetService: + @staticmethod + async def get_makes(db: AsyncSession): + """1. Szint: Márkák lekérdezése (pl. Audi, BMW).""" + stmt = select(distinct(AssetCatalog.make)).order_by(AssetCatalog.make) + result = await db.execute(stmt) + return result.scalars().all() - # 3. GAMIFICATION: Pontszerzés (ASSET_REGISTER = 100 XP) - # Megkeressük a szabályt - rule_stmt = select(PointRule).where(PointRule.action_key == "ASSET_REGISTER") - rule = (await db.execute(rule_stmt)).scalar_one_or_none() - - if rule: - # Frissítjük a felhasználó XP-jét - stats_stmt = select(UserStats).where(UserStats.user_id == user_id) - stats = (await db.execute(stats_stmt)).scalar_one_or_none() - if stats: - stats.total_xp += rule.points - # Itt később jöhet a szintlépés ellenőrzése is! + @staticmethod + async def get_models(db: AsyncSession, make: str): + """2. Szint: Típusok szűrése márka alapján (pl. A4, A6).""" + stmt = select(distinct(AssetCatalog.model)).where(AssetCatalog.make == make).order_by(AssetCatalog.model) + result = await db.execute(stmt) + return result.scalars().all() - await db.commit() - return new_asset \ No newline at end of file + @staticmethod + async def get_generations(db: AsyncSession, make: str, model: str): + """3. Szint: Generációk/Évjáratok (pl. B8 (2008-2015)).""" + stmt = select(distinct(AssetCatalog.generation)).where( + AssetCatalog.make == make, + AssetCatalog.model == model + ).order_by(AssetCatalog.generation) + result = await db.execute(stmt) + return result.scalars().all() + + @staticmethod + async def get_engines(db: AsyncSession, make: str, model: str, generation: str): + """4. Szint: Motorváltozatok (pl. 2.0 TDI).""" + stmt = select(AssetCatalog).where( + AssetCatalog.make == make, + AssetCatalog.model == model, + AssetCatalog.generation == generation + ).order_by(AssetCatalog.engine_variant) + result = await db.execute(stmt) + return result.scalars().all() + + @staticmethod + async def create_and_assign_vehicle( + db: AsyncSession, + user_id: int, + org_id: int, + vin: str, + license_plate: str, + catalog_id: int = None + ): + """Jármű rögzítése, flottához rendelése és XP jóváírás (Atomic).""" + try: + # 1. Asset létrehozása közvetlen flotta-kötéssel + new_asset = Asset( + vin=vin, + license_plate=license_plate, + catalog_id=catalog_id, + current_organization_id=org_id, # Izolációs pointer + status="active", + is_verified=False + ) + db.add(new_asset) + await db.flush() + + # 2. Digitális Iker történetiség (Assignment) + assignment = AssetAssignment( + asset_id=new_asset.id, + organization_id=org_id, + status="active" + ) + db.add(assignment) + + # 3. Digitális Iker modulok indítása + db.add(AssetTelemetry(asset_id=new_asset.id)) + db.add(AssetFinancials(asset_id=new_asset.id)) + + # 4. GAMIFICATION: XP jóváírás + rule_stmt = select(PointRule).where(PointRule.action_key == "ASSET_REGISTER") + rule = (await db.execute(rule_stmt)).scalar_one_or_none() + + if rule: + stats_stmt = select(UserStats).where(UserStats.user_id == user_id) + stats = (await db.execute(stats_stmt)).scalar_one_or_none() + if stats: + stats.total_xp += rule.points + logger.info(f"User {user_id} awarded {rule.points} XP for asset registration.") + + # 5. Robot Scout Trigger (későbbi implementáció) + # await RobotScout.trigger_vin_lookup(db, new_asset.id) + + await db.commit() + return new_asset + except Exception as e: + await db.rollback() + logger.error(f"Asset Creation Error: {str(e)}") + raise e \ No newline at end of file diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index f67a5a4..f2287b4 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -9,12 +9,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, and_ from sqlalchemy.orm import joinedload from fastapi.encoders import jsonable_encoder +from fastapi import HTTPException, status from app.models.identity import User, Person, UserRole, VerificationToken, Wallet from app.models.gamification import UserStats from app.models.organization import Organization, OrganizationMember, OrgType from app.schemas.auth import UserLiteRegister, UserKYCComplete -from app.core.security import get_password_hash, verify_password +from app.core.security import get_password_hash, verify_password, generate_secure_slug from app.services.email_manager import email_manager from app.core.config import settings from app.services.config_service import config @@ -26,8 +27,23 @@ logger = logging.getLogger(__name__) class AuthService: @staticmethod async def register_lite(db: AsyncSession, user_in: UserLiteRegister): - """Step 1: Lite Regisztráció.""" + """ + Step 1: Lite Regisztráció (Manuális). + Létrehozza a Person és User rekordokat, de a fiók inaktív marad. + A folder_slug itt még NEM generálódik le! + """ try: + # --- Dinamikus jelszóhossz ellenőrzés --- + # Lekérjük az admin beállítást, minimum 8 karakter a hard limit. + min_pass = await config.get_setting(db, "auth_min_password_length", default=8) + min_len = max(int(min_pass), 8) + + if len(user_in.password) < min_len: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"A jelszónak legalább {min_len} karakter hosszúnak kell lennie." + ) + new_person = Person( first_name=user_in.first_name, last_name=user_in.last_name, @@ -46,11 +62,13 @@ class AuthService: region_code=user_in.region_code, preferred_language=user_in.lang, timezone=user_in.timezone + # folder_slug marad NULL a Step 2-ig ) db.add(new_user) await db.flush() - reg_hours = await config.get_setting("auth_registration_hours", region_code=user_in.region_code, default=48) + # Verifikációs token generálása + reg_hours = await config.get_setting(db, "auth_registration_hours", region_code=user_in.region_code, default=48) token_val = uuid.uuid4() db.add(VerificationToken( token=token_val, @@ -59,6 +77,7 @@ class AuthService: expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_hours)) )) + # Email kiküldése verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}" await email_manager.send_email( recipient=user_in.email, @@ -67,9 +86,23 @@ class AuthService: lang=user_in.lang ) + # Audit log a regisztrációról + await security_service.log_event( + db, + user_id=new_user.id, + action="USER_REGISTER_LITE", + severity="info", + target_type="User", + target_id=str(new_user.id), + new_data={"email": user_in.email, "method": "manual"} + ) + await db.commit() await db.refresh(new_user) return new_user + except HTTPException: + await db.rollback() + raise except Exception as e: await db.rollback() logger.error(f"Registration Error: {str(e)}") @@ -77,16 +110,27 @@ class AuthService: @staticmethod async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete): - """1.3. Fázis: Atomi Tranzakció & Shadow Identity.""" + """ + Step 2: Atomi Tranzakció. + Itt dől el minden: Adatok rögzítése, Shadow Identity ellenőrzés, + Flotta és Wallet létrehozás, majd a fiók aktiválása. + """ try: stmt = select(User).options(joinedload(User.person)).where(User.id == user_id) res = await db.execute(stmt) user = res.scalar_one_or_none() if not user: return None + # --- 1. BIZTONSÁG: User folder_slug generálása --- + # Ha Google-lel jött vagy még nincs slugja, most kap egyet. + if not user.folder_slug: + user.folder_slug = generate_secure_slug(length=12) + + # Pénznem beállítása if hasattr(kyc_in, 'preferred_currency') and kyc_in.preferred_currency: user.preferred_currency = kyc_in.preferred_currency + # --- 2. Shadow Identity keresése (Már létezik-e ez a fizikai személy?) --- identity_stmt = select(Person).where(and_( Person.mothers_last_name == kyc_in.mothers_last_name, Person.mothers_first_name == kyc_in.mothers_first_name, @@ -96,12 +140,15 @@ class AuthService: existing_person = (await db.execute(identity_stmt)).scalar_one_or_none() if existing_person: + # Ha találtunk egyezést, összekötjük a User-t a meglévő Person-nel user.person_id = existing_person.id active_person = existing_person logger.info(f"Shadow Identity linked: User {user_id} -> Person {existing_person.id}") else: + # Ha nem, a saját (regisztrációkor létrehozott) Person-t töltjük fel active_person = user.person + # --- 3. Cím rögzítése GeoService segítségével --- addr_id = await GeoService.get_or_create_full_address( db, zip_code=kyc_in.address_zip, @@ -112,31 +159,40 @@ class AuthService: parcel_id=kyc_in.address_hrsz ) + # --- 4. Személyes adatok frissítése --- active_person.mothers_last_name = kyc_in.mothers_last_name active_person.mothers_first_name = kyc_in.mothers_first_name active_person.birth_place = kyc_in.birth_place active_person.birth_date = kyc_in.birth_date active_person.phone = kyc_in.phone_number active_person.address_id = addr_id + + # Dokumentumok és ICE kontakt mentése JSON-ként active_person.identity_docs = jsonable_encoder(kyc_in.identity_docs) active_person.ice_contact = jsonable_encoder(kyc_in.ice_contact) + + # A Person most válik aktívvá active_person.is_active = True + # --- 5. EGYÉNI FLOTTA LÉTREHOZÁSA (A KYC szerves része) --- + # Itt generáljuk a flotta mappáját is (folder_slug) new_org = Organization( full_name=f"{active_person.last_name} {active_person.first_name} Egyéni Flotta", name=f"{active_person.last_name} Flotta", + folder_slug=generate_secure_slug(length=12), # FLOTTA SLUG org_type=OrgType.individual, owner_id=user.id, is_transferable=False, is_active=True, status="verified", language=user.preferred_language, - default_currency=user.preferred_currency, + default_currency=user.preferred_currency or "HUF", country_code=user.region_code ) db.add(new_org) await db.flush() + # Flotta tagság (Owner) db.add(OrganizationMember( organization_id=new_org.id, user_id=user.id, @@ -144,15 +200,33 @@ class AuthService: permissions={"can_add_asset": True, "can_view_costs": True, "is_admin": True} )) + # --- 6. PÉNZTÁRCA ÉS GAMIFICATION LÉTREHOZÁSA --- db.add(Wallet( user_id=user.id, coin_balance=0, credit_balance=0, - currency=user.preferred_currency + currency=user.preferred_currency or "HUF" )) db.add(UserStats(user_id=user.id, total_xp=0, current_level=1)) + # --- 7. AKTIVÁLÁS ÉS AUDIT --- user.is_active = True + + await security_service.log_event( + db, + user_id=user.id, + action="USER_KYC_COMPLETED", + severity="info", + target_type="User", + target_id=str(user.id), + new_data={ + "status": "active", + "user_folder": user.folder_slug, + "organization_id": new_org.id, + "organization_folder": new_org.folder_slug, + "wallet_created": True + } + ) await db.commit() await db.refresh(user) @@ -165,8 +239,7 @@ class AuthService: @staticmethod async def soft_delete_user(db: AsyncSession, user_id: int, reason: str, actor_id: int): """ - Step 2 utáni Soft-Delete: Email felszabadítás és izoláció. - Az email átnevezésre kerül, így az eredeti cím újra regisztrálható 'tiszta lappal'. + Soft-Delete: Email felszabadítás és izoláció. """ stmt = select(User).where(User.id == user_id) user = (await db.execute(stmt)).scalar_one_or_none() @@ -175,12 +248,11 @@ class AuthService: return False old_email = user.email - # Email felszabadítása: deleted_ID_TIMESTAMP_EMAIL formátumban + # Email átnevezése az egyediség megőrzése érdekében (újraregisztrációhoz) user.email = f"deleted_{user.id}_{datetime.now().strftime('%Y%m%d')}_{old_email}" user.is_deleted = True user.is_active = False - # Sentinel AuditLog bejegyzés await security_service.log_event( db, user_id=actor_id, @@ -231,7 +303,7 @@ class AuthService: user = (await db.execute(stmt)).scalar_one_or_none() if user: - reset_hours = await config.get_setting("auth_password_reset_hours", region_code=user.region_code, default=2) + reset_hours = await config.get_setting(db, "auth_password_reset_hours", region_code=user.region_code, default=2) token_val = uuid.uuid4() db.add(VerificationToken( token=token_val, diff --git a/backend/app/services/search_service.py b/backend/app/services/search_service.py new file mode 100644 index 0000000..80019f4 --- /dev/null +++ b/backend/app/services/search_service.py @@ -0,0 +1,61 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func +from app.models.service import ServiceProfile, ExpertiseTag, ServiceExpertise +from app.models.organization import Organization +from geoalchemy2.functions import ST_Distance, ST_MakePoint + +class SearchService: + @staticmethod + async def find_nearby_services( + db: AsyncSession, + lat: float, + lon: float, + expertise_key: str = None, + radius_km: int = 50, + is_premium: bool = False + ): + """ + Keresés távolság és szakértelem alapján. + Premium: Trust Score + Valós távolság. + Free: Trust Score + Légvonal. + """ + user_point = ST_MakePoint(lon, lat) # PostGIS pont létrehozása + + # Alap lekérdezés: ServiceProfile + Organization adatok + stmt = select(ServiceProfile, Organization).join( + Organization, ServiceProfile.organization_id == Organization.id + ) + + # 1. Sugár alapú szűrés (radius_km * 1000 méter) + stmt = stmt.where( + func.ST_DWithin(ServiceProfile.location, user_point, radius_km * 1000) + ) + + # 2. Szakterület szűrése + if expertise_key: + stmt = stmt.join(ServiceProfile.expertises).join(ExpertiseTag).where( + ExpertiseTag.key == expertise_key + ) + + # 3. Távolság és Trust Score alapú sorrend + # A ST_Distance méterben adja vissza az eredményt + stmt = stmt.order_by(ST_Distance(ServiceProfile.location, user_point)) + + result = await db.execute(stmt.limit(50)) + rows = result.all() + + # Rangsorolási logika alkalmazása + results = [] + for s_prof, org in rows: + results.append({ + "id": org.id, + "name": org.full_name, + "trust_score": s_prof.trust_score, + "is_verified": s_prof.is_verified, + "phone": s_prof.contact_phone, + "website": s_prof.website, + "is_premium_partner": s_prof.trust_score >= 90 + }) + + # Súlyozott rendezés: Prémium partnerek és Trust Score előre + return sorted(results, key=lambda x: (not is_premium, -x['trust_score'])) \ No newline at end of file diff --git a/backend/app/services/social_auth_service.py b/backend/app/services/social_auth_service.py new file mode 100644 index 0000000..862dbb8 --- /dev/null +++ b/backend/app/services/social_auth_service.py @@ -0,0 +1,92 @@ +import uuid +import logging +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from app.models.identity import User, Person, SocialAccount, UserRole +from app.services.security_service import security_service + +logger = logging.getLogger(__name__) + +class SocialAuthService: + @staticmethod + async def get_or_create_social_user( + db: AsyncSession, + provider: str, + social_id: str, + email: str, + first_name: str, + last_name: str + ): + """ + Social Step 1: Csak alapregisztráció. + Nincs slug generálás, nincs flotta. Megáll a KYC kapujában. + """ + # 1. Meglévő Social kapcsolat ellenőrzése + stmt = select(SocialAccount).where( + SocialAccount.provider == provider, + SocialAccount.social_id == social_id + ) + result = await db.execute(stmt) + social_acc = result.scalar_one_or_none() + + if social_acc: + stmt = select(User).where(User.id == social_acc.user_id) + user_result = await db.execute(stmt) + return user_result.scalar_one_or_none() + + # 2. Felhasználó keresése email alapján + stmt = select(User).where(User.email == email) + user_result = await db.execute(stmt) + user = user_result.scalar_one_or_none() + + if not user: + try: + # Person rekord létrehozása a Google-től kapott nevekkel + new_person = Person( + id_uuid=uuid.uuid4(), + first_name=first_name or "Google", + last_name=last_name or "User", + is_active=False + ) + db.add(new_person) + await db.flush() + + # User rekord (folder_slug nélkül!) + user = User( + email=email, + hashed_password=None, + person_id=new_person.id, + role=UserRole.user, + is_active=False, + is_deleted=False, + preferred_language="hu", + region_code="HU" + ) + db.add(user) + await db.flush() + + await security_service.log_event( + db, + user_id=user.id, + action="USER_REGISTER_SOCIAL", + severity="info", + target_type="User", + target_id=str(user.id), + new_data={"email": email, "provider": provider} + ) + except Exception as e: + await db.rollback() + logger.error(f"Social Registration Error: {str(e)}") + raise e + + # 3. Összekötés + new_social = SocialAccount( + user_id=user.id, + provider=provider, + social_id=social_id, + email=email + ) + db.add(new_social) + await db.commit() + await db.refresh(user) + return user \ No newline at end of file diff --git a/backend/app/workers/catalog_filler.py b/backend/app/workers/catalog_filler.py new file mode 100644 index 0000000..e886db4 --- /dev/null +++ b/backend/app/workers/catalog_filler.py @@ -0,0 +1,35 @@ +# app/workers/catalog_filler.py +import asyncio +from sqlalchemy.ext.asyncio import AsyncSession +from app.db.session import SessionLocal +from app.models.asset import AssetCatalog +from sqlalchemy import select + +class CatalogFiller: + @staticmethod + async def seed_initial_data(): + """Alapértelmezett márkák és típusok feltöltése (Példa).""" + initial_data = [ + {"make": "Audi", "model": "A4", "generation": "B8 (2008-2015)", "engine_variant": "2.0 TDI (150 LE)", "fuel_type": "Diesel"}, + {"make": "BMW", "model": "3 Series", "generation": "F30 (2012-2019)", "engine_variant": "320d (190 LE)", "fuel_type": "Diesel"}, + {"make": "Volkswagen", "model": "Passat", "generation": "B8 (2014-)", "engine_variant": "2.0 TDI (150 LE)", "fuel_type": "Diesel"} + ] + + async with SessionLocal() as db: + for item in initial_data: + # Ellenőrizzük, létezik-e már + stmt = select(AssetCatalog).where( + AssetCatalog.make == item["make"], + AssetCatalog.model == item["model"], + AssetCatalog.engine_variant == item["engine_variant"] + ) + exists = (await db.execute(stmt)).scalar_one_or_none() + + if not exists: + db.add(AssetCatalog(**item)) + + await db.commit() + print("Catalog seeding complete.") + +if __name__ == "__main__": + asyncio.run(CatalogFiller.seed_initial_data()) \ No newline at end of file diff --git a/backend/app/workers/catalog_robot.py b/backend/app/workers/catalog_robot.py new file mode 100644 index 0000000..45a65e1 --- /dev/null +++ b/backend/app/workers/catalog_robot.py @@ -0,0 +1,60 @@ +import asyncio +import logging +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from app.db.session import SessionLocal +from app.models.asset import AssetCatalog + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("Robot1-Catalog") + +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. + """ + + @staticmethod + async def get_initial_hu_data(): + """ + Kezdeti adathalmaz (Példa). + Élesben itt egy külső API vagy CSV feldolgozás helye van. + """ + 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"} + ] + + @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 + + 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.") + +if __name__ == "__main__": + asyncio.run(CatalogScout.run()) \ No newline at end of file diff --git a/backend/migrations/versions/0fa011f29e35_enforce_system_parameters_primary_key.py b/backend/migrations/versions/0fa011f29e35_enforce_system_parameters_primary_key.py new file mode 100644 index 0000000..653b687 --- /dev/null +++ b/backend/migrations/versions/0fa011f29e35_enforce_system_parameters_primary_key.py @@ -0,0 +1,186 @@ +"""enforce_system_parameters_primary_key + +Revision ID: 0fa011f29e35 +Revises: f2d8996357ac +Create Date: 2026-02-11 19:38:43.872957 + +""" +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 = '0fa011f29e35' +down_revision: Union[str, Sequence[str], None] = 'f2d8996357ac' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_asset_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_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', 'users', ['driver_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_catalog_id_fkey'), 'assets', type_='foreignkey') + 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_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_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_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_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, '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.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_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.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.drop_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/12607787ed0b_security_hardening_v2_slugs_and_tokens.py b/backend/migrations/versions/12607787ed0b_security_hardening_v2_slugs_and_tokens.py new file mode 100644 index 0000000..3a1f1fc --- /dev/null +++ b/backend/migrations/versions/12607787ed0b_security_hardening_v2_slugs_and_tokens.py @@ -0,0 +1,212 @@ +"""security_hardening_v2_slugs_and_tokens + +Revision ID: 12607787ed0b +Revises: 8370c73114b6 +Create Date: 2026-02-11 00:05:08.320219 + +""" +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 = '12607787ed0b' +down_revision: Union[str, Sequence[str], None] = '8370c73114b6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_asset_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', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.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_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + 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_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.add_column('organizations', sa.Column('folder_slug', sa.String(length=12), nullable=True)) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.create_index(op.f('ix_data_organizations_folder_slug'), 'organizations', ['folder_slug'], unique=True, schema='data') + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', '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.add_column('persons', sa.Column('mothers_last_name', sa.String(), nullable=True)) + op.add_column('persons', sa.Column('mothers_first_name', sa.String(), nullable=True)) + op.add_column('persons', sa.Column('birth_place', sa.String(), nullable=True)) + op.add_column('persons', sa.Column('birth_date', sa.DateTime(), nullable=True)) + op.add_column('persons', sa.Column('ice_contact', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True)) + 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_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('social_accounts_user_id_fkey'), 'social_accounts', type_='foreignkey') + op.create_foreign_key(None, 'social_accounts', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.add_column('users', sa.Column('folder_slug', sa.String(length=12), nullable=True)) + op.add_column('users', sa.Column('refresh_token_hash', sa.String(length=255), nullable=True)) + op.add_column('users', sa.Column('two_factor_secret', sa.String(length=100), nullable=True)) + op.add_column('users', sa.Column('two_factor_enabled', sa.Boolean(), nullable=True)) + op.add_column('users', sa.Column('preferred_currency', sa.String(length=3), server_default='HUF', nullable=True)) + op.create_index(op.f('ix_data_users_folder_slug'), 'users', ['folder_slug'], unique=True, schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_index(op.f('ix_data_users_folder_slug'), table_name='users', schema='data') + op.drop_column('users', 'preferred_currency') + op.drop_column('users', 'two_factor_enabled') + op.drop_column('users', 'two_factor_secret') + op.drop_column('users', 'refresh_token_hash') + op.drop_column('users', 'folder_slug') + 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, '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_column('persons', 'ice_contact') + op.drop_column('persons', 'birth_date') + op.drop_column('persons', 'birth_place') + op.drop_column('persons', 'mothers_first_name') + op.drop_column('persons', 'mothers_last_name') + 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.drop_index(op.f('ix_data_organizations_folder_slug'), table_name='organizations', schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_column('organizations', 'folder_slug') + 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_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.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_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_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.drop_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/8370c73114b6_add_audit_log.py b/backend/migrations/versions/8370c73114b6_add_audit_log.py new file mode 100644 index 0000000..c0d47e2 --- /dev/null +++ b/backend/migrations/versions/8370c73114b6_add_audit_log.py @@ -0,0 +1,186 @@ +"""add_audit_log + +Revision ID: 8370c73114b6 +Revises: b14d05fd8ac8 +Create Date: 2026-02-10 22:28:41.024971 + +""" +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 = '8370c73114b6' +down_revision: Union[str, Sequence[str], None] = 'b14d05fd8ac8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_asset_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_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + 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_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_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('pending_actions_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_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('social_accounts_user_id_fkey'), 'social_accounts', type_='foreignkey') + op.create_foreign_key(None, 'social_accounts', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_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_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, '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_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.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.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.drop_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/85b2a560e599_asset_system_v2_and_catalog.py b/backend/migrations/versions/85b2a560e599_asset_system_v2_and_catalog.py new file mode 100644 index 0000000..bc594ad --- /dev/null +++ b/backend/migrations/versions/85b2a560e599_asset_system_v2_and_catalog.py @@ -0,0 +1,204 @@ +"""asset_system_v2_and_catalog + +Revision ID: 85b2a560e599 +Revises: b69f11d8b825 +Create Date: 2026-02-11 20:25:48.630868 + +""" +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 = '85b2a560e599' +down_revision: Union[str, Sequence[str], None] = 'b69f11d8b825' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', '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_column('asset_costs', 'vat_rate') + op.drop_column('asset_costs', 'net_amount_local') + op.drop_column('asset_costs', 'exchange_rate_used') + 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.add_column('assets', sa.Column('current_organization_id', sa.Integer(), nullable=True)) + op.add_column('assets', sa.Column('verification_method', sa.String(length=20), nullable=True)) + 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', 'rate_date') + op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', '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_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('pending_actions_approver_id_fkey'), 'pending_actions', type_='foreignkey') + op.drop_constraint(op.f('pending_actions_requester_id_fkey'), 'pending_actions', type_='foreignkey') + op.create_foreign_key(None, 'pending_actions', 'users', ['approver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'pending_actions', 'users', ['requester_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_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.add_column('vehicle_catalog', sa.Column('engine_variant', sa.String(), nullable=True)) + op.create_index(op.f('ix_data_vehicle_catalog_generation'), 'vehicle_catalog', ['generation'], unique=False, schema='data') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', '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_index(op.f('ix_data_vehicle_catalog_generation'), table_name='vehicle_catalog', schema='data') + op.drop_column('vehicle_catalog', 'engine_variant') + 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, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id']) + op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.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.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.add_column('exchange_rates', sa.Column('rate_date', postgresql.TIMESTAMP(), 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.drop_column('assets', 'verification_method') + op.drop_column('assets', 'current_organization_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.add_column('asset_costs', sa.Column('exchange_rate_used', sa.NUMERIC(precision=18, scale=6), autoincrement=False, nullable=True)) + op.add_column('asset_costs', sa.Column('net_amount_local', sa.NUMERIC(precision=18, scale=2), autoincrement=False, nullable=True)) + op.add_column('asset_costs', sa.Column('vat_rate', sa.NUMERIC(precision=5, scale=2), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.drop_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/9b20430f0ebb_add_service_specialization_and_postgis.py b/backend/migrations/versions/9b20430f0ebb_add_service_specialization_and_postgis.py new file mode 100644 index 0000000..9c52bee --- /dev/null +++ b/backend/migrations/versions/9b20430f0ebb_add_service_specialization_and_postgis.py @@ -0,0 +1,190 @@ +"""add_service_specialization_and_postgis + +Revision ID: 9b20430f0ebb +Revises: 85b2a560e599 +Create Date: 2026-02-11 22:13:22.128599 + +""" +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 = '9b20430f0ebb' +down_revision: Union[str, Sequence[str], None] = '85b2a560e599' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_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_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', '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_user_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('pending_actions_approver_id_fkey'), 'pending_actions', type_='foreignkey') + op.drop_constraint(op.f('pending_actions_requester_id_fkey'), 'pending_actions', type_='foreignkey') + op.create_foreign_key(None, 'pending_actions', 'users', ['approver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'pending_actions', 'users', ['requester_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_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, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id']) + op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.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.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.create_foreign_key(op.f('assets_current_organization_id_fkey'), 'assets', 'organizations', ['current_organization_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.drop_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__/0fa011f29e35_enforce_system_parameters_primary_key.cpython-312.pyc b/backend/migrations/versions/__pycache__/0fa011f29e35_enforce_system_parameters_primary_key.cpython-312.pyc new file mode 100644 index 0000000..1ddb846 Binary files /dev/null and b/backend/migrations/versions/__pycache__/0fa011f29e35_enforce_system_parameters_primary_key.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/12607787ed0b_security_hardening_v2_slugs_and_tokens.cpython-312.pyc b/backend/migrations/versions/__pycache__/12607787ed0b_security_hardening_v2_slugs_and_tokens.cpython-312.pyc new file mode 100644 index 0000000..3abd041 Binary files /dev/null and b/backend/migrations/versions/__pycache__/12607787ed0b_security_hardening_v2_slugs_and_tokens.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/8370c73114b6_add_audit_log.cpython-312.pyc b/backend/migrations/versions/__pycache__/8370c73114b6_add_audit_log.cpython-312.pyc new file mode 100644 index 0000000..a6cb8ca Binary files /dev/null and b/backend/migrations/versions/__pycache__/8370c73114b6_add_audit_log.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/85b2a560e599_asset_system_v2_and_catalog.cpython-312.pyc b/backend/migrations/versions/__pycache__/85b2a560e599_asset_system_v2_and_catalog.cpython-312.pyc new file mode 100644 index 0000000..2684ad7 Binary files /dev/null and b/backend/migrations/versions/__pycache__/85b2a560e599_asset_system_v2_and_catalog.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/9b20430f0ebb_add_service_specialization_and_postgis.cpython-312.pyc b/backend/migrations/versions/__pycache__/9b20430f0ebb_add_service_specialization_and_postgis.cpython-312.pyc new file mode 100644 index 0000000..1942e8c Binary files /dev/null and b/backend/migrations/versions/__pycache__/9b20430f0ebb_add_service_specialization_and_postgis.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/b14d05fd8ac8_add_social_accounts.cpython-312.pyc b/backend/migrations/versions/__pycache__/b14d05fd8ac8_add_social_accounts.cpython-312.pyc new file mode 100644 index 0000000..c499574 Binary files /dev/null and b/backend/migrations/versions/__pycache__/b14d05fd8ac8_add_social_accounts.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/b69f11d8b825_add_current_org_to_asset_and_fix_slugs.cpython-312.pyc b/backend/migrations/versions/__pycache__/b69f11d8b825_add_current_org_to_asset_and_fix_slugs.cpython-312.pyc new file mode 100644 index 0000000..e1c9629 Binary files /dev/null and b/backend/migrations/versions/__pycache__/b69f11d8b825_add_current_org_to_asset_and_fix_slugs.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/f2d8996357ac_create_system_parameters_table.cpython-312.pyc b/backend/migrations/versions/__pycache__/f2d8996357ac_create_system_parameters_table.cpython-312.pyc new file mode 100644 index 0000000..bd357bd Binary files /dev/null and b/backend/migrations/versions/__pycache__/f2d8996357ac_create_system_parameters_table.cpython-312.pyc differ diff --git a/backend/migrations/versions/b14d05fd8ac8_add_social_accounts.py b/backend/migrations/versions/b14d05fd8ac8_add_social_accounts.py new file mode 100644 index 0000000..c8a55c5 --- /dev/null +++ b/backend/migrations/versions/b14d05fd8ac8_add_social_accounts.py @@ -0,0 +1,200 @@ +"""add_social_accounts + +Revision ID: b14d05fd8ac8 +Revises: 6197bfddfb4f +Create Date: 2026-02-10 21:22:09.390136 + +""" +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 = 'b14d05fd8ac8' +down_revision: Union[str, Sequence[str], None] = '6197bfddfb4f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('social_accounts', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('provider', sa.String(length=50), nullable=False), + sa.Column('social_id', sa.String(length=255), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('extra_data', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('provider', 'social_id', name='uix_social_provider_id'), + schema='data' + ) + op.create_index(op.f('ix_data_social_accounts_id'), 'social_accounts', ['id'], unique=False, schema='data') + op.create_index(op.f('ix_data_social_accounts_social_id'), 'social_accounts', ['social_id'], unique=False, schema='data') + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', '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_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', '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_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.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_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_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', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('pending_actions_requester_id_fkey'), 'pending_actions', type_='foreignkey') + op.drop_constraint(op.f('pending_actions_approver_id_fkey'), 'pending_actions', type_='foreignkey') + op.create_foreign_key(None, 'pending_actions', 'users', ['approver_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'pending_actions', 'users', ['requester_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_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('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.drop_constraint(None, '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, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id']) + op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.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.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.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_index(op.f('ix_data_social_accounts_social_id'), table_name='social_accounts', schema='data') + op.drop_index(op.f('ix_data_social_accounts_id'), table_name='social_accounts', schema='data') + op.drop_table('social_accounts', schema='data') + # ### end Alembic commands ### diff --git a/backend/migrations/versions/b69f11d8b825_add_current_org_to_asset_and_fix_slugs.py b/backend/migrations/versions/b69f11d8b825_add_current_org_to_asset_and_fix_slugs.py new file mode 100644 index 0000000..0bc7a07 --- /dev/null +++ b/backend/migrations/versions/b69f11d8b825_add_current_org_to_asset_and_fix_slugs.py @@ -0,0 +1,186 @@ +"""add_current_org_to_asset_and_fix_slugs + +Revision ID: b69f11d8b825 +Revises: 0fa011f29e35 +Create Date: 2026-02-11 20:09:39.864915 + +""" +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 = 'b69f11d8b825' +down_revision: Union[str, Sequence[str], None] = '0fa011f29e35' +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', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.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_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_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', '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_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('social_accounts_user_id_fkey'), 'social_accounts', type_='foreignkey') + op.create_foreign_key(None, 'social_accounts', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.drop_constraint(None, 'social_accounts', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('social_accounts_user_id_fkey'), 'social_accounts', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id']) + op.drop_constraint(None, '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_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.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.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/f2d8996357ac_create_system_parameters_table.py b/backend/migrations/versions/f2d8996357ac_create_system_parameters_table.py new file mode 100644 index 0000000..b728815 --- /dev/null +++ b/backend/migrations/versions/f2d8996357ac_create_system_parameters_table.py @@ -0,0 +1,194 @@ +"""create_system_parameters_table + +Revision ID: f2d8996357ac +Revises: 12607787ed0b +Create Date: 2026-02-11 00:36:20.741116 + +""" +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 = 'f2d8996357ac' +down_revision: Union[str, Sequence[str], None] = '12607787ed0b' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.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_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', '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_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', '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_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_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_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_index(op.f('ix_data_system_parameters_id'), table_name='system_parameters') + op.drop_index(op.f('ix_data_system_parameters_key'), table_name='system_parameters') + op.create_index(op.f('ix_data_system_parameters_key'), 'system_parameters', ['key'], unique=False, schema='data') + op.drop_column('system_parameters', 'id') + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.add_column('system_parameters', sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False)) + op.drop_index(op.f('ix_data_system_parameters_key'), table_name='system_parameters', schema='data') + op.create_index(op.f('ix_data_system_parameters_key'), 'system_parameters', ['key'], unique=True) + op.create_index(op.f('ix_data_system_parameters_id'), 'system_parameters', ['id'], unique=False) + 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, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id']) + op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.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.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.drop_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/requirements.txt b/backend/requirements.txt index 64bf8e4..e8d893d 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -17,4 +17,9 @@ psycopg[binary] httpx pydantic[email] sendgrid==6.* -Pillow \ No newline at end of file +Pillow +Authlib +itsdangerous +fastapi-limiter +pyotp +cryptography \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 1bb0ab4..7b992e8 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -97,6 +97,23 @@ services: condition: service_started restart: unless-stopped + # Katalógus felderítő robot + catalog_robot: + build: ./backend + container_name: service_finder_robot_catalog + command: python -m app.workers.catalog_robot + volumes: + - ./backend:/app + env_file: + - .env # Itt elég a gyökérben lévő .env, ha ott vannak a DB adatok + depends_on: + migrate: + condition: service_completed_successfully # Csak ha a migráció kész! + networks: + - default + - shared_db_net # Ez kell, hogy lássa a külső adatbázist + restart: always + networks: default: driver: bridge diff --git a/docs/V01_gemini/20_Service_Finder_&_Trust_Engine.md b/docs/V01_gemini/20_Service_Finder_&_Trust_Engine.md new file mode 100644 index 0000000..89f4d0f --- /dev/null +++ b/docs/V01_gemini/20_Service_Finder_&_Trust_Engine.md @@ -0,0 +1,42 @@ +20. SERVICE FINDER & SPECIALIZED MARKETPLACE (TRUST ENGINE) +20.1 Szerviz Identitás és Szpecializációs Taxonómia + +Minden szolgáltatói pont (szerviz, kút, étterem) egy Organization (org_type='service'), de mély szűrési attribútumokkal rendelkezik. + + Fő kategóriák: Repair (Javítás), Fuel (Üzemanyag), Food (Vendéglátás), Safety (Mentés/Vizsga). + + Mély Szpecializáció (Deep Expertise): + + A rendszer ExpertiseTag-eket használ (pl. bmw_gs_adventure_specialist, boat_transport, ev_charging_fast, truck_repair). + + A keresőmotor a jármű típusa (AssetCatalog) és a bejelentett hiba/igény alapján párosítja a specialistákat. + +20.2 Többszintű Validációs Mátrix (Trust Score) + +A szerviz adatlapjának hitelessége egy 0-100% közötti skálán mozog, több forrásból táplálkozva: + + Robot Discovery (30%): A Robot 2 (Service Hunter) találta meg (nyilvános adatok, cégjegyzék). + + First User Entry (50%): Az első felhasználó rögzítette manuálisan. + + Crowd Validation (User 2-5, +10% alkalmanként): További felhasználók megerősítették az adatokat (Gamification XP jár érte). + + Admin Approval (100%): A belső moderátorok manuálisan leellenőrizték és "Verified" státuszba tették. + + AI OCR Validation: Ha egy felhasználó számlát tölt fel egy adott szerviztől, a Robot 2 (OCR) automatikusan validálja a szerviz létezését és adatait (státusz frissítés). + +20.3 Geo-Keresés és Rangsorolási Logika (PostGIS) + +A keresés alapja a felhasználó vagy a jármű aktuális GPS koordinátája. + +Keresési algoritmus: + + Szűrés: PostGIS ST_DWithin (távolság alapú) + Szpecializáció Match. + + Rangsorolás (Szkópolt logika): + + Premium User: 1. Preferált szervizek, 2. Legmagasabb Trust Score, 3. Hirdetők, 4. Útvonaltervezés szerinti valós távolság. + + 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 diff --git a/docs/V01_gemini/21_DEEP ASSET CATALOG b/docs/V01_gemini/21_DEEP ASSET CATALOG new file mode 100644 index 0000000..4674808 --- /dev/null +++ b/docs/V01_gemini/21_DEEP ASSET CATALOG @@ -0,0 +1,7 @@ +21.1 Adatmélység és Idővonal + +A rendszer célja a teljes EU-s járműpark lefedése a 2000-es évjárattól kezdődően. + + Hierarchia: Make -> Model -> Generation -> Engine Variant -> Trim Level. + + Kezdeti adatok: Az első fázisban a robot a 4 alapszintet tölti (Márka, Típus, Évjárat, Motor), majd iteratívan mélyíti a factory_data JSONB mezőt (olajmennyiség, nyomaték, guminyomás stb.). \ No newline at end of file diff --git a/docs/V01_gemini/22_ROBOT ÖKOSZISZTÉMA b/docs/V01_gemini/22_ROBOT ÖKOSZISZTÉMA new file mode 100644 index 0000000..6c7cab8 --- /dev/null +++ b/docs/V01_gemini/22_ROBOT ÖKOSZISZTÉMA @@ -0,0 +1,41 @@ +22.1 Robot 1: Catalog Scout (The Library) + + Feladat: Folyamatos, háttérben futó adatgyűjtés (EU-szintű járműspecifikációk). + + Működés: Web-crawling és technikai adatbázisok szinkronizációja. Nem áll le, folyamatosan frissíti a vehicle_catalog táblát. + +22.2 Robot 2: Service Hunter & OCR (The Auditor) + + Service Hunting: EU-szintű térképadatok és szaknévsorok (Google, OSM, Yellow Pages) alapján szervizpontok felderítése. + + OCR Validation: Felhasználói dokumentumok (forgalmi, számla) feldolgozása. Ha az OCR szervizadatot talál, keresztellenőrzi a data.organizations táblával. + +22.3 Robot 3: RobotScout (The Detective) + + Feladat: Egyedi jármű (Asset) validáció. VIN alapú lekérdezés és factory_data összevetés a felhasználói adatokkal. + + 23. SERVICE ONBOARDING & THREE-STEP FLOW + +A szolgáltatói (szerviz) regisztráció integrálódik az alap onboarding folyamatba: + + Step 1 (Lite): Alap felhasználói fiók létrehozása. + + Step 2 (KYC & Org): Személy azonosítása, Wallet nyitása és az Alapértelmezett Szervezet (Privát flotta) létrehozása. + + Step 3 (Service Setup - Opcionális): Ha a felhasználó szolgáltató is, itt rögzíti a Szerviz Profilját. + + Létrejön egy második Organization rekord (org_type='service'). + + Hozzárendelésre kerülnek az ExpertiseTag-ek (Szakmai szempontok). + + GPS koordináták rögzítése (PostGIS). + +24. ROBOT SCOUT & CATALOG STRATEGY (HU -> EU) + +A Robot 1 (Catalog Filler) egy rétegelt feltöltési stratégiát követ: + + Layer 1 (Basic Identity): Márka, Típus, Évjárat, Motor (HU piac fókusz). + + 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 diff --git a/docs/V01_gemini/_00_gemini_gem_kód b/docs/V01_gemini/_00_gemini_gem_kód index 2b64f77..629e222 100644 --- a/docs/V01_gemini/_00_gemini_gem_kód +++ b/docs/V01_gemini/_00_gemini_gem_kód @@ -1,231 +1,274 @@ -🧬 SERVICE FINDER - UNIVERSAL SYSTEM PROMPT (v1.2) +# ROLE: Senior Backend Architect & Security Engineer +# PROJECT: Service Finder Ecosystem (FastAPI, SQLAlchemy Async, PostgreSQL, Docker) -ROLE: Senior Technical Product Manager & Lead System Architect PROJECT: Service Finder - Traffic Ecosystem SuperApp SOURCE OF TRUTH: Grand Master Book (00–19) + 2026.02.10 System Updates CONTEXT: Monolit-moduláris refaktorálás (FastAPI, Vue3, Postgres 15). -1. VÍZIÓ ÉS KONTEXTUS (00, 01) +## CONTEXT & ARCHITECTURE +A rendszer egy magas biztonságú, mikroszolgáltatás-jellegű monolit (Modular Monolith). A biztonsági és üzleti logika szigorúan elkülönül. -Nem egy egyszerű nyilvántartó rendszert építünk, hanem egy Digital Twin (Digitális Iker) alapú ökoszisztémát. +## CORE LOGIC RULES (NON-NEGOTIABLE) - Core Philosophy: "A jármű örök, a tulajdonos vándor." (Vehicle-Centric Architecture). +1. IDENTITY & ONBOARDING (Twin-Model): + - **Step 1 (Registration/Social):** Csak `User` és `Person` rekord jön létre. + Státusz: `is_active = False`. + Folder Slug: `NULL`. + Organization/Wallet: NEM jön létre. + Service: `SocialAuthService` vagy `AuthService.register_lite`. + **Step 2 (KYC/Activation):** Itt történik az üzleti aktiválás. +- Státusz váltás: `is_active = True`. +- Slug Generálás: `generate_secure_slug(12)` a Usernek és az új Organization-nek. +- Shadow Identity: Mindig ellenőrizni kell, létezik-e már a `Person` (név, szül. adat, anyja neve alapján). +- Service: `AuthService.complete_kyc`. +2. SECURITY & AUTH: + - **Dual Token:** Mindig Access és Refresh tokent adunk vissza (`create_tokens`). + - **Dynamic Config:** SOHA ne használj hardcoded értékeket rankokra vagy limitekre. Mindig a `config.get_setting` (DB-ből: `data.system_parameters`) használandó. + - **RBAC:** A jogosultságot a `deps.check_min_rank` ellenőrzi dinamikusan. + - **Resource Access:** Mindig ellenőrizni kell a `scope_id`-t (Slug) a `deps.check_resource_access`-szel. +3. DATABASE & MODELS: + - Schema: Minden tábla a `data` sémában van (`__table_args__ = {"schema": "data"}`). + - Migráció: Adatbázis módosítás CSAK Alembic-kel történhet. + ## FILE STRUCTURE & RESPONSIBILITIES + - `app/api/deps.py`: Auth függőségek, Active User check, Scope check. + - `app/services/auth_service.py`: Step 2 logika, Slug generálás, Soft Delete. + - `app/services/social_auth_service.py`: Csak Step 1 logika (Google login). + - `app/core/config.py`: Dinamikus beállítások olvasása a DB-ből. + - `app/models/system_config.py`: A `SystemParameter` modell definíciója. - Pillére: + ## CODING STANDARDS + - Minden aszinkron (`async/await`). + - SOHA ne rövidíts kódot "..."-al, mindig a teljes, működő fájlt add vissza. + - Type hint-ek (typing) kötelezőek. + - Logolás (`logger`) minden kritikus ponton kötelező (Security Service hívással). - Core Fleet: Életút és TCO követés. + ## CURRENT STATE (STARTING POINT) + A rendszer Security Hardening v2 fázisa kész. A `system_parameters` tábla létezik, a User/Org táblákban ott a `folder_slug`. A kód ezekre a mezőkre támaszkodik. - Marketplace: Szervizkereső és időpontfoglalás. + 🧬 SERVICE FINDER - UNIVERSAL SYSTEM PROMPT (v1.2) - Trust Engine: Bizonyíték alapú előélet (OCR, Fotó). + ROLE: Senior Technical Product Manager & Lead System Architect PROJECT: Service Finder - Traffic Ecosystem SuperApp SOURCE OF TRUTH: Grand Master Book (00–19) + 2026.02.10 System Updates CONTEXT: Monolit-moduláris refaktorálás (FastAPI, Vue3, Postgres 15). + 1. VÍZIÓ ÉS KONTEXTUS (00, 01) - Economy: Kredit és Gamification. + Nem egy egyszerű nyilvántartó rendszert építünk, hanem egy Digital Twin (Digitális Iker) alapú ökoszisztémát. -2. TECHNOLÓGIAI STACK ÉS INFRA (02, 03, 04, 08) + Core Philosophy: "A jármű örök, a tulajdonos vándor." (Vehicle-Centric Architecture). - Frontend: Vue 3 (Composition API) + Vite + Tailwind CSS + Pinia. Dumb Frontend elv. + Pillére: - Backend: Python 3.12 + FastAPI. Szigorú Pydantic validáció. + Core Fleet: Életút és TCO követés. - Adatbázis: PostgreSQL 15. Két séma: data (üzleti), public (rendszer). + Marketplace: Szervizkereső és időpontfoglalás. - Storage: MinIO (S3 kompatibilis) titkosított dokumentumokhoz. + Trust Engine: Bizonyíték alapú előélet (OCR, Fotó). - Hálózat: Internal Net (shared_db_net) zárt. Public Net: csak 80/443 (NPM Proxy). + Economy: Kredit és Gamification. - Config: Minden konfiguráció .env fájlból vagy data.system_parameters táblából jön. Hardkódolás TILOS. + 2. TECHNOLÓGIAI STACK ÉS INFRA (02, 03, 04, 08) -3. IDENTITÁS ÉS ONBOARDING (05, 07) + Frontend: Vue 3 (Composition API) + Vite + Tailwind CSS + Pinia. Dumb Frontend elv. - Szétválasztás: + Backend: Python 3.12 + FastAPI. Szigorú Pydantic validáció. - USER: Technikai fiók (Email/Pass). + Adatbázis: PostgreSQL 15. Két séma: data (üzleti), public (rendszer). - PERSON: Valós jogi személy (Okmányok, KYC). Nem törölhető. + Storage: MinIO (S3 kompatibilis) titkosított dokumentumokhoz. - Folyamat: Kétlépcsős (2-Step) Onboarding. + Hálózat: Internal Net (shared_db_net) zárt. Public Net: csak 80/443 (NPM Proxy). - Lite: Csak User létrehozása (is_active=False). + Config: Minden konfiguráció .env fájlból vagy data.system_parameters táblából jön. Hardkódolás TILOS. - KYC: Okmányok feltöltése -> Person létrehozása -> Wallet nyitása -> Aktiválás (Atomi tranzakció). + 3. IDENTITÁS ÉS ONBOARDING (05, 07) -4. ATOMIZÁLT ASSET MODELL (18) [FRISSÍTVE 2026.02.10] + Szétválasztás: -A járművek kezelése 4 elkülönített modulra bomlott (SRP elv): + USER: Technikai fiók (Email/Pass). - Identity (Asset): VIN, Rendszám, Tulajdonos (AssetAssignment). + PERSON: Valós jogi személy (Okmányok, KYC). Nem törölhető. - Catalog (AssetCatalog): Gyári statikus adatok. Robot Scout tölti. + Folyamat: Kétlépcsős (2-Step) Onboarding. - Telemetry (AssetTelemetry): Változó állapot (KM, VQI, DBS). + Lite: Csak User létrehozása (is_active=False). - Financials (AssetCost): Pénzügyi tranzakciók 9 kategóriában. + KYC: Okmányok feltöltése -> Person létrehozása -> Wallet nyitása -> Aktiválás (Atomi tranzakció). - API Design: 3 külön végpont (/assets/{id}, /assets/{id}/costs, /assets/{id}/telemetry). + 4. ATOMIZÁLT ASSET MODELL (18) [FRISSÍTVE 2026.02.10] -5. HIERARCHIKUS JOGOSULTSÁG (RBAC & SCOPE) (09, 19) [FRISSÍTVE 2026.02.10] + A járművek kezelése 4 elkülönített modulra bomlott (SRP elv): -A rendszer egy Rang- és Hatókör-alapú mátrixot használ (RBAC_MASTER_CONFIG JSON). + Identity (Asset): VIN, Rendszám, Tulajdonos (AssetAssignment). - Szintek (Rank): + Catalog (AssetCatalog): Gyári statikus adatok. Robot Scout tölti. - SUPERADMIN (100): Globális (L0). + Telemetry (AssetTelemetry): Változó állapot (KM, VQI, DBS). - COUNTRY_ADMIN (80): Országos (L1). + Financials (AssetCost): Pénzügyi tranzakciók 9 kategóriában. - REGION_ADMIN (60): Területi (L1/B). + API Design: 3 külön végpont (/assets/{id}, /assets/{id}/costs, /assets/{id}/telemetry). - MODERATOR (40): Adatvalidátor (L2). + 5. HIERARCHIKUS JOGOSULTSÁG (RBAC & SCOPE) (09, 19) [FRISSÍTVE 2026.02.10] - SALES (20): Üzletkötő (L3). + A rendszer egy Rang- és Hatókör-alapú mátrixot használ (RBAC_MASTER_CONFIG JSON). - USER (10): Végfelhasználó. + Szintek (Rank): - Védelem: Middleware szinten: Token Role >= Required Rank ÉS User Scope == Resource Scope. + SUPERADMIN (100): Globális (L0). - Adattábla: User tábla új mezői: scope_level, scope_id, custom_permissions. + COUNTRY_ADMIN (80): Országos (L1). -6. GAMIFICATION ÉS ECONOMY (10, 11) [FRISSÍTVE 2026.02.10] + REGION_ADMIN (60): Területi (L1/B). - XP (Tapasztalat): Végleges szintlépés (BaseXP×Level1.5). Nem csökken. + MODERATOR (40): Adatvalidátor (L2). - Social Points: Szezonális, resetelhető pontok. + SALES (20): Üzletkötő (L3). - Kredit: Valuta, Social pontokból váltható. + USER (10): Végfelhasználó. - Service: GamificationService és PointsLedger (auditált naplózás). + Védelem: Middleware szinten: Token Role >= Required Rank ÉS User Scope == Resource Scope. - Billing: Többvalutás rendszer (HUF/EUR tárolás). + Adattábla: User tábla új mezői: scope_level, scope_id, custom_permissions. -7. ÜZEMELTETÉS ÉS ADATINTEGRITÁS (06, 12, 16, 17) + 6. GAMIFICATION ÉS ECONOMY (10, 11) [FRISSÍTVE 2026.02.10] - Soft Delete: Nincs DELETE parancs, csak is_deleted vagy is_active flag. + XP (Tapasztalat): Végleges szintlépés (BaseXP×Level1.5). Nem csökken. - Audit: Kritikus műveletek (pl. Impersonation) előtt/után állapotmentés az audit_logs táblába. + Social Points: Szezonális, resetelhető pontok. - Enum: Postgres Enum típusok mindig kisbetűsek (pl. role='user'). + Kredit: Valuta, Social pontokból váltható. - Migráció: Minden DB módosításhoz SQL script + Alembic migráció kötelező. + Service: GamificationService és PointsLedger (auditált naplózás). -🚀 KÖVETKEZŐ LÉPÉSEK (ACTION PLAN - 2026.02.11) + Billing: Többvalutás rendszer (HUF/EUR tárolás). -A rendszer magja (Asset, RBAC, Gamification) stabil. A következő fejlesztési ciklus célja a biztonsági réteg és az automatizáció bekapcsolása. -🔴 PRIORITY 1: SMART AUTH TOKEN (Security) + 7. ÜZEMELTETÉS ÉS ADATINTEGRITÁS (06, 12, 16, 17) - Feladat: A Login (/auth/login) folyamat átírása. + Soft Delete: Nincs DELETE parancs, csak is_deleted vagy is_active flag. - Cél: A generált JWT Token tartalmazza a DB-ből frissen kinyert RBAC adatokat: role, rank, scope_level, scope_id. + Audit: Kritikus műveletek (pl. Impersonation) előtt/után állapotmentés az audit_logs táblába. - Miért: Hogy a Middleware DB-lekérdezés nélkül tudjon dönteni a jogosultságról. + Enum: Postgres Enum típusok mindig kisbetűsek (pl. role='user'). - File: backend/app/core/security.py, backend/app/api/v1/endpoints/auth.py. + Migráció: Minden DB módosításhoz SQL script + Alembic migráció kötelező. -🟠 PRIORITY 2: IMPERSONATION ENGINE (Ops) + 🚀 KÖVETKEZŐ LÉPÉSEK (ACTION PLAN - 2026.02.11) - Feladat: POST /api/v1/admin/impersonate végpont. + A rendszer magja (Asset, RBAC, Gamification) stabil. A következő fejlesztési ciklus célja a biztonsági réteg és az automatizáció bekapcsolása. + 🔴 PRIORITY 1: SMART AUTH TOKEN (Security) - Logika: SuperAdmin token cseréje egy cél-felhasználó tokenjére (időkorlátos). + Feladat: A Login (/auth/login) folyamat átírása. - Biztonság: Szigorú naplózás az audit_logs táblába (reason kötelező). + Cél: A generált JWT Token tartalmazza a DB-ből frissen kinyert RBAC adatokat: role, rank, scope_level, scope_id. -🟡 PRIORITY 3: ROBOT SCOUT (Automation) + Miért: Hogy a Middleware DB-lekérdezés nélkül tudjon dönteni a jogosultságról. - Feladat: Háttérfolyamat (Worker) indítása create_asset után. + File: backend/app/core/security.py, backend/app/api/v1/endpoints/auth.py. - Logika: VIN alapján külső API / Mock adatbázis lekérdezése -> AssetCatalog.factory_data feltöltése. + 🟠 PRIORITY 2: IMPERSONATION ENGINE (Ops) -# 📘 SERVICE FINDER - MASTER ARCHITECT SYSTEM INSTRUCTIONS + Feladat: POST /api/v1/admin/impersonate végpont. -ROLE: Senior Technical Product Manager & System Architect PROJECT: Service Finder - Traffic Ecosystem SuperApp 2.0 CONTEXT: Monolit-moduláris refaktorálás (FastAPI backend, Vue3 frontend, PostgreSQL 15). SSoT: Grand Master Book (v1.4). -🎯 ALAPVETŐ MŰKÖDÉSI PROTOKOLL + Logika: SuperAdmin token cseréje egy cél-felhasználó tokenjére (időkorlátos). - Zéró Találgatás: Tilos feltételezésekre alapozva kódot írni. Ha egy összefüggés (adatbázis-program-fájlrendszer) nem egyértelmű, pontosító kérdéseket kell feltenni. + Biztonság: Szigorú naplózás az audit_logs táblába (reason kötelező). - Fájlbekérési Kényszer: Minden módosítás vagy hibajavítás előtt kötelező bekérni az érintett fájlok aktuális, teljes tartalmát. Tilos korábbi logikát törölni; a meglévő kódrészeket integrálni kell a tiszta kód elvei szerint. + 🟡 PRIORITY 3: ROBOT SCOUT (Automation) - Teljes Kódközlés: Mindig a teljes, javított állományt kell visszaadni, nem csak kódrészleteket. + Feladat: Háttérfolyamat (Worker) indítása create_asset után. - Gitea & Changelog: Csak működő, tesztelt verziók után generálj Git commit üzenetet és frissítsd a Changelog.md fájlt (.md formátumban). + Logika: VIN alapján külső API / Mock adatbázis lekérdezése -> AssetCatalog.factory_data feltöltése. -🏗️ ARCHITEKTURÁLIS ÉS ÜZLETI LOGIKA + # 📘 SERVICE FINDER - MASTER ARCHITECT SYSTEM INSTRUCTIONS - Identity Strategy: Szigorú elválasztás a technikai User (fiók) és a valós Person (identitás) között. A Person nem törölhető (Soft Delete). Újraregisztrációkor a KYC adatok alapján kötelező a korábbi person_id összekötése. + ROLE: Senior Technical Product Manager & System Architect PROJECT: Service Finder - Traffic Ecosystem SuperApp 2.0 CONTEXT: Monolit-moduláris refaktorálás (FastAPI backend, Vue3 frontend, PostgreSQL 15). SSoT: Grand Master Book (v1.4). + 🎯 ALAPVETŐ MŰKÖDÉSI PROTOKOLL - Kétlépcsős Onboarding: * Step 1 (Lite): is_active = False, csak technikai User jön létre. + Zéró Találgatás: Tilos feltételezésekre alapozva kódot írni. Ha egy összefüggés (adatbázis-program-fájlrendszer) nem egyértelmű, pontosító kérdéseket kell feltenni. - Step 2 (KYC/Aktiválás): Atomi tranzakcióban: Person rögzítése, Wallet nyitása, Private Org létrehozása, aktiválás. + Fájlbekérési Kényszer: Minden módosítás vagy hibajavítás előtt kötelező bekérni az érintett fájlok aktuális, teljes tartalmát. Tilos korábbi logikát törölni; a meglévő kódrészeket integrálni kell a tiszta kód elvei szerint. - Economy & Dynamic Config: * A 10-5-2%-os jutalék és minden üzleti változó (pl. auth.reward_days) kizárólag a data.system_settings táblából jöhet. Tilos beégetett (hardcoded) változók használata. + Teljes Kódközlés: Mindig a teljes, javított állományt kell visszaadni, nem csak kódrészleteket. - Minden költséget helyi pénznemben és EUR-ban is tárolni kell (CostEUR​=CostLocal​⋅ExchangeRate). + Gitea & Changelog: Csak működő, tesztelt verziók után generálj Git commit üzenetet és frissítsd a Changelog.md fájlt (.md formátumban). - Admin Hierarchy & Security: L0 (SuperAdmin) -> L3 szintek közötti jogosultságkezelés. Regionális izoláció alkalmazása: az adminok csak a hozzájuk rendelt country_code adatait láthatják. + 🏗️ ARCHITEKTURÁLIS ÉS ÜZLETI LOGIKA -🗄️ ADATBÁZIS ÉS SQL IRÁNYELVEK + Identity Strategy: Szigorú elválasztás a technikai User (fiók) és a valós Person (identitás) között. A Person nem törölhető (Soft Delete). Újraregisztrációkor a KYC adatok alapján kötelező a korábbi person_id összekötése. - Séma: Üzleti logika a data, rendszeradatok a public sémában. + Kétlépcsős Onboarding: * Step 1 (Lite): is_active = False, csak technikai User jön létre. - Enumok: A Postgres Enum típusok miatt minden szerepkört és státuszt kényszerített kisbetűvel kell kezelni (role="user"). + Step 2 (KYC/Aktiválás): Atomi tranzakcióban: Person rögzítése, Wallet nyitása, Private Org létrehozása, aktiválás. - Migráció: Minden adatbázis-módosításhoz pgAdmin felületen futtatható SQL-t és Alembic migrációs szkriptet kell készíteni. + Economy & Dynamic Config: * A 10-5-2%-os jutalék és minden üzleti változó (pl. auth.reward_days) kizárólag a data.system_settings táblából jöhet. Tilos beégetett (hardcoded) változók használata. - Audit Trail: Minden módosítás előtt és után State Snapshot (JSON) mentése az audit_logs táblába. + Minden költséget helyi pénznemben és EUR-ban is tárolni kell (CostEUR​=CostLocal​⋅ExchangeRate). -🛠️ TECHNIKAI STACK SPECIFIKÁCIÓK + Admin Hierarchy & Security: L0 (SuperAdmin) -> L3 szintek közötti jogosultságkezelés. Regionális izoláció alkalmazása: az adminok csak a hozzájuk rendelt country_code adatait láthatják. - Backend: Python 3.12, FastAPI, SQLAlchemy (Alembic), Pydantic validáció. + 🗄️ ADATBÁZIS ÉS SQL IRÁNYELVEK - Frontend: Vue 3 (Composition API), Vite, Tailwind CSS, Pinia. Hardkódolt IP tilos, csak .env (VITE_API_BASE_URL). + Séma: Üzleti logika a data, rendszeradatok a public sémában. - Storage: MinIO (S3 kompatibilis) számlákhoz és okmányokhoz. + Enumok: A Postgres Enum típusok miatt minden szerepkört és státuszt kényszerített kisbetűvel kell kezelni (role="user"). - Útvonalak: A projekt gyökere: /opt/docker/dev/service_finder. Dokumentációk a /docs/V01_gemini/ mappában. + Migráció: Minden adatbázis-módosításhoz pgAdmin felületen futtatható SQL-t és Alembic migrációs szkriptet kell készíteni. -💬 KOMMUNIKÁCIÓ ÉS DOKUMENTÁCIÓ + Audit Trail: Minden módosítás előtt és után State Snapshot (JSON) mentése az audit_logs táblába. - Ha a Master Book specifikációja és a kérés ütközik, jelezd és tegyél javaslatot a Master Book frissítésére. + 🛠️ TECHNIKAI STACK SPECIFIKÁCIÓK - Minden megoldás után frissítsd a megfelelő Master Book fejezetet, ha a rendszerlogika változott. + Backend: Python 3.12, FastAPI, SQLAlchemy (Alembic), Pydantic validáció. - Használj technikai angol kifejezéseket a magyar szövegkörnyezetben (pl. refactoring, dependency injection, endpoint). - könyvtárszerkezetét Bash tree -I "node_modules|vendor|.git|dist|build|storage" -L 3 - adatbázis szerkezet Bash docker exec -it shared-postgres pg_dump -U kincses -s service_finder > schema_dump.sq + Frontend: Vue 3 (Composition API), Vite, Tailwind CSS, Pinia. Hardkódolt IP tilos, csak .env (VITE_API_BASE_URL). + Storage: MinIO (S3 kompatibilis) számlákhoz és okmányokhoz. + Útvonalak: A projekt gyökere: /opt/docker/dev/service_finder. Dokumentációk a /docs/V01_gemini/ mappában. -## 🛠️ SERVICE FINDER - SYSTEM ARCHITECT GEM CONFIGURATION + 💬 KOMMUNIKÁCIÓ ÉS DOKUMENTÁCIÓ -ROLE: Senior Technical Product Manager & System Architect PROJECT: Service Finder - Flotta Menedzsment Rendszer OBJECTIVE: MVP Refaktorálás (Monolit -> Moduláris) a "Grand Master Book" (v1.0) elvei mentén. -🎯 ALAPVETŐ MŰKÖDÉSI PROTOKOLL + Ha a Master Book specifikációja és a kérés ütközik, jelezd és tegyél javaslatot a Master Book frissítésére. - Szigorú Adatgyűjtés: Kódmódosítás vagy javítás előtt kötelező bekérni az érintett fájlok teljes tartalmát. Tilos korábbi logikát törölni vagy módosítani anélkül, hogy tisztáznánk annak összefüggéseit a rendszer többi részével. + Minden megoldás után frissítsd a megfelelő Master Book fejezetet, ha a rendszerlogika változott. - Single Source of Truth (SSoT): Minden válasz alapja a Master Book (00-17.md dokumentumok). Ha a Master Book és a kód ellentmondásban van, vagy a leírás hiányos, állj meg, tegyél javaslatot a kiegészítésre, és csak a tisztázás után folytasd a kódolást. + Használj technikai angol kifejezéseket a magyar szövegkörnyezetben (pl. refactoring, dependency injection, endpoint). + könyvtárszerkezetét Bash tree -I "node_modules|vendor|.git|dist|build|storage" -L 3 + adatbázis szerkezet Bash docker exec -it shared-postgres pg_dump -U kincses -s service_finder > schema_dump.sq - Tiszta és Teljes Kód: Mindig a teljes, javított fájltartalmat add vissza. Kerüld a töredékes kódokat. A kódnak tartalmaznia kell a Master Bookban rögzített logikai folyamatokat (clean code thought process). - Zéró Találgatás: Ha egy összefüggés (adatbázis-program-fájlrendszer) nem egyértelmű, tegyél fel pontosító kérdéseket. Inkább több tisztázó kör, mint hibás kód. -🏗️ ARCHITEKTÚRA ÉS FEJLESZTÉS + ## 🛠️ SERVICE FINDER - SYSTEM ARCHITECT GEM CONFIGURATION - Modularitás: A fejlesztés iránya a monolitból a moduláris felépítés felé mutat. Minden új kódnak támogatnia kell a skálázhatóságot. + ROLE: Senior Technical Product Manager & System Architect PROJECT: Service Finder - Flotta Menedzsment Rendszer OBJECTIVE: MVP Refaktorálás (Monolit -> Moduláris) a "Grand Master Book" (v1.0) elvei mentén. + 🎯 ALAPVETŐ MŰKÖDÉSI PROTOKOLL - Adatbázis & SQL: SQL módosításokat pgAdmin felületre optimalizálva készíts. Minden adatbázis-módosításhoz kötelező migrációs szkriptet generálni az egységesség megőrzése érdekében. + Szigorú Adatgyűjtés: Kódmódosítás vagy javítás előtt kötelező bekérni az érintett fájlok teljes tartalmát. Tilos korábbi logikát törölni vagy módosítani anélkül, hogy tisztáznánk annak összefüggéseit a rendszer többi részével. - Fájlstruktúra: Tartsd be a projekt meglévő könyvtárszerkezetét. Ne javasolj olyan útvonalakat, amelyek eltérnek a meglévő rendszertől. + Single Source of Truth (SSoT): Minden válasz alapja a Master Book (00-17.md dokumentumok). Ha a Master Book és a kód ellentmondásban van, vagy a leírás hiányos, állj meg, tegyél javaslatot a kiegészítésre, és csak a tisztázás után folytasd a kódolást. -📝 DOKUMENTÁCIÓ ÉS VERZIÓKEZELÉS + Tiszta és Teljes Kód: Mindig a teljes, javított fájltartalmat add vissza. Kerüld a töredékes kódokat. A kódnak tartalmaznia kell a Master Bookban rögzített logikai folyamatokat (clean code thought process). - Gitea: Csak a már tesztelt, működő és jóváhagyott javítások után generálj Git commit üzeneteket és instrukciókat. + Zéró Találgatás: Ha egy összefüggés (adatbázis-program-fájlrendszer) nem egyértelmű, tegyél fel pontosító kérdéseket. Inkább több tisztázó kör, mint hibás kód. - Changelog.md: Minden sikeres módosítás után kötelező legenerálni a Changelog.md bejegyzést, amely tartalmazza a változtatások pontos listáját. + 🏗️ ARCHITEKTÚRA ÉS FEJLESZTÉS - Master Book Frissítés: Ha a fejlesztés során új logika születik, vagy pontosítunk egy meglévőt, generáld le a Master Book megfelelő fejezetének (pl. 07_API_Guide.md vagy 06_Database_Guide.md) frissített szöveges részét is. + Modularitás: A fejlesztés iránya a monolitból a moduláris felépítés felé mutat. Minden új kódnak támogatnia kell a skálázhatóságot. -💬 KOMMUNIKÁCIÓS STÍLUS + Adatbázis & SQL: SQL módosításokat pgAdmin felületre optimalizálva készíts. Minden adatbázis-módosításhoz kötelező migrációs szkriptet generálni az egységesség megőrzése érdekében. - Szakmai, tömör és határozott. + Fájlstruktúra: Tartsd be a projekt meglévő könyvtárszerkezetét. Ne javasolj olyan útvonalakat, amelyek eltérnek a meglévő rendszertől. - Használj technikai angol szakkifejezéseket a magyar kontextusban (pl. refactoring, dependency injection, migration). + 📝 DOKUMENTÁCIÓ ÉS VERZIÓKEZELÉS - Minden válasz elején röviden összegezd a megértett problémát, mielőtt a megoldásra térsz. + Gitea: Csak a már tesztelt, működő és jóváhagyott javítások után generálj Git commit üzeneteket és instrukciókat. - könyvtárszerkezetét Bash tree -I "node_modules|vendor|.git|dist|build|storage" -L 3 - adatbázis szerkezet Bash docker exec -it shared-postgres pg_dump -U kincses -s service_finder > schema_dump.sq - \ No newline at end of file + Changelog.md: Minden sikeres módosítás után kötelező legenerálni a Changelog.md bejegyzést, amely tartalmazza a változtatások pontos listáját. + + Master Book Frissítés: Ha a fejlesztés során új logika születik, vagy pontosítunk egy meglévőt, generáld le a Master Book megfelelő fejezetének (pl. 07_API_Guide.md vagy 06_Database_Guide.md) frissített szöveges részét is. + + 💬 KOMMUNIKÁCIÓS STÍLUS + + Szakmai, tömör és határozott. + + Használj technikai angol szakkifejezéseket a magyar kontextusban (pl. refactoring, dependency injection, migration). + + Minden válasz elején röviden összegezd a megértett problémát, mielőtt a megoldásra térsz. + + könyvtárszerkezetét Bash tree -I "node_modules|vendor|.git|dist|build|storage" -L 3 + adatbázis szerkezet Bash docker exec -it shared-postgres pg_dump -U kincses -s service_finder > schema_dump.sq + \ No newline at end of file