refakotorálás előtti állapot

This commit is contained in:
Roo
2026-03-10 07:34:01 +00:00
parent 4e40af8a08
commit 0304cb8142
39 changed files with 1552 additions and 125 deletions

92
.roo/history.md Normal file
View File

@@ -0,0 +1,92 @@
# Service Finder Fejlesztési Történet
## 17-es Kártya: Billing Engine Service (Epic 3 - Pénzügyi Motor)
**Dátum:** 2026-03-09
**Státusz:** Kész ✅
**Kapcsolódó fájlok:** `backend/app/services/billing_engine.py`, `backend/app/api/v1/endpoints/billing.py`
### Technikai Összefoglaló
A Billing Engine Service-t az Epic 3 (Pénzügyi Motor) keretében implementáltuk, amely a 18-as kártya atomi tranzakciós logikájára épül. Az implementáció egyszerűsített interfészeket biztosít a gyakori számlázási műveletekhez, miközben megtartja az alapvető négyszeres wallet rendszert és a dupla könyvelést.
#### Főbb Implementációk:
1. **Új funkciók a `billing_engine.py`-ban** (689-880 sorok):
- `charge_user()`: Atomiszámlázási tranzakciók felhasználóbarát wrapper-e
- `upgrade_subscription()`: Előfizetési szintek frissítése árképzéssel és wallet levonással
- `record_ledger_entry()`: Közvetlen naplóbejegyzés létrehozása kézi pénzügyi műveletekhez
- `get_user_balance()`: Konszolidált wallet egyenleg lekérdezés minden wallet típusra
2. **Endpoint integráció** a `billing.py`-ban:
- `/upgrade` endpoint most a `upgrade_subscription()` funkciót használja
- `/wallet/balance` endpoint most a `get_user_balance()` funkciót használja
- Az API válasz struktúra változatlan maradt a visszafelé kompatibilitás érdekében
3. **Megtartott alapvető funkciók:**
- Négyszeres wallet rendszer (EARNED, PURCHASED, SERVICE_COINS, VOUCHER)
- Okos levonási sorrend: VOUCHER → SERVICE_COINS → PURCHASED → EARNED
- Dupla könyvelés a FinancialLedger táblában
- Atomis tranzakciós biztonság rollback-kel hibák esetén
- FIFO voucher lejárat 10% díjjal (SZÉP-kártya modell)
#### Tesztelés és Validáció:
A `verify_financial_truth.py` teszt javítva lett és sikeresen validálja:
- Stripe fizetés szimulációt
- Belső ajándék átutalásokat
- Voucher lejáratot díjakkal
- Dupla könyvelés konzisztenciát a wallet-ek és a pénzügyi napló között
Minden teszt sikeresen lefut: "MINDEN TESZT SIKERES! A PÉNZÜGYI MOTOR ATOMBIZTOS!"
#### Függőségek:
- **Bemenet:** Wallet modell, FinancialLedger modell, SubscriptionTier definíciók
- **Kimenet:** Használják a számlázási endpointok, fizetésfeldolgozás és előfizetéskezelés
---
### Korábbi Kártyák Referenciája:
- **15-ös kártya:** Wallet modell és négyszeres wallet rendszer
- **16-os kártya:** FinancialLedger és dupla könyvelés
- **18-as kártya:** Atomis tranzakciós manager és okos levonási logika
- **19-es kártya:** Stripe integráció és fizetési intent kezelés
---
## 20-as Kártya: Subscription Lifecycle Worker (Előfizetés életciklus kezelése)
**Dátum:** 2026-03-09
**Státusz:** Kész ✅
**Kapcsolódó fájlok:** `backend/app/workers/system/subscription_worker.py`
### Technikai Összefoglaló
A 20-as Gitea kártya implementációja a lejárt előfizetések automatikus kezelésére. A worker napi egyszer fut (cron) és atomis tranzakcióban végzi a következőket:
1. **Lekérdezés:** Azokat a User-eket, ahol `subscription_expires_at < NOW()` és `subscription_plan != 'FREE'`
2. **Downgrade:** `subscription_plan = "FREE"`, `is_vip = False`
3. **Naplózás:** Főkönyvi bejegyzés (`SUBSCRIPTION_EXPIRED`) a `billing_engine.record_ledger_entry` segítségével
4. **Értesítés:** Belső dashboard értesítés és email küldése a `NotificationService`-en keresztül
#### Főbb Implementációk:
- **Atomis zárolás:** `WITH FOR UPDATE SKIP LOCKED` a párhuzamos feldolgozás biztonságához
- **Integráció a meglévő rendszerekkel:** A `billing_engine` és `notification_service` modulok használata
- **Hibatűrés:** Egyéni felhasználóhibák nem akadályozzák a teljes folyamatot, statisztikák gyűjtése
- **Logolás:** Dedikált logger (`subscription-worker`) a folyamat nyomon követéséhez
#### Futtatás:
```bash
docker exec sf_api python -m app.workers.system.subscription_worker
```
#### Függőségek:
- **Bemenet:** User modell (`subscription_expires_at`, `subscription_plan`, `is_vip`)
- **Kimenet:** Módosított User rekordok, FinancialLedger bejegyzések, InternalNotification és email értesítések
---
*Megjegyzés a jövőbeli fejlesztésekhez:* A billing engine most már magas szintű funkciókat biztosít, amelyek elfedik a komplex atomis tranzakciós logikát. A jövőbeli kártyáknak ezeket a funkciókat kell használniuk, nem pedig közvetlenül manipulálniuk a wallet-eket vagy naplóbejegyzéseket.

View File

@@ -1,23 +1,24 @@
# Service Finder Projekt Alkotmány
# 🌍 GLOBAL SYSTEM RULES & WORKFLOW (Minden módra érvényes!)
## 1. Működési Alapelvek
- **Elsődleges Igazság (2A):** A forráskód a mérvadó. A Wiki.js dokumentációnak követnie kell a kódot.
- **Munkafolyamat (1B):** Terv (Architect) -> Jóváhagyás -> Megvalósítás (Code) -> Tesztelés -> Dokumentálás.
- **Granularitás (3A):** Minden logikai egység (robot funkció) külön Focalboard kártyát kap.
Te a Service Finder projekt egy specifikus AI ágense vagy. Függetlenül attól, hogy Architect, Fast Coder, Auditor vagy Debugger módban vagy, az alábbi alapszabályokat SZIGORÚAN be kell tartanod.
## 2. Eszközhasználati Szabályok
- **Focalboard:** Minden munkafázist (Doing, Testing, Done) itt kell követni.
- **Gitea:** Minden sikeres teszt után kötelező a commit, a kártya sorszámával a leírásban.
- **Postgres:** A Wiki.js (postgres-wiki) tartalmát minden módosítás után ellenőrizni és frissíteni kell.
## 🛡️ 1. KRITIKUS ADATBÁZIS BIZTONSÁG (DATA SAFETY)
- **SOHA ne törölj éles (dev) adatot!** A `data`, `finance`, `identity` sémák az éles fejlesztői adatbázis részei.
- **Tesztek futtatása:** Bármilyen tesztet (pl. Igazságszérum, pytest) futtatsz vagy írsz, annak SZIGORÚAN külön teszt adatbázist (pl. SQLite in-memory vagy `service_finder_test`) kell használnia.
- **TILOS** a `DROP SCHEMA`, `DROP TABLE`, `TRUNCATE` vagy `Base.metadata.drop_all` parancsok használata az éles `DATABASE_URL` kapcsolaton!
## 3. Minőségbiztosítás (4-igen)
- Nincs késznek jelentett kód automatizált tesztelés nélkül.
- A terminálban futtatott tesztek kimenetét csatolni kell a feladat lezárásához.
- A dokumentációs lánc kötelező elemei:
1. Technikai leírás (kódban)
2. Felhasználói manual vázlat (chatben)
3. Wiki.js frissítés (Postgres-en keresztül).
## ✅ 2. KÖTELEZŐ KÁRTYA LEZÁRÁSI RITUÁLÉ (TASK COMPLETION WORKFLOW)
Mielőtt egy feladatot (Gitea issue/kártya) "Kész"-nek nyilvánítasz a felhasználó felé, KÖTELEZŐ végrehajtanod ezt a két lépést:
## 4. Architect vs. Code elkülönítés
- **Architect (Reasoner R1):** Tervez, auditál, adatbázist elemez, Mermaid diagramokat rajzol, és `/plans/plan.md` fájlokat hoz létre.
- **Code (Fast Coder/Chat):** Szigorúan a `/plans` mappából dolgozik, kódot ír, tesztel és commitol.
1. **Dokumentáció frissítése:** Írj egy rövid, műszaki összefoglalót a megvalósított logikáról a `.roo/history.md` fájl végére.
2. **Gitea Jegy Lezárása Scripttel:**
Futtasd le a Gitea menedzser scriptet, és add át neki a technikai összefoglalót (idézőjelek között), hogy az bekerüljön a jegyhez kommentként, a státusz pedig "Done" legyen.
*Parancs formátuma:*
`python3 /opt/docker/dev/service_finder/.roo/scripts/gitea_manager.py finish <KÁRTYA_SZÁMA> "<Rövid technikai összefoglaló arról, mit csináltál>"`
## 🤖 3. SZEREPKÖRÖK EGYÜTTMŰKÖDÉSE (ROLE INTEGRATION)
- **Orchestrator:** Te bontod le a Gitea kártyákat kisebb feladatokra. Használd a `gitea_manager.py create` parancsot.
- **Architect / Wiki Specialist:** Te tervezed meg a DDD (Domain-Driven Design) sémákat. A terveket a `history.md`-be vagy a megfelelő wiki/specifikációs fájlba írd.
- **Fast Coder:** Te írod a kódot a `logic_spec_*.md` alapján. Mielőtt bezárod a kártyát, ellenőrizd, hogy a szintaxis hibátlan-e.
- **Auditor / Debugger:** Te ellenőrzöd a Coder munkáját. Ha hibát találsz, javítod. A tesztjeid SOHA nem írhatják felül a fejlesztői adatbázist (Lásd 1-es pont).

View File

@@ -1,7 +1,22 @@
"Read Before Write" (Olvasd el, mielőtt írsz): Mielőtt bármilyen meglévő kódot módosítanál, KÖTELEZŐ bekérned vagy beolvasnod a releváns fájlokat. Sose dolgozz feltételezések alapján!
# 🧠 CORE BEHAVIOR & ANTI-HALLUCINATION PROTOCOL
Clean Code & No Harm: Ne okozz kárt a meglévő, jól működő kódbázisban. Csak a célzott problémára fókuszálj.
Ez a te legmélyebb viselkedési szabályzatod. Semmilyen más instrukció nem bírálhatja felül ezeket az alapelveket. Célunk a 100%-os pontosság, a 0% találgatás és a kód maximális biztonsága.
Gondolatmenet (Thought Process): Mielőtt legenerálod a kódot, 2-3 mondatban vázold fel a logikádat, hogy lássam, jó irányba indultál-e el.
## 🚫 1. ZÉRÓ HALLUCINÁCIÓ ÉS TALÁLGATÁS
- **Soha ne mondd, hogy valami "Kész" vagy "Sikeres", amíg nem láttad a terminál kimenetén!** - Ha egy tesztet vagy kódot futtatsz, KÖTELEZŐ megvárnod és elemezned a terminál válaszát. Ha hibát dob (pl. Stack Trace, Exception), azonnal állj meg, és jelezd a felhasználónak.
- **Soha ne találd ki egy fájl elérési útját!** Ha nem vagy 100%-ig biztos benne, hol van egy fájl, használd a `find . -name "fájlneve.py"` parancsot a kereséshez, mielőtt megpróbálod szerkeszteni.
## ❓ 2. A "3x KÉRDEZZ, 1x JAVASOLJ" SZABÁLY
- Ha egy feladat leírása hiányos, vagy egy hibaüzenetből nem egyértelmű a probléma gyökere, **TILOS vakon kódot módosítanod!**
- Először tedd fel a szükséges tisztázó kérdéseket a felhasználónak (pl. "Újraindítottad a konténert?", "Létezik ez a teszt user az adatbázisban?").
- Csak akkor írj vagy módosíts kódot, ha már pontosan érted a kontextust. A stabil, átgondolt logika sokkal fontosabb, mint a gyors, de hibás kódolás.
## 🕵️ 3. "TRUST, BUT VERIFY" (Adatbázis és Állapot ellenőrzés)
- Mielőtt adatbázis műveletet (CRUD) írsz, KÖTELEZŐ ellenőrizned a meglévő adatbázis sémát (használd az SQL `information_schema` lekérdezését, vagy nézd meg a modelleket a kódban).
- Ha arra kérnek, hogy elemezz egy hibát, mindig kérd le a releváns Docker logokat (pl. `sudo docker logs --tail 50 <konténer>`), ne csak az elméletedet oszd meg.
## 🛑 4. KÁRTEVÉS MEGELŐZÉSE
- Meglévő, működő kódot csak akkor módosíthatsz, ha az kifejezetten a feladat része. A módosításokat (Surgical Coding) a lehető legkisebb beavatkozással végezd el.
- Mielőtt egy nagy fájlt felülírsz, mindig készíts róla mentést, vagy olvasd el alaposan, hogy megértsd az eredeti logikát, nehogy véletlenül kitörölj egy fontos függőséget.
Nyelv: Magyar nyelven kommunikálj velem.

View File

@@ -1,3 +1,7 @@
# 🏛️ PROJECT ARCHITECTURE & ENVIRONMENT MAP
Ez a fájl tartalmazza a projekt fizikai felépítését és a futtatási környezet szigorú szabályait. Keresés (`find`) előtt MINDIG ezt a térképet használd iránymutatásként!
Tech Stack: FastAPI (v2, aszinkron), SQLAlchemy (Async), PostgreSQL (Izolált hálózaton), Docker Compose V2.
AI & OCR: Hibrid AI Gateway (Helyi Ollama: 14B Qwen szövegre, Llama Vision képekre. Fallback: Gemini/Groq).
@@ -6,7 +10,30 @@ Identity & Auth: "Dual Entity" modell (Person = hús-vér ember, User = technika
Deduplikáció (MDM): Csak akkor van merge, ha a make, a technical_code és a hengerűrtartalom egyezik. N/A és UNKNOWN fallback kódok generálása az SQL kényszerek miatt.
## 5. SQL és Adatbázis Hibakezelés (Error Handling)
## 🐳 1. KÖRNYEZET ÉS DOCKER SZABÁLYOK (ENVIRONMENT)
- **Operációs rendszer:** Ubuntu/Linux környezetben dolgozunk.
- **Docker Compose (KRITIKUS):** A rendszer az új Docker Compose V2-t használja.
- **TILOS** a kötőjeles `docker-compose` parancs használata!
- **KÖTELEZŐ** a szóközös `docker compose` használata (pl. `sudo docker compose restart sf_api`).
- **Jogosultságok:** Ha egy Docker parancs `permission denied` hibát dob, próbáld meg automatikusan `sudo`-val az elején (pl. `sudo docker exec ...`), de először kérdezz rá, ha bizonytalan vagy.
- **Backend keretrendszer:** FastAPI (Python), aszinkron (async/await) megközelítéssel, SQLAlchemy 2.0+ (asyncpg) adatbázis kapcsolattal.
## 🗺️ 2. PROJEKT TÉRKÉP (DIRECTORY STRUCTURE)
A projekt mappa-szerkezete az alábbi logikát követi. Keresd a fájlokat ezekben a mappákban a funkciójuk szerint:
- **`/backend/app/models/`**: Itt találhatók az adatbázis modellek (SQLAlchemy). Ne feledd a sémákat (identity, finance, data, audit, system)!
- **`/backend/app/api/endpoints/`** (vagy `api/v1/`): Itt vannak a FastAPI végpontok (routerek, endpointok).
- **`/backend/app/services/`**: Itt van az üzleti logika és a "motorok" (pl. `billing_engine.py`, `notification_service.py`).
- **`/backend/app/core/`**: Rendszerbeállítások, konfigurációk, biztonság (pl. `config.py`).
- **`/backend/app/test_in/`**: Belső tesztek, amiket a konténeren belülről, a többi modullal együttműködve futtatunk.
- **`/backend/app/test_outside/`**: Külső integrációs tesztek és szkriptek (pl. a `verify_financial_truth.py`). Ezek futtatása gyakran speciális adatbázis-kezelést igényel.
- **`/.roo/scripts/`**: Az AI és a fejlesztést támogató szkriptek (pl. a `gitea_manager.py`).
## 🧩 3. KÓDOLÁSI ALAPELVEK (ARCHITECTURE RULES)
- **Szeparáció (DDD):** Az adatbázis modellek szigorúan sémákra vannak bontva. Ne keverd a `finance` és a `vehicle` domainek adatait!
- **Aszinkronitás:** Minden I/O és adatbázis művelet aszinkron (`await session.execute(...)`). Ne használj szinkron blokkoló hívásokat a FastAPI végpontokban.
## 4. SQL és Adatbázis Hibakezelés (Error Handling)
- **Unique Constraint hibák:** Ha a PostgreSQL `InvalidColumnReferenceError` vagy `UniqueViolation` hibát dob az `ON CONFLICT` miatt, TILOS találgatni a mezőket!
- **A kötelező megoldás:** Használd az `ON CONFLICT ON CONSTRAINT [korlát_neve] DO NOTHING` vagy `DO UPDATE` szintaxist.
- A pontos korlát (constraint) nevét mindig a pgAdmin-ból vagy a `\d+ táblanév` lekérdezéssel kell kideríteni módosítás előtt.

View File

@@ -1,3 +1,35 @@
Feladatkezelés: A projektmenedzsmenthez MCP Focalboard-ot vagy a projekt gyökerében található KANBAN_AUDIT.md fájlt használunk. Minden munkamenet elején ellenőrizd ezeket, hogy tudd, mi a feladat (Todo) és mi van már kész (Done).
Jelenlegi Fókusz: A következő időszak fő feladata a "Historical Data" (múltbéli költségek, szervizek) bevezetése az occurrence_date mezővel, és a flottavezetőknek szóló AnalyticsService (TCO/km) kidolgozása.
Jelenlegi Fókusz: A következő időszak fő feladata a "Historical Data" (múltbéli költségek, szervizek) bevezetése az occurrence_date mezővel, és a flottavezetőknek szóló AnalyticsService (TCO/km) kidolgozása.
1. Adatbázis Migrációk (Alembic)
Ha az AI (az Architect vagy a Coder) módosít egy adatbázis modellt a models/ mappában, hogyan vezesse át az adatbázison? Az AI hajlamos csak megírni a Python kódot, és elfelejteni az SQL-t, vagy nyers SQL-lel próbálkozni.
Mit adj meg: A pontos parancsot a migrációhoz.
Példa szabály: "Ha módosítasz egy SQLAlchemy modellt, KÖTELEZŐ legenerálnod a migrációs fájlt az Alembic segítségével a konténeren belül: sudo docker exec sf_api alembic revision --autogenerate -m "Leírás", majd futtatnod kell: sudo docker exec sf_api alembic upgrade head. Soha ne módosíts táblaszerkezetet nyers SQL-lel!"
2. Csomagkezelés (Dependencies)
Ha a Roo Code-nak szüksége van egy új Python csomagra (pl. egy új Stripe modulra vagy egy adatbázis driverre), hogyan telepítse?
Mit adj meg: Hol tartod a függőségeket (requirements.txt, Pipfile, vagy pyproject.toml?), és hogyan települjenek.
Példa szabály: "Ha új Python csomagra van szükséged, TILOS csak úgy a host gépen pip install-t futtatni. Add hozzá a csomagot a backend/requirements.txt (vagy megfelelő) fájlhoz, és jelezd a felhasználónak, hogy újra kell építenie a konténert (docker compose build)."
3. Környezeti Változók és Titkok (Secrets & .env)
Az AI-k hajlamosak "lusták" lenni, és teszteléskor vagy fejlesztéskor keménykódolni (hardcode) a jelszavakat, API kulcsokat a fájlokba.
Mit adj meg: A konfiguráció kezelésének módját.
Példa szabály: "SOHA ne hardkódolj API kulcsokat (Stripe, Ollama, Groq), jelszavakat vagy adatbázis URL-eket a kódba! MINDIG a backend/app/core/config.py (Pydantic BaseSettings) fájlt használd, az adatokat pedig a .env fájlból olvasd ki. Ha új környezeti változó kell, írd bele a .env.example fájlba is!"
4. Naplózási Szabvány (Logging)
Főleg a háttérfolyamatoknál (mint a robotok vagy a 20-as kártya Cron-jobja), a sima print() nem elég egy Docker konténerben, mert nehéz nyomon követni.
Mit adj meg: Milyen loggert használsz? (Beépített logging, loguru, stb.)
Példa szabály: "Ne használj sima print() utasításokat a végleges kódban! Használd a projekt beépített loggerét (pl. import logging vagy from app.core.logger import logger). A háttérfolyamatokat részletesen logold (INFO szinten a lépéseket, ERROR szinten a kivételeket Stack Trace-szel)."

View File

@@ -1,4 +1,3 @@
#/opt/docker/dev/service_finder/.roo/scripts/gitea_manager.py TOKEN = "783f58519ee0ca060491dbc07f3dde1d8e48c5dd"
#!/usr/bin/env python3
import requests
import sys
@@ -79,22 +78,26 @@ def create_issue(title, body, categories, milestone_id=None):
def start_issue(issue_num):
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
set_issue_state(issue_num, "Status: In Progress")
# Gitea Stopper elindítása
requests.post(f"{BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_num}/stopwatch/start", headers=HEADERS)
add_comment(issue_num, f"▶️ **Munka megkezdve:** {now}")
print(f"Siker: A #{issue_num} időmérése elindult.")
def finish_issue(issue_num):
def finish_issue(issue_num, custom_message=None):
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
set_issue_state(issue_num, "Status: Done")
# Gitea Stopper leállítása
requests.post(f"{BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_num}/stopwatch/stop", headers=HEADERS)
requests.patch(f"{BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_num}", headers=HEADERS, json={"state": "closed"})
add_comment(issue_num, f"✅ **Munka befejezve:** {now}\n⏱️ *A ráfordított időt a Gitea 'Time Tracking' modulja rögzítette.*")
print(f"Siker: A #{issue_num} lezárva, időmérés megállítva.")
# Itt adjuk hozzá az egyedi AI összefoglalót, ha van
if custom_message:
comment_body = f"✅ **Munka befejezve:** {now}\n\n**Technikai Összefoglaló:**\n{custom_message}\n\n⏱️ *A ráfordított időt a Gitea rögzítette.*"
else:
comment_body = f"✅ **Munka befejezve:** {now}\n⏱️ *A ráfordított időt a Gitea rögzítette.*"
add_comment(issue_num, comment_body)
print(f"Siker: A #{issue_num} lezárva, időmérés megállítva. Komment mentve.")
def get_issue(issue_num):
"""Lekéri a Gitea API-ból az issue adatait és kiírja a címét és leírását."""
url = f"{BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_num}"
res = requests.get(url, headers=HEADERS)
@@ -124,11 +127,9 @@ if __name__ == "__main__":
if len(sys.argv) < 3:
print("Használat: python3 gitea_manager.py [start|finish|create|get] ...")
print(" start <issue_num>")
print(" finish <issue_num>")
print(" finish <issue_num> [\"Custom summary message\"]")
print(" get <issue_num>")
print(" create \"<title>\" \"<body>\" [milestone_id] [category1 category2 ...]")
print(" - milestone_id: opcionális, szám (pl. 5)")
print(" - categories: opcionális, címkék (pl. \"Scope: Backend\" \"Type: Feature\")")
sys.exit(1)
action = sys.argv[1].lower()
@@ -136,22 +137,20 @@ if __name__ == "__main__":
if action == "start":
start_issue(sys.argv[2])
elif action == "finish":
finish_issue(sys.argv[2])
# Ha van 3. paraméter (az üzenet), adjuk át
custom_msg = sys.argv[3] if len(sys.argv) > 3 else None
finish_issue(sys.argv[2], custom_msg)
elif action == "create":
title = sys.argv[2]
body = sys.argv[3]
milestone_id = None
categories = []
# Ha van 4. paraméter, ellenőrizzük, hogy milestone_id lehet-e
if len(sys.argv) > 4:
arg4 = sys.argv[4]
# Ha az arg4 szám (lehet milestone_id), akkor milestone_id-nek vesszük
if arg4.isdigit():
milestone_id = arg4
# A többi paraméter (5. és további) categories
categories = sys.argv[5:] if len(sys.argv) > 5 else []
else:
# Ha nem szám, akkor az arg4 is categories, és a többi is
categories = sys.argv[4:]
create_issue(title, body, categories, milestone_id)
elif action == "get":

View File

@@ -12,6 +12,7 @@ from app.models.payment import PaymentIntent, PaymentIntentStatus
from app.services.config_service import config
from app.services.payment_router import PaymentRouter
from app.services.stripe_adapter import stripe_adapter
from app.services.billing_engine import upgrade_subscription, get_user_balance
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -19,55 +20,24 @@ logger = logging.getLogger(__name__)
@router.post("/upgrade")
async def upgrade_account(target_package: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
"""
Univerzális csomagváltó.
Univerzális csomagváltó a Billing Engine segítségével.
Kezeli az 5+ csomagot, a Rank-ugrást és a különleges 'Service Coin' bónuszokat.
"""
# 1. Lekérjük a teljes csomagmátrixot az adminból
# Példa JSON: {"premium": {"price": 2000, "rank": 5, "type": "credit"}, "service_pro": {"price": 10000, "rank": 30, "type": "coin"}}
package_matrix = await config.get_setting(db, "subscription_packages_matrix")
if target_package not in package_matrix:
raise HTTPException(status_code=400, detail="Érvénytelen csomagválasztás.")
pkg_info = package_matrix[target_package]
price = pkg_info["price"]
# 2. Pénztárca ellenőrzése
stmt = select(Wallet).where(Wallet.user_id == current_user.id)
wallet = (await db.execute(stmt)).scalar_one_or_none()
total_balance = wallet.purchased_credits + wallet.earned_credits
if total_balance < price:
raise HTTPException(status_code=402, detail="Nincs elég kredited a csomagváltáshoz.")
# 3. Levonási logika (Purchased -> Earned sorrend)
if wallet.purchased_credits >= price:
wallet.purchased_credits -= price
else:
remaining = price - wallet.purchased_credits
wallet.purchased_credits = 0
wallet.earned_credits -= remaining
# 4. Speciális Szerviz Logika (Service Coins)
# Ha a csomag típusa 'coin', akkor a szerviz kap egy kezdő Coin csomagot is
if pkg_info.get("type") == "coin":
initial_coins = pkg_info.get("initial_coin_bonus", 100)
wallet.service_coins += initial_coins
logger.info(f"User {current_user.id} upgraded to Service Pro, awarded {initial_coins} coins.")
# 5. Rang frissítése és naplózás
current_user.role = target_package # Pl. 'service_pro' vagy 'vip'
db.add(FinancialLedger(
user_id=current_user.id,
amount=-price,
transaction_type=f"UPGRADE_{target_package.upper()}",
details=pkg_info
))
await db.commit()
return {"status": "success", "package": target_package, "rank_granted": pkg_info["rank"]}
try:
result = await upgrade_subscription(db, current_user.id, target_package)
return {
"status": "success",
"package": target_package,
"price_paid": result.get("price_paid", 0.0),
"new_plan": result.get("new_plan"),
"expires_at": result.get("expires_at"),
"transaction": result.get("transaction")
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Upgrade error: {e}")
raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}")
@router.post("/payment-intent/create")
@@ -331,27 +301,14 @@ async def get_wallet_balance(
current_user: User = Depends(get_current_user)
):
"""
Felhasználó pénztárca egyenlegének lekérdezése.
Felhasználó pénztárca egyenlegének lekérdezése a Billing Engine segítségével.
"""
try:
stmt = select(Wallet).where(Wallet.user_id == current_user.id)
result = await db.execute(stmt)
wallet = result.scalar_one_or_none()
if not wallet:
raise HTTPException(status_code=404, detail="Pénztárca nem található")
return {
"earned": float(wallet.earned_credits),
"purchased": float(wallet.purchased_credits),
"service_coins": float(wallet.service_coins),
"total": float(
wallet.earned_credits +
wallet.purchased_credits +
wallet.service_coins
),
}
balances = await get_user_balance(db, current_user.id)
return balances
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Pénztárca egyenleg lekérdezési hiba: {e}")
raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}")
raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}")

View File

@@ -10,6 +10,7 @@ from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Rating
# 3. Jármű definíciók
from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
from .reference_data import ReferenceLookup
# 4. Szervezeti felépítés
from .organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment, OrgType, OrgUserRole, Branch
@@ -61,7 +62,7 @@ __all__ = [
"AuditLog", "VehicleOwnership", "LogSeverity",
"SecurityAuditLog", "ProcessLog", "FinancialLedger",
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter",
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition",
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition", "ReferenceLookup",
"VehicleType", "FeatureDefinition", "ModelFeatureMap", "LegalDocument", "LegalAcceptance",
"Location", "LocationType"
]

View File

@@ -166,14 +166,13 @@ class VehicleOwnership(Base):
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
acquired_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
disposed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
asset: Mapped["Asset"] = relationship("Asset", back_populates="ownership_history")
# EZ A SOR HIÁNYZIK A KÓDODBÓL ÉS EZ JAVÍTJA A HIBÁT:
# JAVÍTVA: Kapcsolat a User modellhez
user: Mapped["User"] = relationship("User", back_populates="ownership_history")
class AssetTelemetry(Base):
@@ -212,10 +211,27 @@ class ExchangeRate(Base):
rate: Mapped[float] = mapped_column(Numeric(18, 6), nullable=False)
class CatalogDiscovery(Base):
""" Robot munkaterület. """
""" Robot munkaterület a felfedezett modelleknek. """
__tablename__ = "catalog_discovery"
__table_args__ = (UniqueConstraint('make', 'model', name='_make_model_uc'), {"schema": "data"})
__table_args__ = (
# KIBŐVÍTETT EGYEDISÉGI SZABÁLY: Márka + Modell + Osztály + Piac + Évjárat
UniqueConstraint('make', 'model', 'vehicle_class', 'market', 'model_year', name='_make_model_market_year_uc'),
{"schema": "data"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
make: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
model: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
vehicle_class: Mapped[str] = mapped_column(String(50), server_default=text("'car'"), index=True)
# --- ÚJ MEZŐK A STATISZTIKÁHOZ ÉS PIACAZONOSÍTÁSHOZ ---
market: Mapped[str] = mapped_column(String(20), server_default=text("'GLOBAL'"), index=True) # pl: RDW, DVLA, USA_IMPORT
model_year: Mapped[Optional[int]] = mapped_column(Integer, index=True)
# Robot vezérlés
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
source: Mapped[Optional[str]] = mapped_column(String(100)) # pl: STRATEGIST-V2, NHTSA-V1
priority_score: Mapped[int] = mapped_column(Integer, server_default=text("0"))
attempts: Mapped[int] = mapped_column(Integer, server_default=text("0"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

View File

@@ -0,0 +1,20 @@
# /app/app/models/reference_data.py
from sqlalchemy import Column, Integer, String, DateTime, func
from sqlalchemy.dialects.postgresql import JSONB
from app.database import Base
class ReferenceLookup(Base):
__tablename__ = "reference_lookup"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
make = Column(String, nullable=False, index=True)
model = Column(String, nullable=False, index=True)
year = Column(Integer, nullable=True, index=True)
# Itt tároljuk az egységesített adatokat
specs = Column(JSONB, nullable=False)
source = Column(String, nullable=False) # pl: 'os-vehicle-db', 'wikidata'
source_id = Column(String, nullable=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())

View File

@@ -42,6 +42,7 @@ class FeatureDefinition(Base):
class VehicleModelDefinition(Base):
market: Mapped[str] = mapped_column(String(20), server_default=text("'GLOBAL'"), index=True)
"""
Robot v1.1.0 Multi-Tier MDM Master Adattábla.
Az ökoszisztéma technikai igazságforrása.
@@ -126,7 +127,7 @@ class VehicleModelDefinition(Base):
# --- BEÁLLÍTÁSOK ---
__table_args__ = (
UniqueConstraint('make', 'normalized_name', 'variant_code', 'version_code', 'fuel_type', name='uix_vmd_precision'),
UniqueConstraint('make', 'normalized_name', 'variant_code', 'version_code', 'fuel_type', 'market', 'year_from', name='uix_vmd_precision_v2'),
Index('idx_vmd_lookup_fast', 'make', 'normalized_name'),
Index('idx_vmd_engine_bridge', 'make', 'engine_code'),
{"schema": "data"}

View File

@@ -682,3 +682,199 @@ async def get_wallet_info(
Segédfüggvény pénztárca információk lekérdezéséhez.
"""
return await AtomicTransactionManager.get_wallet_summary(db, user_id)
# ==================== Billing Engine Service Functions ====================
async def charge_user(
db: AsyncSession,
user_id: int,
amount: float,
currency: str = "EUR",
transaction_type: str = "service_payment",
description: Optional[str] = None
) -> Dict[str, Any]:
"""
Kredit levonás a felhasználótól intelligens levonási sorrendben.
Args:
db: Database session
user_id: Felhasználó ID
amount: Levonandó összeg
currency: Pénznem (jelenleg csak EUR támogatott)
transaction_type: Tranzakció típusa (pl. "service_payment", "subscription")
description: Opcionális leírás
Returns:
Dict: Tranzakció részletei (AtomicTransactionManager.atomic_billing_transaction eredménye)
"""
if currency != "EUR":
raise ValueError("Only EUR currency is currently supported")
desc = description or f"Charge for {transaction_type}"
return await AtomicTransactionManager.atomic_billing_transaction(
db=db,
user_id=user_id,
amount=amount,
description=desc,
reference_type=transaction_type,
reference_id=None
)
async def upgrade_subscription(
db: AsyncSession,
user_id: int,
target_package: str
) -> Dict[str, Any]:
"""
Felhasználó előfizetésének frissítése (csomagváltás).
Args:
db: Database session
user_id: Felhasználó ID
target_package: Cél csomag neve (pl. "premium", "vip")
Returns:
Dict: Tranzakció részletei és az új előfizetés információi
"""
from app.models.core_logic import SubscriptionTier
from app.models.identity import User
# 1. Ellenőrizze, hogy a cél csomag létezik-e
stmt = select(SubscriptionTier).where(SubscriptionTier.name == target_package)
result = await db.execute(stmt)
tier = result.scalar_one_or_none()
if not tier:
raise ValueError(f"Subscription tier '{target_package}' not found")
# 2. Számítsa ki az árát a csomagnak (egyszerűsítve: fix ár a tier.rules-ból)
price = tier.rules.get("price", 0.0) if tier.rules else 0.0
if price <= 0:
# Ingyenes csomag, nincs levonás
logger.info(f"Upgrading user {user_id} to free tier {target_package}")
# Frissítse a felhasználó subscription_plan mezőjét
user_stmt = select(User).where(User.id == user_id)
user_result = await db.execute(user_stmt)
user = user_result.scalar_one()
user.subscription_plan = target_package
user.subscription_expires_at = datetime.utcnow() + timedelta(days=30) # 30 nap
return {
"success": True,
"message": f"Upgraded to {target_package} (free)",
"new_plan": target_package,
"price_paid": 0.0
}
# 3. Ár kiszámítása a PricingCalculator segítségével
user_stmt = select(User).where(User.id == user_id)
user_result = await db.execute(user_stmt)
user = user_result.scalar_one()
final_price = await PricingCalculator.calculate_final_price(
db=db,
base_amount=price,
country_code=user.region_code,
user_role=user.role
)
# 4. Levonás a felhasználótól
transaction = await charge_user(
db=db,
user_id=user_id,
amount=final_price,
currency="EUR",
transaction_type="subscription_upgrade",
description=f"Upgrade to {target_package} subscription"
)
# 5. Frissítse a felhasználó előfizetési adatait
user.subscription_plan = target_package
user.subscription_expires_at = datetime.utcnow() + timedelta(days=30) # 30 nap
logger.info(f"User {user_id} upgraded to {target_package} for {final_price} EUR")
return {
"success": True,
"transaction": transaction,
"new_plan": target_package,
"price_paid": final_price,
"expires_at": user.subscription_expires_at.isoformat()
}
async def record_ledger_entry(
db: AsyncSession,
user_id: int,
amount: float,
entry_type: LedgerEntryType,
wallet_type: WalletType,
transaction_type: str,
description: str,
reference_type: Optional[str] = None,
reference_id: Optional[int] = None
) -> FinancialLedger:
"""
Közvetlen főkönyvbejegyzés létrehozása (pl. manuális korrekciók).
Megjegyzés: Ez a függvény NEM végez levonást a pénztárcából, csak naplóbejegyzést készít.
A pénztárca egyenleg frissítéséhez használd a charge_user vagy atomic_billing_transaction függvényeket.
Args:
db: Database session
user_id: Felhasználó ID
amount: Összeg
entry_type: DEBIT vagy CREDIT
wallet_type: Pénztárca típus
transaction_type: Tranzakció típusa
description: Leírás
reference_type: Referencia típus
reference_id: Referencia ID
Returns:
FinancialLedger: Létrehozott főkönyvbejegyzés
"""
ledger_entry = FinancialLedger(
user_id=user_id,
amount=Decimal(str(amount)),
entry_type=entry_type,
wallet_type=wallet_type,
transaction_type=transaction_type,
details={
"description": description,
"reference_type": reference_type,
"reference_id": reference_id,
"wallet_type": wallet_type.value
},
transaction_id=uuid.uuid4(),
balance_after=None, # Később számolható
currency="EUR"
)
db.add(ledger_entry)
await db.flush()
logger.info(f"Ledger entry recorded: user={user_id}, amount={amount}, type={entry_type.value}")
return ledger_entry
async def get_user_balance(
db: AsyncSession,
user_id: int
) -> Dict[str, float]:
"""
Felhasználó pénztárca egyenlegének lekérdezése.
Args:
db: Database session
user_id: Felhasználó ID
Returns:
Dict: Pénztárca típusonkénti egyenlegek
"""
wallet_summary = await AtomicTransactionManager.get_wallet_summary(db, user_id)
return wallet_summary["balances"]

View File

@@ -42,14 +42,13 @@ class FinancialTruthTest:
async def setup(self):
print("=== IGAZSÁGSZÉRUM TESZT - Pénzügyi Motor Audit ===")
print("0. ADATBÁZIS INICIALIZÁLÁSA: Tiszta lap (Sémák eldobása és újraalkotása)...")
print("0. ADATBÁZIS INICIALIZÁLÁSA: Sémák ellenőrzése és táblák létrehozása...")
async with engine.begin() as conn:
await conn.execute(text("DROP SCHEMA IF EXISTS audit CASCADE;"))
await conn.execute(text("DROP SCHEMA IF EXISTS identity CASCADE;"))
await conn.execute(text("DROP SCHEMA IF EXISTS data CASCADE;"))
await conn.execute(text("CREATE SCHEMA audit;"))
await conn.execute(text("CREATE SCHEMA identity;"))
await conn.execute(text("CREATE SCHEMA data;"))
# Sémák létrehozása, ha még nem léteznek (deadlock elkerülés)
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS audit;"))
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS identity;"))
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS data;"))
# Táblák létrehozása (ha már léteznek, nem történik semmi)
await conn.run_sync(Base.metadata.create_all)
print("1. TESZT KÖRNYEZET: Teszt felhasználók létrehozása...")

View File

@@ -0,0 +1,194 @@
# /app/app/workers/monitor_dashboard.py
# docker exec sf_api python -m app.workers.monitor_dashboard
import asyncio
import os
import httpx
import pynvml
import psutil
from datetime import datetime, timedelta
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.live import Live
from rich.layout import Layout
from rich.text import Text
from app.core.config import settings
console = Console()
# NVIDIA inicializálása
try:
pynvml.nvmlInit()
gpu_available = True
except Exception:
gpu_available = False
async def get_hardware_stats():
"""Rendszererőforrások: CPU, RAM és GPU"""
stats = {
"cpu_usage": psutil.cpu_percent(interval=None),
"ram_total": psutil.virtual_memory().total // 1024**2,
"ram_used": psutil.virtual_memory().used // 1024**2,
"ram_perc": psutil.virtual_memory().percent,
"gpu": None
}
if gpu_available:
try:
handle = pynvml.nvmlDeviceGetHandleByIndex(0)
stats["gpu"] = {
"name": pynvml.nvmlDeviceGetName(handle),
"temp": pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU),
"load": pynvml.nvmlDeviceGetUtilizationRates(handle).gpu,
"vram_total": pynvml.nvmlDeviceGetMemoryInfo(handle).total // 1024**2,
"vram_used": pynvml.nvmlDeviceGetMemoryInfo(handle).used // 1024**2,
"power": pynvml.nvmlDeviceGetPowerUsage(handle) / 1000
}
except: pass
return stats
async def get_ollama_models():
try:
async with httpx.AsyncClient(timeout=2.0) as client:
resp = await client.get("http://ollama:11434/api/ps")
if resp.status_code == 200:
return [m['name'] for m in resp.json().get("models", [])]
except: return ["Ollama Comm Error"]
return []
async def get_stats(engine):
async with engine.connect() as conn:
# 1. Sebesség adatok
res_hr = await conn.execute(text("SELECT count(*) FROM data.vehicle_model_definitions WHERE status = 'gold_enriched' AND updated_at > NOW() - INTERVAL '1 hour'"))
hr_rate = res_hr.scalar() or 0
res_day = await conn.execute(text("SELECT count(*) FROM data.vehicle_model_definitions WHERE status = 'gold_enriched' AND updated_at > NOW() - INTERVAL '24 hours'"))
day_rate = res_day.scalar() or 0
# 2. Pipeline
res_pipe = await conn.execute(text("""
SELECT
(SELECT count(*) FROM data.catalog_discovery WHERE status = 'pending') as r1,
(SELECT count(*) FROM data.vehicle_model_definitions WHERE status = 'unverified') as r2,
(SELECT count(*) FROM data.vehicle_model_definitions WHERE status = 'awaiting_ai_synthesis') as r3,
(SELECT count(*) FROM data.vehicle_model_definitions WHERE status = 'gold_enriched') as r4
"""))
r_counts = res_pipe.fetchone()
# 3. TOP 7
res_top = await conn.execute(text("SELECT make, count(*) as qty FROM data.vehicle_model_definitions GROUP BY make ORDER BY qty DESC LIMIT 7"))
top_makes = res_top.fetchall()
# 4. AKTIVITÁS (3 példány per robot)
res_r4 = await conn.execute(text("SELECT make, marketing_name FROM data.vehicle_model_definitions WHERE status = 'gold_enriched' ORDER BY updated_at DESC LIMIT 5"))
res_r3 = await conn.execute(text("SELECT make, marketing_name FROM data.vehicle_model_definitions WHERE status = 'ai_synthesis_in_progress' ORDER BY updated_at DESC LIMIT 5"))
res_r12 = await conn.execute(text("SELECT make, model FROM data.catalog_discovery WHERE status = 'processing' ORDER BY updated_at DESC LIMIT 5"))
hw = await get_hardware_stats()
ai = await get_ollama_models()
return (hr_rate, day_rate), r_counts, top_makes, (res_r4.fetchall(), res_r3.fetchall(), res_r12.fetchall()), hw, ai
def make_layout() -> Layout:
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(name="main", ratio=1),
Layout(name="hardware", size=10), # Megnövelt hardver rész
Layout(name="footer", size=3)
)
layout["main"].split_row(
Layout(name="left", ratio=1),
Layout(name="right", ratio=2)
)
layout["left"].split_column(Layout(name="robot_stats"), Layout(name="inventory"))
layout["right"].split_column(Layout(name="live_ops"))
return layout
def update_dashboard(layout, data):
rates, r_counts, top_makes, live_data, hw, ai_models = data
r4_list, r3_list, r12_list = live_data
# Óra (UTC+1 korrekció)
local_time = datetime.now() + timedelta(hours=1)
# HEADER (Változatlan)
layout["header"].update(Panel(
f"🛰️ SENTINEL MISSION CONTROL | [bold yellow]{local_time.strftime('%Y-%m-%d %H:%M:%S')}[/] | AI: [green]{rates[0]}[/] /óra — [cyan]{rates[1]}[/] /nap",
style="bold white on blue"
))
# ROBOT PIPELINE
robot_table = Table(title="🤖 Pipeline Állapot", expand=True, border_style="cyan")
robot_table.add_column("Robot", style="bold")
robot_table.add_column("Várakozik", justify="right")
robot_table.add_row("R1-Hunter", f"{r_counts[0]} db")
robot_table.add_row("R2-Researcher", f"{r_counts[1]} db")
robot_table.add_row("R3-Alchemist", f"{r_counts[2]} db")
robot_table.add_row("R4-Validator", f"{r_counts[3]} db")
layout["robot_stats"].update(robot_table)
# TOP MÁRKÁK
brand_table = Table(title="🚜 Top 7 Márka", expand=True, border_style="magenta")
brand_table.add_column("Márka", style="yellow")
brand_table.add_column("db", justify="right")
for m, q in top_makes: brand_table.add_row(m, str(q))
layout["inventory"].update(brand_table)
# LIVE OPS (Bővítve 5-5 példányra)
ops_table = Table(title="⚡ Aktuális Folyamatok (Utolsó 3/robot)", expand=True, border_style="green")
ops_table.add_column("Robot", width=15)
ops_table.add_column("Márka / Típus")
for r in r4_list: ops_table.add_row("[gold1]R4-VALIDATOR[/]", f"{r[0]} {r[1] or ''}")
ops_table.add_section()
for r in r3_list: ops_table.add_row("[medium_purple1]R3-ALCHEMIST[/]", f"{r[0]} {r[1] or ''}")
ops_table.add_section()
for r in r12_list: ops_table.add_row("[sky_blue1]R1-HUNTER[/]", f"{r[0]} {r[1] or ''}")
layout["live_ops"].update(ops_table)
# HARDWARE & AI (3 OSZLOPOS ELRENDEZÉS)
hw_layout = Layout()
hw_layout.split_row(Layout(name="sys"), Layout(name="gpu"), Layout(name="ai"))
# 1. Rendszer (CPU/RAM)
sys_info = (
f"[bold]CPU Terhelés:[/] [bright_blue]{hw['cpu_usage']}%[/]\n"
f"[bold]RAM Használat:[/] [bright_magenta]{hw['ram_perc']}%[/]\n"
f"({hw['ram_used']} / {hw['ram_total']} MB)"
)
hw_layout["sys"].update(Panel(sys_info, title="💻 System Resources", border_style="bright_blue"))
# 2. GPU
if hw["gpu"]:
g = hw["gpu"]
gpu_info = (
f"[bold]{g['name']}[/]\n"
f"Load: [green]{g['load']}%[/] | Temp: {g['temp']}°C\n"
f"VRAM: {g['vram_used']} / {g['vram_total']} MB"
)
else:
gpu_info = "[red]NVIDIA GPU not detected[/]"
hw_layout["gpu"].update(Panel(gpu_info, title="🔌 GPU Monitor", border_style="orange3"))
# 3. AI Models
ai_info = "[bold]In Memory (VRAM):[/]\n" + ("\n".join([f"🧠 {m}" for m in ai_models]) if ai_models else "No active models.")
hw_layout["ai"].update(Panel(ai_info, title="🤖 AI Stack", border_style="plum1"))
layout["hardware"].update(hw_layout)
layout["footer"].update(Panel(f"Sentinel v2.5 | Kernel: Stabil | Heartbeat: OK", style="italic grey50"))
async def main():
engine = create_async_engine(settings.DATABASE_URL)
layout = make_layout()
with Live(layout, refresh_per_second=1, screen=True):
while True:
try:
data = await get_stats(engine)
update_dashboard(layout, data)
except: pass
await asyncio.sleep(2)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,52 @@
# /opt/docker/dev/service_finder/backend/app/workers/py_to_database.py
import asyncio
import importlib
import os
import sys
from sqlalchemy import text
from app.database import engine, Base
# Biztosítjuk, hogy a Python látja az /app mappát
sys.path.append("/app")
async def sync_database():
print("\n" + ""*60)
print("🚀 SENTINEL DATABASE SYNC - MB2.0")
print(""*60)
# 1. Sémák kényszerítése (hogy ne ezen bukjon el)
async with engine.begin() as conn:
for schema in ["data", "identity", "system", "audit"]:
await conn.execute(text(f"CREATE SCHEMA IF NOT EXISTS {schema}"))
print("✅ Sémák ellenőrizve.")
# 2. Modellek importálása (Szabályos úton)
# A diagnózis alapján ezek a fájlok biztosan ott vannak
model_modules = [
"address", "audit", "document", "history", "legal", "organization",
"security", "social", "system", "vehicle_definitions", "asset",
"core_logic", "gamification", "identity", "logistics", "payment",
"service", "staged_data", "translation"
]
print(f"📂 Modellek betöltése...")
for m in model_modules:
try:
# Ez a módszer regisztrálja a modult a sys.modules-ba
importlib.import_module(f"app.models.{m}")
print(f" 📦 [OK] app.models.{m}")
except Exception as e:
print(f" ⚠️ [INFO] app.models.{m} betöltése (vagy függősége) kihagyva: {e}")
# 3. Fizikai szinkronizáció
print(f"\n🏗️ Táblák létrehozása (Összesen: {len(Base.metadata.tables)} darab)...")
async with engine.begin() as conn:
# Ez a parancs hozza létre az összes hiányzó táblát
await conn.run_sync(Base.metadata.create_all)
print("\n" + ""*60)
print("✅ KÉSZ! Az adatbázis most már tartalmazza a System és Translation táblákat is.")
print(""*60)
if __name__ == "__main__":
asyncio.run(sync_database())

View File

@@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""
🤖 Subscription Lifecycle Worker (Robot-20)
A 20-as Gitea kártya implementációja: Lejárt előfizetések automatikus kezelése.
Folyamat:
1. Lekérdezés: Azokat a User-eket, ahol subscription_expires_at < NOW() ÉS subscription_plan != 'FREE'
2. Downgrade: subscription_plan = "FREE", is_vip = False
3. Naplózás: Főkönyvi bejegyzés (SUBSCRIPTION_EXPIRED)
4. Értesítés: NotificationService küldése a downgrade-ről
Futtatás:
- Napi egyszer (cron) vagy manuálisan: docker exec sf_api python -m app.workers.system.subscription_worker
"""
import asyncio
import logging
from datetime import datetime, timezone
from typing import List
from sqlalchemy import select, update, and_
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import AsyncSessionLocal
from app.models.identity import User
from app.models.audit import FinancialLedger, LedgerEntryType, WalletType
from app.services.billing_engine import record_ledger_entry
from app.services.notification_service import NotificationService
logger = logging.getLogger("subscription-worker")
async def process_expired_subscriptions(db: AsyncSession) -> dict:
"""
Atomikus tranzakcióban feldolgozza a lejárt előfizetéseket.
Returns:
dict: Statisztikák (processed_count, downgraded_users, errors)
"""
now = datetime.now(timezone.utc)
# 1. Lekérdezés: Lejárt előfizetések, amelyek még nem FREE-ek
stmt = select(User).where(
and_(
User.subscription_expires_at < now,
User.subscription_plan != 'FREE',
User.is_deleted == False,
User.is_active == True
)
).with_for_update(skip_locked=True) # Atomikus zárolás
result = await db.execute(stmt)
expired_users: List[User] = result.scalars().all()
stats = {
"processed_count": len(expired_users),
"downgraded_users": [],
"errors": []
}
for user in expired_users:
try:
# 2. Downgrade
old_plan = user.subscription_plan
user.subscription_plan = "FREE"
user.is_vip = False
# subscription_expires_at marad null vagy régi érték (lejárt)
# 3. Főkönyvi bejegyzés (SUBSCRIPTION_EXPIRED)
# Megjegyzés: amount = 0, mert nem történik pénzmozgás, csak naplózás
ledger_entry = await record_ledger_entry(
db=db,
user_id=user.id,
amount=0.0,
entry_type=LedgerEntryType.DEBIT,
wallet_type=WalletType.SYSTEM,
transaction_type="SUBSCRIPTION_EXPIRED",
description=f"Előfizetés lejárt: {old_plan} → FREE",
reference_type="subscription",
reference_id=user.id
)
# 4. Értesítés küldése
await NotificationService.send_notification(
db=db,
user_id=user.id,
title="Előfizetésed lejárt",
message=f"A(z) {old_plan} előfizetésed lejárt, ezért átállítottuk a FREE csomagra. További előnyökért frissíts előfizetést!",
category="billing",
priority="medium",
data={
"old_plan": old_plan,
"new_plan": "FREE",
"user_id": user.id,
"expired_at": user.subscription_expires_at.isoformat() if user.subscription_expires_at else None
},
send_email=True,
email_template="subscription_expired",
email_vars={
"recipient": user.email,
"first_name": user.person.first_name if user.person else "Partnerünk",
"old_plan": old_plan,
"new_plan": "FREE",
"lang": user.preferred_language
}
)
stats["downgraded_users"].append({
"user_id": user.id,
"email": user.email,
"old_plan": old_plan,
"new_plan": "FREE"
})
logger.info(f"User {user.id} ({user.email}) subscription downgraded from {old_plan} to FREE")
except Exception as e:
logger.error(f"Error processing user {user.id}: {e}", exc_info=True)
stats["errors"].append({"user_id": user.id, "error": str(e)})
# Commit a tranzakció
await db.commit()
return stats
async def main():
"""Fő futtató függvény, amelyet a cron hív."""
logger.info("Starting subscription worker...")
async with AsyncSessionLocal() as db:
try:
stats = await process_expired_subscriptions(db)
logger.info(f"Subscription worker completed. Stats: {stats}")
print(f"✅ Subscription worker completed. Processed: {stats['processed_count']}, Downgraded: {len(stats['downgraded_users'])}")
except Exception as e:
logger.error(f"Subscription worker failed: {e}", exc_info=True)
await db.rollback()
raise
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,24 @@
# MB 2.0: Strukturált adat-szótár a források egységesítéséhez
VEHICLE_MAPPING = {
"os-vehicle-db": {
"brand": "make",
"model": "model",
"year": "year",
"specs": {
"engine_hp": "specs.engine.hp",
"fuel": "specs.engine.fuel",
"body": "specs.body.type"
}
}
# Ide jönnek majd a carquery és fipe mappolások
}
def normalize_make(make: str) -> str:
""" Egységes márkanevek (pl. Mercedes-Benz vs Mercedes) """
m = make.upper().strip()
synonyms = {
"MERCEDES": "MERCEDES-BENZ",
"VW": "VOLKSWAGEN",
"ALFA": "ALFA ROMEO"
}
return synonyms.get(m, m)

View File

@@ -0,0 +1,26 @@
# /app/app/workers/vehicle/mapping_rules.py
SOURCE_MAPPINGS = {
"os-vehicle-db": {
"make": "brand",
"model": "model_name",
"year": "release_year",
"power": "specs.engine.hp"
},
"car-query": {
"make": "model_make_id",
"model": "model_name",
"year": "model_year",
"power": "model_engine_power_ps"
}
}
def unify_data(raw_data, source_name):
mapping = SOURCE_MAPPINGS.get(source_name, {})
unified = {
"normalized_make": raw_data.get(mapping.get("make"), "").upper(),
"normalized_model": raw_data.get(mapping.get("model"), "").upper(),
"normalized_year": raw_data.get(mapping.get("year")),
"raw_specs": raw_data # Megtartjuk az eredetit is
}
return unified

View File

@@ -0,0 +1,128 @@
# /opt/docker/dev/service_finder/backend/app/workers/vehicle/robot_report.py
import asyncio
import psutil
import pynvml
from datetime import datetime
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.layout import Layout
from app.core.config import settings
console = Console()
async def get_data():
engine = create_async_engine(settings.DATABASE_URL)
async with engine.connect() as conn:
# Pipeline adatok (R1-R4)
pipe = await conn.execute(text("""
SELECT
(SELECT count(*) FROM data.catalog_discovery WHERE status = 'pending') as r1,
(SELECT count(*) FROM data.vehicle_model_definitions WHERE status = 'unverified') as r2,
(SELECT count(*) FROM data.vehicle_model_definitions WHERE status = 'awaiting_ai_synthesis') as r3,
(SELECT count(*) FROM data.vehicle_model_definitions WHERE status = 'gold_enriched') as r4
"""))
p_res = pipe.fetchone()
# AI Termelés
ai_hr = await conn.execute(text("SELECT count(*) FROM data.vehicle_model_definitions WHERE status = 'gold_enriched' AND updated_at > NOW() - INTERVAL '1 hour'"))
ai_day = await conn.execute(text("SELECT count(*) FROM data.vehicle_model_definitions WHERE status = 'gold_enriched' AND updated_at > NOW() - INTERVAL '24 hours'"))
# Market Matrix (1.3)
market_res = await conn.execute(text("SELECT vehicle_class, market, count(*) FROM data.catalog_discovery GROUP BY 1, 2"))
m_data = market_res.fetchall()
# Robot Top listák (2.1 - 2.3)
r1_top = await conn.execute(text("SELECT make, count(*) FROM data.catalog_discovery WHERE market = 'RDW' GROUP BY 1 ORDER BY 2 DESC LIMIT 5"))
r12_top = await conn.execute(text("SELECT make, count(*) FROM data.catalog_discovery WHERE market = 'USA_IMPORT' GROUP BY 1 ORDER BY 2 DESC LIMIT 5"))
r14_top = await conn.execute(text("SELECT make, count(*) FROM data.catalog_discovery WHERE vehicle_class = 'motorcycle' GROUP BY 1 ORDER BY 2 DESC LIMIT 5"))
# Általános Top (3.1 - 3.3)
pending_top = await conn.execute(text("SELECT make, count(*) FROM data.catalog_discovery WHERE status = 'pending' GROUP BY 1 ORDER BY 2 DESC LIMIT 5"))
gold_top = await conn.execute(text("SELECT make, count(*) FROM data.vehicle_model_definitions WHERE status = 'gold_enriched' GROUP BY 1 ORDER BY 2 DESC LIMIT 5"))
status_stats = await conn.execute(text("SELECT status, count(*) FROM data.vehicle_model_definitions GROUP BY 1 ORDER BY 2 DESC LIMIT 5"))
# Kategória Top (4.1 - 4.3)
cat_tops = {}
for c in ['car', 'motorcycle', 'truck']:
res = await conn.execute(text(f"SELECT make, count(*) FROM data.catalog_discovery WHERE vehicle_class = '{c}' GROUP BY 1 ORDER BY 2 DESC LIMIT 4"))
total = await conn.execute(text(f"SELECT count(*) FROM data.catalog_discovery WHERE vehicle_class = '{c}'"))
cat_tops[c] = {"list": res.fetchall(), "total": total.scalar() or 0}
return {
"p": p_res, "ai": (ai_hr.scalar(), ai_day.scalar()), "markets": m_data,
"r1": r1_top.fetchall(), "r12": r12_top.fetchall(), "r14": r14_top.fetchall(),
"pending": pending_top.fetchall(), "gold": gold_top.fetchall(), "status": status_stats.fetchall(),
"cat": cat_tops
}
def get_hw():
try:
pynvml.nvmlInit()
h = pynvml.nvmlDeviceGetHandleByIndex(0)
info = pynvml.nvmlDeviceGetMemoryInfo(h)
return {"cpu": psutil.cpu_percent(), "ram": psutil.virtual_memory().percent, "gpu": pynvml.nvmlDeviceGetUtilizationRates(h).gpu,
"vram": f"{int(info.used/1024**2)}/{int(info.total/1024**2)}M", "temp": pynvml.nvmlDeviceGetTemperature(h, 0)}
except: return None
def mini_table(data, color="white"):
t = Table(show_header=False, box=None, expand=True, padding=(0,1))
t.add_column("L", ratio=3); t.add_column("R", justify="right", ratio=2)
for r in data: t.add_row(str(r[0])[:12], f"[{color}]{r[1]:,}[/]")
return t
async def main():
try:
d = await get_data(); hw = get_hw()
lay = Layout()
lay.split_column(Layout(name="head", size=3), Layout(name="body"))
lay["body"].split_column(Layout(name="row1"), Layout(name="row2"), Layout(name="row3"), Layout(name="row4"))
for r in ["row1", "row2", "row3", "row4"]:
lay[r].split_row(Layout(name=f"{r}1"), Layout(name=f"{r}2"), Layout(name=f"{r}3"))
# --- FEJLÉC ---
h_str = f"💻 CPU: {hw['cpu']}% | 🧠 RAM: {hw['ram']}% | 📟 VRAM: {hw['vram']} | 🔥 GPU: {hw['gpu']}% ({hw['temp']}°C)" if hw else "Hardware adatok nem elérhetőek"
lay["head"].update(Panel(h_str, title="[bold orange3]SZÁMÍTÓGÉP ADATOK[/]", style="orange3"))
# --- 1. SOR ---
r11_t = Table(show_header=False, box=None, expand=True)
r11_t.add_row("R1 (Hunter)", f"{d['p'][0]:,}"); r11_t.add_row("R2 (Res)", f"{d['p'][1]:,}")
r11_t.add_row("R3 (Wait)", f"{d['p'][2]:,}"); r11_t.add_row("R4 (Gold)", f"[green]{d['p'][3]:,}[/]")
r11_t.add_row("AI HR/DAY", f"[cyan]{d['ai'][0]}[/] / [yellow]{d['ai'][1]}[/]")
lay["row11"].update(Panel(r11_t, title="1.1 PIPE & AI"))
lay["row12"].update(Panel(mini_table(d['gold'], "green"), title="1.2 TOP MÁRKÁK"))
r13_t = Table(show_header=True, box=None, expand=True, header_style="bold blue")
r13_t.add_column("Típus", ratio=2); r13_t.add_column("EU", justify="right"); r13_t.add_column("USA", justify="right")
m_map = {c: {"EU": 0, "USA": 0} for c in ['car', 'motorcycle', 'truck', 'bus']}
for c, m, q in d['markets']:
key = 'USA' if m == 'USA_IMPORT' else 'EU'
if c in m_map: m_map[c][key] += q
for c, v in m_map.items(): r13_t.add_row(c[:4].upper(), f"{v['EU']:,}", f"{v['USA']:,}")
lay["row13"].update(Panel(r13_t, title="1.3 MARKET MATRIX"))
# --- 2. SOR (ROBOTOK) ---
lay["row21"].update(Panel(mini_table(d['r1']), title="2.1 ROBOT 1 (RDW)"))
lay["row22"].update(Panel(mini_table(d['r12']), title="2.2 ROBOT 1.2 (USA)"))
lay["row23"].update(Panel(mini_table(d['r14']), title="2.3 ROBOT 1.4 (BIKE)"))
# --- 3. SOR (ÖSSZESÍTŐ) ---
lay["row31"].update(Panel(mini_table(d['pending']), title="3.1 PENDING TOP"))
lay["row32"].update(Panel(mini_table(d['gold'], "green"), title="3.2 GOLD TOP", border_style="green"))
lay["row33"].update(Panel(mini_table(d['status'], "blue"), title="3.3 STATUS", border_style="blue"))
# --- 4. SOR (KATEGÓRIÁK) ---
for i, (k, l) in enumerate([('car', '4.1 AUTO'), ('motorcycle', '4.2 MOTOR'), ('truck', '4.3 TEHER')], 1):
t = mini_table(d['cat'][k]['list'], "yellow")
t.add_row("[bold]TOTAL[/]", f"[bold yellow]{d['cat'][k]['total']:,}[/]")
lay[f"row4{i}"].update(Panel(t, title=l, border_style="yellow"))
console.print(lay)
except Exception as e:
console.print(f"[bold red]HIBA TÖRTÉNT:[/] {e}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,121 @@
import asyncio
import httpx
import json
import logging
from sqlalchemy import text
from app.database import AsyncSessionLocal
# MB 2.0 Naplózási beállítások
logger = logging.getLogger("Data-Loader-Master")
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
class VehicleDataLoader:
# Ellenőrzött, működő források
SOURCES = {
"car-data-kohut": "https://raw.githubusercontent.com/DanielKohut/car-data/master/car_data.json",
"car-list-matth": "https://raw.githubusercontent.com/matthlavacka/car-list/master/car-list.json"
}
@staticmethod
def normalize_name(name: str) -> str:
""" Alapvető szövegtisztítás: nagybetű, szóközmentesítés. """
if not name: return ""
n = str(name).upper().strip()
# Gyakori szinonimák egységesítése
synonyms = {"VW": "VOLKSWAGEN", "MERCEDES": "MERCEDES-BENZ", "ALFA": "ALFA ROMEO"}
return synonyms.get(n, n)
async def fetch_json(self, url: str):
""" Biztonságos letöltés időtúllépés kezeléssel. """
async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
try:
resp = await client.get(url)
if resp.status_code == 200:
return resp.json()
logger.error(f"❌ Letöltési hiba ({resp.status_code}): {url}")
except Exception as e:
logger.error(f"🚨 Kommunikációs hiba: {url} -> {e}")
return None
def map_source_data(self, source_name, raw_data):
"""
Mapping Layer: Átfordítja a különböző források JSON szerkezetét
a mi egységes data.reference_lookup sémánkra.
"""
unified_entries = []
try:
if source_name == "car-data-kohut":
# Szerkezet: list[{"brand": "...", "models": ["...", ...]}]
for brand_item in raw_data:
make = self.normalize_name(brand_item.get("brand"))
for model in brand_item.get("models", []):
unified_entries.append({
"make": make, "model": self.normalize_name(model),
"year": None, "specs": json.dumps({"source": "kohut"}),
"source": source_name, "source_id": None
})
elif source_name == "car-list-matth":
# Szerkezet: list[{"brand": "...", "models": ["...", ...]}]
for brand_item in raw_data:
make = self.normalize_name(brand_item.get("brand"))
for model in brand_item.get("models", []):
unified_entries.append({
"make": make, "model": self.normalize_name(model),
"year": None, "specs": json.dumps({"source": "matthlavacka"}),
"source": source_name, "source_id": None
})
elif source_name == "os-vehicle-db":
# Szerkezet: list[{"make": "...", "model": "...", "year": ...}]
for item in raw_data:
unified_entries.append({
"make": self.normalize_name(item.get("make")),
"model": self.normalize_name(item.get("model")),
"year": item.get("year"),
"specs": json.dumps(item),
"source": source_name,
"source_id": str(item.get("id", ""))
})
except Exception as e:
logger.error(f"⚠️ Mapping hiba a(z) {source_name} feldolgozásakor: {e}")
return unified_entries
async def save_to_db(self, entries):
""" Batch Upsert folyamat az adatbázisba. """
if not entries: return
async with AsyncSessionLocal() as db:
stmt = text("""
INSERT INTO data.reference_lookup (make, model, year, specs, source, source_id)
VALUES (:make, :model, :year, :specs, :source, :source_id)
ON CONFLICT ON CONSTRAINT _ref_lookup_uc
DO UPDATE SET specs = EXCLUDED.specs, updated_at = NOW()
""")
try:
# 1000-es csomagokban küldjük be
for i in range(0, len(entries), 1000):
batch = entries[i:i+1000]
for row in batch:
await db.execute(stmt, row)
await db.commit()
logger.info(f"💾 Mentve: {min(i + 1000, len(entries))} / {len(entries)}")
except Exception as e:
await db.rollback()
logger.error(f"🚨 Adatbázis hiba: {e}")
async def run_sync(self):
logger.info("🚀 Data Loader (Fast Lane Support) elindul...")
for name, url in self.SOURCES.items():
raw_json = await self.fetch_json(url)
if raw_json:
logger.info(f"🧹 {name} feldolgozása...")
unified = self.map_source_data(name, raw_json)
await self.save_to_db(unified)
logger.info("🏁 Minden forrás szinkronizálva.")
if __name__ == "__main__":
asyncio.run(VehicleDataLoader().run_sync())

View File

@@ -60,8 +60,8 @@ class DiscoveryEngine:
async def seed_manual_bootstrap():
""" 2. FÁZIS: Alapozó adatok rögzítése """
initial_data = [
{"make": "AUDI", "model": "A4", "generation": "B8 (2008-2015)", "vehicle_class": "car"},
{"make": "BMW", "model": "3 SERIES", "generation": "F30 (2012-2019)", "vehicle_class": "car"}
{"make": "AUDI", "model": "A4", "generation": "B8 (2008-2015)"}, # vehicle_class törölve
{"make": "BMW", "model": "3 SERIES", "generation": "F30 (2012-2019)"}
]
try:
async with AsyncSessionLocal() as db:
@@ -131,13 +131,20 @@ class DiscoveryEngine:
elif "Motorfiets" in v_kind: v_class = 'motorcycle'
else: v_class = 'truck'
# A MÁGIA: Különbözeti Szinkronizáció SQL
# A MÁGIA: Különbözeti Szinkronizáció SQL + Explicit Type Casting
query = text("""
INSERT INTO data.catalog_discovery (make, model, vehicle_class, status, priority_score)
SELECT :make, :model, :v_class, 'pending', :priority
SELECT
CAST(:make AS VARCHAR),
CAST(:model AS VARCHAR),
CAST(:v_class AS VARCHAR),
'pending',
:priority
WHERE NOT EXISTS (
SELECT 1 FROM data.vehicle_model_definitions
WHERE make = :make AND marketing_name = :model AND status = 'gold_enriched'
WHERE make = CAST(:make AS VARCHAR)
AND marketing_name = CAST(:model AS VARCHAR)
AND status = 'gold_enriched'
)
ON CONFLICT (make, model)
DO UPDATE SET priority_score = EXCLUDED.priority_score

View File

@@ -0,0 +1,66 @@
# /app/app/workers/vehicle/vehicle_robot_1_2_nhtsa_fetcher.py
import asyncio
import httpx
import logging
from sqlalchemy import text
from app.database import AsyncSessionLocal
logger = logging.getLogger("Robot-1-2-NHTSA-EU-Only")
logging.basicConfig(level=logging.INFO)
class NHTSAFetcher:
API_URL = "https://vpic.nhtsa.dot.gov/api/vehicles/GetModelsForMakeYear/make/{make}/modelyear/{year}?format=json"
@classmethod
async def get_eu_makes(cls):
"""Lekéri azokat a márkákat, amik már benne vannak az adatbázisban EU-s forrásból."""
async with AsyncSessionLocal() as db:
# Csak azokat a márkákat keressük az USA-ban, amiket az EU-ban (RDW) már láttunk
query = text("SELECT DISTINCT make FROM data.catalog_discovery WHERE market = 'EU' OR source = 'RDW'")
res = await db.execute(query)
return [row[0] for row in res.fetchall()]
@classmethod
async def run(cls):
logger.info("🚀 Robot 1.2 (EU-Guided NHTSA) indítása...")
while True:
target_makes = await cls.get_eu_makes()
if not target_makes:
logger.warning("⚠️ Még nincs EU-s márka az adatbázisban. Várakozás...")
await asyncio.sleep(60)
continue
# 2026-tól megyünk vissza a múltba
for year in range(2026, 1950, -1):
async with AsyncSessionLocal() as db:
for make in target_makes:
try:
async with httpx.AsyncClient(timeout=20.0) as client:
url = cls.API_URL.format(make=make, year=year)
resp = await client.get(url)
if resp.status_code != 200: continue
models = resp.json().get("Results", [])
inserted = 0
for m in models:
model_name = m.get("Model_Name").upper().strip()
# USA_IMPORT jelölés, de csak EU-s márkákhoz!
query = text("""
INSERT INTO data.catalog_discovery
(make, model, vehicle_class, status, market, model_year, priority_score, source)
VALUES (:make, :model, 'car', 'pending', 'USA_IMPORT', :year, 5, 'NHTSA-EU-FILTERED')
ON CONFLICT ON CONSTRAINT _make_model_market_year_uc DO NOTHING
""")
res = await db.execute(query, {"make": make, "model": model_name, "year": year})
if res.rowcount > 0: inserted += 1
if inserted > 0:
logger.info(f"{make} ({year}): {inserted} variáns dúsítva az USA-ból.")
await db.commit()
except Exception as e:
logger.error(f"❌ Hiba: {make} {year}: {e}")
await asyncio.sleep(0.5)
if __name__ == "__main__":
asyncio.run(NHTSAFetcher.run())

View File

@@ -0,0 +1,56 @@
import asyncio
import httpx
import logging
from sqlalchemy import text
from app.database import AsyncSessionLocal
logger = logging.getLogger("Robot-1-4-Bike")
logging.basicConfig(level=logging.INFO)
BIKE_MAKES = [
"HONDA", "YAMAHA", "KAWASAKI", "SUZUKI", "HARLEY-DAVIDSON",
"BMW", "DUCATI", "KTM", "TRIUMPH", "APRILIA", "MOTO GUZZI", "INDIAN"
]
class BikeHunter:
API_URL = "https://vpic.nhtsa.dot.gov/api/vehicles/GetModelsForMakeYear/make/{make}/modelyear/{year}/vehicleType/motorcycle?format=json"
@classmethod
async def run(cls):
logger.info("🏍️ Robot 1.4 (Bike Hunter) indítása...")
# 2026-tól 1970-ig pörgetjük a motorokat
years = range(2026, 1969, -1)
async with AsyncSessionLocal() as db:
for year in years:
for make in BIKE_MAKES:
try:
async with httpx.AsyncClient(timeout=20.0) as client:
resp = await client.get(cls.API_URL.format(make=make, year=year))
if resp.status_code != 200: continue
models = resp.json().get("Results", [])
inserted = 0
for m in models:
model_name = m.get("Model_Name").upper().strip()
# TISZTA SQL - Nincs Simon!
query = text("""
INSERT INTO data.catalog_discovery
(make, model, vehicle_class, status, market, model_year, priority_score, source)
VALUES (:make, :model, 'motorcycle', 'pending', 'USA_IMPORT', :year, 8, 'NHTSA-V1-BIKE')
ON CONFLICT ON CONSTRAINT _make_model_market_year_uc DO NOTHING
""")
await db.execute(query, {"make": make, "model": model_name, "year": year})
inserted += 1
if inserted > 0:
logger.info(f"🏍️ {make} ({year}): {inserted} új motor rögzítve.")
await db.commit()
except Exception as e:
logger.error(f"❌ Bike Error {make} ({year}): {e}")
# Évjáratonként egy pici pihenő az API-nak
await asyncio.sleep(0.5)
if __name__ == "__main__":
asyncio.run(BikeHunter.run())

View File

@@ -0,0 +1,66 @@
# /app/app/workers/vehicle/vehicle_robot_1_5_heavy_eu.py
import asyncio
import httpx
import logging
from sqlalchemy import text
from app.database import AsyncSessionLocal
logger = logging.getLogger("Robot-1-5-Heavy-EU")
logging.basicConfig(level=logging.INFO)
class HeavyEUHunter:
# RDW Open Data - Hollandia az EU kapuja
RDW_URL = "https://opendata.rdw.nl/resource/m9d7-ebf2.json"
@classmethod
async def fetch_rdw_heavy(cls, vehicle_type: str):
"""
vehicle_type: 'Vrachtwagen' (Teher), 'Bus', 'Kampeerauto' (Lakóautó)
"""
# Lekérjük az összes egyedi márka-típus párost
query_url = f"{cls.RDW_URL}?voertuigsoort={vehicle_type}&$select=merk,handelsbenaming&$limit=10000"
async with httpx.AsyncClient(timeout=30.0) as client:
try:
resp = await client.get(query_url)
return resp.json() if resp.status_code == 200 else []
except Exception as e:
logger.error(f"❌ RDW Error: {e}")
return []
@classmethod
async def run(cls):
logger.info("🚛 Robot 1.5 (EU Heavy Duty) indítása...")
# Definíciók: RDW név -> Mi kategóriánk
job_list = {
"Vrachtwagen": "truck",
"Bus": "bus",
"Kampeerauto": "rv"
}
async with AsyncSessionLocal() as db:
for rdw_name, internal_class in job_list.items():
logger.info(f"📥 {rdw_name} adatok letöltése...")
data = await cls.fetch_rdw_heavy(rdw_name)
inserted = 0
for item in data:
make = item.get('merk', '').upper().strip()
model = item.get('handelsbenaming', '').upper().strip()
if not make or not model: continue
# Szűrés a kért EU márkákra + amik jönnek az RDW-ből
query = text("""
INSERT INTO data.catalog_discovery
(make, model, vehicle_class, status, market, priority_score, source)
VALUES (:make, :model, :v_class, 'pending', 'EU', 20, 'RDW-HEAVY')
ON CONFLICT ON CONSTRAINT _make_model_market_year_uc DO NOTHING
""")
res = await db.execute(query, {"make": make, "model": model, "v_class": internal_class})
if res.rowcount > 0: inserted += 1
await db.commit()
logger.info(f"{rdw_name}: {inserted} új EU-s nagygép rögzítve.")
if __name__ == "__main__":
asyncio.run(HeavyEUHunter.run())

View File

View File

@@ -159,7 +159,7 @@ class VehicleResearcher:
SET status = 'research_in_progress'
WHERE id = (
SELECT id FROM data.vehicle_model_definitions
WHERE status IN ('unverified', 'awaiting_research')
WHERE status IN ('unverified', 'awaiting_research', 'ACTIVE')
AND attempts < :max_attempts
ORDER BY
CASE WHEN make = 'TOYOTA' THEN 1 ELSE 2 END,

View File

View File

@@ -0,0 +1,28 @@
"""mdm_market_and_year_expansion
Revision ID: 0472f45a7d62
Revises: ddaaee0dc5d2
Create Date: 2026-03-09 12:05:43.937729
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '0472f45a7d62'
down_revision: Union[str, Sequence[str], None] = 'ddaaee0dc5d2'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@@ -0,0 +1,28 @@
"""Add reference lookup table
Revision ID: 365190cf24e5
Revises: 62c259b715b0
Create Date: 2026-03-09 17:17:36.726879
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '365190cf24e5'
down_revision: Union[str, Sequence[str], None] = '62c259b715b0'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@@ -0,0 +1,28 @@
"""Add reference lookup table
Revision ID: 5a8ffc9bf401
Revises: 365190cf24e5
Create Date: 2026-03-09 17:23:19.533190
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '5a8ffc9bf401'
down_revision: Union[str, Sequence[str], None] = '365190cf24e5'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@@ -0,0 +1,28 @@
"""mdm_market_and_year_expansion
Revision ID: 62c259b715b0
Revises: 0472f45a7d62
Create Date: 2026-03-09 12:34:12.318095
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '62c259b715b0'
down_revision: Union[str, Sequence[str], None] = '0472f45a7d62'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@@ -0,0 +1,28 @@
"""Sync reference lookup table
Revision ID: 98814bd15f99
Revises: 5a8ffc9bf401
Create Date: 2026-03-09 17:27:43.099664
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '98814bd15f99'
down_revision: Union[str, Sequence[str], None] = '5a8ffc9bf401'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@@ -36,3 +36,6 @@ apscheduler
pytest
pytest-asyncio
psycopg2-binary
rich
nvidia-ml-py
psutil

View File

@@ -32,7 +32,15 @@ services:
- sf_net
- shared_db_net
restart: unless-stopped
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
# --- SZERVIZ HADOSZTÁLY ---
service_scout:
build: ./backend
@@ -61,6 +69,7 @@ services:
restart: unless-stopped
# profiles: ["disabled"] # Ezzel a sorral letilthatod, hogy automatikusan elinduljon!
service_researcher:
build: ./backend
command: python -u -m app.workers.service.service_robot_2_researcher
@@ -128,6 +137,32 @@ services:
- shared_db_net
restart: unless-stopped
sf_nhtsa_hunter:
build: ./backend
container_name: sf_nhtsa_hunter
command: python -m app.workers.vehicle.vehicle_robot_1_2_nhtsa_fetcher
env_file: .env
restart: unless-stopped
depends_on:
migrate:
condition: service_completed_successfully
networks:
- sf_net
- shared_db_net
sf_bike_hunter:
build: ./backend
container_name: sf_bike_hunter
command: python -m app.workers.vehicle.vehicle_robot_1_4_bike_hunter
env_file: .env
restart: unless-stopped
depends_on:
migrate:
condition: service_completed_successfully
networks:
- sf_net
- shared_db_net
vehicle_researcher:
build: ./backend
# container_name: sf_vehicle_researcher
@@ -143,6 +178,19 @@ services:
- shared_db_net
restart: unless-stopped
sf_heavy_eu:
build: ./backend
container_name: sf_heavy_eu
command: python -m app.workers.vehicle.vehicle_robot_1_5_heavy_eu
env_file: .env
restart: unless-stopped
depends_on:
migrate:
condition: service_completed_successfully
networks:
- sf_net
- shared_db_net
vehicle_alchemist:
build: ./backend
container_name: sf_vehicle_alchemist