feat: v1.7 overhaul - identity hash, triple wallet, financial ledger, and security audit system

This commit is contained in:
2026-02-16 00:42:49 +00:00
parent bb02d4ed59
commit d574d3297d
63 changed files with 3710 additions and 565 deletions

View File

@@ -1,11 +1,12 @@
# /opt/docker/dev/service_finder/backend/app/models/__init__.py
from app.db.base_class import Base
# Identitás és Jogosultság
from .identity import User, Person, Wallet, UserRole, VerificationToken, SocialAccount
# Szervezeti struktúra
from .organization import Organization, OrganizationMember
# Szervezeti struktúra (HOZZÁADVA: OrganizationSalesAssignment)
from .organization import Organization, OrganizationMember, OrganizationSalesAssignment
# Járművek és Eszközök (Digital Twin)
from .asset import (
@@ -13,24 +14,25 @@ from .asset import (
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
)
# Szerviz és Szakértelem (ÚJ)
# Szerviz és Szakértelem
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging
# Földrajzi adatok és Címek
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Branch
# Gamification és Economy
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger
# Rendszerkonfiguráció és Alapok
from .system_config import SystemParameter
# Rendszerkonfiguráció (HASZNÁLJUK a frissített system.py-t!)
from .system import SystemParameter
from .document import Document
from .translation import Translation
# Üzleti logika és Előfizetés
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
# Naplózás és Biztonság
# Naplózás és Biztonság (HOZZÁADVA: audit.py modellek)
from .audit import SecurityAuditLog, OperationalLog, FinancialLedger # <--- KRITIKUS!
from .history import AuditLog, VehicleOwnership
from .security import PendingAction
@@ -42,16 +44,15 @@ ServiceRecord = AssetEvent
__all__ = [
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
"Organization", "OrganizationMember",
"Organization", "OrganizationMember", "OrganizationSalesAssignment",
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
"AssetTelemetry", "AssetReview", "ExchangeRate",
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
"Point_Rule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"SystemParameter", "Document", "Translation", "PendingAction",
"SubscriptionTier", "OrganizationSubscription",
"CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership",
# --- SZERVIZ MODUL (Tisztítva) ---
"SecurityAuditLog", "OperationalLog", "FinancialLedger", # <--- KRITIKUS!
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging",
# --- ALIASOK ---
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord"
]

View File

@@ -1,7 +1,9 @@
import uuid
from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime, Float
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.sql import func
# Hozzáadva: Boolean, text, func
from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime, Float, Boolean, text, func
# PostgreSQL specifikus típusok
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from sqlalchemy.orm import relationship
from app.db.base_class import Base
class GeoPostalCode(Base):
@@ -45,4 +47,44 @@ class Address(Base):
latitude = Column(Float)
longitude = Column(Float)
created_at = Column(DateTime(timezone=True), server_default=func.now())
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Add to /app/models/address.py
class Branch(Base):
"""
Telephely entitás. A fizikai helyszín, ahol a szolgáltatás vagy flotta-kezelés zajlik.
Minden cégnek van legalább egy 'Main' telephelye.
"""
__tablename__ = "branches"
__table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
name = Column(String(100), nullable=False) # pl. "Központi iroda", "Dunakeszi Szerviz"
is_main = Column(Boolean, default=False)
# Részletes címadatok (Denormalizált a gyors kereséshez)
postal_code = Column(String(10), index=True)
city = Column(String(100), index=True)
street_name = Column(String(150))
street_type = Column(String(50))
house_number = Column(String(20))
stairwell = Column(String(20))
floor = Column(String(20))
door = Column(String(20))
hrsz = Column(String(50)) # Helyrajzi szám
# Telephely specifikus adatok
opening_hours = Column(JSONB, server_default=text("'{}'::jsonb"))
branch_rating = Column(Float, default=0.0)
status = Column(String(30), default="active")
is_deleted = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
organization = relationship("Organization", back_populates="branches")
address = relationship("Address")
# Kapcsolat a szerviz értékelésekkel
reviews = relationship("Rating", primaryjoin="and_(Branch.id==foreign(Rating.target_id), Rating.target_type=='branch')")

View File

@@ -24,6 +24,16 @@ class AssetCatalog(Base):
year_to = Column(Integer)
vehicle_class = Column(String)
fuel_type = Column(String, index=True)
# --- ÚJ OSZLOPOK (Ezeket add hozzá!) ---
power_kw = Column(Integer, index=True)
engine_capacity = Column(Integer, index=True)
max_weight_kg = Column(Integer)
axle_count = Column(Integer)
euro_class = Column(String(20))
body_type = Column(String(100))
# ---------------------------------------
engine_code = Column(String)
factory_data = Column(JSONB, server_default=text("'{}'::jsonb"))
@@ -101,11 +111,17 @@ class AssetAssignment(Base):
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
# ÚJ: Telephelyi hozzárendelés
branch_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"), nullable=True)
assigned_at = Column(DateTime(timezone=True), server_default=func.now())
released_at = Column(DateTime(timezone=True), nullable=True)
status = Column(String(30), default="active")
asset = relationship("Asset", back_populates="assignments")
organization = relationship("Organization")
branch = relationship("Branch") # Új kapcsolat
class AssetEvent(Base):
__tablename__ = "asset_events"
@@ -144,4 +160,27 @@ class ExchangeRate(Base):
id = Column(Integer, primary_key=True)
base_currency = Column(String(3), default="EUR")
target_currency = Column(String(3), unique=True)
rate = Column(Numeric(18, 6), nullable=False)
rate = Column(Numeric(18, 6), nullable=False)
class CatalogDiscovery(Base):
"""
Discovery tábla: Ide gyűjtjük a piaci 'neveket' (pl. Citroen C3).
A Robot innen indulva keresi meg az összes létező technikai variánst.
"""
__tablename__ = "catalog_discovery"
id = Column(Integer, primary_key=True, index=True)
make = Column(String(100), nullable=False, index=True)
model = Column(String(100), nullable=False, index=True)
vehicle_class = Column(String(50), index=True) # car, motorcycle, truck, stb.
source = Column(String(50)) # 'hasznaltauto', 'mobile.de'
status = Column(String(20), server_default=text("'pending'"), index=True)
attempts = Column(Integer, default=0)
last_attempt = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
# EGYESÍTETT __table_args__
__table_args__ = (
UniqueConstraint('make', 'model', 'vehicle_class', name='_make_model_class_uc'),
{"schema": "data"}
)

View File

@@ -1,16 +1,56 @@
from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey, text
from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey, text, Numeric, Boolean, BigInteger
from sqlalchemy.sql import func
from app.db.base_class import Base
class AuditLog(Base):
__tablename__ = "audit_logs"
__table_args__ = {"schema": "data"}
class SecurityAuditLog(Base):
""" Kiemelt biztonsági események és a 4-szem elv. """
__tablename__ = "security_audit_logs"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True, index=True)
id = Column(Integer, primary_key=True)
action = Column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST', 'SUB_EXTEND'
actor_id = Column(Integer, ForeignKey("data.users.id")) # Aki kezdeményezte
target_id = Column(Integer, ForeignKey("data.users.id")) # Akivel történt
# 4-szem elv: csak akkor válik élessé, ha ez nem NULL
confirmed_by_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
is_critical = Column(Boolean, default=False) # Szuperadmin hívásoknál True
payload_before = Column(JSON)
payload_after = Column(JSON)
created_at = Column(DateTime(timezone=True), server_default=func.now())
class OperationalLog(Base):
""" Napi üzemi események (Operational). """
__tablename__ = "operational_logs"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True, index=True) # <--- EZ HIÁNYZOTT!
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="SET NULL"), nullable=True)
action = Column(String(100), nullable=False) # pl. "LOGIN", "REGISTER", "DELETE_ASSET"
resource_type = Column(String(50)) # pl. "User", "Asset", "Organization"
action = Column(String(100), nullable=False) # pl. "ADD_VEHICLE", "UPDATE_COST"
resource_type = Column(String(50)) # pl. "Asset", "Expense"
resource_id = Column(String(100))
details = Column(JSON, server_default=text("'{}'::jsonb"))
ip_address = Column(String(45))
created_at = Column(DateTime(timezone=True), server_default=func.now())
class FinancialLedger(Base):
""" Minden pénz- és kreditmozgás központi naplója. """
__tablename__ = "financial_ledger"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("data.users.id"))
person_id = Column(BigInteger, ForeignKey("data.persons.id"))
amount = Column(Numeric(18, 4), nullable=False)
currency = Column(String(10)) # 'HUF', 'CREDIT', 'COIN'
transaction_type = Column(String(50)) # 'PURCHASE', 'HUNTING_COMMISSION', 'FARMING_COMMISSION'
# Üzletkötői követhetőség
related_agent_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
details = Column(JSON, server_default=text("'{}'::jsonb"))
created_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@@ -9,29 +9,34 @@ from app.db.base_class import Base
class UserRole(str, enum.Enum):
superadmin = "superadmin"
admin = "admin"
region_admin = "region_admin"
country_admin = "country_admin"
moderator = "moderator"
sales_agent = "sales_agent"
user = "user"
service = "service"
service_owner = "service_owner"
fleet_manager = "fleet_manager"
driver = "driver"
class Person(Base):
"""
Természetes személy identitása.
A bot által talált személyek is ide kerülnek (is_ghost=True).
Azonosítás: Név + Anyja neve + Születési adatok alapján.
Természetes személy identitása. A DNS szint.
Itt tároljuk az örök adatokat, amik nem vesznek el account törléskor.
"""
__tablename__ = "persons"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(BigInteger, primary_key=True, index=True)
id_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
# --- KRITIKUS: EGYEDI AZONOSÍTÓ HASH (Normalizált adatokból) ---
identity_hash = Column(String(64), unique=True, index=True, nullable=True)
last_name = Column(String, nullable=False)
first_name = Column(String, nullable=False)
phone = Column(String, nullable=True)
# --- TERMÉSZETES AZONOSÍTÓK (Azonosításhoz, nem publikus) ---
mothers_last_name = Column(String)
mothers_first_name = Column(String)
birth_place = Column(String)
@@ -40,8 +45,14 @@ class Person(Base):
identity_docs = Column(JSON, server_default=text("'{}'::jsonb"))
ice_contact = Column(JSON, server_default=text("'{}'::jsonb"))
is_active = Column(Boolean, default=False, nullable=False)
is_ghost = Column(Boolean, default=True, nullable=False) # Bot találta = True, Regisztrált = False
# --- ÖRÖK ADATOK (Person szint) ---
lifetime_xp = Column(BigInteger, server_default=text("0"))
penalty_points = Column(Integer, server_default=text("0")) # 0-3 szint
social_reputation = Column(Numeric(3, 2), server_default=text("1.00")) # 1.00 = 100%
is_sales_agent = Column(Boolean, server_default=text("false"))
is_active = Column(Boolean, default=True, nullable=False)
is_ghost = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
@@ -50,28 +61,39 @@ class Person(Base):
memberships = relationship("OrganizationMember", back_populates="person")
class User(Base):
"""
Login entitás. Bármikor törölhető (GDPR), de Person-höz kötött.
"""
__tablename__ = "users"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=True)
role = Column(Enum(UserRole), default=UserRole.user)
is_active = Column(Boolean, default=False)
is_deleted = Column(Boolean, default=False)
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
folder_slug = Column(String(12), unique=True, index=True)
refresh_token_hash = Column(String(255), nullable=True)
two_factor_secret = Column(String(100), nullable=True)
two_factor_enabled = Column(Boolean, default=False)
# --- ELŐFIZETÉS ÉS VIP (Időkorlátos logika) ---
subscription_plan = Column(String(30), server_default=text("'FREE'"))
subscription_expires_at = Column(DateTime(timezone=True), nullable=True)
is_vip = Column(Boolean, server_default=text("false"))
# --- REFERRAL ÉS SALES (Üzletkötői hálózat) ---
referral_code = Column(String(20), unique=True)
referred_by_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
# Farming üzletkötő (Átruházható cégkezelő)
current_sales_agent_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
is_active = Column(Boolean, default=False)
is_deleted = Column(Boolean, default=False)
folder_slug = Column(String(12), unique=True, index=True)
preferred_language = Column(String(5), server_default="hu")
region_code = Column(String(5), server_default="HU")
preferred_currency = Column(String(3), server_default="HUF")
scope_level = Column(String(30), server_default="individual")
scope_level = Column(String(30), server_default="individual") # global, region, country, entity, individual
scope_id = Column(String(50))
custom_permissions = Column(JSON, server_default=text("'{}'::jsonb"))
@@ -79,18 +101,25 @@ class User(Base):
person = relationship("Person", back_populates="users")
wallet = relationship("Wallet", back_populates="user", uselist=False)
stats = relationship("UserStats", back_populates="user", uselist=False)
ownership_history = relationship("VehicleOwnership", back_populates="user")
owned_organizations = relationship("Organization", back_populates="owner")
social_accounts = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan")
class Wallet(Base):
__tablename__ = "wallets"; __table_args__ = {"schema": "data"}
""" A 3-as felosztású pénztárca. """
__tablename__ = "wallets"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id"), unique=True)
coin_balance = Column(Numeric(18, 2), default=0.00); credit_balance = Column(Numeric(18, 2), default=0.00); currency = Column(String(3), default="HUF")
earned_credits = Column(Numeric(18, 4), server_default=text("0")) # Munka + Referral
purchased_credits = Column(Numeric(18, 4), server_default=text("0")) # Vásárolt
service_coins = Column(Numeric(18, 4), server_default=text("0")) # Csak hirdetésre!
currency = Column(String(3), default="HUF")
user = relationship("User", back_populates="wallet")
# ... (VerificationToken és SocialAccount változatlan) ...
class VerificationToken(Base):
__tablename__ = "verification_tokens"; __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)

View File

@@ -61,6 +61,11 @@ class Organization(Base):
status = Column(String(30), default="pending_verification")
is_deleted = Column(Boolean, default=False)
# --- ÚJ: Előfizetés és Méret korlátok ---
subscription_plan = Column(String(30), server_default=text("'FREE'"), index=True)
base_asset_limit = Column(Integer, server_default=text("1"))
purchased_extra_slots = Column(Integer, server_default=text("0"))
notification_settings = Column(JSON, server_default=text("'{\"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1]}'::jsonb"))
external_integration_config = Column(JSON, server_default=text("'{}'::jsonb"))
@@ -70,6 +75,10 @@ class Organization(Base):
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# --- ÚJ: Dual Twin Tulajdonjog logika ---
# Individual esetén False, Business esetén True
is_ownership_transferable = Column(Boolean, server_default=text("true"))
# Kapcsolatok
assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan")
@@ -77,6 +86,7 @@ class Organization(Base):
owner = relationship("User", back_populates="owned_organizations")
financials = relationship("OrganizationFinancials", back_populates="organization", cascade="all, delete-orphan")
service_profile = relationship("ServiceProfile", back_populates="organization", uselist=False)
branches = relationship("Branch", back_populates="organization", cascade="all, delete-orphan")
class OrganizationFinancials(Base):
"""Cégek éves gazdasági adatai elemzéshez."""
@@ -111,4 +121,15 @@ class OrganizationMember(Base):
organization = relationship("Organization", back_populates="members")
user = relationship("User")
person = relationship("Person", back_populates="memberships")
person = relationship("Person", back_populates="memberships")
class OrganizationSalesAssignment(Base):
"""Összeköti a céget az aktuális üzletkötővel a jutalék miatt."""
__tablename__ = "org_sales_assignments"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
organization_id = Column(Integer, ForeignKey("data.organizations.id"))
agent_user_id = Column(Integer, ForeignKey("data.users.id")) # Ő kapja a Farming díjat
assigned_at = Column(DateTime(timezone=True), server_default=func.now())
is_active = Column(Boolean, default=True)

View File

@@ -86,12 +86,17 @@ class ServiceStaging(Base):
name = Column(String, nullable=False, index=True)
# --- Strukturált cím adatok (A kérésedre bontva) ---
postal_code = Column(String(10), nullable=True, index=True) # Irányítószám
city = Column(String(100), nullable=True, index=True) # Település
street = Column(String(255), nullable=True) # Utca és közterület jellege (pl. Diófa utca)
house_number = Column(String(50), nullable=True) # Házszám, emelet, ajtó
full_address = Column(String, nullable=True) # Az eredeti, egybefüggő cím (ha van)
postal_code = Column(String(10), index=True)
city = Column(String(100), index=True)
street_name = Column(String(150))
street_type = Column(String(50)) # utca, út, tér...
house_number = Column(String(20))
stairwell = Column(String(20)) # lépcsőház
floor = Column(String(20)) # emelet
door = Column(String(20)) # ajtó
hrsz = Column(String(50)) # helyrajzi szám
full_address = Column(String) # Eredeti string (audit célból)
# --- Elérhetőségek ---
contact_phone = Column(String, nullable=True)
email = Column(String, nullable=True)
@@ -111,4 +116,14 @@ class ServiceStaging(Base):
status = Column(String(20), server_default=text("'pending'"), index=True)
trust_score = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
created_at = Column(DateTime(timezone=True), server_default=func.now())
class DiscoveryParameter(Base):
__tablename__ = "discovery_parameters"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
city = Column(String(100), nullable=False)
keyword = Column(String(100), nullable=False) # pl. "autóvillamosság"
country_code = Column(String(2), default="HU")
is_active = Column(Boolean, default=True)
last_run_at = Column(DateTime(timezone=True))

View File

@@ -7,7 +7,11 @@ class SystemParameter(Base):
__table_args__ = {"schema": "data", "extend_existing": True}
key = Column(String, primary_key=True, index=True, nullable=False)
# Csoportosítás az Admin felületnek (pl. 'xp', 'scout', 'routing')
category = Column(String, index=True, server_default="general")
value = Column(JSON, nullable=False)
is_active = Column(Boolean, default=True)
description = Column(String)
# Kötelező audit mező: ki módosította utoljára?
last_modified_by = Column(String, nullable=True)
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())