diff --git a/.env b/.env index 467c423..6864797 100755 --- a/.env +++ b/.env @@ -29,4 +29,65 @@ SENDGRID_API_KEY=SG.SENDGRID_API_KEY=SG.XspCvW0ERPC_zdVI6AgjTw.85MHZyPYnHQbUoVDj FROM_EMAIL=info@profibot.hu # Biztonsági kulcs a tokenekhez (KÖTELEZŐ!) -SECRET_KEY=2dca2ff3bf9b8184e14038d5d08e646b31bd4a5f5ffc7e19d28e294f3bb3760b \ No newline at end of file +SECRET_KEY=2dca2ff3bf9b8184e14038d5d08e646b31bd4a5f5ffc7e19d28e294f3bb3760b +_______________________________________________________________ +# ============================================================================== +# 🛠️ INFRASTRUKTÚRA (Docker & Database) +# ============================================================================== +# Adatbázis alapok +POSTGRES_USER=kincses +POSTGRES_PASSWORD='MiskociA74' +POSTGRES_DB=service_finder + +# Kapcsolati URL a Python számára (Központi shared-postgres) +DATABASE_URL=postgresql+asyncpg://service_finder_app:MiskociA74@shared-postgres:5432/service_finder + +# Migrációhoz használt URL (Alembic számára) +MIGRATION_DATABASE_URL=postgresql+asyncpg://service_finder_app:MiskociA74@shared-postgres:5432/service_finder + +# Redis elérés +REDIS_URL=redis://service_finder_redis:6379/0 + +# ============================================================================== +# 🚀 ALKALMAZÁS BEÁLLÍTÁSOK (FastAPI) +# ============================================================================== +ENV=development +DEBUG=True +PYTHONPATH=/app + +# Biztonsági kulcs a JWT tokenekhez (Generálj egy hosszú véletlen sort!) +# Példa generáláshoz: openssl rand -hex 32 +SECRET_KEY='2dca2ff3bf9b8184e14038d5d08e646b31bd4a5f5ffc7e19d28e294f3bb3760b' +ALGORITHM=HS256 + +# CORS: Milyen címekről érhető el az API? (Vesszővel elválasztva) +CORS_ORIGINS=https://app.profibot.hu,https://dev.profibot.hu,http://localhost:3000,http://192.168.100.10:3000 + +# Frontend címe a kiküldött linkekhez (Visszaigazolás, jelszó-visszaállítás) +FRONTEND_BASE_URL=http://192.168.100.10:3000 + +# ============================================================================== +# 📧 EMAIL RENDSZER (SMTP / SendGrid) +# ============================================================================== +# EMAIL_PROVIDER lehet: 'smtp' vagy 'sendgrid' vagy 'disabled' +EMAIL_PROVIDER=sendgrid +EMAILS_FROM_EMAIL=info@profibot.hu +EMAILS_FROM_NAME='Profibot Service Finder' + +# SendGrid beállítások +SENDGRID_API_KEY=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 + +# SMTP Fallback (Csak ha az EMAIL_PROVIDER=smtp) +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=info@profibot.hu +SMTP_PASSWORD='SAJÁT_APP_PASSWORD' + +# ============================================================================== +# 📦 MINIO (Fájltárolás - NAS-ra kivezetve) +# ============================================================================== +MINIO_ENDPOINT=minio:9000 +MINIO_ROOT_USER=kincses +MINIO_ROOT_PASSWORD='MiskociA74' +MINIO_ACCESS_KEY=kincses +MINIO_SECRET_KEY='MiskociA74' \ No newline at end of file diff --git a/CHANGELOG.md b/archive/old_other/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to archive/old_other/CHANGELOG.md diff --git a/backup_20260128_alap_kesz.sql b/archive/old_other/backup_20260128_alap_kesz.sql similarity index 100% rename from backup_20260128_alap_kesz.sql rename to archive/old_other/backup_20260128_alap_kesz.sql diff --git a/backup_to_nas.sh b/archive/old_other/backup_to_nas.sh similarity index 100% rename from backup_to_nas.sh rename to archive/old_other/backup_to_nas.sh diff --git a/deploy_v16.sh b/archive/old_other/deploy_v16.sh similarity index 100% rename from deploy_v16.sh rename to archive/old_other/deploy_v16.sh diff --git a/docker-compose_2026.02.01.yml b/archive/old_other/docker-compose_2026.02.01.yml similarity index 100% rename from docker-compose_2026.02.01.yml rename to archive/old_other/docker-compose_2026.02.01.yml diff --git a/init_dev.sh b/archive/old_other/init_dev.sh similarity index 100% rename from init_dev.sh rename to archive/old_other/init_dev.sh diff --git a/api_spec.json b/archive/old_specs/api_spec.json similarity index 100% rename from api_spec.json rename to archive/old_specs/api_spec.json diff --git a/api_spec_v2.json b/archive/old_specs/api_spec_v2.json similarity index 100% rename from api_spec_v2.json rename to archive/old_specs/api_spec_v2.json diff --git a/backend/app/__pycache__/main.cpython-312.pyc b/backend/app/__pycache__/main.cpython-312.pyc old mode 100755 new mode 100644 index 5b314fe..b9bd513 Binary files a/backend/app/__pycache__/main.cpython-312.pyc and b/backend/app/__pycache__/main.cpython-312.pyc differ diff --git a/backend/app/api/v1/__pycache__/api.cpython-312.pyc b/backend/app/api/v1/__pycache__/api.cpython-312.pyc old mode 100755 new mode 100644 index 48aecee..636bd31 Binary files a/backend/app/api/v1/__pycache__/api.cpython-312.pyc and b/backend/app/api/v1/__pycache__/api.cpython-312.pyc differ diff --git a/backend/app/api/v1/api.py b/backend/app/api/v1/api.py index 10d663f..90dbf15 100755 --- a/backend/app/api/v1/api.py +++ b/backend/app/api/v1/api.py @@ -1,12 +1,11 @@ from fastapi import APIRouter -from app.api.v1.endpoints import auth, users, vehicles, billing, fleet, expenses, reports +from app.api.v1.endpoints import auth # Fontos a helyes import! api_router = APIRouter() -api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) -api_router.include_router(users.router, prefix="/users", tags=["users"]) -api_router.include_router(billing.router, prefix="/billing", tags=["billing"]) -api_router.include_router(vehicles.router, prefix="/vehicles", tags=["vehicles"]) -api_router.include_router(fleet.router, prefix="/fleet", tags=["fleet"]) -api_router.include_router(expenses.router, prefix="/expenses", tags=["expenses"]) -api_router.include_router(reports.router, prefix="/reports", tags=["reports"]) \ No newline at end of file +# Minden auth funkciót ide gyűjtünk (Register, Login, Recover) +api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"]) + +# Itt jönnek majd a további modulok: +# api_router.include_router(users.router, prefix="/users", tags=["Users"]) +# api_router.include_router(fleet.router, prefix="/fleet", tags=["Fleet"]) \ 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 old mode 100755 new mode 100644 index 07ce30e..7190b55 Binary files a/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc and b/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc differ diff --git a/backend/app/api/v1/endpoints/auth.py b/backend/app/api/v1/endpoints/auth.py index 565aa85..3677062 100755 --- a/backend/app/api/v1/endpoints/auth.py +++ b/backend/app/api/v1/endpoints/auth.py @@ -1,91 +1,34 @@ -from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi import APIRouter, Depends, HTTPException, Request, status from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select, text -from datetime import datetime, timedelta -import hashlib, secrets - from app.db.session import get_db -from app.models.user import User -from app.core.security import get_password_hash -from app.services.email_manager import email_manager -from app.services.config_service import config +from app.schemas.auth import UserRegister, UserLogin, Token +from app.services.auth_service import AuthService router = APIRouter() -@router.post("/register") +@router.post("/register", status_code=status.HTTP_201_CREATED) async def register( - request: Request, - email: str, - password: str, - first_name: str, - last_name: str, + request: Request, + user_in: UserRegister, db: AsyncSession = Depends(get_db) ): - ip = request.client.host - - # 1. BOT-VÉDELEM - throttle_min = await config.get_setting('registration_throttle_minutes', default=10) - check_throttle = await db.execute(text(""" - SELECT count(*) FROM data.audit_logs - WHERE ip_address = :ip AND action = 'USER_REGISTERED' AND created_at > :t - """), {'ip': ip, 't': datetime.utcnow() - timedelta(minutes=int(throttle_min))}) - - if check_throttle.scalar() > 0: - raise HTTPException(status_code=429, detail="Túl sok próbálkozás. Várj pár percet!") + # 1. Email check + is_available = await AuthService.check_email_availability(db, user_in.email) + if not is_available: + raise HTTPException(status_code=400, detail="Az e-mail cím már foglalt.") - # 2. REGISZTRÁCIÓ - res = await db.execute(select(User).where(User.email == email)) - if res.scalars().first(): - raise HTTPException(status_code=400, detail="Ez az email már foglalt.") + # 2. Process + try: + user = await AuthService.register_new_user( + db=db, + user_in=user_in, + ip_address=request.client.host + ) + return {"status": "success", "message": "Regisztráció sikeres!"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Szerver hiba: {str(e)}") - new_user = User( - email=email, - hashed_password=get_password_hash(password), - first_name=first_name, - last_name=last_name, - is_active=False - ) - db.add(new_user) - await db.flush() - - # 3. TOKEN & LOG - raw_token = secrets.token_urlsafe(48) - token_hash = hashlib.sha256(raw_token.encode()).hexdigest() - await db.execute(text(""" - INSERT INTO data.verification_tokens (user_id, token_hash, token_type, expires_at) - VALUES (:u, :t, 'email_verify', :e) - """), {'u': new_user.id, 't': token_hash, 'e': datetime.utcnow() + timedelta(days=2)}) - - await db.execute(text(""" - INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address) - VALUES (:u, 'USER_REGISTERED', '/register', 'POST', :ip) - """), {'u': new_user.id, 'ip': ip}) - - # 4. EMAIL KÜLDÉS - verify_link = f"http://{request.headers.get('host')}/api/v1/auth/verify?token={raw_token}" - email_body = f"
Aktiváld a fiókod: {verify_link}
" - - await email_manager.send_email( - recipient=email, - subject="Regisztráció megerősítése", - body=email_body, - email_type="registration", - user_id=new_user.id - ) - - await db.commit() - return {"message": "Sikeres regisztráció! Ellenőrizd az email fiókodat."} - -@router.get("/verify") -async def verify_account(token: str, db: AsyncSession = Depends(get_db)): - token_hash = hashlib.sha256(token.encode()).hexdigest() - query = text("SELECT user_id FROM data.verification_tokens WHERE token_hash = :t AND is_used = False") - res = await db.execute(query, {'t': token_hash}) - row = res.fetchone() - if not row: - raise HTTPException(status_code=400, detail="Érvénytelen aktiváló link") - - await db.execute(text("UPDATE data.users SET is_active = True WHERE id = :id"), {'id': row[0]}) - await db.execute(text("UPDATE data.verification_tokens SET is_used = True WHERE token_hash = :t"), {'t': token_hash}) - await db.commit() - return {"message": "Fiók aktiválva!"} +@router.post("/login") +async def login(user_in: UserLogin, db: AsyncSession = Depends(get_db)): + # ... A korábbi login logika itt maradhat ... + pass \ 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 old mode 100755 new mode 100644 index 265415e..48a77db Binary files a/backend/app/core/__pycache__/config.cpython-312.pyc and b/backend/app/core/__pycache__/config.cpython-312.pyc differ diff --git a/backend/app/core/__pycache__/security.cpython-312.pyc b/backend/app/core/__pycache__/security.cpython-312.pyc old mode 100755 new mode 100644 index a801246..5854f36 Binary files a/backend/app/core/__pycache__/security.cpython-312.pyc and b/backend/app/core/__pycache__/security.cpython-312.pyc differ diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 060f4af..2de8503 100755 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,48 +1,61 @@ -from typing import Optional +import os +import json +from typing import Any, Optional, List from pydantic_settings import BaseSettings, SettingsConfigDict -from pydantic import computed_field +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession class Settings(BaseSettings): # --- General --- PROJECT_NAME: str = "Traffic Ecosystem SuperApp" - VERSION: str = "2.0.0" + VERSION: str = "1.0.0" API_V1_STR: str = "/api/v1" - DEBUG: bool = False + DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true" # --- Security / JWT --- - SECRET_KEY: str + # Szigorúan .env-ből! + SECRET_KEY: str = os.getenv("SECRET_KEY", "NOT_SET_DANGER") ALGORITHM: str = "HS256" - ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 nap - # --- Password policy (TEST -> laza, PROD -> szigorú) --- - PASSWORD_MIN_LENGTH: int = 4 # TESZT: 4, ÉLES: 10-12 + # --- Database & Cache --- + DATABASE_URL: str = os.getenv("DATABASE_URL") + REDIS_URL: str = os.getenv("REDIS_URL", "redis://service_finder_redis:6379/0") - # --- Database --- - DATABASE_URL: str # már nálad compose-ban meg van adva - - # --- Redis --- - REDIS_URL: str = "redis://service_finder_redis:6379/0" - - # --- Email sending --- - # auto = ha van SENDGRID_API_KEY -> sendgrid api, különben smtp - EMAIL_PROVIDER: str = "auto" # auto | sendgrid | smtp | disabled - - EMAILS_FROM_EMAIL: str = "info@profibot.hu" + # --- Email (Auto Provider) --- + EMAIL_PROVIDER: str = os.getenv("EMAIL_PROVIDER", "auto") + EMAILS_FROM_EMAIL: str = os.getenv("EMAILS_FROM_EMAIL", "info@profibot.hu") EMAILS_FROM_NAME: str = "Profibot" + + # SMTP & SendGrid (Szigorúan .env-ből) + SENDGRID_API_KEY: Optional[str] = os.getenv("SENDGRID_API_KEY") + SMTP_HOST: Optional[str] = os.getenv("SMTP_HOST") + SMTP_PORT: int = int(os.getenv("SMTP_PORT", 587)) + SMTP_USER: Optional[str] = os.getenv("SMTP_USER") + SMTP_PASSWORD: Optional[str] = os.getenv("SMTP_PASSWORD") - # SendGrid API - SENDGRID_API_KEY: Optional[str] = None + # --- External URLs --- + # .env-ben legyen átírva a .10-es IP-re! + FRONTEND_BASE_URL: str = os.getenv("FRONTEND_BASE_URL", "http://localhost:3000") - # SMTP fallback (pl. Gmail App Password vagy más szolgáltató) - SMTP_HOST: Optional[str] = None - SMTP_PORT: int = 587 - SMTP_USER: Optional[str] = None - SMTP_PASSWORD: Optional[str] = None - SMTP_USE_TLS: bool = True - - # Frontend base URL a linkekhez (később NPM/domain) - FRONTEND_BASE_URL: str = "http://192.168.100.43:3000" + # --- Dinamikus Admin Motor --- + async def get_db_setting(self, db: AsyncSession, key_name: str, default: Any = None) -> Any: + """ + Lekéri a paramétert a data.system_settings táblából. + Ezzel érjük el, hogy a kód újraírása nélkül, adminból lehessen + állítani a jutalom napokat, százalékokat, stb. + """ + try: + query = text("SELECT value_json FROM data.system_settings WHERE key_name = :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: + return default + # .env fájl konfigurációja model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", @@ -50,4 +63,4 @@ class Settings(BaseSettings): extra="ignore" ) -settings = Settings() +settings = Settings() \ No newline at end of file diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 84e8621..fc8cd03 100755 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -6,28 +6,44 @@ from jose import jwt, JWTError from app.core.config import settings -# --- JELSZÓ --- +# --- JELSZÓ KEZELÉS --- + def verify_password(plain_password: str, hashed_password: str) -> bool: + """ + Összehasonlítja a nyers jelszót a hash-elt változattal. + """ try: if not hashed_password: return False return bcrypt.checkpw( plain_password.encode("utf-8"), - hashed_password.encode("utf-8"), + hashed_password.encode("utf-8") ) except Exception: return False def get_password_hash(password: str) -> str: + """ + Biztonságos hash-t generál a jelszóból. + """ salt = bcrypt.gensalt() return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8") -# --- JWT --- +# --- JWT TOKEN KEZELÉS --- + def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: + """ + JWT Access tokent generál a megadott adatokkal és lejárati idővel. + """ to_encode = dict(data) - expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)) + expire = datetime.now(timezone.utc) + ( + expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + ) to_encode.update({"exp": expire}) return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) def decode_token(token: str) -> Dict[str, Any]: - return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + """ + Dekódolja a JWT tokent. + """ + return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index a98d4fd..f14c089 100755 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,45 +1,50 @@ +import os from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import text -import os from app.api.v1.api import api_router -from app.api.v2.auth import router as auth_v2_router -from app.models import Base +from app.db.base import Base +from app.db.session import engine @asynccontextmanager async def lifespan(app: FastAPI): - from app.db.session import engine + # Séma és alap táblák ellenőrzése indításkor async with engine.begin() as conn: await conn.execute(text("CREATE SCHEMA IF NOT EXISTS data")) + # Base.metadata.create_all helyett javasolt az Alembic, + # de fejlesztési fázisban a run_sync biztonságos await conn.run_sync(Base.metadata.create_all) yield await engine.dispose() app = FastAPI( - title="Traffic Ecosystem SuperApp 2.0", - version="2.0.0", - openapi_url="/api/v2/openapi.json", + title="Service Finder API", + version="1.0.0", + docs_url="/docs", + openapi_url="/api/v1/openapi.json", lifespan=lifespan ) +# BIZTONSÁG: CORS beállítások .env-ből +# Ha nincs megadva, csak a localhost-ot engedi +origins = os.getenv("CORS_ORIGINS", "http://localhost:3000").split(",") + app.add_middleware( CORSMiddleware, - allow_origins=[ - "http://192.168.100.43:3000", # A szerver címe a böngészőben - "http://localhost:3000", # Helyi teszteléshez - ], + allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) - -# ÚTVONALAK INTEGRÁCIÓJA +# ÚTVONALAK KONSZOLIDÁCIÓJA (V2 törölve, minden a V1 alatt) app.include_router(api_router, prefix="/api/v1") -app.include_router(auth_v2_router, prefix="/api/v2/auth") @app.get("/", tags=["health"]) async def root(): - return {"status": "online", "version": "2.0.0", "docs": "/docs"} - \ No newline at end of file + return { + "status": "online", + "version": "1.0.0", + "environment": os.getenv("ENV", "production") + } \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index cfd7330..3bffbb8 100755 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,6 +1,7 @@ from app.db.base import Base -from .user import User, UserRole +from .identity import User, Person, Wallet, UserRole # ÚJ központ from .company import Company, CompanyMember, VehicleAssignment +from .organization import Organization, OrgType from .vehicle import ( Vehicle, VehicleOwnership, @@ -13,12 +14,12 @@ from .vehicle import ( VehicleVariant ) -# Alias a kompatibilitás kedvéért +# Aliasok a kompatibilitás kedvéért UserVehicle = Vehicle __all__ = [ - "Base", "User", "UserRole", "Vehicle", "VehicleOwnership", "VehicleBrand", - "EngineSpec", "ServiceProvider", "ServiceRecord", "Company", + "Base", "User", "Person", "Wallet", "UserRole", "Vehicle", "VehicleOwnership", + "VehicleBrand", "EngineSpec", "ServiceProvider", "ServiceRecord", "Company", "CompanyMember", "VehicleAssignment", "UserVehicle", "VehicleCategory", - "VehicleModel", "VehicleVariant" + "VehicleModel", "VehicleVariant", "Organization", "OrgType" ] \ No newline at end of file diff --git a/backend/app/models/__pycache__/__init__.cpython-312.pyc b/backend/app/models/__pycache__/__init__.cpython-312.pyc old mode 100755 new mode 100644 index 4e78382..46a3708 Binary files a/backend/app/models/__pycache__/__init__.cpython-312.pyc and b/backend/app/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/identity.cpython-312.pyc b/backend/app/models/__pycache__/identity.cpython-312.pyc new file mode 100644 index 0000000..3537f4b Binary files /dev/null and b/backend/app/models/__pycache__/identity.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/organization.cpython-312.pyc b/backend/app/models/__pycache__/organization.cpython-312.pyc new file mode 100644 index 0000000..1f38794 Binary files /dev/null and b/backend/app/models/__pycache__/organization.cpython-312.pyc differ diff --git a/backend/app/models/identity.py b/backend/app/models/identity.py new file mode 100644 index 0000000..d439acf --- /dev/null +++ b/backend/app/models/identity.py @@ -0,0 +1,73 @@ +# backend/app/models/identity.py +import uuid +import enum +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum +from sqlalchemy.orm import relationship +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.sql import func +from app.db.base import Base + +class UserRole(str, enum.Enum): + ADMIN = "admin" + USER = "user" + SERVICE = "service" + FLEET_MANAGER = "fleet_manager" + +class Person(Base): + __tablename__ = "persons" + __table_args__ = {"schema": "data"} + + id = Column(Integer, primary_key=True, index=True) + id_uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False) + + last_name = Column(String, nullable=False) + first_name = Column(String, nullable=False) + mothers_name = Column(String, nullable=True) + birth_place = Column(String, nullable=True) + birth_date = Column(DateTime, nullable=True) + + identity_docs = Column(JSON, server_default=text("'{}'::jsonb")) + medical_emergency = Column(JSON, server_default=text("'{}'::jsonb")) + ice_contact = Column(JSON, server_default=text("'{}'::jsonb")) + + users = relationship("User", back_populates="person") + +class User(Base): + __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=False) + + # Technikai mezők átmentése a régi user.py-ból + role = Column(Enum(UserRole), default=UserRole.USER) + is_active = Column(Boolean, default=True) + is_superuser = Column(Boolean, default=False) + is_company = Column(Boolean, default=False) + company_name = Column(String, nullable=True) + tax_number = Column(String, nullable=True) + region_code = Column(String, default="HU") + + is_deleted = Column(Boolean, default=False) + deleted_at = Column(DateTime, nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + person_id = Column(Integer, ForeignKey("data.persons.id"), nullable=True) + + person = relationship("Person", back_populates="users") + wallet = relationship("Wallet", back_populates="user", uselist=False) + # Az Organization kapcsolathoz (ha szükséges az import miatt) + owned_organizations = relationship("Organization", backref="owner") + +class Wallet(Base): + __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) + xp_balance = Column(Integer, default=0) + + user = relationship("User", back_populates="wallet") \ No newline at end of file diff --git a/backend/app/models/organization.py b/backend/app/models/organization.py index 9cfe124..1e59f2e 100755 --- a/backend/app/models/organization.py +++ b/backend/app/models/organization.py @@ -1,5 +1,5 @@ import enum -from sqlalchemy import Column, Integer, String, Boolean, Enum, DateTime +from sqlalchemy import Column, Integer, String, Boolean, Enum, DateTime, ForeignKey from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.db.base import Base @@ -10,11 +10,6 @@ class OrgType(str, enum.Enum): FLEET_OWNER = "fleet_owner" CLUB = "club" -class UITheme(str, enum.Enum): - LIGHT = "light" - DARK = "dark" - SYSTEM = "system" - class Organization(Base): __tablename__ = "organizations" __table_args__ = {"schema": "data"} @@ -23,14 +18,11 @@ class Organization(Base): name = Column(String, nullable=False) org_type = Column(Enum(OrgType), default=OrgType.INDIVIDUAL) - # Új UI beállítások a V2-höz - theme = Column(Enum(UITheme), default=UITheme.SYSTEM) - logo_url = Column(String, nullable=True) + # Spec 2.2: Az owner_id a magánszemély flottájának tulajdonosát jelöli + owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) is_active = Column(Boolean, default=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), onupdate=func.now()) - - # Kapcsolatok - # members = relationship("OrganizationMember", back_populates="organization") - vehicles = relationship("UserVehicle", back_populates="current_org") + + # Kapcsolatok (UserVehicle modell megléte esetén) + vehicles = relationship("UserVehicle", back_populates="current_org", cascade="all, delete-orphan") \ No newline at end of file diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 7b20eff..eebc867 100755 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -1,34 +1,6 @@ -import enum -from sqlalchemy import Column, Integer, String, Boolean, Date, DateTime -from sqlalchemy.orm import relationship -from sqlalchemy.sql import func -from app.db.base import Base - -class UserRole(str, enum.Enum): - ADMIN = "admin" - USER = "user" - SERVICE = "service" - FLEET_MANAGER = "fleet_manager" - -class User(Base): - __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=False) - first_name = Column(String) - last_name = Column(String) - birthday = Column(Date, nullable=True) - role = Column(String, default=UserRole.USER) - is_active = Column(Boolean, default=True) - is_superuser = Column(Boolean, default=False) - is_company = Column(Boolean, default=False) - company_name = Column(String, nullable=True) - tax_number = Column(String, nullable=True) - region_code = Column(String, default="HU") - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), onupdate=func.now()) +# DEPRECATED: Minden funkció átkerült az app.models.identity modulba. +# Ez a fájl csak a kompatibilitás miatt maradt meg, de táblát nem definiál. +from .identity import User, UserRole # Kapcsolatok # memberships = relationship("OrganizationMember", back_populates="user", cascade="all, delete-orphan") diff --git a/backend/app/schemas/__pycache__/auth.cpython-312.pyc b/backend/app/schemas/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000..fc44ca8 Binary files /dev/null and b/backend/app/schemas/__pycache__/auth.cpython-312.pyc differ diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index d95040c..67f875d 100755 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -1,16 +1,27 @@ -from pydantic import BaseModel, EmailStr, Field +from pydantic import BaseModel, EmailStr, Field, validator from typing import Optional +class UserRegister(BaseModel): + email: EmailStr + password: str = Field(..., min_length=8) + first_name: str = Field(..., min_length=2) + last_name: str = Field(..., min_length=2) + region_code: str = Field(default="HU", min_length=2, max_length=2) # ISO kód: HU, DE, AT stb. + device_id: Optional[str] = None # Eszköz azonosító a biztonsághoz + invite_token: Optional[str] = None + + @validator('region_code') + def validate_region(cls, v): + return v.upper() if v else v + +# EZ HIÁNYZOTT: Az azonosításhoz (login) szükséges séma +class UserLogin(BaseModel): + email: EmailStr + password: str + class Token(BaseModel): access_token: str token_type: str class TokenData(BaseModel): - email: Optional[str] = None - -class UserRegister(BaseModel): - email: EmailStr - password: str = Field(..., min_length=8) - full_name: str - region_code: str = "HU" - device_id: str # Az eszköz egyedi azonosítója a védelemhez \ No newline at end of file + email: Optional[str] = None \ No newline at end of file diff --git a/backend/app/services/__pycache__/auth_service.cpython-312.pyc b/backend/app/services/__pycache__/auth_service.cpython-312.pyc new file mode 100644 index 0000000..0f3c3e1 Binary files /dev/null and b/backend/app/services/__pycache__/auth_service.cpython-312.pyc differ diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py new file mode 100644 index 0000000..99cec23 --- /dev/null +++ b/backend/app/services/auth_service.py @@ -0,0 +1,81 @@ +from datetime import datetime, timezone +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, and_, text + +from app.models.identity import User, Person, Wallet +from app.models.organization import Organization, OrgType +from app.schemas.auth import UserRegister +from app.core.security import get_password_hash +from app.services.email_manager import email_manager + +class AuthService: + @staticmethod + async def register_new_user(db: AsyncSession, user_in: UserRegister, ip_address: str): + """ + Master Book v1.0 szerinti atomikus regisztrációs folyamat. + """ + # Az AsyncSession.begin() biztosítja az ATOMICitást + async with db.begin_nested(): # beágyazott tranzakció a biztonságért + # 1. Person létrehozása (Identity Level) + new_person = Person( + first_name=user_in.first_name, + last_name=user_in.last_name + ) + db.add(new_person) + await db.flush() # ID generáláshoz + + # 2. User létrehozása (Technical Access) + new_user = User( + email=user_in.email, + hashed_password=get_password_hash(user_in.password), + person_id=new_person.id, + is_active=True + ) + db.add(new_user) + await db.flush() + + # 3. Economy: Wallet inicializálás (0 Coin, 0 XP) + new_wallet = Wallet( + user_id=new_user.id, + coin_balance=0.00, + xp_balance=0 + ) + db.add(new_wallet) + + # 4. Fleet: Automatikus Privát Flotta létrehozása + new_org = Organization( + name=f"{user_in.last_name} {user_in.first_name} saját flottája", + org_type=OrgType.INDIVIDUAL, + owner_id=new_user.id + ) + db.add(new_org) + + # 5. Audit Log (SQLAlchemy Core hívással a sebességért) + audit_stmt = text(""" + INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address, created_at) + VALUES (:uid, 'USER_REGISTERED', '/api/v1/auth/register', 'POST', :ip, :now) + """) + await db.execute(audit_stmt, { + "uid": new_user.id, + "ip": ip_address, + "now": datetime.now(timezone.utc) + }) + + # 6. Üdvözlő email (Subject paraméter nélkül - Spec v1.1) + try: + await email_manager.send_email( + recipient=user_in.email, + template_key="registration", + variables={"first_name": user_in.first_name}, + user_id=new_user.id + ) + except Exception: + pass # Email hiba ne állítsa meg a tranzakciót + + return new_user + + @staticmethod + async def check_email_availability(db: AsyncSession, email: str) -> bool: + query = select(User).where(and_(User.email == email, User.is_deleted == False)) + result = await db.execute(query) + return result.scalar_one_or_none() is None \ No newline at end of file diff --git a/docs/1_PROJECT_BRAIN_FLEET.md b/docs/Old_versions/1_PROJECT_BRAIN_FLEET.md similarity index 100% rename from docs/1_PROJECT_BRAIN_FLEET.md rename to docs/Old_versions/1_PROJECT_BRAIN_FLEET.md diff --git a/docs/2_MODULE_STATUS_FLEET.md b/docs/Old_versions/2_MODULE_STATUS_FLEET.md similarity index 100% rename from docs/2_MODULE_STATUS_FLEET.md rename to docs/Old_versions/2_MODULE_STATUS_FLEET.md diff --git a/docs/3_IMPLEMENTED_FEATURES.md b/docs/Old_versions/3_IMPLEMENTED_FEATURES.md similarity index 100% rename from docs/3_IMPLEMENTED_FEATURES.md rename to docs/Old_versions/3_IMPLEMENTED_FEATURES.md diff --git a/docs/4_BACKLOG_FLEET.md b/docs/Old_versions/4_BACKLOG_FLEET.md similarity index 100% rename from docs/4_BACKLOG_FLEET.md rename to docs/Old_versions/4_BACKLOG_FLEET.md diff --git a/docs/5_TECH_DEBT_FLEET.md b/docs/Old_versions/5_TECH_DEBT_FLEET.md similarity index 100% rename from docs/5_TECH_DEBT_FLEET.md rename to docs/Old_versions/5_TECH_DEBT_FLEET.md diff --git a/docs/6_ROADMAP_FLEET.md b/docs/Old_versions/6_ROADMAP_FLEET.md similarity index 100% rename from docs/6_ROADMAP_FLEET.md rename to docs/Old_versions/6_ROADMAP_FLEET.md diff --git a/docs/AI üzemeltetése.md b/docs/Old_versions/AI üzemeltetése.md similarity index 100% rename from docs/AI üzemeltetése.md rename to docs/Old_versions/AI üzemeltetése.md diff --git a/docs/DB_STATE_FLEET_2026-01-28.md b/docs/Old_versions/DB_STATE_FLEET_2026-01-28.md similarity index 100% rename from docs/DB_STATE_FLEET_2026-01-28.md rename to docs/Old_versions/DB_STATE_FLEET_2026-01-28.md diff --git a/docs/Naplócsomag b/docs/Old_versions/Naplócsomag similarity index 100% rename from docs/Naplócsomag rename to docs/Old_versions/Naplócsomag diff --git a/docs/Projekt értékelés.md b/docs/Old_versions/Projekt értékelés.md similarity index 100% rename from docs/Projekt értékelés.md rename to docs/Old_versions/Projekt értékelés.md diff --git a/Promptok gemekhez.txt b/docs/Old_versions/Promptok gemekhez.txt similarity index 100% rename from Promptok gemekhez.txt rename to docs/Old_versions/Promptok gemekhez.txt diff --git a/Service_finder Rendszerspecifikáció es feljesztes.txt b/docs/Old_versions/Service_finder Rendszerspecifikáció es feljesztes.txt similarity index 100% rename from Service_finder Rendszerspecifikáció es feljesztes.txt rename to docs/Old_versions/Service_finder Rendszerspecifikáció es feljesztes.txt diff --git a/_Adatbázis_állalot_napló.txt b/docs/Old_versions/_Adatbázis_állalot_napló.txt similarity index 100% rename from _Adatbázis_állalot_napló.txt rename to docs/Old_versions/_Adatbázis_állalot_napló.txt diff --git a/_Horgony_megjegyzések.txt b/docs/Old_versions/_Horgony_megjegyzések.txt similarity index 100% rename from _Horgony_megjegyzések.txt rename to docs/Old_versions/_Horgony_megjegyzések.txt diff --git a/_Projekt Állapot jelentés.txt b/docs/Old_versions/_Projekt Állapot jelentés.txt similarity index 100% rename from _Projekt Állapot jelentés.txt rename to docs/Old_versions/_Projekt Állapot jelentés.txt diff --git a/_valtozok_konyve.txt b/docs/Old_versions/_valtozok_konyve.txt similarity index 100% rename from _valtozok_konyve.txt rename to docs/Old_versions/_valtozok_konyve.txt diff --git a/lista.txt b/docs/Old_versions/lista.txt similarity index 100% rename from lista.txt rename to docs/Old_versions/lista.txt diff --git a/mappak.txt b/docs/Old_versions/mappak.txt similarity index 100% rename from mappak.txt rename to docs/Old_versions/mappak.txt diff --git a/projekt_terkep.txt b/docs/Old_versions/projekt_terkep.txt similarity index 100% rename from projekt_terkep.txt rename to docs/Old_versions/projekt_terkep.txt diff --git a/docs/teljes_log b/docs/Old_versions/teljes_log similarity index 100% rename from docs/teljes_log rename to docs/Old_versions/teljes_log diff --git a/docs/V01_chatgpt/00_README.md b/docs/V01_chatgpt/00_README.md new file mode 100644 index 0000000..a1a365a --- /dev/null +++ b/docs/V01_chatgpt/00_README.md @@ -0,0 +1,15 @@ +# Master Grand Book v1.0 – Service Finder / Traffic Ecosystem SuperApp + +Ez a dokumentáció a projekt **kanonikus tudásbázisa**. + +Két párhuzamos könyvtár létezik: +- V01_chatgpt – technikai, mérnöki, architekturális megközelítés +- V01_gemini – alternatív gondolkodás, validáció, kiegészítő perspektíva + +Cél: +- Tudás megőrzése +- Döntések visszakövethetősége +- Fejlesztési minőség mérése (kód + beállítás + hibajavítás hatékonyság) +- Új projektek benchmark alapja + +Ez a v1.0 verzió a **baseline állapot** dokumentálása. diff --git a/docs/V01_chatgpt/01_Project_Overview.md b/docs/V01_chatgpt/01_Project_Overview.md new file mode 100644 index 0000000..bb01b21 --- /dev/null +++ b/docs/V01_chatgpt/01_Project_Overview.md @@ -0,0 +1,24 @@ +# Projekt áttekintés + +Projekt neve: Traffic Ecosystem SuperApp 2.0 (Service Finder) + +Cél: +Egy moduláris platform létrehozása, amely: +- kezeli a járművek életciklusát, +- nyilvántartja a költségeket, eseményeket, szervizeket, +- összeköti a felhasználókat valós szolgáltatókkal, +- automatizált adatgyűjtést végez (discovery botok), +- skálázható SaaS modellben működik. + +Fő modulok: +- Auth / User / Organization +- Fleet & Vehicle Lifecycle +- Service Provider Marketplace +- Billing / Credits / Subscription +- Gamification & Social +- Discovery Bots (adatgyűjtés) +- Dokumentumfeldolgozás (OCR pipeline – tervezett) + +Non-goals (v1.0): +- Teljes üzleti automatizmus +- Külső fizetési gateway éles integráció diff --git a/docs/V01_chatgpt/02_Architecture_System_Context.md b/docs/V01_chatgpt/02_Architecture_System_Context.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/V01_chatgpt/03_Dev_Environment_Runbook.md b/docs/V01_chatgpt/03_Dev_Environment_Runbook.md new file mode 100644 index 0000000..f1cf55b --- /dev/null +++ b/docs/V01_chatgpt/03_Dev_Environment_Runbook.md @@ -0,0 +1,19 @@ +# Fejlesztői környezet + +Indítás: +- docker compose up -d + +Alapszolgáltatások: +- API: :8000 +- Frontend: :3001 +- MinIO: :9000 / :9001 +- Redis: belső háló + +Tipikus ellenőrzések: +- API online: GET / +- OpenAPI: /api/v2/openapi.json +- Frontend betölt + +Ismert jellegzetességek: +- v1 és v2 API párhuzamosan él +- .env alapú konfiguráció diff --git a/docs/V01_chatgpt/06_Database_Guide.md b/docs/V01_chatgpt/06_Database_Guide.md new file mode 100644 index 0000000..c6dc3b0 --- /dev/null +++ b/docs/V01_chatgpt/06_Database_Guide.md @@ -0,0 +1,26 @@ +# Adatbázis – Baseline állapot + +DB: PostgreSQL (shared-postgres) + +Séma: data + +Táblák száma: ~55 + +Kulcs entitások: +- users +- persons +- companies +- vehicles, vehicle_models, vehicle_variants +- service_providers, service_specialties +- fuel_stations +- credit_logs, vouchers, subscriptions +- audit_logs + +Migráció: +- Alembic +- Head rev: 5aed26900f0b +- Persons + owner_person_id implementálva + +Seed: +- fuel_stations ~7300 +- service_providers ~7200 diff --git a/docs/V01_chatgpt/07_API_Guide.md b/docs/V01_chatgpt/07_API_Guide.md new file mode 100644 index 0000000..f6e759f --- /dev/null +++ b/docs/V01_chatgpt/07_API_Guide.md @@ -0,0 +1,13 @@ +# API – Áttekintés + +Verziók: +- v1: üzleti modulok (fleet, billing, reports) +- v2: auth és új generációs endpointok + +Elvek: +- JWT alapú auth +- Verziózott API +- OpenAPI dokumentált + +Megjegyzés: +A v1 → v2 egységesítés külön roadmap tétel. diff --git a/docs/V01_chatgpt/13_Roadmap_Tech_Debt.md b/docs/V01_chatgpt/13_Roadmap_Tech_Debt.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/V01_chatgpt/14_Anchor_Log_Timeline.md b/docs/V01_chatgpt/14_Anchor_Log_Timeline.md new file mode 100644 index 0000000..0ddf062 --- /dev/null +++ b/docs/V01_chatgpt/14_Anchor_Log_Timeline.md @@ -0,0 +1,10 @@ +# Anchor Log – döntési napló + +Ez a fejezet rögzíti: +- fontos architekturális döntéseket, +- API-szerződés változásokat, +- adatmodell átalakításokat, +- stratégiai irányváltásokat. + +Cél: +- később visszakövethető legyen, miért úgy épült a rendszer, ahogy. diff --git a/docs/V01_chatgpt/15_Changelog.md b/docs/V01_chatgpt/15_Changelog.md new file mode 100644 index 0000000..8e7f607 --- /dev/null +++ b/docs/V01_chatgpt/15_Changelog.md @@ -0,0 +1,6 @@ +# Changelog + +v1.0 – Baseline +- Mester dokumentum struktúra létrehozva +- DB baseline rögzítve +- API verziózás dokumentálva diff --git a/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md b/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md new file mode 100644 index 0000000..1e9be66 --- /dev/null +++ b/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md @@ -0,0 +1,40 @@ +# 🔐 AUTHENTICATION & IDENTITY SPECIFICATION (v1.0) + +## I. AZONOSÍTÁSI STRATÉGIA +A rendszer szétválasztja a **technikai hozzáférést** (User) és a **valós identitást** (Person). + +### 1. Identitás szintek +- **User (Login):** Email + Jelszó. Csak a belépéshez és a munkamenethez kell. +- **Person (Identity):** Vezetéknév, Keresztnév, Anyja neve, Születési adatok, Okmányok. +- **Azonosító:** Minden Person kap egy globális egyedi azonosítót (UUID). + +### 2. Soft Delete & Re-regisztráció +- **Nincs fizikai törlés:** A felhasználó csak egy `is_hidden` vagy `deleted_at` flag-et kap. +- **Ismételt regisztráció:** Ha az email/név/okmány alapján a rendszer felismeri a visszatérőt: + - Új technikai User fiók jön létre. + - Ez az új fiók a korábbi Person ID-hoz kapcsolódik. + - **Adat-izoláció:** A felhasználó csak az új regisztráció dátuma utáni eseményeket látja. A régi adatok a háttérben maradnak (statisztika, sofőr elemzés), de számára rejtettek. + +## II. BŐVÍTETT ADATTÁR (KYC & SAFETY) +A `persons` tábla az alábbi adatcsoportokat tartalmazza (Progresszív feltöltéssel): +- **Alapadatok:** `last_name`, `first_name`, `birth_place`, `birth_date`, `mothers_name`. +- **Hivatalos okmányok:** Személyi ig. szám, Jogosítvány (szám + kategóriák + érvényesség), Lakcímkártya, TAJ, Adóazonosító. +- **Vészhelyzeti adatok (Safety):** Vércsoport, Allergia, Értesítendő személy (ICE) neve és telefonszáma. +- **Jutalom:** A teljes körű adategyeztetésért 2 hét PRÉMIUM tagság jár. + +## III. JUTALÉK ÉS GAZDASÁG +### 1. Piramis rendszer (3 szint) +Meghívó lánc alapján számolt jóváírás: +- **1. szint (Közvetlen):** 10% +- **2. szint:** 5% +- **3. szint:** 2% +*A százalékok a befizetés pillanatában érvényes admin beállítások alapján rögzülnek a tranzakcióban (Snapshot).* + +### 2. Wallets +Minden regisztrációnál létrejön: +- **Coin Wallet:** Belső fizetőeszköz (Kredit). +- **XP Ledger:** Tapasztalati pontok (Verseny és rangsor). + +## IV. MODERÁCIÓ ÉS VALIDÁLÁS +- **Validált vélemény:** Csak igazolt ott-tartózkodás (GPS) vagy számlafotó után adható. +- **Fellebbezés:** A szerviz kérheti a vélemény felülvizsgálatát, amit a Moderátorok/Validátorok bírálnak el. \ No newline at end of file diff --git a/docs/V01_gemini/05_Security_Model.md b/docs/V01_gemini/05_Security_Model.md deleted file mode 100644 index 41cc9cc..0000000 --- a/docs/V01_gemini/05_Security_Model.md +++ /dev/null @@ -1,17 +0,0 @@ -(Biztonság és Identitás.) -# 🔐 SECURITY & IDENTITY MODEL - -## 1. Identitás Kezelés (Person vs User) -- **Person:** Természetes személy (GDPR alany). `deleted_at` esetén nem töröljük, csak minden személyes adatmezőt (név, email, tel) hashelünk/anonimizálunk, de a statisztikai ID megmarad. -- **User:** Belépési fiók. Egy Person-höz több User tartozhat. -- **Company:** Céges entitás. Tulajdonosa egy Person. - -## 2. Authentication -- **Token:** JWT (JSON Web Token) HS256. -- **Password:** Argon2 hash. -- **Anti-Enumeration:** "Ha létezik ilyen email cím, küldtünk egy levelet" (nem áruljuk el, hogy regisztrált-e). - -## 3. Soft Delete Logika -Minden táblában (`users`, `vehicles`, `events`) kötelező a `deleted_at`. -- **API szinten:** Minden lekérdezéshez automatikusan hozzáadódik a `WHERE deleted_at IS NULL`. -- **Admin szinten:** Láthatóak a törölt elemek is (Audit célból). \ No newline at end of file diff --git a/docs/V01_gemini/06_Database_Guide.md b/docs/V01_gemini/06_Database_Guide.md index 37fae90..0ecfcaf 100644 --- a/docs/V01_gemini/06_Database_Guide.md +++ b/docs/V01_gemini/06_Database_Guide.md @@ -1,6 +1,26 @@ (Az Adatbázis Bibliája.) # 🗄️ DATABASE GUIDE +# 🗄️ DATABASE GUIDE & DATA INTEGRITY (v1.0) + +## 1. Soft Delete & Újraregisztráció Logika +A rendszerben nincs fizikai törlés. A `data.users` tábla az alábbi módon kezeli a visszatérő felhasználókat: + +- **Indexelés:** Az `email` mezőn egy *Partial Unique Index* (`idx_user_email_active_only`) található. +- **Működés:** - Ha `is_deleted = FALSE`, az email nem használható újra. + - Ha a felhasználó törli magát (`is_deleted = TRUE`), az email felszabadul. + - Új regisztrációkor a rendszer új `user_id`-t generál, de ha a KYC adatok egyeznek, ugyanahhoz a `person_id`-hoz kapcsolja az új fiókot. + +## 2. Person (Személyazonosság) - KYC & Safety +A `data.persons` tábla tárolja a banki szintű azonosításhoz szükséges adatokat: +- **Szétválasztott nevek:** `last_name` és `first_name` a pontos azonosításhoz. +- **JSONB mezők:** Rugalmas adatszerkezet az okmányokhoz (`identity_docs`) és vészhelyzeti adatokhoz (`medical_emergency`). +- **Jutalom Trigger:** A profil 100%-os kitöltése (név, szül. adatok, okmányok) automatikusan aktiválja a 14 napos PRÉMIUM csomagot. + +## 3. Economy (Pénztárca & Referral) +- **Wallet:** Minden regisztrációkor létrejön egy rekord a `data.wallets` táblában (0 Coin, 0 XP). +- **Referral Snapshot:** A jutalékok kifizetésekor a rendszer rögzíti a tranzakció pillanatában érvényes százalékot (`commission_percentage`), így a későbbi admin módosítások nem érintik a múltbeli elszámolásokat. + ## Sémák - `public`: Csak technikai táblák (pl. Alembic version). - `data`: Az üzleti logika 55 táblája. @@ -14,4 +34,16 @@ ## Migrációs Állapot - **Eszköz:** Alembic. - **Current Head:** `10b73fee8967`. -- **Hiányzó láncszem:** A `persons` tábla létrehozása és a meglévő `users` tábla migrációja (Ba \ No newline at end of file +- **Hiányzó láncszem:** A `persons` tábla létrehozása és a meglévő `users` tábla migrációja (Ba + +## 4. Regionalizáció és Multi-Currency (EU Scope) +A rendszer fel van készítve az EU-s piacra: +- **`data.regional_settings`**: Tárolja az országkódokat (ISO 3166-1), az alapértelmezett nyelvet és a helyi pénznemet. +- **`data.exchange_rates`**: Napi frissítésű váltószámok (Base: EUR). +- **Valuta Logika:** - Minden költséget a rögzítéskori **helyi pénznemben** (`currency_code`) és az akkori váltószámmal átszámított **EUR-ban** is elmentünk. + - Képlet: $$Cost_{EUR} = Cost_{Local} \cdot ExchangeRate$$ + - Ez biztosítja, hogy a nemzetközi flották egységes kimutatást kapjanak. + +## 5. Dinamikus Paraméterezés (System Settings) +- **`auth.reward_days`**: Adminból állítható egész szám (alapértelmezett: 14). +- **`auth.reward_tier`**: Melyik csomagot kapja (alapértelmezett: PREMIUM). \ No newline at end of file diff --git a/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md b/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md new file mode 100644 index 0000000..45c8dab --- /dev/null +++ b/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md @@ -0,0 +1,52 @@ +# 🏁 REGISZTRÁCIÓS ÉS AUTH PROTOKOLL (v1.1) + +## 1. Hibakezelési Jegyzet (TypeError fix) +A rendszer korábbi verzióiban az `EmailManager` hívása paraméter-eltérést okozott. +- **Megoldás:** A `send_email` hívásakor tilos a `subject` paraméter átadása, mivel azt a szerviz a `template_key` alapján generálja a belső szótárából. + +## 2. Adatbázis Integritás +Az `Organization` tábla bővült az `owner_id` mezővel, amely a magánszemély (Individual) flottájának tulajdonosát jelöli. +- Minden regisztrációkor létrejön egy automatikus flotta. +- A flotta típusa: `OrgType.INDIVIDUAL`. + +## 3. Dinamikus Paraméterek +A regisztrációt követő jutalmak (pl. 14 napos prémium) a `data.system_settings` táblából kerülnek kiolvasásra. +Keresett kulcs: `auth.reward_days`. + +# 🏁 REGISZTRÁCIÓ, MEGHÍVÓK ÉS API PROTOKOLL (v1.0) + +## 1. Regisztrációs Flow (Atomcsapás-biztos tranzakció) +Minden új regisztráció egyetlen adatbázis-tranzakcióban (`Atomic`) hajtja végre az alábbiakat: +1. **User & Person létrehozása:** Alapidentitás rögzítése. +2. **Wallet inicializálás:** 0 Coin és 0 XP egyenleggel. +3. **Privát Flotta (Private Org):** Létrejön a felhasználó saját cége, ahol ő a tulajdonos. +4. **Meghívó feldolgozása:** - Ha `Personal Invite`: Bekötés a 10-5-2% jutalék láncba. + - Ha `Company Invite`: Másodlagos kapcsolat létrehozása a meghívó céghez (Role: Driver/Admin). + +## 2. Meghívó Küldés Logikája (Invitation Engine) +- **Generálás:** Admin vagy jogosult User generál egy egyedi `invite_token`-t. +- **Típusok:** + - `REG_ONLY`: Csak a rendszerbe hív. + - `COMPANY_JOIN`: Meghatározott cégbe és pozícióba hív. +- **Jutalék számítás:** + A jóváírandó kredit $C$: + $$C = P_{amount} \cdot \frac{R_{level}}{100}$$ + *Ahol $P$ a befizetett összeg, $R$ pedig az aktuális szint (10, 5 vagy 2) értéke.* + +## 3. API Végpontok (Baseline v1) +- `POST /api/v1/auth/register`: Komplett onboarding folyamat. +- `POST /api/v1/auth/invite/send`: Meghívó generálása és küldése. +- `GET /api/v1/auth/invite/verify/{token}`: Token ellenőrzése regisztráció előtt. + +## 4. Jelszó Helyreállítási Protokoll (Recovery) +A rendszer két szintű helyreállítást biztosít: + +### A) Standard (Email alapú) +- `POST /api/v1/auth/forgot-password` -> Email kiküldése ideiglenes tokennel. + +### B) Szigorú (Banki szintű / KYC alapú) +- **Végpont:** `POST /api/v1/auth/recover-identity` +- **Kötelező adatok:** Vezetéknév, Keresztnév, Anyja neve, Személyi igazolvány száma. +- **Logika:** 1. A rendszer azonosítja a `Person` rekordot. + 2. Ha sikeres, a rendszer kiküld egy visszaállító linket a Person-höz tartozó **elsődleges telefonszámra (SMS)** vagy a **legutolsó aktív Email címre**. + 3. Sikeres helyreállítás után a felhasználónak kötelezően jelszót kell cserélnie. \ No newline at end of file diff --git a/docs/V01_gemini/10_Billing_Credits_Subscriptions.md b/docs/V01_gemini/10_Billing_Credits_Subscriptions.md index c930070..3171424 100644 --- a/docs/V01_gemini/10_Billing_Credits_Subscriptions.md +++ b/docs/V01_gemini/10_Billing_Credits_Subscriptions.md @@ -1,25 +1,35 @@ -(Az Üzleti Modell - A legfontosabb frissítés.) -# 💰 BILLING, CREDITS & SUBSCRIPTIONS +# 💰 BILLING, CREDITS AND MULTI-CURRENCY (v1.0) -## 1. Előfizetési Csomagok (SaaS) +## 1. Regionális és Valuta Logika (EU Scope) +A rendszer támogatja a többnyelvű és többvalutás elszámolást. Minden pénzügyi tranzakció két értéket tárol: +1. **Local Cost:** Helyi pénznemben rögzített összeg (pl. 45.000 Ft). +2. **Standard Cost (EUR):** A rögzítéskori középárfolyamon átszámított euró érték. -| Csomag | Ár (Havi) | Jármű | User | Funkciók | -| :--- | :--- | :--- | :--- | :--- | -| **FREE** | 0 Ft | 1 db | 1 | Geo Keresés (Sugár), Reklám, Nincs Export. | -| **PREMIUM** | ~1.490 Ft | 3 db | 1 | Útvonal Keresés, Nincs Reklám, Excel Export, Dokumentum Tár. | -| **PREMIUM+** | ~2.990 Ft | 5 db | 4 (Család) | Családi megosztás, Trust Score részletek. | -| **VIP** | Egyedi | 10+ | 5+ | Flotta funkciók, API, Sofőr App. | +**Átszámítási képlet:** +$$Cost_{EUR} = Cost_{Local} \cdot ExchangeRate$$ -## 2. A "Free -> Premium" Szabály (Q10 Solution) -- A Free időszakban rögzített adatok **láthatóak maradnak**, de **nem képezik részét** a Prémium Elemzéseknek (TCO, Trendek). -- **Feloldás:** Visszamenőleges elemzéshez "Retroaktív Csomag" vagy folyamatos előfizetés szükséges. +## 2. Előfizetési Csomagok (Adminból állítható) +A csomagok limiteit (járműszám, funkciók) a `system_settings` tábla szabályozza. -## 3. Kredit Ökonómia (Coin) -- **Szerzés:** Adatfeltöltés (50 Coin), Meghívás (200 Coin), Validálás (5 Coin). -- **Költés:** Prémium előfizetés vásárlása, Skin-ek, Extra lekérdezések. -- **Kifizetés:** Nincs automatikus kifizetés. Nagy mennyiség esetén (pl. Üzletkötő) egyedi szerződés (Megbízási/Számlás) alapján, vagy jövőben Blokklánc (Stablecoin). +| Csomag | Jármű Limit | Kiemelt funkciók | +| :--- | :--- | :--- | +| **FREE** | 1 db | Csak GEO keresés, alap költséglog, nincs dokumentum/export. | +| **PREMIUM** | 3 db | Teljes dokumentum/fotó tár, útvonal alapú kereső, export. | +| **PREMIUM+** | 5 db | 5 felhasználó, flotta-szintű statisztika, TCO elemzés. | +| **VIP** | 10 db + | Bővíthető slotok, egyedi szerviz partnerek kezelése. | -## 4. Befizetési Technológiák -- **Stripe:** Nemzetközi kártyás fizetés. -- **Barion / SimplePay:** Magyar specifikus fizetés. -- **Coin Pack:** Mikrotanzakciók (pl. 500 Coin = 1000 Ft). \ No newline at end of file +## 3. Evidence & Trust Engine (Bizonyíték kezelés) +A rendszer csak azokat a szerviz eseményeket tekinti **hitelesnek (Verified)**, amelyekhez tartozik: +- **Fotó:** Kilométeróra állásról és munkalapról. +- **Digitális számla:** Feltöltött PDF vagy kép. +- **GPS Check-in:** Igazolás, hogy a felhasználó valóban a szerviznél tartózkodott. + +## 4. Szerviz Minősítési Rendszer +- Csak érvényes szerviz esemény után adható értékelés. +- **Fellebbezés:** A szolgáltató kérheti a valótlan/troll vélemény felülvizsgálatát. +- **Validátorok:** Magas rangú felhasználók pontokért/kreditért ellenőrizhetik a vitatott bejegyzéseket. + +## 5. Lejárat és Helyreállítás +- **Grace Period (30 nap):** Csak rögzítés lehetséges, statisztika/lekérdezés zárolva. +- **Zárolás (60 nap):** A fiók írásvédetté válik. +- **Helyreállítás:** 6 hónapon belül visszamenőleges befizetéssel minden funkció (és a Free korszak adatai) aktiválódik. \ No newline at end of file diff --git a/docs/V01_gemini/16_TESTING_AND_DEPLOYMENT_GUIDE.md b/docs/V01_gemini/16_TESTING_AND_DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..68fb2d2 --- /dev/null +++ b/docs/V01_gemini/16_TESTING_AND_DEPLOYMENT_GUIDE.md @@ -0,0 +1,22 @@ +# 🧪 TESZTELÉSI ÉS ÉLESÍTÉSI ÚTMUTATÓ (v1.0) + +## 1. Előkészületek a távoli teszteléshez +Mielőtt elindítanád a teszteket, győződj meg róla, hogy a háttérfolyamatok frissültek: +1. A `.env` fájl mentve van a helyes jelszavakkal. +2. A konténerek újraépítése és indítása: + `docker compose up -d --build` (Ez kényszeríti a Python kódot az új verzióra). +3. Ellenőrizd a logokat: `docker logs -f service_finder_api` (Itt látod, ha hiba van induláskor). + +## 2. Tesztelési Forgatókönyvek (End-to-End) + +### A) Új Regisztráció Teszt (Clean Registration) +- **Endpoint:** `POST /api/v1/auth/register` +- **Adat (JSON):** +```json +{ + "email": "teszt.felhasznalo@profibot.hu", + "password": "nagyonerospassword123", + "first_name": "János", + "last_name": "Teszt", + "region_code": "HU" +} \ No newline at end of file diff --git a/migrations/versions/__pycache__/fba92ed020b1_merge_identity_v1.cpython-312.pyc b/migrations/versions/__pycache__/fba92ed020b1_merge_identity_v1.cpython-312.pyc new file mode 100644 index 0000000..c5bfef7 Binary files /dev/null and b/migrations/versions/__pycache__/fba92ed020b1_merge_identity_v1.cpython-312.pyc differ diff --git a/migrations/versions/fba92ed020b1_merge_identity_v1.py b/migrations/versions/fba92ed020b1_merge_identity_v1.py new file mode 100644 index 0000000..af8356f --- /dev/null +++ b/migrations/versions/fba92ed020b1_merge_identity_v1.py @@ -0,0 +1,945 @@ +"""merge_identity_v1 + +Revision ID: fba92ed020b1 +Revises: 5aed26900f0b +Create Date: 2026-02-04 21:31:43.854642 + +""" +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 = 'fba92ed020b1' +down_revision: Union[str, Sequence[str], None] = '5aed26900f0b' +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_table('user_vehicle_equipment') + op.drop_table('credit_logs') + op.drop_table('votes') + op.drop_table('audit_logs') + op.drop_index(op.f('ix_data_level_configs_id'), table_name='level_configs') + op.drop_table('level_configs') + op.drop_table('vouchers') + op.drop_index(op.f('ix_data_verification_tokens_id'), table_name='verification_tokens') + op.drop_index(op.f('ix_data_verification_tokens_token'), table_name='verification_tokens') + op.drop_index(op.f('ix_verification_tokens_lookup'), table_name='verification_tokens') + op.drop_index(op.f('ix_verification_tokens_user'), table_name='verification_tokens') + op.drop_index(op.f('uq_verification_tokens_token_hash'), table_name='verification_tokens', postgresql_where='(token_hash IS NOT NULL)') + op.drop_table('verification_tokens') + op.drop_index(op.f('ix_data_regional_settings_id'), table_name='regional_settings') + op.drop_table('regional_settings') + op.drop_index(op.f('ix_data_vehicle_ownership_id'), table_name='vehicle_ownership') + op.drop_table('vehicle_ownership') + op.drop_table('user_scores') + op.drop_index(op.f('idx_vm_slug'), table_name='vehicle_models') + op.drop_index(op.f('ix_data_vehicle_models_id'), table_name='vehicle_models') + op.drop_table('vehicle_models') + op.drop_index(op.f('ix_data_email_templates_id'), table_name='email_templates') + op.drop_index(op.f('ix_data_email_templates_type'), table_name='email_templates') + op.drop_table('email_templates') + op.drop_index(op.f('ix_data_points_ledger_id'), table_name='points_ledger') + op.drop_table('points_ledger') + op.drop_table('bot_discovery_logs') + op.drop_table('equipment_items') + op.drop_index(op.f('ix_data_organization_members_id'), table_name='organization_members') + op.drop_table('organization_members') + op.drop_index(op.f('idx_settings_lookup'), table_name='system_settings') + op.drop_index(op.f('ix_data_system_settings_key'), table_name='system_settings') + op.drop_table('system_settings') + op.drop_table('user_credits') + op.drop_table('referrals') + op.drop_index(op.f('ix_data_vehicle_variants_id'), table_name='vehicle_variants') + op.drop_table('vehicle_variants') + op.drop_table('subscription_notification_rules') + op.drop_index(op.f('ix_data_badges_id'), table_name='badges') + op.drop_table('badges') + op.drop_index(op.f('ix_data_legal_acceptances_id'), table_name='legal_acceptances') + op.drop_table('legal_acceptances') + op.drop_table('service_specialties') + op.drop_table('competitions') + op.drop_table('credit_transactions') + op.drop_table('locations') + op.drop_index(op.f('ix_data_legal_documents_id'), table_name='legal_documents') + op.drop_table('legal_documents') + op.drop_table('email_providers') + op.drop_table('subscription_tiers') + op.drop_index(op.f('ix_data_email_logs_email'), table_name='email_logs') + op.drop_index(op.f('ix_data_email_logs_id'), table_name='email_logs') + op.drop_table('email_logs') + op.drop_table('organization_locations') + op.drop_table('vehicle_events') + op.drop_table('vehicle_expenses') + op.drop_table('credit_rules') + op.drop_index(op.f('ix_data_email_provider_configs_id'), table_name='email_provider_configs') + op.drop_table('email_provider_configs') + op.drop_table('org_subscriptions') + op.drop_index(op.f('ix_data_user_badges_id'), table_name='user_badges') + op.drop_table('user_badges') + op.drop_index(op.f('idx_vc_slug'), table_name='vehicle_categories') + op.drop_index(op.f('ix_data_vehicle_categories_id'), table_name='vehicle_categories') + op.drop_table('vehicle_categories') + op.drop_index(op.f('ix_data_user_vehicles_id'), table_name='user_vehicles') + op.drop_index(op.f('ix_data_user_vehicles_license_plate'), table_name='user_vehicles') + op.drop_index(op.f('ix_data_user_vehicles_vin'), table_name='user_vehicles') + op.drop_table('user_vehicles') + op.drop_table('fuel_stations') + op.drop_table('alembic_version') + op.drop_index(op.f('ix_data_translations_id'), table_name='translations') + op.drop_index(op.f('ix_data_translations_key'), table_name='translations') + op.drop_index(op.f('ix_data_translations_lang_code'), table_name='translations') + op.drop_table('translations') + op.drop_table('service_reviews') + op.drop_index(op.f('ix_data_user_stats_id'), table_name='user_stats') + op.drop_table('user_stats') + op.drop_index(op.f('ix_data_point_rules_action_key'), table_name='point_rules') + op.drop_index(op.f('ix_data_point_rules_id'), table_name='point_rules') + op.drop_table('point_rules') + op.drop_index(op.f('ix_companies_owner_person_id'), table_name='companies') + op.create_index(op.f('ix_data_companies_id'), 'companies', ['id'], unique=False, schema='data') + op.drop_constraint(op.f('fk_companies_owner_person'), 'companies', type_='foreignkey') + op.drop_constraint(op.f('companies_owner_id_fkey'), 'companies', type_='foreignkey') + op.create_foreign_key(None, 'companies', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_column('companies', 'owner_person_id') + op.alter_column('company_members', 'role', + existing_type=sa.VARCHAR(length=50), + type_=postgresql.ENUM('owner', 'manager', 'driver', name='companyrole', schema='data'), + nullable=False, + existing_server_default=sa.text("'driver'::companyrole")) + op.create_index(op.f('ix_data_company_members_id'), 'company_members', ['id'], unique=False, schema='data') + op.drop_constraint(op.f('company_members_company_id_fkey'), 'company_members', type_='foreignkey') + op.drop_constraint(op.f('company_members_user_id_fkey'), 'company_members', type_='foreignkey') + op.create_foreign_key(None, 'company_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'company_members', 'companies', ['company_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_index(op.f('idx_engine_code'), table_name='engine_specs') + op.create_index(op.f('ix_data_engine_specs_id'), 'engine_specs', ['id'], unique=False, schema='data') + op.create_unique_constraint(None, 'engine_specs', ['engine_code'], schema='data') + op.drop_column('engine_specs', 'emissions_class') + op.drop_column('engine_specs', 'phases') + op.drop_column('engine_specs', 'default_service_interval_hours') + op.drop_column('engine_specs', 'onboard_charger_kw') + op.drop_column('engine_specs', 'battery_capacity_kwh') + op.drop_index(op.f('idx_org_slug'), table_name='organizations') + op.drop_index(op.f('ix_data_organizations_tax_number'), table_name='organizations') + 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.drop_column('organizations', 'theme') + op.drop_column('organizations', 'validation_status') + op.drop_column('organizations', 'founded_at') + op.drop_column('organizations', 'ui_theme') + op.drop_column('organizations', 'tax_number') + op.drop_column('organizations', 'slug') + op.drop_column('organizations', 'country_code') + op.add_column('persons', sa.Column('id_uuid', sa.UUID(), nullable=False)) + op.add_column('persons', sa.Column('last_name', sa.String(), nullable=False)) + op.add_column('persons', sa.Column('first_name', sa.String(), nullable=False)) + op.add_column('persons', sa.Column('mothers_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('identity_docs', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True)) + op.add_column('persons', sa.Column('medical_emergency', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True)) + op.add_column('persons', sa.Column('ice_contact', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True)) + op.alter_column('persons', 'id', + existing_type=sa.BIGINT(), + type_=sa.Integer(), + existing_nullable=False, + autoincrement=True) + op.create_index(op.f('ix_data_persons_id'), 'persons', ['id'], unique=False, schema='data') + op.create_unique_constraint(None, 'persons', ['id_uuid'], schema='data') + op.drop_column('persons', 'updated_at') + op.drop_column('persons', 'is_active') + op.drop_column('persons', 'reputation_score') + op.drop_column('persons', 'created_at') + op.drop_column('persons', 'risk_level') + op.alter_column('service_providers', 'search_tags', + existing_type=sa.TEXT(), + type_=sa.String(), + existing_nullable=True) + op.create_index(op.f('ix_data_service_providers_id'), 'service_providers', ['id'], unique=False, schema='data') + op.drop_column('service_providers', 'handled_vehicle_types') + op.drop_column('service_providers', 'verification_status') + op.drop_column('service_providers', 'specialized_brands') + op.drop_constraint(op.f('service_records_provider_id_fkey'), 'service_records', type_='foreignkey') + op.drop_constraint(op.f('service_records_vehicle_id_fkey'), 'service_records', type_='foreignkey') + op.create_foreign_key(None, 'service_records', 'service_providers', ['provider_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'service_records', 'vehicles', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_column('service_records', 'invoice_path') + op.drop_column('service_records', 'parts_quality_index') + op.drop_column('service_records', 'description') + op.drop_column('service_records', 'is_accident_repair') + op.drop_column('service_records', 'rating_impact_score') + op.alter_column('users', 'hashed_password', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('users', 'role', + existing_type=sa.VARCHAR(), + type_=sa.Enum('ADMIN', 'USER', 'SERVICE', 'FLEET_MANAGER', name='userrole'), + existing_nullable=True, + existing_server_default=sa.text("'user'::character varying")) + op.alter_column('users', 'is_deleted', + existing_type=sa.BOOLEAN(), + nullable=True, + existing_server_default=sa.text('false')) + op.alter_column('users', 'deleted_at', + existing_type=postgresql.TIMESTAMP(timezone=True), + type_=sa.DateTime(), + existing_nullable=True) + op.alter_column('users', 'person_id', + existing_type=sa.BIGINT(), + type_=sa.Integer(), + existing_nullable=True) + op.drop_index(op.f('idx_user_email_active_only'), table_name='users', postgresql_where='((is_deleted IS FALSE) AND (deleted_at IS NULL))') + op.drop_index(op.f('ix_users_is_deleted'), table_name='users') + op.drop_index(op.f('ix_users_person_id'), table_name='users') + op.create_index(op.f('ix_data_users_email'), 'users', ['email'], unique=True, schema='data') + op.drop_constraint(op.f('fk_users_person'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_column('users', 'previous_login_count') + op.drop_column('users', 'first_name') + op.drop_column('users', 'is_gdpr_deleted') + op.drop_column('users', 'verified_at') + op.drop_column('users', 'is_banned') + op.drop_column('users', 'birthday') + op.drop_column('users', 'last_name') + op.alter_column('vehicle_assignments', 'vehicle_id', + existing_type=sa.INTEGER(), + type_=sa.UUID(), + existing_nullable=False) + op.drop_constraint(op.f('vehicle_assignments_company_id_fkey'), 'vehicle_assignments', type_='foreignkey') + op.drop_constraint(op.f('vehicle_assignments_vehicle_id_fkey'), 'vehicle_assignments', type_='foreignkey') + op.drop_constraint(op.f('vehicle_assignments_driver_id_fkey'), 'vehicle_assignments', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_assignments', 'companies', ['company_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_assignments', 'vehicles', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_assignments', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_index(op.f('idx_vb_slug'), table_name='vehicle_brands') + op.drop_constraint(op.f('vehicle_brands_cat_name_key'), 'vehicle_brands', type_='unique') + op.create_unique_constraint(None, 'vehicle_brands', ['slug'], schema='data') + op.drop_constraint(op.f('vehicle_brands_category_id_fkey'), 'vehicle_brands', type_='foreignkey') + op.drop_column('vehicle_brands', 'country_code') + op.drop_column('vehicle_brands', 'category_id') + op.drop_column('vehicle_brands', 'origin_country') + op.drop_index(op.f('idx_vehicle_company'), table_name='vehicles') + op.drop_index(op.f('idx_vehicle_plate'), table_name='vehicles') + op.drop_index(op.f('idx_vehicle_vin'), table_name='vehicles') + op.drop_constraint(op.f('vehicles_engine_spec_id_fkey'), 'vehicles', type_='foreignkey') + op.drop_constraint(op.f('vehicles_current_company_id_fkey'), 'vehicles', type_='foreignkey') + op.drop_constraint(op.f('fk_vehicle_brand'), 'vehicles', type_='foreignkey') + op.create_foreign_key(None, 'vehicles', 'engine_specs', ['engine_spec_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicles', 'vehicle_brands', ['brand_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicles', 'companies', ['current_company_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_column('vehicles', 'custom_specs') + op.drop_column('vehicles', 'odometer_at_last_check') + op.drop_column('vehicles', 'factory_snapshot') + op.alter_column('wallets', 'id', + existing_type=sa.BIGINT(), + type_=sa.Integer(), + existing_nullable=False, + autoincrement=True) + op.alter_column('wallets', 'user_id', + existing_type=sa.INTEGER(), + nullable=True) + op.alter_column('wallets', 'xp_balance', + existing_type=sa.BIGINT(), + type_=sa.Integer(), + existing_nullable=True, + existing_server_default=sa.text('0')) + op.create_index(op.f('ix_data_wallets_id'), 'wallets', ['id'], unique=False, schema='data') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_column('wallets', 'updated_at') + op.drop_column('wallets', 'created_at') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('wallets', sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True)) + op.add_column('wallets', sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_index(op.f('ix_data_wallets_id'), table_name='wallets', schema='data') + op.alter_column('wallets', 'xp_balance', + existing_type=sa.Integer(), + type_=sa.BIGINT(), + existing_nullable=True, + existing_server_default=sa.text('0')) + op.alter_column('wallets', 'user_id', + existing_type=sa.INTEGER(), + nullable=False) + op.alter_column('wallets', 'id', + existing_type=sa.Integer(), + type_=sa.BIGINT(), + existing_nullable=False, + autoincrement=True) + op.add_column('vehicles', sa.Column('factory_snapshot', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True)) + op.add_column('vehicles', sa.Column('odometer_at_last_check', sa.NUMERIC(precision=15, scale=2), server_default=sa.text('0'), autoincrement=False, nullable=True)) + op.add_column('vehicles', sa.Column('custom_specs', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'vehicles', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicles', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicles', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('fk_vehicle_brand'), 'vehicles', 'vehicle_brands', ['brand_id'], ['id']) + op.create_foreign_key(op.f('vehicles_current_company_id_fkey'), 'vehicles', 'companies', ['current_company_id'], ['id']) + op.create_foreign_key(op.f('vehicles_engine_spec_id_fkey'), 'vehicles', 'engine_specs', ['engine_spec_id'], ['id']) + op.create_index(op.f('idx_vehicle_vin'), 'vehicles', ['identification_number'], unique=False) + op.create_index(op.f('idx_vehicle_plate'), 'vehicles', ['license_plate'], unique=False) + op.create_index(op.f('idx_vehicle_company'), 'vehicles', ['current_company_id'], unique=False) + op.add_column('vehicle_brands', sa.Column('origin_country', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('vehicle_brands', sa.Column('category_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('vehicle_brands', sa.Column('country_code', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.create_foreign_key(op.f('vehicle_brands_category_id_fkey'), 'vehicle_brands', 'vehicle_categories', ['category_id'], ['id']) + op.drop_constraint(None, 'vehicle_brands', schema='data', type_='unique') + op.create_unique_constraint(op.f('vehicle_brands_cat_name_key'), 'vehicle_brands', ['category_id', 'name'], postgresql_nulls_not_distinct=False) + op.create_index(op.f('idx_vb_slug'), 'vehicle_brands', ['slug'], unique=True) + op.drop_constraint(None, 'vehicle_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_assignments_driver_id_fkey'), 'vehicle_assignments', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('vehicle_assignments_vehicle_id_fkey'), 'vehicle_assignments', 'user_vehicles', ['vehicle_id'], ['id']) + op.create_foreign_key(op.f('vehicle_assignments_company_id_fkey'), 'vehicle_assignments', 'companies', ['company_id'], ['id']) + op.alter_column('vehicle_assignments', 'vehicle_id', + existing_type=sa.UUID(), + type_=sa.INTEGER(), + existing_nullable=False) + op.add_column('users', sa.Column('last_name', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('birthday', sa.DATE(), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('is_banned', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('verified_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('is_gdpr_deleted', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('first_name', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('previous_login_count', sa.INTEGER(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('fk_users_person'), 'users', 'persons', ['person_id'], ['id']) + op.drop_index(op.f('ix_data_users_email'), table_name='users', schema='data') + op.create_index(op.f('ix_users_person_id'), 'users', ['person_id'], unique=False) + op.create_index(op.f('ix_users_is_deleted'), 'users', ['is_deleted', 'deleted_at'], unique=False) + op.create_index(op.f('idx_user_email_active_only'), 'users', ['email'], unique=True, postgresql_where='((is_deleted IS FALSE) AND (deleted_at IS NULL))') + op.alter_column('users', 'person_id', + existing_type=sa.Integer(), + type_=sa.BIGINT(), + existing_nullable=True) + op.alter_column('users', 'deleted_at', + existing_type=sa.DateTime(), + type_=postgresql.TIMESTAMP(timezone=True), + existing_nullable=True) + op.alter_column('users', 'is_deleted', + existing_type=sa.BOOLEAN(), + nullable=False, + existing_server_default=sa.text('false')) + op.alter_column('users', 'role', + existing_type=sa.Enum('ADMIN', 'USER', 'SERVICE', 'FLEET_MANAGER', name='userrole'), + type_=sa.VARCHAR(), + existing_nullable=True, + existing_server_default=sa.text("'user'::character varying")) + op.alter_column('users', 'hashed_password', + existing_type=sa.VARCHAR(), + nullable=True) + op.add_column('service_records', sa.Column('rating_impact_score', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True)) + op.add_column('service_records', sa.Column('is_accident_repair', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True)) + op.add_column('service_records', sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('service_records', sa.Column('parts_quality_index', sa.NUMERIC(precision=3, scale=2), server_default=sa.text('1.0'), autoincrement=False, nullable=True)) + op.add_column('service_records', sa.Column('invoice_path', sa.TEXT(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'service_records', schema='data', type_='foreignkey') + op.drop_constraint(None, 'service_records', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_records_vehicle_id_fkey'), 'service_records', 'vehicles', ['vehicle_id'], ['id']) + op.create_foreign_key(op.f('service_records_provider_id_fkey'), 'service_records', 'service_providers', ['provider_id'], ['id']) + op.add_column('service_providers', sa.Column('specialized_brands', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'[]'::jsonb"), autoincrement=False, nullable=True)) + op.add_column('service_providers', sa.Column('verification_status', sa.VARCHAR(length=20), server_default=sa.text("'pending'::character varying"), autoincrement=False, nullable=True)) + op.add_column('service_providers', sa.Column('handled_vehicle_types', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text('\'["passenger_car"]\'::jsonb'), autoincrement=False, nullable=True)) + op.drop_index(op.f('ix_data_service_providers_id'), table_name='service_providers', schema='data') + op.alter_column('service_providers', 'search_tags', + existing_type=sa.String(), + type_=sa.TEXT(), + existing_nullable=True) + op.add_column('persons', sa.Column('risk_level', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=False)) + op.add_column('persons', sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False)) + op.add_column('persons', sa.Column('reputation_score', sa.NUMERIC(precision=10, scale=2), server_default=sa.text('0'), autoincrement=False, nullable=False)) + op.add_column('persons', sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=False)) + op.add_column('persons', sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'persons', schema='data', type_='unique') + op.drop_index(op.f('ix_data_persons_id'), table_name='persons', schema='data') + op.alter_column('persons', 'id', + existing_type=sa.Integer(), + type_=sa.BIGINT(), + existing_nullable=False, + autoincrement=True) + op.drop_column('persons', 'ice_contact') + op.drop_column('persons', 'medical_emergency') + op.drop_column('persons', 'identity_docs') + op.drop_column('persons', 'birth_date') + op.drop_column('persons', 'birth_place') + op.drop_column('persons', 'mothers_name') + op.drop_column('persons', 'first_name') + op.drop_column('persons', 'last_name') + op.drop_column('persons', 'id_uuid') + op.add_column('organizations', sa.Column('country_code', sa.CHAR(length=2), server_default=sa.text("'HU'::bpchar"), autoincrement=False, nullable=True)) + op.add_column('organizations', sa.Column('slug', sa.VARCHAR(length=100), autoincrement=False, nullable=True)) + op.add_column('organizations', sa.Column('tax_number', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('organizations', sa.Column('ui_theme', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('organizations', sa.Column('founded_at', sa.DATE(), autoincrement=False, nullable=True)) + op.add_column('organizations', sa.Column('validation_status', postgresql.ENUM('NOT_VALIDATED', 'PENDING', 'VALIDATED', 'REJECTED', name='validationstatus'), autoincrement=False, nullable=True)) + op.add_column('organizations', sa.Column('theme', sa.VARCHAR(), server_default=sa.text("'system'::character varying"), autoincrement=False, nullable=True)) + 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_index(op.f('ix_data_organizations_tax_number'), 'organizations', ['tax_number'], unique=False) + op.create_index(op.f('idx_org_slug'), 'organizations', ['slug'], unique=True) + op.add_column('engine_specs', sa.Column('battery_capacity_kwh', sa.NUMERIC(precision=10, scale=2), autoincrement=False, nullable=True)) + op.add_column('engine_specs', sa.Column('onboard_charger_kw', sa.NUMERIC(precision=5, scale=2), autoincrement=False, nullable=True)) + op.add_column('engine_specs', sa.Column('default_service_interval_hours', sa.INTEGER(), server_default=sa.text('500'), autoincrement=False, nullable=True)) + op.add_column('engine_specs', sa.Column('phases', sa.INTEGER(), server_default=sa.text('3'), autoincrement=False, nullable=True)) + op.add_column('engine_specs', sa.Column('emissions_class', sa.VARCHAR(length=20), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'engine_specs', schema='data', type_='unique') + op.drop_index(op.f('ix_data_engine_specs_id'), table_name='engine_specs', schema='data') + op.create_index(op.f('idx_engine_code'), 'engine_specs', ['engine_code'], unique=False) + op.drop_constraint(None, 'company_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'company_members', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('company_members_user_id_fkey'), 'company_members', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(op.f('company_members_company_id_fkey'), 'company_members', 'companies', ['company_id'], ['id'], ondelete='CASCADE') + op.drop_index(op.f('ix_data_company_members_id'), table_name='company_members', schema='data') + op.alter_column('company_members', 'role', + existing_type=postgresql.ENUM('owner', 'manager', 'driver', name='companyrole', schema='data'), + type_=sa.VARCHAR(length=50), + nullable=True, + existing_server_default=sa.text("'driver'::companyrole")) + op.add_column('companies', sa.Column('owner_person_id', sa.BIGINT(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'companies', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('companies_owner_id_fkey'), 'companies', 'users', ['owner_id'], ['id']) + op.create_foreign_key(op.f('fk_companies_owner_person'), 'companies', 'persons', ['owner_person_id'], ['id']) + op.drop_index(op.f('ix_data_companies_id'), table_name='companies', schema='data') + op.create_index(op.f('ix_companies_owner_person_id'), 'companies', ['owner_person_id'], unique=False) + op.create_table('point_rules', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('action_key', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('point_rules_pkey')) + ) + op.create_index(op.f('ix_data_point_rules_id'), 'point_rules', ['id'], unique=False) + op.create_index(op.f('ix_data_point_rules_action_key'), 'point_rules', ['action_key'], unique=True) + op.create_table('user_stats', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('total_points', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('current_level', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('last_activity', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('user_stats_user_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('user_stats_pkey')), + sa.UniqueConstraint('user_id', name=op.f('user_stats_user_id_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_index(op.f('ix_data_user_stats_id'), 'user_stats', ['id'], unique=False) + op.create_table('service_reviews', + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), + sa.Column('provider_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('service_record_id', sa.UUID(), autoincrement=False, nullable=True), + sa.Column('is_anonymous', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), + sa.Column('overall_stars', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('detailed_ratings', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text('\'{"comm": 0, "tech": 0, "clean": 0, "price": 0}\'::jsonb'), autoincrement=False, nullable=True), + sa.Column('comment', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.CheckConstraint('overall_stars >= 1 AND overall_stars <= 5', name=op.f('service_reviews_overall_stars_check')), + sa.ForeignKeyConstraint(['provider_id'], ['service_providers.id'], name=op.f('service_reviews_provider_id_fkey')), + sa.ForeignKeyConstraint(['service_record_id'], ['service_records.id'], name=op.f('service_reviews_service_record_id_fkey')), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('service_reviews_user_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('service_reviews_pkey')), + sa.UniqueConstraint('user_id', 'provider_id', 'created_at', name=op.f('service_reviews_user_id_provider_id_created_at_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_table('translations', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('key', sa.VARCHAR(length=100), autoincrement=False, nullable=False), + sa.Column('lang_code', sa.VARCHAR(length=5), autoincrement=False, nullable=False), + sa.Column('value', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('is_published', sa.BOOLEAN(), autoincrement=False, nullable=True), + sa.Column('lang', sa.VARCHAR(length=10), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('translations_pkey')), + sa.UniqueConstraint('key', 'lang_code', name=op.f('uq_translation_key_lang'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_index(op.f('ix_data_translations_lang_code'), 'translations', ['lang_code'], unique=False) + op.create_index(op.f('ix_data_translations_key'), 'translations', ['key'], unique=False) + op.create_index(op.f('ix_data_translations_id'), 'translations', ['id'], unique=False) + op.create_table('alembic_version', + sa.Column('version_num', sa.VARCHAR(length=32), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('version_num', name=op.f('alembic_version_pkc')) + ) + op.create_table('fuel_stations', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=False), + sa.Column('brand_name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('location_city', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('latitude', sa.NUMERIC(precision=10, scale=8), autoincrement=False, nullable=True), + sa.Column('longitude', sa.NUMERIC(precision=11, scale=8), autoincrement=False, nullable=True), + sa.Column('amenities', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text('\'{"food": false, "shop": false, "car_wash": "none"}\'::jsonb'), autoincrement=False, nullable=True), + sa.Column('fuel_types', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text('\'{"diesel": true, "petrol_95": true, "ev_fast_charge": false}\'::jsonb'), autoincrement=False, nullable=True), + sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('fuel_stations_pkey')) + ) + op.create_table('user_vehicles', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('vin', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('license_plate', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('variant_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('color', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('purchase_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.Column('purchase_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), + sa.Column('current_odometer', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), + sa.Column('extras', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), + sa.Column('current_org_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.Column('is_deleted', sa.BOOLEAN(), autoincrement=False, nullable=True), + sa.Column('tire_size_front', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('tire_size_rear', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('tire_dot_code', sa.VARCHAR(length=10), autoincrement=False, nullable=True), + sa.Column('custom_service_interval_km', sa.INTEGER(), server_default=sa.text('20000'), autoincrement=False, nullable=True), + sa.Column('last_service_km', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True), + sa.Column('vin_verified', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), + sa.Column('vin_deadline', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['current_org_id'], ['organizations.id'], name=op.f('user_vehicles_current_org_id_fkey')), + sa.ForeignKeyConstraint(['variant_id'], ['vehicle_variants.id'], name=op.f('user_vehicles_variant_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('user_vehicles_pkey')) + ) + op.create_index(op.f('ix_data_user_vehicles_vin'), 'user_vehicles', ['vin'], unique=True) + op.create_index(op.f('ix_data_user_vehicles_license_plate'), 'user_vehicles', ['license_plate'], unique=False) + op.create_index(op.f('ix_data_user_vehicles_id'), 'user_vehicles', ['id'], unique=False) + op.create_table('vehicle_categories', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('slug', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('vehicle_categories_pkey')), + sa.UniqueConstraint('name', name=op.f('vehicle_categories_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_index(op.f('ix_data_vehicle_categories_id'), 'vehicle_categories', ['id'], unique=False) + op.create_index(op.f('idx_vc_slug'), 'vehicle_categories', ['slug'], unique=True) + op.create_table('user_badges', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('badge_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('earned_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['badge_id'], ['badges.id'], name=op.f('user_badges_badge_id_fkey')), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('user_badges_user_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('user_badges_pkey')) + ) + op.create_index(op.f('ix_data_user_badges_id'), 'user_badges', ['id'], unique=False) + op.create_table('org_subscriptions', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('org_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('tier_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('valid_from', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.Column('valid_until', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), + sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), + sa.Column('auto_renew', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), + sa.Column('trial_ends_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['org_id'], ['organizations.id'], name=op.f('org_subscriptions_org_id_fkey')), + sa.ForeignKeyConstraint(['tier_id'], ['subscription_tiers.id'], name=op.f('org_subscriptions_tier_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('org_subscriptions_pkey')) + ) + op.create_table('email_provider_configs', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('provider_type', sa.VARCHAR(length=20), autoincrement=False, nullable=True), + sa.Column('priority', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('settings', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), + sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), + sa.Column('fail_count', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('max_fail_threshold', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('success_rate', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('email_provider_configs_pkey')), + sa.UniqueConstraint('name', name=op.f('email_provider_configs_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_index(op.f('ix_data_email_provider_configs_id'), 'email_provider_configs', ['id'], unique=False) + op.create_table('credit_rules', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('rule_key', sa.VARCHAR(length=50), autoincrement=False, nullable=False), + sa.Column('amount', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=False), + sa.Column('label', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('credit_rules_pkey')), + sa.UniqueConstraint('rule_key', name=op.f('credit_rules_rule_key_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_table('vehicle_expenses', + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), + sa.Column('vehicle_id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('category', postgresql.ENUM('PURCHASE_PRICE', 'TRANSFER_TAX', 'ADMIN_FEE', 'VEHICLE_TAX', 'INSURANCE', 'REFUELING', 'SERVICE', 'PARKING', 'TOLL', 'FINE', 'TUNING_ACCESSORIES', 'OTHER', name='expense_category_enum'), autoincrement=False, nullable=False), + sa.Column('amount', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=False), + sa.Column('date', sa.DATE(), server_default=sa.text('CURRENT_DATE'), autoincrement=False, nullable=True), + sa.Column('odometer_value', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=True), + sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['vehicle_id'], ['vehicles.id'], name=op.f('vehicle_expenses_vehicle_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('vehicle_expenses_pkey')) + ) + op.create_table('vehicle_events', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('service_provider_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('event_type', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('odometer_reading', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('event_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['vehicle_id'], ['user_vehicles.id'], name=op.f('vehicle_events_vehicle_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('vehicle_events_pkey')) + ) + op.create_table('organization_locations', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('organization_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('label', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('address', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('latitude', sa.NUMERIC(precision=10, scale=8), autoincrement=False, nullable=True), + sa.Column('longitude', sa.NUMERIC(precision=11, scale=8), autoincrement=False, nullable=True), + sa.Column('is_main_location', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], name=op.f('organization_locations_organization_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('organization_locations_pkey')) + ) + op.create_table('email_logs', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('type', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('sent_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.Column('recipient', sa.VARCHAR(length=255), autoincrement=False, nullable=True), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('provider_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('status', sa.VARCHAR(length=50), server_default=sa.text("'sent'::character varying"), autoincrement=False, nullable=True), + sa.Column('email_type', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('email_logs_pkey')) + ) + op.create_index(op.f('ix_data_email_logs_id'), 'email_logs', ['id'], unique=False) + op.create_index(op.f('ix_data_email_logs_email'), 'email_logs', ['email'], unique=False) + op.create_table('subscription_tiers', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('rules', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True), + sa.Column('is_custom', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), + sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('subscription_tiers_pkey')), + sa.UniqueConstraint('name', name=op.f('subscription_tiers_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_table('email_providers', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(length=50), autoincrement=False, nullable=False), + sa.Column('priority', sa.INTEGER(), server_default=sa.text('1'), autoincrement=False, nullable=True), + sa.Column('provider_type', sa.VARCHAR(length=10), server_default=sa.text("'SMTP'::character varying"), autoincrement=False, nullable=True), + sa.Column('host', sa.VARCHAR(length=255), autoincrement=False, nullable=True), + sa.Column('port', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('username', sa.VARCHAR(length=255), autoincrement=False, nullable=True), + sa.Column('password_hash', sa.VARCHAR(length=255), autoincrement=False, nullable=True), + sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), + sa.Column('daily_limit', sa.INTEGER(), server_default=sa.text('300'), autoincrement=False, nullable=True), + sa.Column('current_daily_usage', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('email_providers_pkey')), + sa.UniqueConstraint('name', name=op.f('unique_provider_name'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_table('legal_documents', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('title', sa.VARCHAR(length=255), autoincrement=False, nullable=True), + sa.Column('content', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('version', sa.VARCHAR(length=20), autoincrement=False, nullable=False), + sa.Column('region_code', sa.VARCHAR(length=5), autoincrement=False, nullable=True), + sa.Column('language', sa.VARCHAR(length=5), autoincrement=False, nullable=True), + sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('legal_documents_pkey')) + ) + op.create_index(op.f('ix_data_legal_documents_id'), 'legal_documents', ['id'], unique=False) + op.create_table('locations', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=True), + sa.Column('address', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('latitude', sa.NUMERIC(precision=9, scale=6), autoincrement=False, nullable=True), + sa.Column('longitude', sa.NUMERIC(precision=9, scale=6), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('locations_pkey')) + ) + op.create_table('credit_transactions', + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('amount', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=False), + sa.Column('reason', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('credit_transactions_user_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('credit_transactions_pkey')) + ) + op.create_table('competitions', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('start_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.Column('end_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('competitions_pkey')) + ) + op.create_table('service_specialties', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('parent_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('slug', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['parent_id'], ['service_specialties.id'], name=op.f('service_specialties_parent_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('service_specialties_pkey')), + sa.UniqueConstraint('slug', name=op.f('service_specialties_slug_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_table('legal_acceptances', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('document_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('accepted_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.Column('ip_address', sa.VARCHAR(length=45), autoincrement=False, nullable=True), + sa.Column('user_agent', sa.TEXT(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['document_id'], ['legal_documents.id'], name=op.f('legal_acceptances_document_id_fkey')), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('legal_acceptances_user_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('legal_acceptances_pkey')) + ) + op.create_index(op.f('ix_data_legal_acceptances_id'), 'legal_acceptances', ['id'], unique=False) + op.create_table('badges', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('icon_url', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('badges_pkey')), + sa.UniqueConstraint('name', name=op.f('badges_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_index(op.f('ix_data_badges_id'), 'badges', ['id'], unique=False) + op.create_table('subscription_notification_rules', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('subscription_type', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('days_before', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('template_key', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('subscription_notification_rules_pkey')) + ) + op.create_table('vehicle_variants', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('model_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('engine_size', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), + sa.Column('power_kw', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), + sa.Column('spec_data', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), + sa.Column('fuel_type', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('engine_code', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('cylinder_capacity', sa.INTEGER(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['model_id'], ['vehicle_models.id'], name=op.f('vehicle_variants_model_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('vehicle_variants_pkey')) + ) + op.create_index(op.f('ix_data_vehicle_variants_id'), 'vehicle_variants', ['id'], unique=False) + op.create_table('referrals', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('referrer_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('referee_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('commission_level', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('commission_percentage', sa.NUMERIC(precision=5, scale=2), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.CheckConstraint('commission_level = ANY (ARRAY[1, 2, 3])', name=op.f('referrals_commission_level_check')), + sa.ForeignKeyConstraint(['referee_id'], ['users.id'], name=op.f('referrals_referee_id_fkey')), + sa.ForeignKeyConstraint(['referrer_id'], ['users.id'], name=op.f('referrals_referrer_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('referrals_pkey')) + ) + op.create_table('user_credits', + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('balance', sa.NUMERIC(precision=15, scale=2), server_default=sa.text('0'), autoincrement=False, nullable=True), + sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('user_credits_user_id_fkey')), + sa.PrimaryKeyConstraint('user_id', name=op.f('user_credits_pkey')) + ) + op.create_table('system_settings', + sa.Column('key_name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('value_json', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), + sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('region_code', sa.VARCHAR(length=5), autoincrement=False, nullable=True), + sa.Column('tier_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('org_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('key', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('value', sa.TEXT(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('key_name', name=op.f('system_settings_pkey')) + ) + op.create_index(op.f('ix_data_system_settings_key'), 'system_settings', ['key_name'], unique=False) + op.create_index(op.f('idx_settings_lookup'), 'system_settings', ['key_name', sa.literal_column("COALESCE(region_code, ''::character varying)"), sa.literal_column('COALESCE(tier_id, 0)'), sa.literal_column('COALESCE(org_id, 0)')], unique=True) + op.create_table('organization_members', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('organization_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('role', postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'owner', 'manager', 'driver', 'service', name='orguserrole'), autoincrement=False, nullable=True), + sa.Column('is_permanent', sa.BOOLEAN(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], name=op.f('organization_members_org_id_fkey'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('organization_members_user_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('organization_members_pkey')), + sa.UniqueConstraint('organization_id', 'user_id', name=op.f('unique_user_org'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_index(op.f('ix_data_organization_members_id'), 'organization_members', ['id'], unique=False) + op.create_table('equipment_items', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=False), + sa.Column('category', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('equipment_items_pkey')) + ) + op.create_table('bot_discovery_logs', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('category', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('brand_name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('model_name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('action_taken', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('discovered_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('bot_discovery_logs_pkey')) + ) + op.create_table('points_ledger', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('reason', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('points_ledger_user_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('points_ledger_pkey')) + ) + op.create_index(op.f('ix_data_points_ledger_id'), 'points_ledger', ['id'], unique=False) + op.create_table('email_templates', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('type', postgresql.ENUM('REGISTRATION', 'PASSWORD_RESET', 'GDPR_NOTICE', name='emailtype'), autoincrement=False, nullable=True), + sa.Column('subject', sa.VARCHAR(length=255), autoincrement=False, nullable=False), + sa.Column('body_html', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('key', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('lang', sa.VARCHAR(length=10), autoincrement=False, nullable=True), + sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('email_templates_pkey')), + sa.UniqueConstraint('key', 'lang', name=op.f('unique_email_key_lang'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_index(op.f('ix_data_email_templates_type'), 'email_templates', ['type'], unique=True) + op.create_index(op.f('ix_data_email_templates_id'), 'email_templates', ['id'], unique=False) + op.create_table('vehicle_models', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('brand_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('category_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('year_start', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('year_end', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('slug', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['brand_id'], ['vehicle_brands.id'], name=op.f('vehicle_models_brand_id_fkey')), + sa.ForeignKeyConstraint(['category_id'], ['vehicle_categories.id'], name=op.f('vehicle_models_category_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('vehicle_models_pkey')), + sa.UniqueConstraint('brand_id', 'name', name=op.f('vehicle_models_brand_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_index(op.f('ix_data_vehicle_models_id'), 'vehicle_models', ['id'], unique=False) + op.create_index(op.f('idx_vm_slug'), 'vehicle_models', ['brand_id', 'slug'], unique=True) + op.create_table('user_scores', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('competition_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('last_updated', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['competition_id'], ['competitions.id'], name=op.f('user_scores_competition_id_fkey')), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('user_scores_user_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('user_scores_pkey')), + sa.UniqueConstraint('user_id', 'competition_id', name=op.f('uq_user_competition_score'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_table('vehicle_ownership', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('role', sa.VARCHAR(length=20), autoincrement=False, nullable=True), + sa.Column('license_plate', sa.VARCHAR(length=20), autoincrement=False, nullable=True), + sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), + sa.Column('start_date', sa.DATE(), server_default=sa.text('CURRENT_DATE'), autoincrement=False, nullable=True), + sa.Column('end_date', sa.DATE(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('vehicle_ownership_user_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('vehicle_ownership_pkey')) + ) + op.create_index(op.f('ix_data_vehicle_ownership_id'), 'vehicle_ownership', ['id'], unique=False) + op.create_table('regional_settings', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('country_code', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('currency_code', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.Column('language_code', sa.CHAR(length=2), server_default=sa.text("'hu'::bpchar"), autoincrement=False, nullable=True), + sa.Column('is_eu_member', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('regional_settings_pkey')), + sa.UniqueConstraint('country_code', name=op.f('regional_settings_country_code_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_index(op.f('ix_data_regional_settings_id'), 'regional_settings', ['id'], unique=False) + op.create_table('verification_tokens', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('token', sa.VARCHAR(length=255), autoincrement=False, nullable=True), + sa.Column('token_type', postgresql.ENUM('email_verify', 'password_reset', 'api_key', name='tokentype'), autoincrement=False, nullable=True), + sa.Column('expires_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.Column('token_hash', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('is_used', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), + sa.Column('used_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('verification_tokens_user_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('verification_tokens_pkey')) + ) + op.create_index(op.f('uq_verification_tokens_token_hash'), 'verification_tokens', ['token_hash'], unique=True, postgresql_where='(token_hash IS NOT NULL)') + op.create_index(op.f('ix_verification_tokens_user'), 'verification_tokens', ['user_id', 'token_type', sa.literal_column('created_at DESC')], unique=False) + op.create_index(op.f('ix_verification_tokens_lookup'), 'verification_tokens', ['token_type', 'is_used', 'expires_at'], unique=False) + op.create_index(op.f('ix_data_verification_tokens_token'), 'verification_tokens', ['token'], unique=True) + op.create_index(op.f('ix_data_verification_tokens_id'), 'verification_tokens', ['id'], unique=False) + op.create_table('vouchers', + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), + sa.Column('code', sa.VARCHAR(length=20), autoincrement=False, nullable=False), + sa.Column('value', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.Column('expires_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), + sa.Column('batch_id', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('is_used', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), + sa.Column('used_by', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('used_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['used_by'], ['users.id'], name=op.f('vouchers_used_by_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('vouchers_pkey')), + sa.UniqueConstraint('code', name=op.f('vouchers_code_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_table('level_configs', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('level_number', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('min_points', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('rank_name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('level_configs_pkey')), + sa.UniqueConstraint('level_number', name=op.f('level_configs_level_number_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_index(op.f('ix_data_level_configs_id'), 'level_configs', ['id'], unique=False) + op.create_table('audit_logs', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('action', sa.VARCHAR(length=100), autoincrement=False, nullable=False), + sa.Column('endpoint', sa.VARCHAR(length=255), autoincrement=False, nullable=True), + sa.Column('method', sa.VARCHAR(length=10), autoincrement=False, nullable=True), + sa.Column('payload', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True), + sa.Column('ip_address', sa.VARCHAR(length=45), autoincrement=False, nullable=True), + sa.Column('user_agent', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('audit_logs_user_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('audit_logs_pkey')) + ) + op.create_table('votes', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('provider_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('vote_value', sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('votes_user_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('votes_pkey')), + sa.UniqueConstraint('user_id', 'provider_id', name=op.f('uq_user_provider_vote'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_table('credit_logs', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('org_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('amount', sa.NUMERIC(precision=10, scale=2), autoincrement=False, nullable=True), + sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['org_id'], ['organizations.id'], name=op.f('credit_logs_org_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('credit_logs_pkey')) + ) + op.create_table('user_vehicle_equipment', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('user_vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('equipment_item_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('source', postgresql.ENUM('factory', 'aftermarket', name='equipment_source'), server_default=sa.text("'factory'::equipment_source"), autoincrement=False, nullable=True), + sa.Column('installed_at', sa.DATE(), autoincrement=False, nullable=True), + sa.Column('notes', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['equipment_item_id'], ['equipment_items.id'], name=op.f('user_vehicle_equipment_equipment_item_id_fkey')), + sa.ForeignKeyConstraint(['user_vehicle_id'], ['user_vehicles.id'], name=op.f('user_vehicle_equipment_user_vehicle_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('user_vehicle_equipment_pkey')) + ) + # ### end Alembic commands ###