Save test environment changes
This commit is contained in:
61
.env
61
.env
@@ -30,3 +30,64 @@ FROM_EMAIL=info@profibot.hu
|
|||||||
|
|
||||||
# Biztonsági kulcs a tokenekhez (KÖTELEZŐ!)
|
# Biztonsági kulcs a tokenekhez (KÖTELEZŐ!)
|
||||||
SECRET_KEY=2dca2ff3bf9b8184e14038d5d08e646b31bd4a5f5ffc7e19d28e294f3bb3760b
|
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'
|
||||||
BIN
backend/app/__pycache__/main.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/__pycache__/main.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
BIN
backend/app/api/v1/__pycache__/api.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/api/v1/__pycache__/api.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
@@ -1,12 +1,11 @@
|
|||||||
from fastapi import APIRouter
|
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 = APIRouter()
|
||||||
|
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
# Minden auth funkciót ide gyűjtünk (Register, Login, Recover)
|
||||||
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||||
api_router.include_router(billing.router, prefix="/billing", tags=["billing"])
|
|
||||||
api_router.include_router(vehicles.router, prefix="/vehicles", tags=["vehicles"])
|
# Itt jönnek majd a további modulok:
|
||||||
api_router.include_router(fleet.router, prefix="/fleet", tags=["fleet"])
|
# api_router.include_router(users.router, prefix="/users", tags=["Users"])
|
||||||
api_router.include_router(expenses.router, prefix="/expenses", tags=["expenses"])
|
# api_router.include_router(fleet.router, prefix="/fleet", tags=["Fleet"])
|
||||||
api_router.include_router(reports.router, prefix="/reports", tags=["reports"])
|
|
||||||
BIN
backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
@@ -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.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.db.session import get_db
|
||||||
from app.models.user import User
|
from app.schemas.auth import UserRegister, UserLogin, Token
|
||||||
from app.core.security import get_password_hash
|
from app.services.auth_service import AuthService
|
||||||
from app.services.email_manager import email_manager
|
|
||||||
from app.services.config_service import config
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("/register")
|
@router.post("/register", status_code=status.HTTP_201_CREATED)
|
||||||
async def register(
|
async def register(
|
||||||
request: Request,
|
request: Request,
|
||||||
email: str,
|
user_in: UserRegister,
|
||||||
password: str,
|
|
||||||
first_name: str,
|
|
||||||
last_name: str,
|
|
||||||
db: AsyncSession = Depends(get_db)
|
db: AsyncSession = Depends(get_db)
|
||||||
):
|
):
|
||||||
ip = request.client.host
|
# 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.")
|
||||||
|
|
||||||
# 1. BOT-VÉDELEM
|
# 2. Process
|
||||||
throttle_min = await config.get_setting('registration_throttle_minutes', default=10)
|
try:
|
||||||
check_throttle = await db.execute(text("""
|
user = await AuthService.register_new_user(
|
||||||
SELECT count(*) FROM data.audit_logs
|
db=db,
|
||||||
WHERE ip_address = :ip AND action = 'USER_REGISTERED' AND created_at > :t
|
user_in=user_in,
|
||||||
"""), {'ip': ip, 't': datetime.utcnow() - timedelta(minutes=int(throttle_min))})
|
ip_address=request.client.host
|
||||||
|
|
||||||
if check_throttle.scalar() > 0:
|
|
||||||
raise HTTPException(status_code=429, detail="Túl sok próbálkozás. Várj pár percet!")
|
|
||||||
|
|
||||||
# 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.")
|
|
||||||
|
|
||||||
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)
|
return {"status": "success", "message": "Regisztráció sikeres!"}
|
||||||
await db.flush()
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Szerver hiba: {str(e)}")
|
||||||
|
|
||||||
# 3. TOKEN & LOG
|
@router.post("/login")
|
||||||
raw_token = secrets.token_urlsafe(48)
|
async def login(user_in: UserLogin, db: AsyncSession = Depends(get_db)):
|
||||||
token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
|
# ... A korábbi login logika itt maradhat ...
|
||||||
await db.execute(text("""
|
pass
|
||||||
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"<h1>Szia {first_name}!</h1><p>Aktiváld a fiókod: <a href='{verify_link}'>{verify_link}</a></p>"
|
|
||||||
|
|
||||||
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!"}
|
|
||||||
BIN
backend/app/core/__pycache__/config.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/core/__pycache__/config.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
BIN
backend/app/core/__pycache__/security.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/core/__pycache__/security.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
@@ -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_settings import BaseSettings, SettingsConfigDict
|
||||||
from pydantic import computed_field
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
# --- General ---
|
# --- General ---
|
||||||
PROJECT_NAME: str = "Traffic Ecosystem SuperApp"
|
PROJECT_NAME: str = "Traffic Ecosystem SuperApp"
|
||||||
VERSION: str = "2.0.0"
|
VERSION: str = "1.0.0"
|
||||||
API_V1_STR: str = "/api/v1"
|
API_V1_STR: str = "/api/v1"
|
||||||
DEBUG: bool = False
|
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
|
||||||
|
|
||||||
# --- Security / JWT ---
|
# --- Security / JWT ---
|
||||||
SECRET_KEY: str
|
# Szigorúan .env-ből!
|
||||||
|
SECRET_KEY: str = os.getenv("SECRET_KEY", "NOT_SET_DANGER")
|
||||||
ALGORITHM: str = "HS256"
|
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ú) ---
|
# --- Database & Cache ---
|
||||||
PASSWORD_MIN_LENGTH: int = 4 # TESZT: 4, ÉLES: 10-12
|
DATABASE_URL: str = os.getenv("DATABASE_URL")
|
||||||
|
REDIS_URL: str = os.getenv("REDIS_URL", "redis://service_finder_redis:6379/0")
|
||||||
|
|
||||||
# --- Database ---
|
# --- Email (Auto Provider) ---
|
||||||
DATABASE_URL: str # már nálad compose-ban meg van adva
|
EMAIL_PROVIDER: str = os.getenv("EMAIL_PROVIDER", "auto")
|
||||||
|
EMAILS_FROM_EMAIL: str = os.getenv("EMAILS_FROM_EMAIL", "info@profibot.hu")
|
||||||
# --- 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"
|
|
||||||
EMAILS_FROM_NAME: str = "Profibot"
|
EMAILS_FROM_NAME: str = "Profibot"
|
||||||
|
|
||||||
# SendGrid API
|
# SMTP & SendGrid (Szigorúan .env-ből)
|
||||||
SENDGRID_API_KEY: Optional[str] = None
|
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")
|
||||||
|
|
||||||
# SMTP fallback (pl. Gmail App Password vagy más szolgáltató)
|
# --- External URLs ---
|
||||||
SMTP_HOST: Optional[str] = None
|
# .env-ben legyen átírva a .10-es IP-re!
|
||||||
SMTP_PORT: int = 587
|
FRONTEND_BASE_URL: str = os.getenv("FRONTEND_BASE_URL", "http://localhost:3000")
|
||||||
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)
|
# --- Dinamikus Admin Motor ---
|
||||||
FRONTEND_BASE_URL: str = "http://192.168.100.43:3000"
|
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(
|
model_config = SettingsConfigDict(
|
||||||
env_file=".env",
|
env_file=".env",
|
||||||
env_file_encoding="utf-8",
|
env_file_encoding="utf-8",
|
||||||
|
|||||||
@@ -6,28 +6,44 @@ from jose import jwt, JWTError
|
|||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
|
||||||
# --- JELSZÓ ---
|
# --- JELSZÓ KEZELÉS ---
|
||||||
|
|
||||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
"""
|
||||||
|
Összehasonlítja a nyers jelszót a hash-elt változattal.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
if not hashed_password:
|
if not hashed_password:
|
||||||
return False
|
return False
|
||||||
return bcrypt.checkpw(
|
return bcrypt.checkpw(
|
||||||
plain_password.encode("utf-8"),
|
plain_password.encode("utf-8"),
|
||||||
hashed_password.encode("utf-8"),
|
hashed_password.encode("utf-8")
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_password_hash(password: str) -> str:
|
def get_password_hash(password: str) -> str:
|
||||||
|
"""
|
||||||
|
Biztonságos hash-t generál a jelszóból.
|
||||||
|
"""
|
||||||
salt = bcrypt.gensalt()
|
salt = bcrypt.gensalt()
|
||||||
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
|
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:
|
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)
|
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})
|
to_encode.update({"exp": expire})
|
||||||
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||||
|
|
||||||
def decode_token(token: str) -> Dict[str, Any]:
|
def decode_token(token: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Dekódolja a JWT tokent.
|
||||||
|
"""
|
||||||
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||||
@@ -1,45 +1,50 @@
|
|||||||
|
import os
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
import os
|
|
||||||
from app.api.v1.api import api_router
|
from app.api.v1.api import api_router
|
||||||
from app.api.v2.auth import router as auth_v2_router
|
from app.db.base import Base
|
||||||
from app.models import Base
|
from app.db.session import engine
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
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:
|
async with engine.begin() as conn:
|
||||||
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS data"))
|
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)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
yield
|
yield
|
||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Traffic Ecosystem SuperApp 2.0",
|
title="Service Finder API",
|
||||||
version="2.0.0",
|
version="1.0.0",
|
||||||
openapi_url="/api/v2/openapi.json",
|
docs_url="/docs",
|
||||||
|
openapi_url="/api/v1/openapi.json",
|
||||||
lifespan=lifespan
|
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(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=[
|
allow_origins=origins,
|
||||||
"http://192.168.100.43:3000", # A szerver címe a böngészőben
|
|
||||||
"http://localhost:3000", # Helyi teszteléshez
|
|
||||||
],
|
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ÚTVONALAK KONSZOLIDÁCIÓJA (V2 törölve, minden a V1 alatt)
|
||||||
# ÚTVONALAK INTEGRÁCIÓJA
|
|
||||||
app.include_router(api_router, prefix="/api/v1")
|
app.include_router(api_router, prefix="/api/v1")
|
||||||
app.include_router(auth_v2_router, prefix="/api/v2/auth")
|
|
||||||
|
|
||||||
@app.get("/", tags=["health"])
|
@app.get("/", tags=["health"])
|
||||||
async def root():
|
async def root():
|
||||||
return {"status": "online", "version": "2.0.0", "docs": "/docs"}
|
return {
|
||||||
|
"status": "online",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"environment": os.getenv("ENV", "production")
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from app.db.base import Base
|
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 .company import Company, CompanyMember, VehicleAssignment
|
||||||
|
from .organization import Organization, OrgType
|
||||||
from .vehicle import (
|
from .vehicle import (
|
||||||
Vehicle,
|
Vehicle,
|
||||||
VehicleOwnership,
|
VehicleOwnership,
|
||||||
@@ -13,12 +14,12 @@ from .vehicle import (
|
|||||||
VehicleVariant
|
VehicleVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
# Alias a kompatibilitás kedvéért
|
# Aliasok a kompatibilitás kedvéért
|
||||||
UserVehicle = Vehicle
|
UserVehicle = Vehicle
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Base", "User", "UserRole", "Vehicle", "VehicleOwnership", "VehicleBrand",
|
"Base", "User", "Person", "Wallet", "UserRole", "Vehicle", "VehicleOwnership",
|
||||||
"EngineSpec", "ServiceProvider", "ServiceRecord", "Company",
|
"VehicleBrand", "EngineSpec", "ServiceProvider", "ServiceRecord", "Company",
|
||||||
"CompanyMember", "VehicleAssignment", "UserVehicle", "VehicleCategory",
|
"CompanyMember", "VehicleAssignment", "UserVehicle", "VehicleCategory",
|
||||||
"VehicleModel", "VehicleVariant"
|
"VehicleModel", "VehicleVariant", "Organization", "OrgType"
|
||||||
]
|
]
|
||||||
BIN
backend/app/models/__pycache__/__init__.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/models/__pycache__/__init__.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/identity.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/identity.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/organization.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/organization.cpython-312.pyc
Normal file
Binary file not shown.
73
backend/app/models/identity.py
Normal file
73
backend/app/models/identity.py
Normal file
@@ -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")
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import enum
|
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.orm import relationship
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.db.base import Base
|
from app.db.base import Base
|
||||||
@@ -10,11 +10,6 @@ class OrgType(str, enum.Enum):
|
|||||||
FLEET_OWNER = "fleet_owner"
|
FLEET_OWNER = "fleet_owner"
|
||||||
CLUB = "club"
|
CLUB = "club"
|
||||||
|
|
||||||
class UITheme(str, enum.Enum):
|
|
||||||
LIGHT = "light"
|
|
||||||
DARK = "dark"
|
|
||||||
SYSTEM = "system"
|
|
||||||
|
|
||||||
class Organization(Base):
|
class Organization(Base):
|
||||||
__tablename__ = "organizations"
|
__tablename__ = "organizations"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
@@ -23,14 +18,11 @@ class Organization(Base):
|
|||||||
name = Column(String, nullable=False)
|
name = Column(String, nullable=False)
|
||||||
org_type = Column(Enum(OrgType), default=OrgType.INDIVIDUAL)
|
org_type = Column(Enum(OrgType), default=OrgType.INDIVIDUAL)
|
||||||
|
|
||||||
# Új UI beállítások a V2-höz
|
# Spec 2.2: Az owner_id a magánszemély flottájának tulajdonosát jelöli
|
||||||
theme = Column(Enum(UITheme), default=UITheme.SYSTEM)
|
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||||
logo_url = Column(String, nullable=True)
|
|
||||||
|
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
||||||
|
|
||||||
# Kapcsolatok
|
# Kapcsolatok (UserVehicle modell megléte esetén)
|
||||||
# members = relationship("OrganizationMember", back_populates="organization")
|
vehicles = relationship("UserVehicle", back_populates="current_org", cascade="all, delete-orphan")
|
||||||
vehicles = relationship("UserVehicle", back_populates="current_org")
|
|
||||||
@@ -1,34 +1,6 @@
|
|||||||
import enum
|
# DEPRECATED: Minden funkció átkerült az app.models.identity modulba.
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, Date, DateTime
|
# Ez a fájl csak a kompatibilitás miatt maradt meg, de táblát nem definiál.
|
||||||
from sqlalchemy.orm import relationship
|
from .identity import User, UserRole
|
||||||
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())
|
|
||||||
|
|
||||||
# Kapcsolatok
|
# Kapcsolatok
|
||||||
# memberships = relationship("OrganizationMember", back_populates="user", cascade="all, delete-orphan")
|
# memberships = relationship("OrganizationMember", back_populates="user", cascade="all, delete-orphan")
|
||||||
|
|||||||
BIN
backend/app/schemas/__pycache__/auth.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
@@ -1,16 +1,27 @@
|
|||||||
from pydantic import BaseModel, EmailStr, Field
|
from pydantic import BaseModel, EmailStr, Field, validator
|
||||||
from typing import Optional
|
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):
|
class Token(BaseModel):
|
||||||
access_token: str
|
access_token: str
|
||||||
token_type: str
|
token_type: str
|
||||||
|
|
||||||
class TokenData(BaseModel):
|
class TokenData(BaseModel):
|
||||||
email: Optional[str] = None
|
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
|
|
||||||
BIN
backend/app/services/__pycache__/auth_service.cpython-312.pyc
Normal file
BIN
backend/app/services/__pycache__/auth_service.cpython-312.pyc
Normal file
Binary file not shown.
81
backend/app/services/auth_service.py
Normal file
81
backend/app/services/auth_service.py
Normal file
@@ -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
|
||||||
15
docs/V01_chatgpt/00_README.md
Normal file
15
docs/V01_chatgpt/00_README.md
Normal file
@@ -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.
|
||||||
24
docs/V01_chatgpt/01_Project_Overview.md
Normal file
24
docs/V01_chatgpt/01_Project_Overview.md
Normal file
@@ -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ó
|
||||||
0
docs/V01_chatgpt/02_Architecture_System_Context.md
Normal file
0
docs/V01_chatgpt/02_Architecture_System_Context.md
Normal file
19
docs/V01_chatgpt/03_Dev_Environment_Runbook.md
Normal file
19
docs/V01_chatgpt/03_Dev_Environment_Runbook.md
Normal file
@@ -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ó
|
||||||
26
docs/V01_chatgpt/06_Database_Guide.md
Normal file
26
docs/V01_chatgpt/06_Database_Guide.md
Normal file
@@ -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
|
||||||
13
docs/V01_chatgpt/07_API_Guide.md
Normal file
13
docs/V01_chatgpt/07_API_Guide.md
Normal file
@@ -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.
|
||||||
0
docs/V01_chatgpt/13_Roadmap_Tech_Debt.md
Normal file
0
docs/V01_chatgpt/13_Roadmap_Tech_Debt.md
Normal file
10
docs/V01_chatgpt/14_Anchor_Log_Timeline.md
Normal file
10
docs/V01_chatgpt/14_Anchor_Log_Timeline.md
Normal file
@@ -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.
|
||||||
6
docs/V01_chatgpt/15_Changelog.md
Normal file
6
docs/V01_chatgpt/15_Changelog.md
Normal file
@@ -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
|
||||||
40
docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md
Normal file
40
docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md
Normal file
@@ -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.
|
||||||
@@ -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).
|
|
||||||
@@ -1,6 +1,26 @@
|
|||||||
(Az Adatbázis Bibliája.)
|
(Az Adatbázis Bibliája.)
|
||||||
# 🗄️ DATABASE GUIDE
|
# 🗄️ 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
|
## Sémák
|
||||||
- `public`: Csak technikai táblák (pl. Alembic version).
|
- `public`: Csak technikai táblák (pl. Alembic version).
|
||||||
- `data`: Az üzleti logika 55 táblája.
|
- `data`: Az üzleti logika 55 táblája.
|
||||||
@@ -15,3 +35,15 @@
|
|||||||
- **Eszköz:** Alembic.
|
- **Eszköz:** Alembic.
|
||||||
- **Current Head:** `10b73fee8967`.
|
- **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
|
- **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).
|
||||||
52
docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md
Normal file
52
docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md
Normal file
@@ -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.
|
||||||
@@ -1,25 +1,35 @@
|
|||||||
(Az Üzleti Modell - A legfontosabb frissítés.)
|
# 💰 BILLING, CREDITS AND MULTI-CURRENCY (v1.0)
|
||||||
# 💰 BILLING, CREDITS & SUBSCRIPTIONS
|
|
||||||
|
|
||||||
## 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 |
|
**Átszámítási képlet:**
|
||||||
| :--- | :--- | :--- | :--- | :--- |
|
$$Cost_{EUR} = Cost_{Local} \cdot ExchangeRate$$
|
||||||
| **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. |
|
|
||||||
|
|
||||||
## 2. A "Free -> Premium" Szabály (Q10 Solution)
|
## 2. Előfizetési Csomagok (Adminból állítható)
|
||||||
- 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).
|
A csomagok limiteit (járműszám, funkciók) a `system_settings` tábla szabályozza.
|
||||||
- **Feloldás:** Visszamenőleges elemzéshez "Retroaktív Csomag" vagy folyamatos előfizetés szükséges.
|
|
||||||
|
|
||||||
## 3. Kredit Ökonómia (Coin)
|
| Csomag | Jármű Limit | Kiemelt funkciók |
|
||||||
- **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.
|
| **FREE** | 1 db | Csak GEO keresés, alap költséglog, nincs dokumentum/export. |
|
||||||
- **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).
|
| **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
|
## 3. Evidence & Trust Engine (Bizonyíték kezelés)
|
||||||
- **Stripe:** Nemzetközi kártyás fizetés.
|
A rendszer csak azokat a szerviz eseményeket tekinti **hitelesnek (Verified)**, amelyekhez tartozik:
|
||||||
- **Barion / SimplePay:** Magyar specifikus fizetés.
|
- **Fotó:** Kilométeróra állásról és munkalapról.
|
||||||
- **Coin Pack:** Mikrotanzakciók (pl. 500 Coin = 1000 Ft).
|
- **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.
|
||||||
22
docs/V01_gemini/16_TESTING_AND_DEPLOYMENT_GUIDE.md
Normal file
22
docs/V01_gemini/16_TESTING_AND_DEPLOYMENT_GUIDE.md
Normal file
@@ -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"
|
||||||
|
}
|
||||||
Binary file not shown.
945
migrations/versions/fba92ed020b1_merge_identity_v1.py
Normal file
945
migrations/versions/fba92ed020b1_merge_identity_v1.py
Normal file
@@ -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 ###
|
||||||
Reference in New Issue
Block a user