Save test environment changes

This commit is contained in:
2026-02-04 21:58:57 +00:00
parent 5dd5692d83
commit a57d5333d4
67 changed files with 1603 additions and 239 deletions

61
.env
View File

@@ -30,3 +30,64 @@ FROM_EMAIL=info@profibot.hu
# Biztonsági kulcs a tokenekhez (KÖTELEZŐ!)
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

Binary file not shown.

BIN
backend/app/api/v1/__pycache__/api.cpython-312.pyc Executable file → Normal file

Binary file not shown.

View File

@@ -1,12 +1,11 @@
from fastapi import APIRouter
from app.api.v1.endpoints import auth, users, vehicles, billing, fleet, expenses, reports
from app.api.v1.endpoints import auth # Fontos a helyes import!
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(billing.router, prefix="/billing", tags=["billing"])
api_router.include_router(vehicles.router, prefix="/vehicles", tags=["vehicles"])
api_router.include_router(fleet.router, prefix="/fleet", tags=["fleet"])
api_router.include_router(expenses.router, prefix="/expenses", tags=["expenses"])
api_router.include_router(reports.router, prefix="/reports", tags=["reports"])
# Minden auth funkciót ide gyűjtünk (Register, Login, Recover)
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
# Itt jönnek majd a további modulok:
# api_router.include_router(users.router, prefix="/users", tags=["Users"])
# api_router.include_router(fleet.router, prefix="/fleet", tags=["Fleet"])

Binary file not shown.

View File

@@ -1,91 +1,34 @@
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, text
from datetime import datetime, timedelta
import hashlib, secrets
from app.db.session import get_db
from app.models.user import User
from app.core.security import get_password_hash
from app.services.email_manager import email_manager
from app.services.config_service import config
from app.schemas.auth import UserRegister, UserLogin, Token
from app.services.auth_service import AuthService
router = APIRouter()
@router.post("/register")
@router.post("/register", status_code=status.HTTP_201_CREATED)
async def register(
request: Request,
email: str,
password: str,
first_name: str,
last_name: str,
user_in: UserRegister,
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
throttle_min = await config.get_setting('registration_throttle_minutes', default=10)
check_throttle = await db.execute(text("""
SELECT count(*) FROM data.audit_logs
WHERE ip_address = :ip AND action = 'USER_REGISTERED' AND created_at > :t
"""), {'ip': ip, 't': datetime.utcnow() - timedelta(minutes=int(throttle_min))})
if check_throttle.scalar() > 0:
raise HTTPException(status_code=429, detail="Túl sok próbálkozás. Várj pár percet!")
# 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
# 2. Process
try:
user = await AuthService.register_new_user(
db=db,
user_in=user_in,
ip_address=request.client.host
)
db.add(new_user)
await db.flush()
return {"status": "success", "message": "Regisztráció sikeres!"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Szerver hiba: {str(e)}")
# 3. TOKEN & LOG
raw_token = secrets.token_urlsafe(48)
token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
await db.execute(text("""
INSERT INTO data.verification_tokens (user_id, token_hash, token_type, expires_at)
VALUES (:u, :t, 'email_verify', :e)
"""), {'u': new_user.id, 't': token_hash, 'e': datetime.utcnow() + timedelta(days=2)})
await db.execute(text("""
INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address)
VALUES (:u, 'USER_REGISTERED', '/register', 'POST', :ip)
"""), {'u': new_user.id, 'ip': ip})
# 4. EMAIL KÜLDÉS
verify_link = f"http://{request.headers.get('host')}/api/v1/auth/verify?token={raw_token}"
email_body = f"<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!"}
@router.post("/login")
async def login(user_in: UserLogin, db: AsyncSession = Depends(get_db)):
# ... A korábbi login logika itt maradhat ...
pass

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

Binary file not shown.

View File

@@ -1,48 +1,61 @@
from typing import Optional
import os
import json
from typing import Any, Optional, List
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import computed_field
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
class Settings(BaseSettings):
# --- General ---
PROJECT_NAME: str = "Traffic Ecosystem SuperApp"
VERSION: str = "2.0.0"
VERSION: str = "1.0.0"
API_V1_STR: str = "/api/v1"
DEBUG: bool = False
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
# --- Security / JWT ---
SECRET_KEY: str
# Szigorúan .env-ből!
SECRET_KEY: str = os.getenv("SECRET_KEY", "NOT_SET_DANGER")
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 nap
# --- Password policy (TEST -> laza, PROD -> szigorú) ---
PASSWORD_MIN_LENGTH: int = 4 # TESZT: 4, ÉLES: 10-12
# --- Database & Cache ---
DATABASE_URL: str = os.getenv("DATABASE_URL")
REDIS_URL: str = os.getenv("REDIS_URL", "redis://service_finder_redis:6379/0")
# --- Database ---
DATABASE_URL: str # már nálad compose-ban meg van adva
# --- Redis ---
REDIS_URL: str = "redis://service_finder_redis:6379/0"
# --- Email sending ---
# auto = ha van SENDGRID_API_KEY -> sendgrid api, különben smtp
EMAIL_PROVIDER: str = "auto" # auto | sendgrid | smtp | disabled
EMAILS_FROM_EMAIL: str = "info@profibot.hu"
# --- Email (Auto Provider) ---
EMAIL_PROVIDER: str = os.getenv("EMAIL_PROVIDER", "auto")
EMAILS_FROM_EMAIL: str = os.getenv("EMAILS_FROM_EMAIL", "info@profibot.hu")
EMAILS_FROM_NAME: str = "Profibot"
# SendGrid API
SENDGRID_API_KEY: Optional[str] = None
# SMTP & SendGrid (Szigorúan .env-ből)
SENDGRID_API_KEY: Optional[str] = os.getenv("SENDGRID_API_KEY")
SMTP_HOST: Optional[str] = os.getenv("SMTP_HOST")
SMTP_PORT: int = int(os.getenv("SMTP_PORT", 587))
SMTP_USER: Optional[str] = os.getenv("SMTP_USER")
SMTP_PASSWORD: Optional[str] = os.getenv("SMTP_PASSWORD")
# SMTP fallback (pl. Gmail App Password vagy más szolgáltató)
SMTP_HOST: Optional[str] = None
SMTP_PORT: int = 587
SMTP_USER: Optional[str] = None
SMTP_PASSWORD: Optional[str] = None
SMTP_USE_TLS: bool = True
# --- External URLs ---
# .env-ben legyen átírva a .10-es IP-re!
FRONTEND_BASE_URL: str = os.getenv("FRONTEND_BASE_URL", "http://localhost:3000")
# Frontend base URL a linkekhez (később NPM/domain)
FRONTEND_BASE_URL: str = "http://192.168.100.43:3000"
# --- Dinamikus Admin Motor ---
async def get_db_setting(self, db: AsyncSession, key_name: str, default: Any = None) -> Any:
"""
Lekéri a paramétert a data.system_settings táblából.
Ezzel érjük el, hogy a kód újraírása nélkül, adminból lehessen
állítani a jutalom napokat, százalékokat, stb.
"""
try:
query = text("SELECT value_json FROM data.system_settings WHERE key_name = :key")
result = await db.execute(query, {"key": key_name})
row = result.fetchone()
if row and row[0] is not None:
return row[0]
return default
except Exception:
return default
# .env fájl konfigurációja
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",

View File

@@ -6,28 +6,44 @@ from jose import jwt, JWTError
from app.core.config import settings
# --- JELSZÓ ---
# --- JELSZÓ KEZELÉS ---
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Összehasonlítja a nyers jelszót a hash-elt változattal.
"""
try:
if not hashed_password:
return False
return bcrypt.checkpw(
plain_password.encode("utf-8"),
hashed_password.encode("utf-8"),
hashed_password.encode("utf-8")
)
except Exception:
return False
def get_password_hash(password: str) -> str:
"""
Biztonságos hash-t generál a jelszóból.
"""
salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
# --- JWT ---
# --- JWT TOKEN KEZELÉS ---
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
"""
JWT Access tokent generál a megadott adatokkal és lejárati idővel.
"""
to_encode = dict(data)
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
expire = datetime.now(timezone.utc) + (
expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def decode_token(token: str) -> Dict[str, Any]:
"""
Dekódolja a JWT tokent.
"""
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])

View File

@@ -1,45 +1,50 @@
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import text
import os
from app.api.v1.api import api_router
from app.api.v2.auth import router as auth_v2_router
from app.models import Base
from app.db.base import Base
from app.db.session import engine
@asynccontextmanager
async def lifespan(app: FastAPI):
from app.db.session import engine
# Séma és alap táblák ellenőrzése indításkor
async with engine.begin() as conn:
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS data"))
# Base.metadata.create_all helyett javasolt az Alembic,
# de fejlesztési fázisban a run_sync biztonságos
await conn.run_sync(Base.metadata.create_all)
yield
await engine.dispose()
app = FastAPI(
title="Traffic Ecosystem SuperApp 2.0",
version="2.0.0",
openapi_url="/api/v2/openapi.json",
title="Service Finder API",
version="1.0.0",
docs_url="/docs",
openapi_url="/api/v1/openapi.json",
lifespan=lifespan
)
# BIZTONSÁG: CORS beállítások .env-ből
# Ha nincs megadva, csak a localhost-ot engedi
origins = os.getenv("CORS_ORIGINS", "http://localhost:3000").split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://192.168.100.43:3000", # A szerver címe a böngészőben
"http://localhost:3000", # Helyi teszteléshez
],
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ÚTVONALAK INTEGRÁCIÓJA
# ÚTVONALAK KONSZOLIDÁCIÓJA (V2 törölve, minden a V1 alatt)
app.include_router(api_router, prefix="/api/v1")
app.include_router(auth_v2_router, prefix="/api/v2/auth")
@app.get("/", tags=["health"])
async def root():
return {"status": "online", "version": "2.0.0", "docs": "/docs"}
return {
"status": "online",
"version": "1.0.0",
"environment": os.getenv("ENV", "production")
}

View File

@@ -1,6 +1,7 @@
from app.db.base import Base
from .user import User, UserRole
from .identity import User, Person, Wallet, UserRole # ÚJ központ
from .company import Company, CompanyMember, VehicleAssignment
from .organization import Organization, OrgType
from .vehicle import (
Vehicle,
VehicleOwnership,
@@ -13,12 +14,12 @@ from .vehicle import (
VehicleVariant
)
# Alias a kompatibilitás kedvéért
# Aliasok a kompatibilitás kedvéért
UserVehicle = Vehicle
__all__ = [
"Base", "User", "UserRole", "Vehicle", "VehicleOwnership", "VehicleBrand",
"EngineSpec", "ServiceProvider", "ServiceRecord", "Company",
"Base", "User", "Person", "Wallet", "UserRole", "Vehicle", "VehicleOwnership",
"VehicleBrand", "EngineSpec", "ServiceProvider", "ServiceRecord", "Company",
"CompanyMember", "VehicleAssignment", "UserVehicle", "VehicleCategory",
"VehicleModel", "VehicleVariant"
"VehicleModel", "VehicleVariant", "Organization", "OrgType"
]

BIN
backend/app/models/__pycache__/__init__.cpython-312.pyc Executable file → Normal file

Binary file not shown.

View 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")

View File

@@ -1,5 +1,5 @@
import enum
from sqlalchemy import Column, Integer, String, Boolean, Enum, DateTime
from sqlalchemy import Column, Integer, String, Boolean, Enum, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base import Base
@@ -10,11 +10,6 @@ class OrgType(str, enum.Enum):
FLEET_OWNER = "fleet_owner"
CLUB = "club"
class UITheme(str, enum.Enum):
LIGHT = "light"
DARK = "dark"
SYSTEM = "system"
class Organization(Base):
__tablename__ = "organizations"
__table_args__ = {"schema": "data"}
@@ -23,14 +18,11 @@ class Organization(Base):
name = Column(String, nullable=False)
org_type = Column(Enum(OrgType), default=OrgType.INDIVIDUAL)
# Új UI beállítások a V2-höz
theme = Column(Enum(UITheme), default=UITheme.SYSTEM)
logo_url = Column(String, nullable=True)
# Spec 2.2: Az owner_id a magánszemély flottájának tulajdonosát jelöli
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Kapcsolatok
# members = relationship("OrganizationMember", back_populates="organization")
vehicles = relationship("UserVehicle", back_populates="current_org")
# Kapcsolatok (UserVehicle modell megléte esetén)
vehicles = relationship("UserVehicle", back_populates="current_org", cascade="all, delete-orphan")

View File

@@ -1,34 +1,6 @@
import enum
from sqlalchemy import Column, Integer, String, Boolean, Date, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base import Base
class UserRole(str, enum.Enum):
ADMIN = "admin"
USER = "user"
SERVICE = "service"
FLEET_MANAGER = "fleet_manager"
class User(Base):
__tablename__ = "users"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
first_name = Column(String)
last_name = Column(String)
birthday = Column(Date, nullable=True)
role = Column(String, default=UserRole.USER)
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
is_company = Column(Boolean, default=False)
company_name = Column(String, nullable=True)
tax_number = Column(String, nullable=True)
region_code = Column(String, default="HU")
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# DEPRECATED: Minden funkció átkerült az app.models.identity modulba.
# Ez a fájl csak a kompatibilitás miatt maradt meg, de táblát nem definiál.
from .identity import User, UserRole
# Kapcsolatok
# memberships = relationship("OrganizationMember", back_populates="user", cascade="all, delete-orphan")

Binary file not shown.

View File

@@ -1,16 +1,27 @@
from pydantic import BaseModel, EmailStr, Field
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional
class UserRegister(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8)
first_name: str = Field(..., min_length=2)
last_name: str = Field(..., min_length=2)
region_code: str = Field(default="HU", min_length=2, max_length=2) # ISO kód: HU, DE, AT stb.
device_id: Optional[str] = None # Eszköz azonosító a biztonsághoz
invite_token: Optional[str] = None
@validator('region_code')
def validate_region(cls, v):
return v.upper() if v else v
# EZ HIÁNYZOTT: Az azonosításhoz (login) szükséges séma
class UserLogin(BaseModel):
email: EmailStr
password: str
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
email: Optional[str] = None
class UserRegister(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8)
full_name: str
region_code: str = "HU"
device_id: str # Az eszköz egyedi azonosítója a védelemhez

View 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

View 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.

View 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ó

View 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ó

View 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

View 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.

View File

View 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.

View 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

View 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.

View File

@@ -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).

View File

@@ -1,6 +1,26 @@
(Az Adatbázis Bibliája.)
# 🗄️ DATABASE GUIDE
# 🗄️ DATABASE GUIDE & DATA INTEGRITY (v1.0)
## 1. Soft Delete & Újraregisztráció Logika
A rendszerben nincs fizikai törlés. A `data.users` tábla az alábbi módon kezeli a visszatérő felhasználókat:
- **Indexelés:** Az `email` mezőn egy *Partial Unique Index* (`idx_user_email_active_only`) található.
- **Működés:** - Ha `is_deleted = FALSE`, az email nem használható újra.
- Ha a felhasználó törli magát (`is_deleted = TRUE`), az email felszabadul.
- Új regisztrációkor a rendszer új `user_id`-t generál, de ha a KYC adatok egyeznek, ugyanahhoz a `person_id`-hoz kapcsolja az új fiókot.
## 2. Person (Személyazonosság) - KYC & Safety
A `data.persons` tábla tárolja a banki szintű azonosításhoz szükséges adatokat:
- **Szétválasztott nevek:** `last_name` és `first_name` a pontos azonosításhoz.
- **JSONB mezők:** Rugalmas adatszerkezet az okmányokhoz (`identity_docs`) és vészhelyzeti adatokhoz (`medical_emergency`).
- **Jutalom Trigger:** A profil 100%-os kitöltése (név, szül. adatok, okmányok) automatikusan aktiválja a 14 napos PRÉMIUM csomagot.
## 3. Economy (Pénztárca & Referral)
- **Wallet:** Minden regisztrációkor létrejön egy rekord a `data.wallets` táblában (0 Coin, 0 XP).
- **Referral Snapshot:** A jutalékok kifizetésekor a rendszer rögzíti a tranzakció pillanatában érvényes százalékot (`commission_percentage`), így a későbbi admin módosítások nem érintik a múltbeli elszámolásokat.
## Sémák
- `public`: Csak technikai táblák (pl. Alembic version).
- `data`: Az üzleti logika 55 táblája.
@@ -15,3 +35,15 @@
- **Eszköz:** Alembic.
- **Current Head:** `10b73fee8967`.
- **Hiányzó láncszem:** A `persons` tábla létrehozása és a meglévő `users` tábla migrációja (Ba
## 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).

View 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.

View File

@@ -1,25 +1,35 @@
(Az Üzleti Modell - A legfontosabb frissítés.)
# 💰 BILLING, CREDITS & SUBSCRIPTIONS
# 💰 BILLING, CREDITS AND MULTI-CURRENCY (v1.0)
## 1. Előfizetési Csomagok (SaaS)
## 1. Regionális és Valuta Logika (EU Scope)
A rendszer támogatja a többnyelvű és többvalutás elszámolást. Minden pénzügyi tranzakció két értéket tárol:
1. **Local Cost:** Helyi pénznemben rögzített összeg (pl. 45.000 Ft).
2. **Standard Cost (EUR):** A rögzítéskori középárfolyamon átszámított euró érték.
| Csomag | Ár (Havi) | Jármű | User | Funkciók |
| :--- | :--- | :--- | :--- | :--- |
| **FREE** | 0 Ft | 1 db | 1 | Geo Keresés (Sugár), Reklám, Nincs Export. |
| **PREMIUM** | ~1.490 Ft | 3 db | 1 | Útvonal Keresés, Nincs Reklám, Excel Export, Dokumentum Tár. |
| **PREMIUM+** | ~2.990 Ft | 5 db | 4 (Család) | Családi megosztás, Trust Score részletek. |
| **VIP** | Egyedi | 10+ | 5+ | Flotta funkciók, API, Sofőr App. |
**Átszámítási képlet:**
$$Cost_{EUR} = Cost_{Local} \cdot ExchangeRate$$
## 2. A "Free -> Premium" Szabály (Q10 Solution)
- A Free időszakban rögzített adatok **láthatóak maradnak**, de **nem képezik részét** a Prémium Elemzéseknek (TCO, Trendek).
- **Feloldás:** Visszamenőleges elemzéshez "Retroaktív Csomag" vagy folyamatos előfizetés szükséges.
## 2. Előfizetési Csomagok (Adminból állítható)
A csomagok limiteit (járműszám, funkciók) a `system_settings` tábla szabályozza.
## 3. Kredit Ökonómia (Coin)
- **Szerzés:** Adatfeltöltés (50 Coin), Meghívás (200 Coin), Validálás (5 Coin).
- **Költés:** Prémium előfizetés vásárlása, Skin-ek, Extra lekérdezések.
- **Kifizetés:** Nincs automatikus kifizetés. Nagy mennyiség esetén (pl. Üzletkötő) egyedi szerződés (Megbízási/Számlás) alapján, vagy jövőben Blokklánc (Stablecoin).
| Csomag | Jármű Limit | Kiemelt funkciók |
| :--- | :--- | :--- |
| **FREE** | 1 db | Csak GEO keresés, alap költséglog, nincs dokumentum/export. |
| **PREMIUM** | 3 db | Teljes dokumentum/fotó tár, útvonal alapú kereső, export. |
| **PREMIUM+** | 5 db | 5 felhasználó, flotta-szintű statisztika, TCO elemzés. |
| **VIP** | 10 db + | Bővíthető slotok, egyedi szerviz partnerek kezelése. |
## 4. Befizetési Technológiák
- **Stripe:** Nemzetközi kártyás fizetés.
- **Barion / SimplePay:** Magyar specifikus fizetés.
- **Coin Pack:** Mikrotanzakciók (pl. 500 Coin = 1000 Ft).
## 3. Evidence & Trust Engine (Bizonyíték kezelés)
A rendszer csak azokat a szerviz eseményeket tekinti **hitelesnek (Verified)**, amelyekhez tartozik:
- **Fotó:** Kilométeróra állásról és munkalapról.
- **Digitális számla:** Feltöltött PDF vagy kép.
- **GPS Check-in:** Igazolás, hogy a felhasználó valóban a szerviznél tartózkodott.
## 4. Szerviz Minősítési Rendszer
- Csak érvényes szerviz esemény után adható értékelés.
- **Fellebbezés:** A szolgáltató kérheti a valótlan/troll vélemény felülvizsgálatát.
- **Validátorok:** Magas rangú felhasználók pontokért/kreditért ellenőrizhetik a vitatott bejegyzéseket.
## 5. Lejárat és Helyreállítás
- **Grace Period (30 nap):** Csak rögzítés lehetséges, statisztika/lekérdezés zárolva.
- **Zárolás (60 nap):** A fiók írásvédetté válik.
- **Helyreállítás:** 6 hónapon belül visszamenőleges befizetéssel minden funkció (és a Free korszak adatai) aktiválódik.

View 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"
}

View 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 ###