From 7249aa5809859343c9ec543e6d05d479de760ed5 Mon Sep 17 00:00:00 2001 From: Kincses Date: Sat, 7 Feb 2026 13:42:46 +0000 Subject: [PATCH] FEAT: Corporate onboarding implemented with Tax ID validation (HU) and isolated NAS storage --- .../api/v1/__pycache__/api.cpython-312.pyc | Bin 540 -> 818 bytes backend/app/api/v1/api.py | 14 ++- .../__pycache__/assets.cpython-312.pyc | Bin 0 -> 4926 bytes .../__pycache__/organizations.cpython-312.pyc | Bin 0 -> 3793 bytes backend/app/api/v1/endpoints/assets.py | 104 ++++++++++++++++++ backend/app/api/v1/endpoints/organizations.py | 70 ++++++++++++ .../core/__pycache__/config.cpython-312.pyc | Bin 3410 -> 3410 bytes .../__pycache__/validators.cpython-312.pyc | Bin 0 -> 3008 bytes backend/app/core/validators.py | 47 ++++++++ .../__pycache__/organization.cpython-312.pyc | Bin 2666 -> 3510 bytes .../__pycache__/vehicle.cpython-312.pyc | Bin 5920 -> 6059 bytes backend/app/models/organization.py | 46 +++++--- backend/app/models/vehicle.py | 11 +- .../schemas/__pycache__/asset.cpython-312.pyc | Bin 0 -> 1691 bytes .../__pycache__/organization.cpython-312.pyc | Bin 0 -> 1276 bytes backend/app/schemas/asset.py | 27 +++++ backend/app/schemas/organization.py | 20 ++++ backend/app/services/robot_manager.py | 26 ++++- docker-compose.yml | 1 + docs/V01_gemini/06_Database_Guide.md | 12 +- .../07_REGISTRATION_INVITATION_AND_API.md | 8 +- docs/V01_gemini/15_Changelog.md | 9 +- .../19_ADMIN_AND_PERMISSIONS_SPEC.md | 33 +++++- 23 files changed, 399 insertions(+), 29 deletions(-) create mode 100644 backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc create mode 100644 backend/app/api/v1/endpoints/__pycache__/organizations.cpython-312.pyc create mode 100644 backend/app/api/v1/endpoints/assets.py create mode 100644 backend/app/api/v1/endpoints/organizations.py create mode 100644 backend/app/core/__pycache__/validators.cpython-312.pyc create mode 100644 backend/app/core/validators.py create mode 100644 backend/app/schemas/__pycache__/asset.cpython-312.pyc create mode 100644 backend/app/schemas/__pycache__/organization.cpython-312.pyc create mode 100644 backend/app/schemas/asset.py create mode 100644 backend/app/schemas/organization.py diff --git a/backend/app/api/v1/__pycache__/api.cpython-312.pyc b/backend/app/api/v1/__pycache__/api.cpython-312.pyc index 2742d464d2ada657d81cff1a8e46f2fa76113d0b..e3466a7a21b1b5f47052ab88011f6ed3bb91be90 100644 GIT binary patch delta 473 zcmbQkvWboFG%qg~0}%Asv}gJ=P2`hc(qo*cA!n4!8pWE+7RAQMz{HTsxEiDc2%^|i z*|P*8Y-BQ(IZJ3_fvO~SNfw~wI+c1HT3NG%kd1<{Q#evM(;3&Wu4abt85wZsX2YtR zD}@_XcO{ReHSbH1S(=QuI2{8#gYrvDQj0WMZm}emmSo&wPfjdJ%*ju`#gaiEG0c|Z} zpPb1UC@l(PG6He&L?H2jnURt4E`#iS2G`3Bu9Gh@I=Nk82>Zasz{q`_LGmJl vx-)e*a2}92z;%H^@(P1DvZU^HDZPtQdKb*yPt;%Viw7#W%8*bb3^W)3$xmj# delta 180 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 literal 0 HcmV?d00001 diff --git a/backend/app/api/v1/endpoints/assets.py b/backend/app/api/v1/endpoints/assets.py new file mode 100644 index 0000000..9ebc461 --- /dev/null +++ b/backend/app/api/v1/endpoints/assets.py @@ -0,0 +1,104 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from app.db.session import get_db +from app.schemas.asset import AssetCreate, AssetResponse +from app.models.vehicle import Asset, VehicleCatalog +from app.models.organization import Organization +from app.core.validators import VINValidator +from app.core.config import settings +import os +import logging + +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) +): + """ + Ú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." + ) + + # 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() + + 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." + ) + + # 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 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" + ) + 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!" + ) + + # 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. 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)) + + os.makedirs(asset_path, exist_ok=True) + logger.info(f"NAS mappa létrehozva: {asset_path}") + + 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 diff --git a/backend/app/api/v1/endpoints/organizations.py b/backend/app/api/v1/endpoints/organizations.py new file mode 100644 index 0000000..5419ca3 --- /dev/null +++ b/backend/app/api/v1/endpoints/organizations.py @@ -0,0 +1,70 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from app.db.session import get_db +from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse +from app.models.organization import Organization, OrgType +from app.core.config import settings +import os +import re +import logging + +router = APIRouter() +logger = logging.getLogger(__name__) + +@router.post("/onboard", response_model=CorpOnboardResponse, status_code=status.HTTP_201_CREATED) +async def onboard_organization( + org_in: CorpOnboardIn, + db: AsyncSession = Depends(get_db) +): + """ + Új szervezet (cég/szerviz) rögzítése. + - Magyar adószám validáció (XXXXXXXX-Y-ZZ). + - Duplikáció ellenőrzés adószám alapján. + - NAS mappa és DB rekord létrehozása. + """ + + # 1. Magyar adószám validáció + if org_in.country_code == "HU": + if not re.match(r"^\d{8}-\d-\d{2}$", org_in.tax_number): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Érvénytelen magyar adószám formátum! (Példa: 12345678-1-12)" + ) + + # 2. Duplikáció ellenőrzés + stmt_exist = select(Organization).where(Organization.tax_number == org_in.tax_number) + result_exist = await db.execute(stmt_exist) + if result_exist.scalar_one_or_none(): + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Ezzel az adószámmal már regisztráltak céget!" + ) + + # 3. Mentés (Dinamikus státusszal és kisbetűs Enummal) + new_org = Organization( + name=org_in.name, + tax_number=org_in.tax_number, + reg_number=org_in.reg_number, + headquarters_address=org_in.headquarters_address, + country_code=org_in.country_code, + org_type=OrgType.business, # Most már kisbetűs 'business' kerül beküldésre + status="pending_verification" + ) + + db.add(new_org) + await db.flush() # ID generálás a NAS-hoz + + # 4. NAS Mappa létrehozása + try: + base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data") + org_path = os.path.join(base_path, "organizations", str(new_org.id)) + os.makedirs(org_path, exist_ok=True) + logger.info(f"NAS mappa létrehozva szervezetnek: {org_path}") + except Exception as e: + logger.error(f"NAS hiba az onboardingnál: {e}") + + await db.commit() + await db.refresh(new_org) + + return {"organization_id": new_org.id, "status": new_org.status} \ No newline at end of file diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc index 48a77db0aee79d8ac6ade072493153472ba437e6..2754663252494d94315d74c1901a76185921f085 100644 GIT binary patch delta 37 qcmca4bxDf%G%qg~0}!Y$Xv>o-?y$Bvh<#qi`5ki0CAB9J@!KMOW0TD!S42>WM3M1mU zhKO{>VKGK<8VBV(|LBieU6yp(ADhPkEFcYyV1h;*nnbWB6Nly;m5FpszGKryTw5bW zFe1M+;!Zn*E><$s+drhnbfTIG%A`p>&RkS8Q#2mM_8h!%!7IVLSpo~Zj;_nsoeL<3 zF2Bs-Ja`JGFLVBDPhso^G7FBN%Y5$F9)feIuobk*^c04(XcWUnxruaQ%g!Bp4HTzk z9T6vSeTqpDaf9>_FUVq20{Oy8SN8SMD z4#C;UIbf$=uoS`CC1%|%xO>E`dxge6vLE!`6c!qUc2HP2Aavgnx{X5jpwMj+y3Jho zD||y9L!{+Pe8cHMX7Sbz$XS$ij!LBUn%gWb^pdv1F8uet%;FhLLK)cQS;vfnynUqs zA?hHz<6g6@pg-d%=+8%)fEFsg9A#ay}d&*G8)z6 zW>d!Z(J#9Wj60>^Z?d-8m}44=&N>R>pWhoc1AQ+edlWB7fBPcz)l@COW^&GRZ@c#z>=Fno^TA%@R0e1YL%hDR7~p_ZjqhT9kpG5j{e zhZsK0a67~AFx(OJ(|VYJ?%<-13!Viuz(q9|l_2umQ85#JlcWEw@k$uEb0UmK0DLBG zg|UK%G3UrhGngCYZ%(#w1~$SDZB-$(l)Fm#Qi4v(OM_vZxgb0#%@BcRMN^tSHf|aQ z-$sb|+#Kb_LVY0q|EThH7<)k2Q4NH@9=?wkU5mz2>#fOKzU9DO|H}5CYC>NR+aa#0 z!+Ad_*y%Hh%m~JvRB&ZfOHA0VZOPwGLo+=+JigNt8aF)3X37e_@ z**xaaFp429Ud*8l7b>gxM9=BJx$-Ap{Ucxfx^Guj`lkz(S9~&^n_jC4E{A?BYrYp+ z=cx_i$I!sz%9;(AT;Y9OQg#z>NE}seyLr_@+jlIg7TUFU>5PRM-dgUmP;=YeRtp{O zxWCIn$BsYnTj)gip*>$;|3iDev**@^&RfI7AFf^eNWdq;vJ)&n5taoKmIV@)1>%;U z3M4EGBrFRgEDIzo3nVNHWLf4AHmS56TrIM2RREl|aotXE*T%c|fx|WqHiFAGZV7?Y zHf}!xZrixC3xntOM-M)Q!F&7beLR34UVX~HvaV-7R2jzEq7Av!Q~UR-z)ykjH|HL- zeKWis2(MR#vpulBI%R41tpnLUfb|EK&;D^|^Sp6y@P6I;p`Pq1=;UjjKfHM1QRTk1 zl6}wrS{8VQu&4IV5K0N!7Y39f`nKa(cRN~b_neTV?;IH9D^<+%j2~BxsAv7d!NLcn=>T-tCAKe<2N`U`gsEboS}-G~5e{no-o4 znpFA!$raU9R0UG`;HB^9V@kvM)4h$#YHsrK>TRl;jY>ETz+C;klAuOr_4Zh>iAm=M zyO^s_jl%>fzV7$b* zx7V1^;13DzGV2KBXCquQMbShmZc?~D(wa&m(FB2Vo|@2uaORI`X3$~Pvd5z7nqlzs zSW&D}EeA?uRE@%N(h=}g&FpxkWM3cN54uJd2&h~)5Bm7s3l9d?MlL<7ie^1e0?NYd z{Or=;vj6tb@`vkzBads}SUhuUo2bu4|IK$GqsqM-NcMO)VG`62r<6MqiK`P@B*MIr z$V7sqVjP!5B3Dyt%pNH*Ot{mdux;ZBLQjDvlP@I_F?w1+qDM`70iZaW=`cs)*ySe; z<$|Y;(F5WddhGOXIO?6XH=`Q}nnm)AH=7c2RfztLcd>wOr*{!3&@2H38bNvL0AyL< r^TPIaLnCawW}LZ?THbWhkAcGPU6JIF?->6DdHy9gV(&8qLQMA`3x@hT literal 0 HcmV?d00001 diff --git a/backend/app/core/validators.py b/backend/app/core/validators.py new file mode 100644 index 0000000..f59bf8f --- /dev/null +++ b/backend/app/core/validators.py @@ -0,0 +1,47 @@ +import re + +class VINValidator: + @staticmethod + def validate(vin: str) -> bool: + """VIN (Vehicle Identification Number) ellenőrzése ISO 3779 szerint.""" + vin = vin.upper().strip() + + # Alapvető formátum: 17 karakter, tiltott betűk (I, O, Q) nélkül + if not re.match(r"^[A-Z0-9]{17}$", vin) or any(c in vin for c in "IOQ"): + return False + + # Karakterértékek táblázata + values = { + 'A':1, 'B':2, 'C':3, 'D':4, 'E':5, 'F':6, 'G':7, 'H':8, 'J':1, 'K':2, 'L':3, 'M':4, + 'N':5, 'P':7, 'R':9, 'S':2, 'T':3, 'U':4, 'V':5, 'W':6, 'X':7, 'Y':8, 'Z':9, + '0':0, '1':1, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9 + } + + # Súlyozás a pozíciók alapján + weights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2] + + try: + # 1. Összegzés: érték * súly + total = sum(values[vin[i]] * weights[i] for i in range(17)) + + # 2. Maradék számítás 11-el + check_digit = total % 11 + + # 3. A 10-es maradékot 'X'-nek jelöljük + expected = 'X' if check_digit == 10 else str(check_digit) + + # 4. Összevetés a 9. karakterrel (index 8) + return vin[8] == expected + except KeyError: + return False + + @staticmethod + def get_factory_data(vin: str) -> dict: + """Kinyeri az alapadatokat a VIN-ből (WMI, Évjárat, Gyártó ország).""" + # Ez a 'Mágikus Gomb' alapja + countries = {"1": "USA", "2": "Kanada", "J": "Japán", "W": "Németország", "S": "Anglia"} + return { + "country": countries.get(vin[0], "Ismeretlen"), + "year_code": vin[9], # Modellév kódja + "wmi": vin[0:3] # World Manufacturer Identifier + } \ No newline at end of file diff --git a/backend/app/models/__pycache__/organization.cpython-312.pyc b/backend/app/models/__pycache__/organization.cpython-312.pyc index 915fd291082aad6e9b5d3fbd0e12954df1e26aa1..06b63af2427c62eeb4aed0f1127c7815d5db023f 100644 GIT binary patch literal 3510 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 diff --git a/backend/app/models/__pycache__/vehicle.cpython-312.pyc b/backend/app/models/__pycache__/vehicle.cpython-312.pyc index f5cb6b29da7c7bca886b1cf840b5c0922df08436..775573c8e0bb5c4db7bb3a8e39e284ff273eb93f 100644 GIT binary patch delta 1301 zcma))OH30{6oz}D4=6L!7KS3Ft))D)f~}%4Q4uQ$BBneHi3y1z)6N8{wQcUSBz2(} zO*F>%$XN)u0iy{9OdyS0b?3swM`R(Buwcc;g(kRhlpGTMg{7Hs6l>Di(zYgnWQc_S*a#GDUV385EanmmTxvb492!7=~(c6#)@07SA+k zrcByYU+RmuDa-15q!40VU0VUpEgIyA*;;K z5w^`9xpJ#HX4u4Dw``nu{!y^C=Ta-oIBFnWB2;a&e|^`+-}JGrTDe*#^RA7WU$52t zRjbo$ZrLMy=XcK)7d#Es>s6oZ+0xyRs}1;RBU8{eL0(B z#>ZH2)Q1W`qMkwH`sj$1j9-;9@l=xTWhHo88tMz+o+T!!F-camGW|}@Gfi@&H_%A; zVS5vz8L=M`M4(m5F=)nYHZE%>EH_Oi9pE6bp#sh*rc2gnpx3 zYOAq5tge|^TQZ|+7OFc@=}AyDD|_X~ zi4Y%kO|(GIY{?RTaJ*|-bWI+a_CGl`n_Ci(jz{3G-32zY6I9M=db@W?JO}qUuQ{y$ zNPdBPO-KY%f~TPaE1e9#1ewiiurP(exJ+ws=PaTd76oru_a@;QGwuy|cMu{SK>c|{ z2yC{;`!{pKh-fe3p9~^cL!5yxwv~eA2#$OHquoJBKdd?$%(`s_xN=XY?kkE-1z%PF z@nuA52!Yp3Er?M_mJjX3TtBK27vX35q&56!8GGcYgqIbof31LAUJ0q!9z?s&{#?*zz zMC0o$P-3($Of>DrbYt)X7-OO)vSAVz_z_GuvU1K;M2S%*ncuzl+;jiue`ao>J>SN@ zXBdk~f4Ijtk}bAZ>|Ii|N+3^jr8ndWp_Up2D_p96Rvi_*rQ;M!VA|Y8zQQf@3fX`b zdX&00ru0IN=1H#flwKuvpF|a%1%jF;Q9sca?*P zLkI!Uia;C69vvE$$s{GsjOs_!Mpkm>jq=LA9>gXV=rpgn9f{e9R}!bha-?@ zUtd|XkLD)U9f425&d-i6a5Fq@i1ove)l-Tw&j}faP1a8jqHq}e4IVmz(-G)v;0KLz zaa^C!5f%7dd;w(=^G_l|u+p%6c)Mo?gA<4yZH;3L5r)~uFBOa9r6Fr8AyG)#nk)7a zux#^>7@p4Hfr=;he>_o`8baXzQECw9;Ja;RH^!ovjhKd<{ZYkO6q+2HWEi|ouI=9! zV!N~%bxwoiJmuNZdKPyON%-JQRVrj*&~=AQ!bg{{(P-EBK8BYpY`Qq_pBh?i-^B$f nlal+D>(Jp2aZVB;_hMxej^z%Xgvrg}vI)m>7_#mT3h%%V7^C5} diff --git a/backend/app/models/organization.py b/backend/app/models/organization.py index da087ee..f5c9e30 100755 --- a/backend/app/models/organization.py +++ b/backend/app/models/organization.py @@ -1,14 +1,18 @@ import enum -from sqlalchemy import Column, Integer, String, Boolean, Enum, DateTime, ForeignKey +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON +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 class OrgType(str, enum.Enum): - INDIVIDUAL = "individual" - SERVICE = "service" - FLEET_OWNER = "fleet_owner" - CLUB = "club" + # A tagok neveit kisbetűre állítjuk, hogy egyezzenek a Postgres Enum értékekkel + individual = "individual" + service = "service" + service_provider = "service_provider" + fleet_owner = "fleet_owner" + club = "club" + business = "business" class Organization(Base): __tablename__ = "organizations" @@ -16,24 +20,38 @@ class Organization(Base): id = Column(Integer, primary_key=True, index=True) name = Column(String, nullable=False) - # A stabilitás miatt VARCHAR-ként kezeljük a DB-ben - org_type = Column(String(50), default="individual") - # A flotta technikai tulajdonosa (User) + # PG_ENUM használata a Python Enum-mal szinkronizálva + org_type = Column( + PG_ENUM(OrgType, name="orgtype", inherit_schema=True), + default=OrgType.individual + ) + + tax_number = Column(String(20), unique=True, index=True) + reg_number = Column(String(50)) + headquarters_address = Column(String(255)) + country_code = Column(String(2), default="HU") + + 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={}) + owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) - - # Üzleti szabályok is_active = Column(Boolean, default=True) is_transferable = Column(Boolean, default=True) - - # Verifikáció is_verified = Column(Boolean, default=False) verification_expires_at = Column(DateTime(timezone=True), nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) - # Kapcsolatok (Asset-re hivatkozunk a Vehicle helyett) assets = relationship("Asset", back_populates="organization", cascade="all, delete-orphan") members = relationship("OrganizationMember", back_populates="organization") owner = relationship("User", back_populates="owned_organizations") @@ -48,5 +66,5 @@ class OrganizationMember(Base): organization = relationship("Organization", back_populates="members") -# --- EZT A SORT TEDD KÍVÜLRE, A MARGÓRA --- +# Kompatibilitási réteg a korábbi kódokhoz Organization.vehicles = Organization.assets \ No newline at end of file diff --git a/backend/app/models/vehicle.py b/backend/app/models/vehicle.py index f898bae..2e8752d 100755 --- a/backend/app/models/vehicle.py +++ b/backend/app/models/vehicle.py @@ -20,15 +20,17 @@ class VehicleCatalog(Base): engine_type = Column(String(50)) engine_power_kw = Column(Integer) - # --- EZ A SOR HIÁNYZOTT --- + # 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" @@ -52,13 +54,16 @@ class Asset(Base): 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") + catalog_entry = relationship("VehicleCatalog", back_populates="assets") events = relationship("AssetEvent", back_populates="asset", cascade="all, delete-orphan") ratings = relationship("AssetRating", back_populates="asset") diff --git a/backend/app/schemas/__pycache__/asset.cpython-312.pyc b/backend/app/schemas/__pycache__/asset.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27aea2958c7b9feca1d84b9e55ace41955dbcc6a GIT binary patch literal 1691 zcmZ8hOH3O_7@qa6?e!}r;ZYt1tV$JZC5W0x=_RU?Kp(UMQW7q1q|M+N%!Yk9v%9E9 zJs^>&$<4Rq2vQG4aEMf?>Y+Cud!aH1vl8l|CvH`ts+3dze{39Jr2Xfc|26;okNrKD zOA}c9gCDFPRYLy4!`X_r!O04MSA-F!*u+taiW4iw9JQz_$cx#U6EDUkR_%n7EG8w^ zY~9g|x)3&P^h2upBf7w*7SOi6AMxcWJ6x|%Z5$EaYaQR*J``K-sd9mPMlxXq&c zGH&J?wJk;iPZ&z1&sPG=bEyqjTU?x(Mji-xU^%>mp3YXH4Nl$wcttoVDvT6kix3}I znZjaw>Tay1Q`C5zCzy5{j%{h2#aUu6xvRA`l3WK3{hUUMrS~%L-Y4bRjy%V%ob#XN z8Lkh2EAWx;=qRk~oK9B8a22K1oN+D6-1U7PObHH;n+!h$_T9p&wN~G@slOh0K~Pww z0k!KtmqWk6ZSE9irYD|I$VY~8za%V~s3=`REw5}^?D#4;2{DY1KLKMwR|77%U!XR4 zUH8}P+YXc=?O3j9bGIC<0H)uyvB-d3q^?=+u>$D02Z#|9S6~x)6yDjjz{t{K9y}o-B+_)pyEp@p352o;zklnoAuX4uQoix2x7_VcCWU z*0+~vVU>&eZ}yY=FK(3gM47tQI>iov_ZYcIH%&A(%_wb}j>ke9an>}ShSY9J1eQ=_ zF+|mJgD7vB)OEc;dhtzDbV-pP2+@t2_%MOZBYJ`CkQ1Qf?Z9*D01~ zvQiZ|P?DgL&w)HAhkZk@r}ifv9`r44d{t8$+Tvlpr}kiL;my7MY4afecw_2CcSC!8 zI56_s|LN(k3kL&tHfC!#8`_=2!O<;k|HiimgO4_*UyRpozMN=ij}C_lTiyFN<`0JM zZ_L*o9BB8&@Y%f=+P>Ye>xP? zJPEcbOsU?dcvn!JwrDZD3pjzQkPbn0F zyFsgqOgD6vxQG%ylq>Y1ndLYzlLi+ajU9>W1Z(;3(M&0-)tI5a?YR)h&JK)i>f9|-($gy(?wKv+v*zL;-n7aIkz#@pIDDvzuQwQ%JhHH|LwZ{SB?Zna*v_ zw9bRW4Q;rY&2N6)>_X9hni3!VhX85Ch3@8zOr%$;jJknU68LLw4F~@CqXgvNa?7&y zBP|Rq)(ToiDSI4UTnaVPZ?~rIQ!xortb>o>Z&gv0Kgq2>$gLwXbVPcO$ndEaSH4kd U#wo%5^jcOKtLa`X`T zyF`+}5+tx!h;$?s)+~69WgE+t&Uy4u;b33Tz%x-7dmXP)($%?ElJjOhrzFp%F0Zwt zh$TFh+LbX6VOQx!E0MaCsxaoNqX(Kr0?Rw8T!I;VX2vt7D~!c0(T*h81B|_B^9W~x zo|END4>87*q?L30xQsD{;8hjmBRK*kKPP5*nwmFjZou-{OiQivBz$dBZlxXNV+sm| zH=6)?OS(fNTT9(DgIiC#XKFih@9*!^dbd8gxwtcRKqu8G3^FOFa}*H%A4lN?KoTfY zLlqeIs2YwY;$dW+1SRQ7UzRc68&fi@{YDvCt8_V8ZE+7O46uP41`bl(jLvjI7RW4P=xDZNanf80vSq5kq< zq0pnLOhlNhvzJnZYhlyWjrKCAxi$bQ{R2olA)q#ZreHG#uYZV4U;qV&Whg-8Tpep{ zWO{Rcd+vZv7hj5lmf)EEv?F+uhfRf-Rbi!S4{i(Wt2lx`^bKyzQ-80j{n_FsJy6#n w3V$q;&47G&EXz71R}abLF{Rd(orPlp&GCR|-9a0g<6+xs>@|K6Xw1HT1DDP$Q2+n{ literal 0 HcmV?d00001 diff --git a/backend/app/schemas/asset.py b/backend/app/schemas/asset.py new file mode 100644 index 0000000..efdd68d --- /dev/null +++ b/backend/app/schemas/asset.py @@ -0,0 +1,27 @@ +from pydantic import BaseModel, Field, field_validator +from typing import Optional +from uuid import UUID +from datetime import datetime + +class AssetCreate(BaseModel): + catalog_id: int = Field(..., description="A kiválasztott katalógus elem ID-ja") + vin: str = Field(..., min_length=17, max_length=17, description="17 karakteres alvázszám") + license_plate: str = Field(..., min_length=1, max_length=20) + name: Optional[str] = Field(None, description="Egyedi elnevezés (pl. 'Céges furgon')") + organization_id: int = Field(..., description="Melyik flottába kerüljön") + + # Opcionális: Kezdő km óra állás, szín, stb. később bővíthető + +class AssetResponse(BaseModel): + id: UUID + uai: str + catalog_id: int + organization_id: int + name: str + asset_type: str + current_plate_number: str + status: str + created_at: datetime + + class Config: + from_attributes = True \ No newline at end of file diff --git a/backend/app/schemas/organization.py b/backend/app/schemas/organization.py new file mode 100644 index 0000000..a0d8c76 --- /dev/null +++ b/backend/app/schemas/organization.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel, Field +from typing import Optional, List + +class ContactCreate(BaseModel): + full_name: str + email: str + phone: Optional[str] + contact_type: str = "primary" + +class CorpOnboardIn(BaseModel): + name: str + tax_number: str + country_code: str = "HU" + reg_number: Optional[str] + headquarters_address: str + contacts: Optional[List[ContactCreate]] = [] + +class CorpOnboardResponse(BaseModel): + organization_id: int + status: str = "pending_verification" \ No newline at end of file diff --git a/backend/app/services/robot_manager.py b/backend/app/services/robot_manager.py index 6afcbcc..d2dbe7d 100644 --- a/backend/app/services/robot_manager.py +++ b/backend/app/services/robot_manager.py @@ -1,5 +1,7 @@ import asyncio -from .harvester_robot import CarHarvester # A korábbi CarHarvester-t nevezzük át így +from datetime import datetime +# Frissített importok az új fájlnevekhez: +from .harvester_cars import CarHarvester from .harvester_bikes import BikeHarvester from .harvester_trucks import TruckHarvester @@ -7,6 +9,8 @@ class RobotManager: @staticmethod async def run_full_sync(db): """Sorban lefuttatja az összes robotot.""" + print(f"🕒 Szinkronizáció indítva: {datetime.now()}") + robots = [ CarHarvester(), BikeHarvester(), @@ -15,8 +19,22 @@ class RobotManager: for robot in robots: try: - await robot.run(db) - # Pihenő a kategóriák között, hogy ne kapjunk IP-tiltást + # Itt a robot lekéri az API-tól az ÖSSZES márkát és frissít + await robot.run(db) await asyncio.sleep(5) except Exception as e: - print(f"❌ Hiba a {robot.category} robotnál: {e}") \ No newline at end of file + print(f"❌ Hiba a {robot.category} robotnál: {e}") + + @staticmethod + async def schedule_nightly_run(db): + """ + Egyszerű ciklus, ami figyeli az időt. + Ha éjjel 2 óra van, elindítja a teljes szinkront. + """ + while True: + now = datetime.now() + # Ha hajnali 2 és 2:01 között vagyunk, indítás + if now.hour == 2 and now.minute == 0: + await RobotManager.run_full_sync(db) + await asyncio.sleep(70) # Várunk, hogy ne induljon el többször ugyanabban a percben + await asyncio.sleep(30) # 30 másodpercenként ellenőrizzük az időt \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index cd45319..5c2fa5e 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - ./backend:/app - ./alembic.ini:/app/alembic.ini - ./migrations:/app/migrations + - /mnt/nas/app_data:/mnt/nas/app_data environment: PYTHONPATH: /app DATABASE_URL: ${MIGRATION_DATABASE_URL} diff --git a/docs/V01_gemini/06_Database_Guide.md b/docs/V01_gemini/06_Database_Guide.md index e2ee2ef..e30cca2 100644 --- a/docs/V01_gemini/06_Database_Guide.md +++ b/docs/V01_gemini/06_Database_Guide.md @@ -96,4 +96,14 @@ A rendszer fel van készítve az EU-s piacra: - **Person:** Egyéni teljesítmény, megbízhatóság. - **Service (Szerviz):** Szolgáltatási minőség. - **Vehicle (Jármű):** Műszaki állapot és előélet. - - *Megjegyzés:* A Cég (mint flotta) nem kap önálló értékelést, a hírneve a tagjai és járművei minősítéséből adódik össze. \ No newline at end of file + - *Megjegyzés:* A Cég (mint flotta) nem kap önálló értékelést, a hírneve a tagjai és járművei minősítéséből adódik össze. + + ## 4. CRM és Szervezeti Kapcsolattartók +A `data.organization_contacts` tábla felelős a flottákhoz tartozó humán kapcsolattartók kezeléséért. +- **Dinamikus beállítások:** A `data.organizations` tábla `notification_settings` (JSONB) mezője szabályozza, ki és mikor kapjon értesítést. +- **Külső szinkron:** Az `external_crm_id` biztosítja a kapcsolatot külső vállalatirányítási rendszerekkel (API-n keresztül). + +## 4.1 Szervezeti és CRM Adatmodell +- **data.organizations**: Bővítve `tax_number`, `reg_number`, `headquarters_address` és `is_deleted` mezőkkel. +- **data.organization_contacts**: Új tábla a Mini-CRM funkciókhoz (kapcsolattartók típus szerint: billing, primary, operational). +- **Audit**: Minden státuszmódosítás és adatváltozás snapshot-olva az `audit_logs` táblába. \ No newline at end of file diff --git a/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md b/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md index a9ea2b0..77d8336 100644 --- a/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md +++ b/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md @@ -158,4 +158,10 @@ PASSWORD_RESET_TOKEN_EXPIRE_HOURS=1 EMAIL_PROVIDER=sendgrid EMAILS_FROM_EMAIL=info@profibot.hu EMAILS_FROM_NAME='Profibot Service Finder' -SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx \ No newline at end of file +SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx + +## 4. Corporate Onboarding (Céges regisztráció) +A folyamat célja, hogy egy már létező Person (vagy új User) saját szervezetet (Organization) alapítson. +- **Többszintű validáció:** Kötelező adószám (VAT/Tax ID) ellenőrzés (HU esetén formátum + VIES API). +- **Hierarchia:** A regisztráló automatikusan `owner` rangot kap. +- **Izoláció:** Minden cég saját mappastruktúrát kap a NAS-on az okmányok izolált kezelése érdekében. \ No newline at end of file diff --git a/docs/V01_gemini/15_Changelog.md b/docs/V01_gemini/15_Changelog.md index 9a9fe0e..4ea211e 100644 --- a/docs/V01_gemini/15_Changelog.md +++ b/docs/V01_gemini/15_Changelog.md @@ -145,4 +145,11 @@ A fejlesztések rendben tartásához javaslom a **`17_DEVELOPER_NOTES_AND_PITFAL ### ✨ Multi-Robot System - **Kategória Robotok:** Elkészült a Car, Bike és Truck harvester moduláris szerkezete. - **Robot Manager:** Központi vezérlő az ütemezett és sorrendi adatgyűjtéshez. -- **Katalógus Kereső:** Üzembe helyezve a `/catalog/search` végpont a Swaggerben. \ No newline at end of file +- **Katalógus Kereső:** Üzembe helyezve a `/catalog/search` végpont a Swaggerben. + +## [0.4.5] - 2026-02-07 +### ✨ Asset Management & Infrastructure +- **Asset Endpoint:** `POST /api/v1/assets/` élesítve VIN validációval. +- **NAS Integration:** Automata mappastruktúra létrehozása az eszközöknek (`/assets/{uuid}`). +- **Data Model:** `privacy_level` és `status` mezők hozzáadva az Asset modellhez. +- **Bugfix:** SQLAlchemy `TypeError` javítva a modell és a séma szinkronizálásával. \ No newline at end of file diff --git a/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md b/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md index d3a8d15..9e44683 100644 --- a/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md +++ b/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md @@ -32,4 +32,35 @@ Minden módosítás előtt a rendszer menti az aktuális rekord állapotát (JSO ## 5. Adminisztrátori Meghívók - Adminisztrátort csak kézi meghívóval lehet felvenni. -- **Lejárati idő:** Minden admin meghívó token 24 óráig érvényes. \ No newline at end of file +- **Lejárati idő:** Minden admin meghívó token 24 óráig érvényes. + +## 6. Értesítési Engine és Lejárati Figyelmeztetések + +A rendszer proaktív figyelmeztető rendszert alkalmaz minden előfizetői szinten (Individual és Corporate egyaránt). + +### A) Előfizetés és Pénzügyi Értesítések +- **Hatókör:** Minden fizetős csomag (Lite+, VIP, VIP+, Corporate). +- **Logika:** Automatikus értesítés küldése 30, 15, 7 és 1 nappal a csomag lejárta előtt. +- **Csatornák:** Push notification, Email és a Mini-CRM kontakt személyek értesítése. + +### B) Jármű Okmányok és Technikai Lejáratok +A rendszer figyeli az eszközökhöz rögzített metaadatokat: +- **Forgalmi engedély:** Műszaki vizsga lejárata. +- **Biztosítás:** Kötelező (KGFB) és CASCO fordulónapok. +- **Lízing/Szerződés:** Szerződéses futamidő vége. +- **Okmányok:** Hajólevél, lajstrom, emelőgép vizsga stb. + +### C) CRM Kontaktok és Kapcsolattartás +Minden szervezet (Organization) esetében kötelező megadni legalább egy **Adminisztratív Kontaktot**. +- **Több cég kezelése:** Egy Person több szervezetben is betölthet `owner` vagy `fleet_manager` szerepkört. +- **CRM Mezők:** Név, beosztás, közvetlen elérhetőség (fizetésért felelős, operatív felelős). + +## 7. Corporate Onboarding és Validációs Szintek +A cégek rögzítése háromlépcsős ellenőrzésen esik át: +1. **Tier 1 (Automata):** Adószám alapú validáció (HU/VIES API). +2. **Tier 2 (AI/OCR):** Feltöltött dokumentumok (Alapító okirat) intelligens elemzése. +3. **Tier 3 (Human):** Adminisztrátori jóváhagyás (L2/L3 szint), ha az automata folyamat bizonytalan. + +## 8. B2B Jutalék és MLM Kivételek +- **Direct Referral:** Cég által meghívott másik cég esetén csak 1. szintű (L1) jutalék jár. +- **MLM Korlát:** Szervezetek nem építhetnek többszintű hálózatot, a kifizetés fix üzleti megállapodás alapú. \ No newline at end of file