From 24d35fe0c1761f4ff65e3df8589e96bf89a0d19c Mon Sep 17 00:00:00 2001 From: Kincses Date: Sun, 8 Feb 2026 23:41:07 +0000 Subject: [PATCH] feat: stabilize KYC, international assets and multi-currency schema - Split mother's name in KYC (last/first) - Added mileage_unit and fuel_type to Assets - Expanded AssetCost for international VAT and original currency - Fixed SQLAlchemy IndexError in asset catalog lookup - Added exchange_rate and ratings tables to models --- .../app/api/__pycache__/deps.cpython-312.pyc | Bin 2151 -> 2564 bytes backend/app/api/deps.py | 14 +- .../__pycache__/assets.cpython-312.pyc | Bin 4926 -> 7525 bytes .../__pycache__/catalog.cpython-312.pyc | Bin 2057 -> 2058 bytes backend/app/api/v1/endpoints/assets.py | 190 +++++++++------- backend/app/api/v1/endpoints/catalog.py | 2 +- .../app/db/__pycache__/base.cpython-312.pyc | Bin 551 -> 689 bytes .../db/__pycache__/base_class.cpython-312.pyc | Bin 0 -> 771 bytes backend/app/db/base.py | 16 +- backend/app/db/base_class.py | 13 ++ backend/app/models/__init__.py | 43 ++-- .../__pycache__/__init__.cpython-312.pyc | Bin 711 -> 890 bytes .../__pycache__/address.cpython-312.pyc | Bin 0 -> 2800 bytes .../models/__pycache__/asset.cpython-312.pyc | Bin 0 -> 7192 bytes .../__pycache__/gamification.cpython-312.pyc | Bin 4897 -> 6263 bytes .../__pycache__/identity.cpython-312.pyc | Bin 4671 -> 5083 bytes .../__pycache__/organization.cpython-312.pyc | Bin 4097 -> 4462 bytes backend/app/models/address.py | 45 ++++ backend/app/models/asset.py | 133 +++++++++++ backend/app/models/gamification.py | 44 +++- backend/app/models/identity.py | 29 +-- backend/app/models/organization.py | 41 ++-- backend/app/models/vehicle.py | 122 ---------- .../schemas/__pycache__/asset.cpython-312.pyc | Bin 1691 -> 2419 bytes .../schemas/__pycache__/auth.cpython-312.pyc | Bin 2628 -> 2898 bytes backend/app/schemas/asset.py | 46 ++-- backend/app/schemas/auth.py | 11 +- .../__pycache__/auth_service.cpython-312.pyc | Bin 5791 -> 13709 bytes backend/app/services/auth_service.py | 213 ++++++++++++++---- docs/V01_gemini/06_Database_Guide.md | 15 +- docs/V01_gemini/07_API_Guide.md | 13 +- docs/V01_gemini/11_Gamification_Social.md | 8 +- docs/V01_gemini/15_Changelog.md | 43 +++- .../18_ASSET_AND_FLEET_SPECIFICATION.md | 15 +- 34 files changed, 709 insertions(+), 347 deletions(-) mode change 100755 => 100644 backend/app/db/__pycache__/base.cpython-312.pyc create mode 100644 backend/app/db/__pycache__/base_class.cpython-312.pyc create mode 100644 backend/app/db/base_class.py create mode 100644 backend/app/models/__pycache__/address.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/asset.cpython-312.pyc create mode 100644 backend/app/models/address.py create mode 100644 backend/app/models/asset.py delete mode 100755 backend/app/models/vehicle.py diff --git a/backend/app/api/__pycache__/deps.cpython-312.pyc b/backend/app/api/__pycache__/deps.cpython-312.pyc index 70ce68af6950e6a4dc6f0e68830be5355e04cfc7..4eb33d6833c5a221aa9fab5a805d735d176ae135 100644 GIT binary patch delta 1425 zcmcIkO>7%Q6rS1tUB~N990}pq*Mr%rfp6^1t}FNOVMi6nYiB0+Scy6 zD8^NyQlT7rAXk}e?ifCRl+0*RjkV%F)@j(Xw39NzoB z_jbN{^J#Z~F>Yt{?~+Lk(4}_Ps{csq`pC+Ulx+Y65G25bCWJBO2xoFM^MrQ=Q*cF7 zbR|>j^#mfjimCLlNK{udwH}s8!W}Y)dRQh&S2y(@R!GV*WBL9asL5@w- z7T7fZ*{%GOyHQQPgK~9!!G| z;2mQc+njeSX@(YWXI-j`piXAw1(JoFPe8XB?ZrHwW*;j+=4Lnf{>eq2C zpv@)h5bU>t2kX^h5Ccp)cl~R)^@3KUD;j=g^W*rPy%@eF;BZ@*hemi-d@iB+S8ZZ@ zR=tM9tavK4#5ePjhy5lABDrBNle&!}x#?ris-kdLd}(2Vs>Ck(lT`0l@aHP%rh$^& zF;09hpE$_nH(Kkh&&IbhpPt$;P3@MZzPj|y!o7w2@ZOcZ(z(6d`TgPZYtlg~yPqoV zriy#1(vDL4Sucbo>2;`uzSO#`fgxjct~&!k`U3o`TLStV{M(Oz2)AbB+2e3qg|m5K zTRS&JN#_il9TPgQ%#2d_v)Y z)od4ARwTtw&`Qd~P2XN5*s9yj;M$83?+2b2-}o;0jYid95E{lnNpABUuW9VTFs}LB!a*Iy2rkBX8blV>671 z)Fz0ykS=r4LNo}XT_HHjHbpIJVkm;Bb|blnR^2!DGNGWeIOqG$z2`gkocER*KL?fP ze!mBp4LrS-?3eEsTNz!>zgN zSR@|9t9k2KB0j^f`RiCFiV@HPb?hQRBcz28aDZjqz2R9$_A3Cikp`CHvEND5S%bqS zIo*rhx(|nSKMqYwy0Ro{jrl+<_|>kCp^EFmjFrr!CK zZGK1GQXJ$oNQLYhNTHittIuk3jte|}!ncRT8*Hk3+%kVt6rm^-gj6#vaxG_yVnO`J z$Pq_kFY401SBa?>Iw&*^|7~HddsPn7<^9#;&=0q@hx{ChxpMdEs&M)INL-DpR%RNf z)$*D-TV6}&Ele1jjZfv-(Ih5`cy>NrUM1znY8o3VDX&?0J~`d3CXy57RZG<~6GuHN zGpdOgjnPeU90llUsk=?a*YUNSg-!cInLg$!;#>loxWXlMnvk@E@=QEWGI6~k=d#$m zn$#}AycVY645tMLe-Dn#Ey-%IGLLT?q0RiW(@bX4I;6}q-12p@jYGxWM)cp<<@ sq User: """ Dependency, amely visszaadja az aktuálisan bejelentkezett felhasználót. - Ha a token érvénytelen vagy a felhasználó nem létezik, hibát dob. + Támogatja a 'dev_bypass_active' tokent a fejlesztői teszteléshez. """ + # FEJLESZTŐI BYPASS + if token == "dev_bypass_active": + result = await db.execute(select(User).where(User.id == 1)) + return result.scalar_one() + payload = decode_token(token) if not payload: raise HTTPException( @@ -33,7 +39,6 @@ async def get_current_user( detail="Token azonosítási hiba." ) - # Felhasználó lekérése az adatbázisból result = await db.execute(select(User).where(User.id == int(user_id))) user = result.scalar_one_or_none() @@ -48,8 +53,5 @@ async def get_current_user( status_code=status.HTTP_403_FORBIDDEN, detail="Ez a fiók korábban törlésre került." ) - - # Megjegyzés: is_active ellenőrzést szándékosan nem teszünk itt, - # hogy a KYC folyamatot (Step 2) be tudja fejezni a még nem aktív user is. return user \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc index bb82254c74a043c769895b0c169afe3d065fd96e..b1a5b8dc08a47840b00f35e8ae1c9e7f10ee071f 100644 GIT binary patch literal 7525 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 literal 4926 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 S+|91Us62TcdpL_JPznIinj-R!}b#>`p-lAXMd bool: + vin = vin.upper() + if len(vin) != 17: + return False + if any(c in vin for c in "IOQ"): + return False + return True + router = APIRouter() logger = logging.getLogger(__name__) @router.post("/", response_model=AssetResponse, status_code=status.HTTP_201_CREATED) async def create_asset( asset_in: AssetCreate, - db: AsyncSession = Depends(get_db) - # Később ide jön: current_user: User = Depends(get_current_active_user) + target_org_id: int = None, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) ): - """ - Új jármű (Asset) rögzítése a flottába. - - Validálja a VIN-t (MOD 11 checksum). - - Ellenőrzi a Katalógus elemet. - - Létrehozza a NAS mappát a dokumentumoknak. - """ - - # 1. VIN Validáció (Szigorú Checksum) - # A GEM protokoll szerint kötelező a validátor használata - is_valid_vin = VINValidator.validate(asset_in.vin) - if not is_valid_vin: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Érvénytelen alvázszám (VIN)! A Checksum ellenőrzés sikertelen." - ) + # 1. VIN Validáció + if not VINValidator.validate(asset_in.vin): + raise HTTPException(status_code=400, detail="Érvénytelen alvázszám (VIN) formátum!") - # 2. Katalógus elem ellenőrzése - stmt_catalog = select(VehicleCatalog).where(VehicleCatalog.id == asset_in.catalog_id) - result_catalog = await db.execute(stmt_catalog) - catalog_item = result_catalog.scalar_one_or_none() + # 2. Célflotta ellenőrzése + if not target_org_id: + stmt_org = select(Organization).join(OrganizationMember).where( + and_( + OrganizationMember.user_id == current_user.id, + Organization.org_type == OrgType.individual + ) + ) + org = (await db.execute(stmt_org)).scalar_one_or_none() + if not org: + raise HTTPException(status_code=404, detail="Privát flotta nem található. KYC szükséges.") + final_org_id = org.id + else: + # Céges jogosultság ellenőrzése + stmt_mem = select(OrganizationMember).where( + and_( + OrganizationMember.organization_id == target_org_id, + OrganizationMember.user_id == current_user.id + ) + ) + member = (await db.execute(stmt_mem)).scalar_one_or_none() + if not member or (member.role != "owner" and not (member.permissions or {}).get("can_add_asset")): + raise HTTPException(status_code=403, detail="Nincs jogod ehhez a flottához!") + final_org_id = target_org_id + + # 3. Katalógus ellenőrzése + stmt_cat = select(AssetCatalog).where( + and_( + AssetCatalog.make.ilike(asset_in.make), # Simán ilike, nem kell func() köré + AssetCatalog.model.ilike(asset_in.model) + ) + ) + catalog_item = (await db.execute(stmt_cat)).scalar_one_or_none() if not catalog_item: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="A kiválasztott járműtípus nem található a katalógusban." + catalog_item = AssetCatalog( + make=asset_in.make, + model=asset_in.model, + vehicle_class=asset_in.vehicle_class, + fuel_type=asset_in.fuel_type ) + db.add(catalog_item) + await db.flush() - # 3. Szervezet ellenőrzése (Létezik-e, és van-e joga - jogultságkezelés később) - stmt_org = select(Organization).where(Organization.id == asset_in.organization_id) - result_org = await db.execute(stmt_org) - org_item = result_org.scalar_one_or_none() - - if not org_item: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="A megadott flotta/szervezet nem található." + # 4. Asset létrehozása vagy betöltése (Shadow Identity) + stmt_exist = select(Asset).where(Asset.vin == asset_in.vin.upper()) + new_asset = (await db.execute(stmt_exist)).scalar_one_or_none() + + if not new_asset: + new_asset = Asset( + vin=asset_in.vin.upper(), + license_plate=asset_in.license_plate, + name=asset_in.name or f"{asset_in.make} {asset_in.model}", + year_of_manufacture=asset_in.year_of_manufacture, + fuel_type=asset_in.fuel_type, # JAVÍTVA: Most már átadjuk + vehicle_class=asset_in.vehicle_class, # JAVÍTVA: Most már átadjuk + mileage_unit=asset_in.reading_unit, # JAVÍTVA: Most már átadjuk + catalog_id=catalog_item.id, + quality_index=1.00, + system_mileage=0 ) + db.add(new_asset) + await db.flush() - # 4. Asset Duplikáció ellenőrzése (Ugyanaz a VIN nem szerepelhet 2x aktívként) - # Megj: A UAI mező tárolja a VIN-t (Unique Asset Identifier) - stmt_exist = select(Asset).where( - Asset.uai == asset_in.vin.upper(), - Asset.status != "deleted" + # 5. Assignment + new_assignment = AssetAssignment( + asset_id=new_asset.id, + organization_id=final_org_id, + status="active" ) - result_exist = await db.execute(stmt_exist) - if result_exist.scalar_one_or_none(): - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="Ez a jármű (VIN) már szerepel a rendszerben!" - ) + db.add(new_assignment) - # 5. Mentés az adatbázisba - new_asset = Asset( - uai=asset_in.vin.upper(), # A VIN a fő azonosító - catalog_id=asset_in.catalog_id, - organization_id=asset_in.organization_id, - asset_type=catalog_item.category, # 'car', 'van', 'motorcycle', etc. - name=asset_in.name or f"{catalog_item.brand} {catalog_item.model}", - current_plate_number=asset_in.license_plate.upper(), - status="active", - privacy_level="private" # Alapértelmezett - ) - - db.add(new_asset) - await db.commit() - await db.refresh(new_asset) + # 6. Kezdő KM esemény + if asset_in.current_reading: + db.add(AssetEvent( + asset_id=new_asset.id, + event_type="initial_reading", + recorded_mileage=asset_in.current_reading, + description="Kezdeti óraállás rögzítése", + data={"source": "user_registration"} + )) - # 6. NAS Tároló Létrehozása - # Útvonal: /mnt/nas/app_data/assets/{uuid}/ - # A settings.NAS_STORAGE_PATH-ot használjuk (GEM protokoll) try: - # Ha a settings-ben nincs definiálva, fallback a hardcoded path-ra (biztonsági háló) - base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data/assets") - asset_path = os.path.join(base_path, str(new_asset.id)) + await db.commit() + await db.refresh(new_asset) - os.makedirs(asset_path, exist_ok=True) - logger.info(f"NAS mappa létrehozva: {asset_path}") + # 7. NAS mappa struktúra + nas_base = getattr(settings, "NAS_STORAGE_PATH", "/opt/docker/dev/service_finder/nas/assets") + asset_path = os.path.join(nas_base, str(new_asset.id)) + os.makedirs(os.path.join(asset_path, "docs"), exist_ok=True) + os.makedirs(os.path.join(asset_path, "photos"), exist_ok=True) - except OSError as e: - logger.error(f"CRITICAL: Nem sikerült létrehozni a NAS mappát: {e}") - # Nem dobunk hibát a usernek, mert az adatbázisba bekerült, de riasztunk logban - - return new_asset \ No newline at end of file + return new_asset + except Exception as e: + await db.rollback() + logger.error(f"Asset Creation Error: {str(e)}") + raise HTTPException(status_code=500, detail="Hiba a mentés során.") \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/catalog.py b/backend/app/api/v1/endpoints/catalog.py index bbe3368..1bd059d 100644 --- a/backend/app/api/v1/endpoints/catalog.py +++ b/backend/app/api/v1/endpoints/catalog.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, or_ from app.db.session import get_db -from app.models.vehicle import VehicleCatalog +from app.models import AssetCatalog from typing import List router = APIRouter() diff --git a/backend/app/db/__pycache__/base.cpython-312.pyc b/backend/app/db/__pycache__/base.cpython-312.pyc old mode 100755 new mode 100644 index beade1b6b6918b56d96b747c41f9053ca4b6367e..dcf3c65a9985fc54d9f84dd1ccc35c887cdabbfa GIT binary patch literal 689 zcmYjP&2G~`5Z;Y{VkdSQ$Fx0ET;SwO`T(tvN_H$jy5o zpGZkcDv6?uQsh0^Qy%k_&wLfIK!q%9^PcRhh(#)9u^O;}8nR)V_hq6|mZ}jOwQ(T# z)R>J`#xgZw6P2^P&4==i+GqRffE`e>=@-#2T%(}Qf5?p}eAhoKOD&9X=}a_>##k=T zn^L$K!V9Z~ux({WFE?v^#(!>vcHu&3)6_0|A$0Y+y5hEK>dWShs9pGyODSy8bIDw< zcwK#Hi*C}5o(i=T8rlVC#=w}Qy`6K|pjo+i7r{bq(9*~6M2#I|9DZTBH7;2+Rc*}^ zQLco3RvfyFuh-LZIbDLcS67l7qdl43+miV;nQzJLn#{K3(JeXoPEPa@ G=>7s?ipS^x literal 551 zcmYjOF-ycS6n<&i^X|M;K@i+BTETO15)n=W5kwHTE+N-MEu`08(yG11 z^dA(QoZL=M7blZe(Fb|?Ufz3OzPvnlyKOMWp01)L1n_EuUu*n;*_HwKz=0zPF$xf} z*pW_*1B}3d1K@Z9+!aXRN4M?@+`{u4Z#M8z+3xFN81}PFX%j8(i%>GfvglImF)hNn zrS?WeWwRx--GhJtaR?la0$eygE?wI(F51MHg~U*4P0kWUn3Ux7xt{ES80wCg-ubp@GqIDSlM5w+2<{)h`P40xl`-i hRd&91*m~8tDMkprz{U%#e>6PQy4}9J`2+*1)^9c5jD-LI diff --git a/backend/app/db/__pycache__/base_class.cpython-312.pyc b/backend/app/db/__pycache__/base_class.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c224a8bdae84ea90e1f1f6d273f144b8e84abc9 GIT binary patch literal 771 zcmY*XF>ezw6n^&IrAJG0GzbJtR4qccq#I%=NFb!LkOi^aVmW66sqQYReJKsffYc(v z#2Eep>hEA;0d)XbLKHEvrDQ;2;`uI@fTjEV`90tJzGwSGy}k@MZoPli-6sG)?4lap zj7;w%8G`^p5=cozh~+e)sS`P=8@U81Natv_ox(N<=MaQ@M9WZ>iM*i~)_&SxNQ`qQ z8-~<0xMo7e30FMt?nzTG7g8{u=PI5REhcBSnBGJ(1_==n5Ya~{E?q&kFe*J{v|u&i zNbfNf?x8z3uw)Gw_b%7c(5^5(V;N5+W2UBLK2TYVhcF-3zSW%GKp4XacIK%C*{NJD zLQv^F#6H_0RcA0~yrlO`D%*IRXl-?_{<*x#`~Bh*oy`uqVt5c*xB5d*aU_y-edCN9Rh}2cQCkcc9K$-mtJ#fr~_6M|cS*{>s7jm&WSBgNYBd z^Ak$EjkobHz-j7W;M4}n8us@#UP3J&_Pg1WQvZtHO?VPNmFci0_w&|24? str: + return cls.__name__.lower() \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 86cf85f..63d30ad 100755 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,19 +1,15 @@ -from app.db.base import Base +from app.db.base_class import Base from .identity import User, Person, Wallet, UserRole, VerificationToken -from .organization import Organization, OrgType -from .vehicle import ( - VehicleCatalog, - Asset, - AssetEvent, - AssetRating, - ServiceProvider, - Vehicle, # Alias az Asset-re a kompatibilitás miatt - ServiceRecord # Alias az AssetEvent-re a kompatibilitás miatt -) +from .organization import Organization, OrganizationMember +from .asset import Asset, AssetCatalog, AssetCost, AssetEvent +from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType +from .gamification import UserStats, PointsLedger -# Aliasok a kód többi részének szinkronizálásához -UserVehicle = Vehicle -VehicleOwnership = Asset +# Aliasok a kompatibilitás és a tiszta kód érdekében +Vehicle = Asset +UserVehicle = Asset +VehicleCatalog = AssetCatalog +ServiceRecord = AssetEvent __all__ = [ "Base", @@ -23,14 +19,19 @@ __all__ = [ "UserRole", "VerificationToken", "Organization", - "OrgType", - "VehicleCatalog", + "OrganizationMember", "Asset", - "AssetEvent", - "AssetRating", - "ServiceProvider", + "AssetCatalog", + "AssetCost", + "AssetEvent", + "Address", + "GeoPostalCode", + "GeoStreet", + "GeoStreetType", + "UserStats", + "PointsLedger", "Vehicle", "UserVehicle", - "ServiceRecord", - "VehicleOwnership" + "VehicleCatalog", + "ServiceRecord" ] \ No newline at end of file diff --git a/backend/app/models/__pycache__/__init__.cpython-312.pyc b/backend/app/models/__pycache__/__init__.cpython-312.pyc index c06132a386c336dd89b735ea39577934585b57a0..42da96aa9397f979d002f8f6f477bc2359fec1d5 100644 GIT binary patch delta 492 zcmXv~J#X7E5G57cifqgAhhoQRvjhbSEkU~`@L&YCmnJdlwrIH&w0KbkNR~ko0n#PT z*40qQO#L$%IvNEG0)hTQkg-!Kxgl_m@9~azkGwDbE!n?qyNvWI-2VvfmUb8aT)v(R zF=7TQh4rXG8c~xpqZVmJ4sr5xGu(~Zq#bog2P3GGJsn-J$v*6m7d~R!zc6`uZMo&; z-RGO;1^4MJ_)cXI&(0v40(T3l*cSq%s^;4%l{AcfWo=X}q$=lha0N4|tIa-R9E4Ez z2;#BM)9^HApe!vXl7sG)Wj6G~^&DJ7Ssw-PiEL3RRCOE&Gb!Ey^Y!$0@&yl*V`FE` zclE=S`3b%SUI?m^U3nI@|K!=6Jpq0dc<>&)n6q>DKsD%me#E9nQ!3!A7g8aT74Dac znt)7hv12KdZj2{y498VfpGLuD;N@Sz9X&fYz}p$?WU!Y(HyIiy7q2wMIz&&y;}*vF z0Uf4j^aqVnbey8z3iVPnSfN3R-aeL4rM@uNwbr7tZX7IX>-zq}ekyb-#;wT@b%%e~ C7nwQ$ delta 315 zcmeyxcAS;>G%qg~0}$+;(3WX6kynzjV4`}kQm#OhK(1hvV6IS<5F#wgKBc1@0n2O?70{fp8=DhpCI*>CZMrDkL%=cGC(mL%rnr{7|AEG|whxy1#h zT+34PN^Wt3*+GdVnR)5A_=8i6$}*Ev1B&v?GE-8EZm~lRxWx;V2}(`QFG|rAoV<$B z&Eyt$XmM&0R7sH#(7m?=pgjNbywsxNjLZT*O|e_ti3J6EDM@-siN&dt{h1s&*vla9 zntX{-ck*W@3pZJyUPd4;HUSbJm>C%v?=r~TXK=sD;C`P$|2q>0Bhz;VAn_4Id=O*c Z7i-|YAuMr$LEsaM5I0jJdyzCyBLF*UV(tI{ diff --git a/backend/app/models/__pycache__/address.cpython-312.pyc b/backend/app/models/__pycache__/address.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1aef3dadc6d210dc9e454b60dedff0220129bef8 GIT binary patch literal 2800 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 Qfa9uT>Ga<@{AGvy4|ylrO#lD@ literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/gamification.cpython-312.pyc b/backend/app/models/__pycache__/gamification.cpython-312.pyc index bc8d34058be2a24de2d2f7c8c0a77d0062e43a87..0ded2200594c6966c46c41d122b147c00cf876dc 100644 GIT binary patch literal 6263 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 XF&|jLU{LHf_*^3^v;Tp*6_Wo8GyQuy delta 2111 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 diff --git a/backend/app/models/__pycache__/organization.cpython-312.pyc b/backend/app/models/__pycache__/organization.cpython-312.pyc index 468ac5b193fde71a6d42258c9731789d5720a909..1b52a980576f3a7162279b5040d0add71734f20f 100644 GIT binary patch delta 2405 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 aT@3`|lN8E~K1h${>F#ZK7PO29G diff --git a/backend/app/models/address.py b/backend/app/models/address.py new file mode 100644 index 0000000..1cbf4ec --- /dev/null +++ b/backend/app/models/address.py @@ -0,0 +1,45 @@ +import uuid +from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime +from sqlalchemy.dialects.postgresql import UUID as PG_UUID +from sqlalchemy.sql import func +from app.db.base_class import Base + +class GeoPostalCode(Base): + __tablename__ = "geo_postal_codes" + __table_args__ = {"schema": "data"} + + id = Column(Integer, primary_key=True) + country_code = Column(String(5), default="HU") + zip_code = Column(String(10), nullable=False) + city = Column(String(100), nullable=False) + +class GeoStreet(Base): + __tablename__ = "geo_streets" + __table_args__ = {"schema": "data"} + + id = Column(Integer, primary_key=True) + postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id")) + name = Column(String(200), nullable=False) + +class GeoStreetType(Base): + __tablename__ = "geo_street_types" + __table_args__ = {"schema": "data"} + + id = Column(Integer, primary_key=True) + name = Column(String(50), unique=True, nullable=False) + +class Address(Base): + __tablename__ = "addresses" + __table_args__ = {"schema": "data"} + + id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id")) + street_name = Column(String(200), nullable=False) + street_type = Column(String(50), nullable=False) + house_number = Column(String(50), nullable=False) + stairwell = Column(String(20)) + floor = Column(String(20)) + door = Column(String(20)) + parcel_id = Column(String(50)) # HRSZ + full_address_text = Column(Text) + created_at = Column(DateTime(timezone=True), server_default=func.now()) \ No newline at end of file diff --git a/backend/app/models/asset.py b/backend/app/models/asset.py new file mode 100644 index 0000000..7d7d83c --- /dev/null +++ b/backend/app/models/asset.py @@ -0,0 +1,133 @@ +import uuid +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text +from sqlalchemy.orm import relationship +from sqlalchemy.dialects.postgresql import UUID as PG_UUID +from sqlalchemy.sql import func +from app.db.base_class import Base + +class AssetCatalog(Base): + """Központi jármű katalógus (Admin/Bot által tölthető)""" + __tablename__ = "vehicle_catalog" + __table_args__ = {"schema": "data"} + + id = Column(Integer, primary_key=True, index=True) + make = Column(String, index=True, nullable=False) + model = Column(String, index=True, nullable=False) + generation = Column(String) + year_from = Column(Integer) + year_to = Column(Integer) + vehicle_class = Column(String) # land, sea, air + fuel_type = Column(String) + engine_code = Column(String) + + assets = relationship("Asset", back_populates="catalog") + +class Asset(Base): + """A Jármű Identitás (Digital Twin törzsadatok)""" + __tablename__ = "assets" + __table_args__ = {"schema": "data"} + + id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + vin = Column(String(17), unique=True, index=True, nullable=False) + license_plate = Column(String(20), index=True) + name = Column(String) + year_of_manufacture = Column(Integer) + + catalog_id = Column(Integer, ForeignKey("data.vehicle_catalog.id")) + + # Nemzetközi mutatók + quality_index = Column(Numeric(3, 2), default=1.00) + system_mileage = Column(Integer, default=0) + mileage_unit = Column(String(10), default="km") # Nemzetközi: km, miles, hours + + is_verified = Column(Boolean, default=False) + status = Column(String(20), default="active") + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + catalog = relationship("AssetCatalog", back_populates="assets") + assignments = relationship("AssetAssignment", back_populates="asset") + events = relationship("AssetEvent", back_populates="asset") + costs = relationship("AssetCost", back_populates="asset") + +class AssetAssignment(Base): + """Birtoklás követése (Kié a jármű és mettől meddig)""" + __tablename__ = "asset_assignments" + __table_args__ = {"schema": "data"} + + 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) + + assigned_at = Column(DateTime(timezone=True), server_default=func.now()) + released_at = Column(DateTime(timezone=True), nullable=True) + + status = Column(String(30), default="active") + notes = Column(String) + + asset = relationship("Asset", back_populates="assignments") + organization = relationship("Organization", back_populates="assets") + +class AssetEvent(Base): + """Élettörténeti események (Szerviz, km-óra állások, balesetek)""" + __tablename__ = "asset_events" + __table_args__ = {"schema": "data"} + + 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) + + event_type = Column(String(50), nullable=False) + event_date = Column(DateTime(timezone=True), server_default=func.now()) + + recorded_mileage = Column(Integer) + description = Column(String) + data = Column(JSON, server_default=text("'{}'::jsonb")) + + asset = relationship("Asset", back_populates="events") + +class AssetCost(Base): + """ + Költségkezelő modell: Bruttó/Nettó/ÁFA és deviza támogatás. + """ + __tablename__ = "asset_costs" + __table_args__ = {"schema": "data"} + + 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) + + cost_type = Column(String(50), nullable=False) # fuel, service, tax, insurance, toll + + # Pénzügyi adatok + amount = Column(Numeric(18, 2), nullable=False) # Bruttó összeg + net_amount = Column(Numeric(18, 2)) # Nettó összeg + vat_amount = Column(Numeric(18, 2)) # ÁFA érték + vat_rate = Column(Numeric(5, 2)) # ÁFA kulcs (pl. 27.00) + + # Nemzetközi deviza kezelés + currency = Column(String(3), default="HUF") # Riportálási deviza + original_currency = Column(String(3)) # Számla eredeti devizája + exchange_rate_at_cost = Column(Numeric(18, 6)) # Rögzítéskori árfolyam + + date = Column(DateTime(timezone=True), server_default=func.now()) + description = Column(String) + invoice_id = Column(String) + mileage_at_cost = Column(Integer) + + data = Column(JSON, server_default=text("'{}'::jsonb")) + + asset = relationship("Asset", back_populates="costs") + +class ExchangeRate(Base): + """Napi árfolyamok tárolása (ECB/MNB adatok alapján)""" + __tablename__ = "exchange_rates" + __table_args__ = {"schema": "data"} + + id = Column(Integer, primary_key=True, index=True) + base_currency = Column(String(3), default="EUR") + target_currency = Column(String(3), nullable=False) + rate = Column(Numeric(18, 6), nullable=False) + rate_date = Column(DateTime(timezone=False), index=True) + provider = Column(String(50), default="ECB") + updated_at = Column(DateTime(timezone=True), server_default=func.now()) \ No newline at end of file diff --git a/backend/app/models/gamification.py b/backend/app/models/gamification.py index 7576baa..4c92c49 100755 --- a/backend/app/models/gamification.py +++ b/backend/app/models/gamification.py @@ -1,8 +1,14 @@ +import uuid from datetime import datetime -from typing import Optional +from typing import Optional, TYPE_CHECKING from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean -from sqlalchemy.orm import Mapped, mapped_column -from app.db.base import Base +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.dialects.postgresql import UUID as PG_UUID +from app.db.base_class import Base + +# Típusvizsgálathoz a körkörös import elkerülése érdekében +if TYPE_CHECKING: + from app.models.identity import User # Közös beállítás az összes táblához ebben a fájlban SCHEMA_ARGS = {"schema": "data"} @@ -36,21 +42,26 @@ class PointsLedger(Base): __tablename__ = "points_ledger" __table_args__ = SCHEMA_ARGS id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) - # JAVÍTVA: data.users.id hivatkozás user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id")) points: Mapped[int] = mapped_column(Integer) reason: Mapped[str] = mapped_column(String) created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now()) + + # Kapcsolat a felhasználóhoz + user: Mapped["User"] = relationship("User") class UserStats(Base): __tablename__ = "user_stats" __table_args__ = SCHEMA_ARGS - id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) - # JAVÍTVA: data.users.id hivatkozás - user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"), unique=True) - total_points: Mapped[int] = mapped_column(Integer, default=0) + # user_id a PK, mert 1:1 kapcsolat a User-rel + user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"), primary_key=True) + total_xp: Mapped[int] = mapped_column(Integer, default=0) + social_points: Mapped[int] = mapped_column(Integer, default=0) current_level: Mapped[int] = mapped_column(Integer, default=1) - last_activity: Mapped[datetime] = mapped_column(DateTime, default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now()) + + # EZ A JAVÍTÁS: A visszamutató kapcsolat definiálása + user: Mapped["User"] = relationship("User", back_populates="stats") class Badge(Base): __tablename__ = "badges" @@ -64,7 +75,18 @@ class UserBadge(Base): __tablename__ = "user_badges" __table_args__ = SCHEMA_ARGS id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) - # JAVÍTVA: data.users.id hivatkozás user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id")) badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.badges.id")) - earned_at: Mapped[datetime] = mapped_column(DateTime, default=func.now()) \ No newline at end of file + earned_at: Mapped[datetime] = mapped_column(DateTime, default=func.now()) + + user: Mapped["User"] = relationship("User") + +class Rating(Base): # <--- Az új értékelési modell + __tablename__ = "ratings" + __table_args__ = SCHEMA_ARGS + id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + author_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id")) + target_type: Mapped[str] = mapped_column(String(20)) + target_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True)) + score: Mapped[int] = mapped_column(Integer) + comment: Mapped[Optional[str]] = mapped_column(String) \ No newline at end of file diff --git a/backend/app/models/identity.py b/backend/app/models/identity.py index 051398a..33d5f61 100644 --- a/backend/app/models/identity.py +++ b/backend/app/models/identity.py @@ -2,9 +2,9 @@ import uuid import enum from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger from sqlalchemy.orm import relationship -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.dialects.postgresql import UUID as PG_UUID from sqlalchemy.sql import func -from app.db.base import Base +from app.db.base_class import Base class UserRole(str, enum.Enum): admin = "admin" @@ -18,23 +18,22 @@ class Person(Base): __table_args__ = {"schema": "data"} id = Column(BigInteger, primary_key=True, index=True) - id_uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False) + id_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False) + address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True) last_name = Column(String, nullable=False) first_name = Column(String, nullable=False) - mothers_name = Column(String, nullable=True) + mothers_last_name = Column(String, nullable=True) + mothers_first_name = Column(String, nullable=True) birth_place = Column(String, nullable=True) birth_date = Column(DateTime, nullable=True) phone = Column(String, nullable=True) - # JSONB mezők az okmányoknak és orvosi adatoknak identity_docs = Column(JSON, server_default=text("'{}'::jsonb")) medical_emergency = Column(JSON, server_default=text("'{}'::jsonb")) ice_contact = Column(JSON, server_default=text("'{}'::jsonb")) - # KYC státusz is_active = Column(Boolean, default=False, nullable=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) @@ -47,28 +46,32 @@ class User(Base): 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) region_code = Column(String, default="HU") - is_deleted = Column(Boolean, default=False) person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True) person = relationship("Person", back_populates="users") wallet = relationship("Wallet", back_populates="user", uselist=False) - owned_organizations = relationship("Organization", back_populates="owner", lazy="select") + # Itt a trükk: csak a string hivatkozás marad, így nincs import hiba, + # de a SQLAlchemy tudni fogja, hogy a UserStats-ra gondolunk. + stats = relationship("UserStats", back_populates="user", uselist=False) + + owned_organizations = relationship("Organization", back_populates="owner", lazy="select") created_at = Column(DateTime(timezone=True), server_default=func.now()) class Wallet(Base): + """Kétoszlopos pénztárca: Coin (Szerviz) és Kredit (Prémium).""" __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) + credit_balance = Column(Numeric(18, 2), default=0.00) + currency = Column(String(3), default="HUF") user = relationship("User", back_populates="wallet") @@ -77,9 +80,9 @@ class VerificationToken(Base): __table_args__ = {"schema": "data"} id = Column(Integer, primary_key=True, index=True) - token = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False) + token = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False) user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False) - token_type = Column(String(20), nullable=False) # 'registration' vagy 'password_reset' + token_type = Column(String(20), nullable=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) expires_at = Column(DateTime(timezone=True), nullable=False) is_used = Column(Boolean, default=False) \ No newline at end of file diff --git a/backend/app/models/organization.py b/backend/app/models/organization.py index 25e17d4..c931de6 100755 --- a/backend/app/models/organization.py +++ b/backend/app/models/organization.py @@ -1,9 +1,11 @@ import enum -from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON +import uuid +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM from sqlalchemy.orm import relationship from sqlalchemy.sql import func -from app.db.base import Base +from app.db.base_class import Base +from sqlalchemy.dialects.postgresql import UUID as PG_UUID class OrgType(str, enum.Enum): individual = "individual" @@ -19,18 +21,21 @@ class Organization(Base): id = Column(Integer, primary_key=True, index=True) + # ÚJ MEZŐ: Egységes címkezelés (GeoService hibrid) + address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True) + # --- NÉVKEZELÉS --- full_name = Column(String, nullable=False) # Teljes hivatalos név - name = Column(String, nullable=False) # Rövidített cégnév (pl. ProfiBot Kft.) - display_name = Column(String(50)) # Alkalmazáson belüli rövidítés (pl. ProfiBot) + name = Column(String, nullable=False) # Rövidített cégnév + display_name = Column(String(50)) # Alkalmazáson belüli rövidítés - # --- ATOMIZÁLT CÍMKEZELÉS --- + # --- ATOMIZÁLT CÍMKEZELÉS (Kompatibilitási réteg) --- address_zip = Column(String(10)) address_city = Column(String(100)) address_street_name = Column(String(150)) - address_street_type = Column(String(50)) # utca, út, tér, dűlő, stb. + address_street_type = Column(String(50)) address_house_number = Column(String(20)) - address_hrsz = Column(String(50)) # Helyrajzi szám + address_hrsz = Column(String(50)) address_stairwell = Column(String(20)) address_floor = Column(String(20)) address_door = Column(String(20)) @@ -40,7 +45,6 @@ class Organization(Base): tax_number = Column(String(20), unique=True, index=True) reg_number = Column(String(50)) - # PG_ENUM használata a Python Enum-mal szinkronizálva org_type = Column( PG_ENUM(OrgType, name="orgtype", inherit_schema=True), default=OrgType.individual @@ -49,13 +53,8 @@ class Organization(Base): status = Column(String(30), default="pending_verification") is_deleted = Column(Boolean, default=False) - notification_settings = Column(JSON, default={ - "notify_owner": True, - "notify_manager": True, - "notify_contact": True, - "alert_days_before": [30, 15, 7, 1] - }) - external_integration_config = Column(JSON, default={}) + 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")) owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) is_active = Column(Boolean, default=True) @@ -67,8 +66,8 @@ class Organization(Base): updated_at = Column(DateTime(timezone=True), onupdate=func.now()) # Kapcsolatok - assets = relationship("Asset", back_populates="organization", cascade="all, delete-orphan") - members = relationship("OrganizationMember", back_populates="organization") + assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan") + members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan") owner = relationship("User", back_populates="owned_organizations") class OrganizationMember(Base): @@ -77,9 +76,13 @@ class OrganizationMember(Base): id = Column(Integer, primary_key=True, index=True) organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False) user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False) - role = Column(String, default="driver") + role = Column(String, default="driver") # owner, manager, driver, service_staff + + # JAVÍTVA: Jogosultságok JSONB mezője (can_add_asset, etc.) + permissions = Column(JSON, server_default=text("'{}'::jsonb")) organization = relationship("Organization", back_populates="members") + user = relationship("app.models.identity.User") # Visszamutató kapcsolat a felhasználóra -# Kompatibilitási réteg a korábbi kódokhoz +# Kompatibilitási réteg Organization.vehicles = Organization.assets \ No newline at end of file diff --git a/backend/app/models/vehicle.py b/backend/app/models/vehicle.py deleted file mode 100755 index 2e8752d..0000000 --- a/backend/app/models/vehicle.py +++ /dev/null @@ -1,122 +0,0 @@ -from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, Numeric, DateTime, JSON, Date -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import relationship -from sqlalchemy.sql import func -import uuid -from app.db.base import Base - -# 1. GLOBÁLIS KATALÓGUS (A rendszer agya) -class VehicleCatalog(Base): - __tablename__ = "vehicle_catalog" - __table_args__ = {"schema": "data"} - - id = Column(Integer, primary_key=True, index=True) - brand = Column(String(100), nullable=False) - model = Column(String(100), nullable=False) - generation = Column(String(100)) - year_from = Column(Integer) - year_to = Column(Integer) - category = Column(String(50)) - engine_type = Column(String(50)) - engine_power_kw = Column(Integer) - - # Robot státusz és gyári adatok - verification_status = Column(String(20), default="verified") - factory_specs = Column(JSON, default={}) - maintenance_plan = Column(JSON, default={}) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), onupdate=func.now()) - - # Kapcsolat az egyedi példányok felé - assets = relationship("Asset", back_populates="catalog_entry") - -# 2. EGYEDI ESZKÖZ (Asset) - A felhasználó tulajdona -class Asset(Base): - __tablename__ = "assets" - __table_args__ = {"schema": "data"} - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - uai = Column(String(100), unique=True, nullable=False) # VIN, HIN vagy Serial Number - organization_id = Column(Integer, ForeignKey("data.organizations.id")) - catalog_id = Column(Integer, ForeignKey("data.vehicle_catalog.id"), nullable=True) - - asset_type = Column(String(20), nullable=False) # car, boat, plane - name = Column(String(255)) # "Kincses E39" - - # Dinamikus állapot - current_plate_number = Column(String(20)) - current_country_code = Column(String(2)) - odometer_value = Column(Numeric(12, 2), default=0) - operating_hours = Column(Numeric(12, 2), default=0) - - # Egyedi DNS (Gyári config + utólagos módosítások) - factory_config = Column(JSON, default={}) - aftermarket_mods = Column(JSON, default={}) - - # Állapot és láthatóság (EZ HIÁNYZOTT) - status = Column(String(50), default="active") - privacy_level = Column(String(20), default="private") - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), onupdate=func.now()) - - # Kapcsolatok - organization = relationship("Organization", back_populates="assets") - catalog_entry = relationship("VehicleCatalog", back_populates="assets") - events = relationship("AssetEvent", back_populates="asset", cascade="all, delete-orphan") - ratings = relationship("AssetRating", back_populates="asset") - -# 3. DIGITÁLIS SZERVIZKÖNYV / ESEMÉNYTÁR -class AssetEvent(Base): - __tablename__ = "asset_events" - __table_args__ = {"schema": "data"} - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - asset_id = Column(UUID(as_uuid=True), ForeignKey("data.assets.id")) - event_type = Column(String(50)) # SERVICE, REPAIR, INSPECTION, ACCIDENT, PLATE_CHANGE - - odometer_at_event = Column(Numeric(12, 2)) - hours_at_event = Column(Numeric(12, 2)) - - description = Column(String) - cost = Column(Numeric(12, 2)) - currency = Column(String(3), default="EUR") - - is_verified = Column(Boolean, default=False) - attachments = Column(JSON, default=[]) # Számlák, fotók linkjei - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - asset = relationship("Asset", back_populates="events") - -# 4. EMOCIONÁLIS ÉRTÉKELÉS -class AssetRating(Base): - __tablename__ = "asset_ratings" - __table_args__ = {"schema": "data"} - - id = Column(Integer, primary_key=True) - asset_id = Column(UUID(as_uuid=True), ForeignKey("data.assets.id")) - user_id = Column(Integer, ForeignKey("data.users.id")) - - comfort_score = Column(Integer) # 1-10 - experience_score = Column(Integer) # 1-10 - practicality_score = Column(Integer) # 1-10 - comment = Column(String) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - asset = relationship("Asset", back_populates="ratings") - -# 5. SZOLGÁLTATÓK (Szerelők, Partnerek) -class ServiceProvider(Base): - __tablename__ = "service_providers" - __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True, index=True) - name = Column(String(255), nullable=False) - official_brand_partner = Column(Boolean, default=False) - technical_rating_pct = Column(Integer, default=80) - location_city = Column(String(100)) - is_active = Column(Boolean, default=True) - -# --- KOMPATIBILITÁSI RÉTEG --- -Vehicle = Asset -ServiceRecord = AssetEvent \ No newline at end of file diff --git a/backend/app/schemas/__pycache__/asset.cpython-312.pyc b/backend/app/schemas/__pycache__/asset.cpython-312.pyc index 27aea2958c7b9feca1d84b9e55ace41955dbcc6a..a65dd0f8dcb3dc3eb22318feed3c4f405332e48e 100644 GIT binary patch literal 2419 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 delta 353 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 avDLmZxM*wpruo<0Ge)-N3kD$>!+!%@be5C= delta 2733 zcmZ`*Yiv}<6`tAq+PiPt3;VRaey-PE7K}~pKrmi|!6p<4PWsqLSyp?m&6<6;Ck&Y3x9X74#?_WS2=?6UpIYBd4UVn3ZtZ8+v^Zs?c`e$|_?#;h4z%m#r3BtC7= zIARW+3v=)IipI1n84C`tQi6Wkm2}odWO$8xpe_?#&fBxETwY^34a88t(H@0ERju3C)89fi;ozP zEBW89x#yFOe*DnT=^SMx?lQaZTo^M>@gV`f%>>P6B6^ebjo-6)rNepa&x{Eg3J`xg z;B+jq3;foO8KwZn1zgoygX`@g4s*Z4W$wrP3||pL9`9if;A?!FRZ#amcLT=1M7B8b z8>|!mllMRYJB06b-R=Qz!7F__x_ZEsE2IyQ9wvQ96Hs5Q>~01qI%6ar4&k|0FaE1= z=rv>I*SrpXWTYxMXO^qHm z)m0rmbL!+PgC~!~2cjc`@mJ5BKrVdKblfN+KiyD^4dy*2F)L5T)!cbGi#yFOP9&ND zK4E^firP>kq^sN^@ zwA7ZWomboD+Hm`t#s9$K-{kQf3-XRYV6A;tcI(I!d(X;#!k?CnxX*gL_*-(=OK{i^ zFGu+yJGg6t$NJd2=4daOEHQ8hvP*1;@?CI}Eyhn?N#rlL6p|hO5kXI1Z4kVsL^a$OBFk*aC|_Rqgm-pcYo+Ux)qpG;d^Q zIMs!x0{(u9DX`<-ZQt8OVi&muVw1Kn*yq*SEe(n1mxIKQ!(>xwbnC4k6j=O?ptE1% z3!F+5MQ>`b!U}vP#TJDMn2((6%GUOQwRDf{W(=cPEnUV7!YKgyR2R7lJ9vlvS@QBB>>dq&Jan)hmH5 zzn{1|E~l00rCYn+NGZy6Vrf2|UV2wunpXm;thB`v0V$PDWK!qzN+2zdFU_NbswUEr zqbD^_BCno{SCTu9@3d7FL&T_N+m<0&kyS)5jUoi5ckb3DyDAF~=^1K>ZZxJAk4TJQNO6(CbhiWrrv@C>)?bzbYD_&`W`q7Ic_`i^35K zg9JjRt;*C0PLmU|ikeY1hFlcU(yhDI%=*3K=W=xtog$${&30dA$6{S);XVT_N7h7 z=j&W)XMM@-zad-`O1_SgukL4_+ch^|TkVW~*f6l-8`u=NFBU=OZ~kWdxcyY|6T7#3 z7y^6Cv$Bm?e_C#4tprWLTKAbk{})_}?i_4~cinKXfxYXA){)8kJ#es@eZP0WL?+7y zIM~WAGcL-#aIlSC_6d|XLyE0BwoyD85;bEyPJ&d9$2C(tp2Nkq{N-*JPoGn?6OVTXT=dMEJuxv6N#>ADAUm2GLvB3Z z-B6^Ft%+l@dV;tm@2o_(4O7sse2y~xjCzb2UnDmPo>wpOR9)bRK&>dO_&FdiYiRhhm bt%=?h(ffAqs@Qyyf5M)H(DxWn)DQ4)<`SeX diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 48ccebd..0f5a127 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -4,28 +4,32 @@ import uuid from datetime import datetime, timedelta, timezone from typing import Optional -# SQLAlchemy importok from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select, cast, String, func +from sqlalchemy import select, and_ from sqlalchemy.orm import joinedload +from fastapi.encoders import jsonable_encoder -# Modell és Schema importok - EZ HIÁNYZOTT! from app.models.identity import User, Person, UserRole, VerificationToken, Wallet -from app.models.organization import Organization -from app.schemas.auth import UserLiteRegister, UserKYCComplete # <--- Ez javítja a hibát +from app.models.gamification import UserStats # <--- Innen importáljuk mostantól! +from app.models.organization import Organization, OrganizationMember, OrgType +from app.schemas.auth import UserLiteRegister, UserKYCComplete from app.core.security import get_password_hash, verify_password from app.services.email_manager import email_manager from app.core.config import settings -from app.services.config_service import config # A dinamikus beállításokhoz +from app.services.config_service import config +from app.services.geo_service import GeoService logger = logging.getLogger(__name__) class AuthService: @staticmethod async def register_lite(db: AsyncSession, user_in: UserLiteRegister): - """Step 1: Alapszintű regisztráció...""" + """ + Step 1: Lite Regisztráció (Master Book 1.1) + Új User és ideiglenes Person rekord létrehozása. + """ try: - # 1. Person alap létrehozása + # Ideiglenes Person rekord a KYC-ig new_person = Person( first_name=user_in.first_name, last_name=user_in.last_name, @@ -34,36 +38,29 @@ class AuthService: db.add(new_person) await db.flush() - # 2. User fiók new_user = User( email=user_in.email, hashed_password=get_password_hash(user_in.password), person_id=new_person.id, role=UserRole.user, is_active=False, + is_deleted=False, region_code=user_in.region_code ) db.add(new_user) await db.flush() - # --- DINAMIKUS TOKEN LEJÁRAT --- - reg_hours = await config.get_setting( - "auth_registration_hours", - region_code=user_in.region_code, - default=48 - ) - + # Regisztrációs token generálása + reg_hours = await config.get_setting("auth_registration_hours", region_code=user_in.region_code, default=48) token_val = uuid.uuid4() - new_token = VerificationToken( + db.add(VerificationToken( token=token_val, user_id=new_user.id, token_type="registration", expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_hours)) - ) - db.add(new_token) - await db.flush() - - # 4. Email küldés + )) + + # Email küldés (Master Book 3.2: Nincs manuális subject) verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}" await email_manager.send_email( recipient=user_in.email, @@ -80,32 +77,139 @@ class AuthService: raise e @staticmethod - async def initiate_password_reset(db: AsyncSession, email: str): - """Jelszó-visszaállítás indítása dinamikus lejárattal.""" - stmt = select(User).where(User.email == email, User.is_deleted == False) - res = await db.execute(stmt) - user = res.scalar_one_or_none() - - if user: - now = datetime.now(timezone.utc) - - # --- DINAMIKUS JELSZÓ RESET LEJÁRAT --- - reset_hours = await config.get_setting( - "auth_password_reset_hours", - region_code=user.region_code, - default=2 + async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete): + """ + 1.3. Fázis: Atomi Tranzakció & Shadow Identity + Felismeri a visszatérő Person-t, de új User-ként, izolált flottával indít. + """ + try: + # 1. Aktuális technikai User lekérése + stmt = select(User).options(joinedload(User.person)).where(User.id == user_id) + res = await db.execute(stmt) + user = res.scalar_one_or_none() + if not user: return None + + # 2. Shadow Identity Ellenőrzése (Anyja neve + Születési hely + Idő alapján) + # Globális keresés, régiótól függetlenül + identity_stmt = select(Person).where(and_( + Person.mothers_last_name == kyc_in.mothers_last_name, + Person.mothers_first_name == kyc_in.mothers_first_name, + Person.birth_place == kyc_in.birth_place, + Person.birth_date == kyc_in.birth_date + )) + existing_person = (await db.execute(identity_stmt)).scalar_one_or_none() + + if existing_person: + # Visszatérő identitás: A User-t a régi Person-hoz kötjük + user.person_id = existing_person.id + active_person = existing_person + logger.info(f"Shadow Identity linked: User {user_id} -> Person {existing_person.id}") + else: + active_person = user.person + + # 3. Címkezelés + addr_id = await GeoService.get_or_create_full_address( + db, + zip_code=kyc_in.address_zip, + city=kyc_in.address_city, + street_name=kyc_in.address_street_name, + street_type=kyc_in.address_street_type, + house_number=kyc_in.address_house_number, + parcel_id=kyc_in.address_hrsz ) - # ... (Rate limit ellenőrzés marad változatlan) ... + # 4. Person adatok frissítése (mindig a legfrissebbet tároljuk) + active_person.mothers_last_name = kyc_in.mothers_last_name + active_person.mothers_first_name = kyc_in.mothers_first_name + active_person.birth_place = kyc_in.birth_place + active_person.birth_date = kyc_in.birth_date + active_person.phone = kyc_in.phone_number + active_person.address_id = addr_id + active_person.identity_docs = jsonable_encoder(kyc_in.identity_docs) + active_person.ice_contact = jsonable_encoder(kyc_in.ice_contact) + active_person.is_active = True + # 5. Új, izolált INDIVIDUAL szervezet (4.2.3) + new_org = Organization( + full_name=f"{active_person.last_name} {active_person.first_name} Egyéni Flotta", + name=f"{active_person.last_name} Flotta", + org_type=OrgType.individual, + owner_id=user.id, + is_transferable=False, + is_active=True, + status="verified" + ) + db.add(new_org) + await db.flush() + + # 6. Tagság és Jogosultságok + db.add(OrganizationMember( + organization_id=new_org.id, + user_id=user.id, + role="owner", + permissions={"can_add_asset": True, "can_view_costs": True, "is_admin": True} + )) + + # 7. Wallet & Stats (Friss kezdés 0 ponttal) + db.add(Wallet(user_id=user.id, coin_balance=0, credit_balance=0)) + db.add(UserStats(user_id=user.id, total_xp=0, current_level=1)) + + # 8. Aktiválás + user.is_active = True + + await db.commit() + await db.refresh(user) + return user + except Exception as e: + await db.rollback() + logger.error(f"KYC Atomi Tranzakció Hiba: {str(e)}") + raise e + + @staticmethod + async def verify_email(db: AsyncSession, token_str: str): + try: + token_uuid = uuid.UUID(token_str) + stmt = select(VerificationToken).where( + and_( + VerificationToken.token == token_uuid, + VerificationToken.is_used == False, + VerificationToken.expires_at > datetime.now(timezone.utc) + ) + ) + res = await db.execute(stmt) + token = res.scalar_one_or_none() + if not token: return False + + token.is_used = True + await db.commit() + return True + except: + return False + + @staticmethod + async def authenticate(db: AsyncSession, email: str, password: str): + stmt = select(User).where(and_(User.email == email, User.is_deleted == False)) + res = await db.execute(stmt) + user = res.scalar_one_or_none() + if user and verify_password(password, user.hashed_password): + return user + return None + + @staticmethod + async def initiate_password_reset(db: AsyncSession, email: str): + # Csak aktív (nem törölt) felhasználónak engedünk jelszót resetelni + stmt = select(User).where(and_(User.email == email, User.is_deleted == False)) + user = (await db.execute(stmt)).scalar_one_or_none() + + if user: + reset_hours = await config.get_setting("auth_password_reset_hours", region_code=user.region_code, default=2) token_val = uuid.uuid4() - new_token = VerificationToken( + db.add(VerificationToken( token=token_val, user_id=user.id, token_type="password_reset", - expires_at=now + timedelta(hours=int(reset_hours)) - ) - db.add(new_token) + expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reset_hours)) + )) reset_link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}" await email_manager.send_email( @@ -115,7 +219,30 @@ class AuthService: ) await db.commit() return "success" - return "not_found" - # ... (többi metódus: verify_email, complete_kyc, authenticate, reset_password maradnak) ... \ No newline at end of file + @staticmethod + async def reset_password(db: AsyncSession, email: str, token_str: str, new_password: str): + try: + token_uuid = uuid.UUID(token_str) + stmt = select(VerificationToken).join(User).where( + and_( + User.email == email, + VerificationToken.token == token_uuid, + VerificationToken.token_type == "password_reset", + VerificationToken.is_used == False, + VerificationToken.expires_at > datetime.now(timezone.utc) + ) + ) + token_rec = (await db.execute(stmt)).scalar_one_or_none() + if not token_rec: return False + + user_stmt = select(User).where(User.id == token_rec.user_id) + user = (await db.execute(user_stmt)).scalar_one() + user.hashed_password = get_password_hash(new_password) + token_rec.is_used = True + + await db.commit() + return True + except: + return False \ No newline at end of file diff --git a/docs/V01_gemini/06_Database_Guide.md b/docs/V01_gemini/06_Database_Guide.md index eb76a24..bafa391 100644 --- a/docs/V01_gemini/06_Database_Guide.md +++ b/docs/V01_gemini/06_Database_Guide.md @@ -183,4 +183,17 @@ A rendszer normalizált címkezelést alkalmaz az adatminőség biztosítása é - `data.geo_street_types`: Közterület típusok szótára (út, utca, tér...). ### 5.2 GIS Adatok -Minden telephely koordinátája `GEOGRAPHY(POINT, 4326)` típusként van tárolva, amely lehetővé teszi a PostGIS alapú távolságmérést. \ No newline at end of file +Minden telephely koordinátája `GEOGRAPHY(POINT, 4326)` típusként van tárolva, amely lehetővé teszi a PostGIS alapú távolságmérést. + +### 5. Hibrid Címkezelési Modell +A rendszer az adatintegritás és a sebesség érdekében hibrid modellt használ. + +- **Centralizált adattárolás**: A `data.addresses` tábla tárolja a normalizált címeket (UUID alapú). +- **Öntanuló szótárak**: A `data.geo_postal_codes` és `data.geo_streets` táblák automatikusan bővülnek minden új rögzítésnél. +- **Denormalizált GPS adatok**: Az `organization_locations` tábla közvetlenül tárolja a koordinátákat a JOIN-nélküli PostGIS lekérdezésekhez. + +| Tábla | Funkció | +| :--- | :--- | +| `data.addresses` | Konkrét házszám szintű címek (Hibrid hivatkozási pont). | +| `data.geo_postal_codes` | Irányítószám és város kapcsolata (HU/EU támogatás). | +| `data.user_stats` | Felhasználói XP, szintek és strike-ok tárolása. | \ No newline at end of file diff --git a/docs/V01_gemini/07_API_Guide.md b/docs/V01_gemini/07_API_Guide.md index 0b1988b..6a93e72 100644 --- a/docs/V01_gemini/07_API_Guide.md +++ b/docs/V01_gemini/07_API_Guide.md @@ -64,4 +64,15 @@ Az e-mail küldési folyamatokra az alábbi korlátok vonatkoznak: - **Retry Cooldown:** Újraigénylés legkorábban 60 másodperc után lehetséges. - **Óránkénti Limit:** Maximum 3 kérelem / e-mail cím. - **Napi Limit:** Maximum 10 kérelem / e-mail cím. -- **Zárolás:** A napi limit túllépése esetén a fiók biztonsági okokból 24 órára zárolja a küldési funkciót az adott címre. \ No newline at end of file +- **Zárolás:** A napi limit túllépése esetén a fiók biztonsági okokból 24 órára zárolja a küldési funkciót az adott címre. + +### 4. Geo és Kereső Végpontok + +#### GET `/api/v1/services/suggest-street` +- **Cél**: Autocomplete támogatás a frontendnek. +- **Paraméterek**: `zip_code` (string), `q` (részleges utcanév). + +#### GET `/api/v1/services/search` +- **Cél**: Kétlépcsős szervizkereső. +- **Free mód**: Légvonalbeli távolságmérés (Radius). +- **Premium mód**: Útvonal-idő alapú kalkuláció és forgalmi becslés. \ No newline at end of file diff --git a/docs/V01_gemini/11_Gamification_Social.md b/docs/V01_gemini/11_Gamification_Social.md index 5ccf125..b74e38c 100644 --- a/docs/V01_gemini/11_Gamification_Social.md +++ b/docs/V01_gemini/11_Gamification_Social.md @@ -81,4 +81,10 @@ A fejlődés nem csak dicsőség, hanem gazdasági előny is. * **Level 20:** Egyedi avatar keret + állandó 5% kedvezmény a hirdetési árakból (céges esetén). ### 6.3 Éves/Havi Szezonok -Minden hónap végén az első 3 helyezett extra Kreditet vagy "Voucher"-t kap, amit a partnereinknél (szervizeknél) válthat be. \ No newline at end of file +Minden hónap végén az első 3 helyezett extra Kreditet vagy "Voucher"-t kap, amit a partnereinknél (szervizeknél) válthat be. + +### 3. Jutalmazási Szabályok (Social Points) +- **Célcsoport**: Kizárólag természetes személyek (`role: user`, `driver`). +- **Kizárások**: Szervezetek (Organizations) és Adminisztrátorok nem gyűjtenek XP-t. +- **Logika**: Minden `PointsLedger` bejegyzés kötelezően hivatkozik egy `user_id`-ra. +- **Mezőnevek**: Adatbázis szinten a pontok az `id`, `user_id`, `points`, `reason` mezőkben tárolódnak. \ No newline at end of file diff --git a/docs/V01_gemini/15_Changelog.md b/docs/V01_gemini/15_Changelog.md index f2488d9..3732cd0 100644 --- a/docs/V01_gemini/15_Changelog.md +++ b/docs/V01_gemini/15_Changelog.md @@ -168,4 +168,45 @@ A fejlesztések rendben tartásához javaslom a **`17_DEVELOPER_NOTES_AND_PITFAL DATA: Atomizált címmezők hozzáadva a data.organizations táblához. - LOGIC: Háromszintű névkezelés (Hivatalos, Rövid, Display) bevezetve. \ No newline at end of file + LOGIC: Háromszintű névkezelés (Hivatalos, Rövid, Display) bevezetve. + + ## [2.1.0] - 2026-02-08 + +### Hozzáadva (Added) +- **Hibrid Címkezelő Rendszer**: Új `data.addresses` központi tábla, amely UUID alapú azonosítással köti össze a személyeket, cégeket és szervizeket. +- **Öntanuló GeoService**: A rendszer rögzítéskor automatikusan bővíti a ZIP-kód, város és utcanév szótárakat. +- **Autocomplete API**: `/api/v1/services/suggest-street` végpont a frontend gépelés közbeni támogatásához. +- **Kétlépcsős Keresés**: `/api/v1/services/search` végpont, amely megkülönbözteti a légvonalbeli (Free) és útvonal/idő alapú (Premium) kalkulációt. +- **PostGIS Integráció**: Tűpontos GPS távolságmérés `geography` casting használatával. + +### Javítva (Fixed) +- **Gamifikációs hiba**: A `PointsLedger` modellben javítva a `points` mezőnév (TypeError: points_change fix). +- **Adatbázis Inkonzisztencia**: Létrehozva a hiányzó `data.user_stats` tábla. +- **Import hibák**: `Optional`, `UploadFile` és `File` hiányzó importok pótolva a szolgáltatás végpontokon. + +### Változtatva (Changed) +- **Címkezelési Logika**: A cégek és magánszemélyek mostantól egységes, bontott címstruktúrát (zip, city, street, street_type, house_number, parcel_id) használnak. +- **XP Szűrés**: A szervizek és cégek rögzítésekor a rendszer mostantól csak a természetes személyeknek (User/Driver) oszt social pontokat. + +## [2.1.0] - 2026-02-08 +- **Feat**: Hibrid címkezelő rendszer bevezetése (UUID alapú `data.addresses`). +- **Feat**: Öntanuló Geo-logika (Auto-create ZIP/Street). +- **Feat**: Kétlépcsős (Free/Premium) szervizkereső API. +- **Fix**: `points_ledger` mezőnév szinkronizáció. +- **Fix**: `data.user_stats` tábla inicializálása. + +## [2026-02-08] - Nemzetközi Asset és KYC Stabilizáció + +### Hozzáadva +- **Nemzetközi paraméterek**: `mileage_unit` (km/miles/hours) és `reading_unit` támogatása az Asset modellben. +- **Pénzügyi alapok**: `AssetCost` modell bővítése nettó/bruttó összeggel, ÁFA kulccsal és deviza-kezeléssel. +- **Árfolyamkezelés**: `exchange_rates` tábla implementálása MNB/ECB alapú napi frissítéshez. +- **Értékelési rendszer**: `ratings` tábla létrehozva (user, asset, service_provider szinten). + +### Javítva +- **KYC Folyamat**: Anyja neve bontása vezetéknévre és keresztnévre a magyar/nemzetközi szabványok szerint. +- **Database Schema**: Számos hiányzó oszlop pótolva (`asset_events.data`, `asset_events.event_date`, `user_stats.total_xp`). +- **SQLAlchemy hiba**: Javítva az `IndexError` a katalógus lekérdezésnél és az import hiba a `PG_UUID` kapcsán. + +### Megjegyzés +A rendszer most már képes egyetlen KYC folyamat alatt aktiválni a felhasználót, létrehozni az egyéni flottáját, inicializálni a pénztárcáját (Kredit/Coin) és rögzíteni az első járművét kezdő km-óra állással. \ No newline at end of file diff --git a/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md b/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md index a5839ea..7495c58 100644 --- a/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md +++ b/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md @@ -173,4 +173,17 @@ Minden telephely rögzítésekor az alábbi bontott címadatok kötelezőek: - Opcionális: Helyrajzi szám (parcel_id) külterületi vagy HRSZ alapú azonosításhoz. ### 4.2 Validációs Folyamat -A rögzített címek automatikusan bekerülnek a Master Geo adatbázisba, építve a rendszer globális címjegyzékét. \ No newline at end of file +A rögzített címek automatikusan bekerülnek a Master Geo adatbázisba, építve a rendszer globális címjegyzékét. + +## 5. Járművek és Költségek (MVP) +A járműadatok kezelése hibrid módon történik. + +### 5.1 Jármű Katalógus +- A rendszer egy központi katalógust (`asset_catalog`) épít. +- Új rögzítéskor a rendszer először a katalógusból kínál fel opciókat (Dropdown). +- Ha a modell nem létezik, a rendszer automatikusan felveszi (Self-learning catalog). + +### 5.2 Költségkövetés (TCO) +- Minden Asset-hez rögzíthető költség (`asset_costs`). +- Kötelező adatok: Kategória, Összeg, Dátum. +- Opcionális: Km óra állása (az amortizáció és szervizintervallum számításához). \ No newline at end of file