From 09a0430384cd3d508af0c7c7ca6c9eedd351b6e4 Mon Sep 17 00:00:00 2001 From: Kincses Date: Wed, 11 Feb 2026 22:47:38 +0000 Subject: [PATCH] feat: Asset Catalog system, PostGIS integration and RobotScout V1 --- backend/.env | 12 +- backend/app/__pycache__/main.cpython-312.pyc | Bin 1460 -> 1662 bytes .../app/api/__pycache__/deps.cpython-312.pyc | Bin 3580 -> 5362 bytes backend/app/api/deps.py | 103 +++++-- .../__pycache__/auth.cpython-312.pyc | Bin 6867 -> 10197 bytes backend/app/api/v1/endpoints/auth.py | 197 +++++++----- backend/app/api/v1/endpoints/catalog.py | 63 ++-- .../core/__pycache__/config.cpython-312.pyc | Bin 3025 -> 3593 bytes .../core/__pycache__/security.cpython-312.pyc | Bin 3218 -> 3939 bytes backend/app/core/config.py | 32 +- backend/app/core/security.py | 81 ++--- backend/app/main.py | 16 +- backend/app/models/__init__.py | 44 ++- .../models/__pycache__/asset.cpython-312.pyc | Bin 8866 -> 8992 bytes .../__pycache__/identity.cpython-312.pyc | Bin 5061 -> 6888 bytes .../__pycache__/organization.cpython-312.pyc | Bin 4509 -> 4564 bytes .../__pycache__/system_config.cpython-312.pyc | Bin 1251 -> 1322 bytes backend/app/models/asset.py | 38 ++- backend/app/models/audit.py | 16 + backend/app/models/identity.py | 81 ++--- backend/app/models/organization.py | 9 +- backend/app/models/service.py | 59 ++++ backend/app/models/system_config.py | 14 +- .../__pycache__/auth_service.cpython-312.pyc | Bin 15627 -> 17552 bytes .../social_auth_service.cpython-312.pyc | Bin 0 -> 4053 bytes backend/app/services/asset_service.py | 122 ++++++-- backend/app/services/auth_service.py | 94 +++++- backend/app/services/search_service.py | 61 ++++ backend/app/services/social_auth_service.py | 92 ++++++ backend/app/workers/catalog_filler.py | 35 +++ backend/app/workers/catalog_robot.py | 60 ++++ ...5_enforce_system_parameters_primary_key.py | 186 ++++++++++++ ..._security_hardening_v2_slugs_and_tokens.py | 212 +++++++++++++ .../versions/8370c73114b6_add_audit_log.py | 186 ++++++++++++ ...5b2a560e599_asset_system_v2_and_catalog.py | 204 +++++++++++++ ..._add_service_specialization_and_postgis.py | 190 ++++++++++++ ...tem_parameters_primary_key.cpython-312.pyc | Bin 0 -> 19772 bytes ...dening_v2_slugs_and_tokens.cpython-312.pyc | Bin 0 -> 23246 bytes ...8370c73114b6_add_audit_log.cpython-312.pyc | Bin 0 -> 19724 bytes ...sset_system_v2_and_catalog.cpython-312.pyc | Bin 0 -> 22258 bytes ...specialization_and_postgis.cpython-312.pyc | Bin 0 -> 20278 bytes ...fd8ac8_add_social_accounts.cpython-312.pyc | Bin 0 -> 21392 bytes ...org_to_asset_and_fix_slugs.cpython-312.pyc | Bin 0 -> 19774 bytes ...te_system_parameters_table.cpython-312.pyc | Bin 0 -> 20954 bytes .../b14d05fd8ac8_add_social_accounts.py | 200 +++++++++++++ ..._add_current_org_to_asset_and_fix_slugs.py | 186 ++++++++++++ ...996357ac_create_system_parameters_table.py | 194 ++++++++++++ backend/requirements.txt | 7 +- docker-compose.yml | 17 ++ .../20_Service_Finder_&_Trust_Engine.md | 42 +++ docs/V01_gemini/21_DEEP ASSET CATALOG | 7 + docs/V01_gemini/22_ROBOT ÖKOSZISZTÉMA | 41 +++ docs/V01_gemini/_00_gemini_gem_kód | 281 ++++++++++-------- 53 files changed, 2756 insertions(+), 426 deletions(-) create mode 100644 backend/app/models/audit.py create mode 100644 backend/app/models/service.py create mode 100644 backend/app/services/__pycache__/social_auth_service.cpython-312.pyc create mode 100644 backend/app/services/search_service.py create mode 100644 backend/app/services/social_auth_service.py create mode 100644 backend/app/workers/catalog_filler.py create mode 100644 backend/app/workers/catalog_robot.py create mode 100644 backend/migrations/versions/0fa011f29e35_enforce_system_parameters_primary_key.py create mode 100644 backend/migrations/versions/12607787ed0b_security_hardening_v2_slugs_and_tokens.py create mode 100644 backend/migrations/versions/8370c73114b6_add_audit_log.py create mode 100644 backend/migrations/versions/85b2a560e599_asset_system_v2_and_catalog.py create mode 100644 backend/migrations/versions/9b20430f0ebb_add_service_specialization_and_postgis.py create mode 100644 backend/migrations/versions/__pycache__/0fa011f29e35_enforce_system_parameters_primary_key.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/12607787ed0b_security_hardening_v2_slugs_and_tokens.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/8370c73114b6_add_audit_log.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/85b2a560e599_asset_system_v2_and_catalog.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/9b20430f0ebb_add_service_specialization_and_postgis.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/b14d05fd8ac8_add_social_accounts.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/b69f11d8b825_add_current_org_to_asset_and_fix_slugs.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/f2d8996357ac_create_system_parameters_table.cpython-312.pyc create mode 100644 backend/migrations/versions/b14d05fd8ac8_add_social_accounts.py create mode 100644 backend/migrations/versions/b69f11d8b825_add_current_org_to_asset_and_fix_slugs.py create mode 100644 backend/migrations/versions/f2d8996357ac_create_system_parameters_table.py create mode 100644 docs/V01_gemini/20_Service_Finder_&_Trust_Engine.md create mode 100644 docs/V01_gemini/21_DEEP ASSET CATALOG create mode 100644 docs/V01_gemini/22_ROBOT ÖKOSZISZTÉMA 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 de9f99739227cef9096a372ceb49bb574c2cfe4b..efe3ef5eeeb79427de06f56c5c77856e8ae839bd 100644 GIT binary patch delta 840 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 PW+#vvnTH6LAl4rMcQOGZ 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 03380350bb4b50478f0601979bdcf7ddff2d6650..eb002517595b44c093851692f3e4240976d3565b 100644 GIT binary patch literal 10197 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 literal 6867 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 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 0da2b5049fb190a40c676878c1a22dcb3e8dfb88..3d05279a0a95009d47bb0534d0afc831284393fe 100644 GIT binary patch delta 1426 zcmZ`&PfR0K7@xM&(w09fQfN!NFcexSE$srr3bL3|hEjK!lFs02HI75)QAVbe%uGoj z#6qHp>w3V<1pU;8EiBcHZ~tK*d1XU3=1!T7GVNJgh>!tvkSLB zu;!v>42%db3sb_h@XA*+U*d2kxN@C6NzZe035MiWvXqYLFQhV=6h(4LDoxTXm!8*M&ngU=%#!Tx_;b3(`7JTNJsRiB z>UuoSOVYcme10QdD3uBl&^xYrB`9hxk-UpG-L+4|i{fTk1$rlyNu_Bnn_x+fN-w2Z zEcF~Z6L4+VHc_#b zA49=OTrLW1R8M3A6n2PJP(7BU>ep)ZSWyX{5O|f3{d*nU7BBNfpaNMTV0cx6U-eXq z{nuuZhXqE2CyLdR0C$KTs_W<)zbI#8~oOLsBm3e ztsbjH^+X{Au*R1qb)*DOApGBdcZwk?_#B9BUMj=qlFckV?|k;Mdw69aKMZ2M|~h1QCP~3?djp z5JnI|5JfP8Aco);0tW*0^T@AiOO77@EOMK#0A0TF*o0vpVn26!wfl~geSDuf?62cv zTEj8v3fJ)7M;&!M+z5th_{}4wj)ye2Q>)yr;hl$&O`hAZo4qyMb(pW?Jq;pI!#zi^ zJkaQ6Yq-d2QO?ltiDHk=NL5KD2Dl-F-+}k?oXs% z1Ta^q(e7?^_ch$UMqAfYht*{}pyXEeYJ%_$nbQ7tzTeLv#gNg6x_zct63P;omEVFC hPKYUS?P0Uumok}bAMHKGkiE#7u@={Z=>xk_`41B;iy{C3 delta 827 zcmYk2&rcIk5XaxL+p^2H7@^QYv0W&otU_A~C80qK==y^|(Sm3;nA-3jQt7YkE+i$W zkYJ*6!Mqq_!hyS93>Ops0TM4Yy_hQD>cInz96b8o3XymC%*=P@%}nNH+gE4(7rVU< zkSFYHrL+9UdX`~&_8OQ$#&1JBY#b&j=ulmVMoly_y=GH)sx3HMGxW@ZY%MgyL&?;= zsu%s&+D)xPl~6OO@SRk&pQ-3P(K^m(epBmGedfG?8AV_>>Vz_~!HdWbFCqT|4|_H( z9<{3mpaATJK^TJJ4}I@BvJH7^chM{Nko&+ zmGjpVQ}G!kel0mRI;|w{CMNJ}>pBB)Lv2)K=}|T;YgWkOL~V4Oi?5QR2{K7qNoAH% z^0A)JNunWhqAd3QK^W@cm+dbl18m zWc-;&ZDHc-d4iwd&pJ4&;RW{(hpW^wS9UzW!$MqS1oyYb)>rW(mceg?9Y)5Hx_7fq z@}t70bB5-kz39%Go8kh6heD!gr?60TQ1~c1Df|>&6aodky%xn+M2}~Hy5cE#;^<2Z z0M9|y3YuGSQM|!N*C)1pWo`iPi-SU>#Cdlc%UlFE*l=&O#5HZ}Bv-Wx7WeMj5g_K1 zZkqjR)0F0tzo}YvVY}UFA3q%>pVP*0pZ)3mTU0S+x=V#0&gS8A290QUNJv%zdM^Pv q2*Z2_!U1p{0_hNp902zL2pw}njP=FZF(9V8!T_6NE4IO#?fwFBw8h*2 diff --git a/backend/app/core/__pycache__/security.cpython-312.pyc b/backend/app/core/__pycache__/security.cpython-312.pyc index 45a8a51055c1f46cec4579a79c5a53dd04038c53..69906b25b1719aadcc0e6b02b3b98005db312353 100644 GIT binary patch literal 3939 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 v&M*P0zC zI5MZGx=N%klvZj$ibRn55CoM@s&u-(QH4sC`hpvzm~ARl^>Pq5p8EUhENIHva)+9$|z99c57xWrd`WbtD}E zmpODXDf$_a5c{~ zBd6+?)TgDHCHCZsgK>u?W~Q0t{@L}B1f>Rz3zpj?jA^-Xa~;Fd>(@B>h?C=-L^}wOv;z)Q zFEVsQsGGk5GLK4vDHH_{VnSUZ)2Lk&mQaRYYDdA2C$(86;hV>w)YcqEM-byDuuD6l zqJTzGQJ6-z#8EUY#Kl4%3@!iOG-n9h#>nZ*e`Fa|Q593m0-0I;cRRK`@j>!9)OMMK_DhStfIVYJE`XLc1;_UoHR+gKs8qE$``E=Jij(p+_4=4Y{lPE@$XypA1?b3FP>TPf3WEk_josu z=<$C2){XqJ|02N|dj<1FXy2LKuzq#=N9fO<9;f&hNdRiO>!7E%a04xlk&W$dp)>z2 zm_7j=UTzBw*h^JeBL(bTjeNC_g>CHX=w>|Mq&JM>j=~AhxV-$BQ8Ho7%y3+5K<eveCKOgNZ;V4SlO*Qh~Ak8w$O9DJw#UMW76 zo&@<2gM&*gD{UWs{(}|Y*;U_#vhTt( z*NU(Ih19lxakZuK{HX2S-Vu#~8=YE}31h z*!G*y-Li=4?j=Viyggd)mBjDjOxS_N z1+ji#0kKGZ{R|JtJ4(sXaPsCIgTOY7w*LR_4M{8 zhKExlS1%<7Q;8eb28I%;D+7a{j3kCFxsd0{mU8c}x}5bn31YQVY%u+maod=6l7qNpMt-zQuX0TniAL}Rej!+{k*gnbdEwb z7l6#74KE7E<~{d3zw^xwymCd?!fo^W@9kfhc&IS#w9h&Z;|A5z4fC= z^L)p>j{Boe-K)pXm5-l$erxsOX!+vk%ckV)rHZflh15*H4+pfI_NjLd*wtkG?P-{8 z1%=F38iloMrD1fJ@Gvfbi`Gq9C<%5lu73#8Rx)-pB;yWPm|L}da%BpvXGR?(8Mge` zBnl8^O4T(CX?OXNsdj;7dABUf9E{6Vljwv+Y%`+Wymx^UzAZ%TK4>ggUM=c|ir)*` z{%TaOA>9vp=b)N=G9WEU&gfcG%zpS!{?NbLex}@h=J}1)#PxFG`is6BFPld1xGJIM zJHfBld=Z{cqpv~{o=>CIeEQ2DzL!tq!eBgV`#{y`Z6ANf77p2170D?QgN(PY$;ZW@Mee--QmWZN(0S9^?X z3#ppA`y==c7Ie~0`B#%C4D$;haCkv@jZT%(sn_Ue86Ev63cN-K{)O7X?jy~WSW6|^ zTnR@ifl$RCtc1c`5)8eGc>Ru9_eQH%h%NSRAZWfkVmHs)H#xV_DhdO_q6q&FXqUuI zyM5!02rc*V;^`%E@$7@#1_JiW8_%@z`D=Fn#V=BuT(;363(@;98Jb1+QgkW(`RS#J O#|O&F*|%I~-|K%mPv@!t 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 5e69cee080b3c7bf5a133b2868502e47d4253fed..6ebea6c765be9919d2cf0e93a3c360ac7da63f53 100644 GIT binary patch delta 3680 zcmbVPT})%g6}}f6+kmhC{|)wqg#~YR3HuYGO?D};i`l?pmL_RcZxV9Dy#e!MZ^t&0 zNgA_NQl-_ZC7p-Tv{fN#m8I*dp-9oL`p^ffPi-nBcmO|CLVZGtT5aUK1HRo}t-PC%DjUzxSgpc&wX5sz$^g@Prok=~p&Vk?>Z z(rG1gMaoFCsmbghtTs5CdZoBJn@Ee(#Z@x*m-QJOEN)C@(_+U6P&1Nvrbm3P`1@PH zKxN4lK5#sSgIyLXEEsCh%urpgN@#kTk1-?+5EOLq7#TVwfoo-4_!!rg6C=%}30#nU8em{Dr^ty)yo((yCnDml z;@YfSTutW0Gh*L@So~-vm0n1s7c#}QIZ2$8Ca0xNat`!~3xH;gy*0#7_gn!z1h|hr zFoye{f`O7jn#d&Pl{LO($|UBLg%spV5C)J)C9}6=DWedKHWEg0^C+2=l8FT=lSn0L zgDJ?1wB0nOi$zY-hhev&4JA*~f+@_m)4!Seecp*IAxbhco;sp9o{*_f7j`=56_R*0 z0Z9n6jn65WsT4g`*KH5sQVmBmsa$vINpc=1XW8-uw(y7;G(sai2I+xmII!btJf4Pq zO7%dJ`+W}$oHe-I zbH8KTa%FLF*AZHNW6ROGI80aSV|BL&Rl}Pw&1tA%iyx$)Hgrf&g6W@I;E<(G35#92 zGa-Y6E&;udr@oJMTHWb5w$KT&h>J91rv>2zf_m)O!h3;Wj+1tTQTm!W8@h(gs?)=A zN)Rqni|~B|JJvyF-5-R1^F#F47BN4GGH4>1LP#JmgLGhW3HQ&i~(HR5BsV(jM!zN7_Dw!wA>tZ>)Dz+lSEhjZs^K=VLTy4^jeY>0sTea7X#;5ZC|vO#n6I>s}DY^kAN&7Y1GyI_?VC1cgd{Cg9tkLDsGW zcAr!pBJ7<2<5su=%#}(CecN;k{Z}BE7qn{2u7zqz(5lxY*wp|MF4X5C#s_gwt7s7z z_p^QgFep1J(NVB#aihKmoU*In);RS&;HgN*)%U>R`#{Y3pel^eH-e4%FAtH|(egV8 z(+F4~_gQ(?Vn?k%>=Y+ZM2i^mJ?O^Z8PJS$3&IJ6FGf)FmxK^Xvuof7^lIaXnHZ6< zBHW}OH$L8lrHK3xpk$Bkw#DeAqRhHo}O;{BejK|RU>Ew9l-ycekgR)2cZ}7 zpAZ2&V4*j{5vx@@M_bMovD3H1r{e4;c?Xx%4Ltu%HB;Dif=9D&060i4b~aa`AB)(( zV)yDTN9|%y7pt=`f>w1(`~o=y^h0_I;7xAF)k@c`e)_(6?xfxObJN|{ZQtYfJMOps zx#K~{pH6Mt2W}6kw>2t8LgWUuHowk~(mT!F!^~NOz_a^@m28%BEJWFPvE{1lD48zJ zrR40a(nCG~T^|0@Kc+%UOVGhzUb;}`;8X4h^ZoqWSIZoJ%l$lkrls`>GvBo&mpS;9 k8%_MvORh2ppR&u&w=C()9DK?rjeL|}y6_o?UuO7!0gI{uf&c&j delta 3152 zcmbVOUu;ul6z}cYt?hdIZ~ediTi1nN)Q!P}zy>(jy179?U?5XcyWPuH-CA?o4N8a` ze_|97M$UsHYJwVNWHlM?L5wlJkZ9xu+#cN37kw~Uj0j0hlo$QZ*FW3JFvKSJcfaqP z^PPMCedqK_=)-p1Etb{D$nU3j-il=$*L91G{H_dcIb9*4oLBIQjB>gR`7&8=gOZEo3Hxe}Qs)Ck`dRm@8Uqv`( zUc*;rv_+aF&2l7QR}n)Lc|$7EQ1WPa z&5}%}vS^eR`2yj=?{@cLblbpd3z9`CbX1A1X4oPPT`bkCTgEr%`4*BaJ$PN5%}_jR zFX3$%a}bJ?H}OoSswh=WX=dI+zH2Qe)!@`L~ zTo9e$Ra<->Wb0|>L$P=?-XDt&B_(APOM#P%Ug%Rl*CZJZ3WKr!Az{xkaxgguBkaXhpDQe~s| zXPt9$+l}D7Zs&Lh)M~@F@s0(1;C9`nZ`JemeRmX=+-F=cqjUgh=a^O)V1rB(Ot99j z&E#{7CIq}NIWwNe6$UEInOAof1@O{~SlnVALO0xFM?5=lS>jqN(h%z5jP7(f^|_TA z6VU&`w1L^+8;hb$6G9(CKLRzVnXDGE|48yzh8LAc7YGcHhx`sdle73RS?4XTdVGuV zkP6`_%o;jZwY?452%EuSJfE|@4Q-#jY2+BD6YiQkCB}!YF`}KEc);?cwbYL!Nd?u% zc+r8XoCsSHXu^-+ik76JhscFF$N#XNISS{h_HO7wT?Y|Z1k8k_z^|9)qZ8Q(ZNQp8 z&iT}dJ`s1mb6&NQ>!u@}qO#C?$X@@l5B~91k1e%O+J{Od$*HBrYa(3iN>w~t(8TL9 z`eIMPu4Lg2yfI@c((v0BzN#SKYfGCsi`b5?7|9idKC5$#R>?;60ZSz$juL!ep|MVg z1JQWwL^Ksk#A(cFq%r1;7-X>p0V^sO{a#$r=xg^ZI~nHe|B(f%s2fl2LfDN!FF?MM z%4`9q2ZFZRXAU51DHq!1)|KW2U8H{QBa7r!0%v7#!S+cmJMEYq*l+)h3Bh)Ud({vu z(l)YGWVF6sK(%Obn1s=Xn;HZ^LauG$ir(Z$yM!W20+L1;72>H#3Vw7nta@8QD2fn) zpfi8dLuk7^B%i(I+{3_M)$HD4b}jh|F_J3K6>6Z!#ah7nG4d>Le28nybM3qW+!dP# zltOV<_TpSp3OI*6yt$xn+3>*0<0Dhcnz0r1_TrH7@%Dmrub=K6#eHqs$yJLT@RiH8 zYWT%*^!*sZn+QCDKwyzZH-W2iq!mjt*AQr$UPJ~htkbwUP9V$$9+j{s9xG$CGoOL9 zdv}XuhzqGmbT~0WvNx3Ij}A$y(P%0nMpJ@h5{~x|M&knl-9|=|0xwpe6?%jo_}<;H z4tt6CIsvJi7s*_GsHJqE078sB$qmrx`K;0AzC3WHZ>r|q_?+#j+Y0l7-g0SRGJ3@b z!*lw;c;|w{`$5H&e5&;$-JGN0w!#X(cn;+1y#s58k&$lXhNO2?F0k;ox5ieikzV?z zk7S6Y|7h-eIsMW<8e`hXnSk@HjnJWT0nILJ@7b5T_;j++p z^ed(weCpfQj9ph!VhysQ@$+zL0-|u9ZtrJaI8F^(r z7G^*5Mq5^f?`%DM;}1Mt$*h~;voi9^I#o>bggGlCudLb3_$L%u8F^)&R52lDqUle< J2r<;|e*tz(go*$F diff --git a/backend/app/models/__pycache__/identity.cpython-312.pyc b/backend/app/models/__pycache__/identity.cpython-312.pyc index 4e6e0e8563f64bec889e99bdc929300f59cbe154..765bf7249de7c79c01506a11c0bebf0d18ecd961 100644 GIT binary patch literal 6888 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 Pd-(_b|G{C(;PJl!VWXq< delta 2292 zcma)7Urbw77{9mwp}lQy>FrC%VMJ6Ikl|g!K~^1-S7LI z@7#0G_x-+ef9v|8$@-Av${G0iSKo`jX`8vy zL2Z4aBIz7=QrnOalCE(V%V?M(Ml`<5h$hLsoT-sKlqdtDTym{z$s!Ch?xn05SW5}( zqbvukwS@Ik)&{J-gsq%sgZv(OSTtzH=hzASHM^yAf{)+Vc=5U>&|3ixB2WgGTnq3n zv&~m2=e8{s%D}dnSHjy?w=%TttXIx&bHd^vs)issWonrh*iUecHaFtXFj9qRUT0wp zejAo`O~D#>!iZiHM5Acj;P&_CA}m~PQMX|&&H~?atpoq=^5gGx<$l=d|Hk~ID;Hqj zQl)4Y`3=YZidDE;KhL(}6@9>P1l|I*;_vmnTs86$$4{UVbB1@?5!o{`1LU~4D9xne zsrd%2#HuR-4~DD)KrWqC zkT+L#+OR1!DbZb=F!p!t+FeUG*Yv7Aq(rj3d&RIOsC=7pE^Bct9ZiQ-OHdiub^2C1 z@9HuyZ?&IQonhrXZa1BF?^--7u8c{wbT1ARRwj#*LTD;AxK`VNH%(tISRm8I{0skO z(e$)BqP1kaSP7G8E7n1@myEmj=kvw!;(Z)+3$+EENpj5MQ`RTCOXfFF&M#J$aDn-%U^VI@ zwTUpTfc3;|0Jww#fP+CTY9Uk;0SI7F2;g%T9l@WPg;6~?InDGOYNy8Ve$4fB9uz1|0s*pn zxdob)2-N?qK;zcP0uiOLC`u6P8+Up(L#MXtPe15X?Jp>iKMjnp_KqQa_EsXC4;BM?&I1x9n*ml;(Z{mMwzCM>_QGKQ~h+3J+xthXkuyL;(g z>sHsW>W#n?c6s-zX{|x^wBccsExOBjSDG`&RPKZ_n5Fj7`&}v*0S}tMpLyy2Wz{mG z3}ihGn}K5w465hCrcTKE8q;sxzpDD8k96)rhmZ}E4#AW5AKS;k$r)dxWswg`;Rz?k zQn3(i?oeC|vZw{ec=x~vOxh!r$!Ui#4;HjE2`(ZD0sj4#c`2cIG}#i3`-57!+>ZSAZI+=fAMU8cp%8;OtUkh1oD+=L+p|7MnT}D z6A(#CH0XY=y_5Kp1bR;p>;uT@Q?sv1Gsr}UBmvrJ<&>RB&0jB`x@4%JZv5C;-ErW| zC7D75M&Xh_03dk_mP#7;2vL03<~zKvuX_QNKMlvN)wy&fEveR)#i2r;$b*7as25-H zjyiHy`9>m^n3|T7^PyRkL{|Kjw_(8sF?2J}j6*0QjhN$O*RO}f$mL{eWFcHm+}nU@{NA>0(+}7&yaim Xq>1e&BXG+*%h=Wx-CqpcXfpl<5<(Rr diff --git a/backend/app/models/__pycache__/organization.cpython-312.pyc b/backend/app/models/__pycache__/organization.cpython-312.pyc index e92395d8935e3847e43210030509f06faf22c4d6..7b5c6d5d6fa98d37902a4da599dff851ccf29b99 100644 GIT binary patch delta 1254 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 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 2ed4e667707387fe3f57493b1e77368b4252cb5b..31580d8fd47efe6a7defd98ebf260c922da65d64 100644 GIT binary patch delta 6569 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 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 0000000000000000000000000000000000000000..1ddb8462bcdcadcb90fa83332b9d04c5a102b864 GIT binary patch literal 19772 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 V{E$CuG_D$dyp{)_?-?Zx{y%@FpX>kt literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a6cb8ca8bbe34b48dfeceaaccc0b5ddce95553e7 GIT binary patch literal 19724 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2684ad76ea61fa1f8617d7539bb7b976de0ad8c5 GIT binary patch literal 22258 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 AC;$Ke literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c499574634df3a6fe560dad6a5dbda804b642cb4 GIT binary patch literal 21392 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bd357bd23243330c4e30a475f904e8fb37dba7ea GIT binary patch literal 20954 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 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