refakotorálás előtti állapot
This commit is contained in:
92
.roo/history.md
Normal file
92
.roo/history.md
Normal 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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)."
|
||||
@@ -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":
|
||||
|
||||
@@ -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)}")
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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())
|
||||
20
backend/app/models/reference_data.py
Normal file
20
backend/app/models/reference_data.py
Normal 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())
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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...")
|
||||
|
||||
194
backend/app/workers/monitor_dashboard.py
Normal file
194
backend/app/workers/monitor_dashboard.py
Normal 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())
|
||||
52
backend/app/workers/py_to_database.py
Normal file
52
backend/app/workers/py_to_database.py
Normal 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())
|
||||
140
backend/app/workers/system/subscription_worker.py
Normal file
140
backend/app/workers/system/subscription_worker.py
Normal 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())
|
||||
24
backend/app/workers/vehicle/mapping_dictionary.py
Normal file
24
backend/app/workers/vehicle/mapping_dictionary.py
Normal 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)
|
||||
26
backend/app/workers/vehicle/mapping_rules.py
Normal file
26
backend/app/workers/vehicle/mapping_rules.py
Normal 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
|
||||
128
backend/app/workers/vehicle/robot_report.py
Normal file
128
backend/app/workers/vehicle/robot_report.py
Normal 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())
|
||||
121
backend/app/workers/vehicle/vehicle_data_loader.py
Normal file
121
backend/app/workers/vehicle/vehicle_data_loader.py
Normal 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())
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
56
backend/app/workers/vehicle/vehicle_robot_1_4_bike_hunter.py
Normal file
56
backend/app/workers/vehicle/vehicle_robot_1_4_bike_hunter.py
Normal 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())
|
||||
66
backend/app/workers/vehicle/vehicle_robot_1_5_heavy_eu.py
Normal file
66
backend/app/workers/vehicle/vehicle_robot_1_5_heavy_eu.py
Normal 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())
|
||||
0
backend/app/workers/vehicle/vehicle_robot_1_catalog_hunter.py
Executable file → Normal file
0
backend/app/workers/vehicle/vehicle_robot_1_catalog_hunter.py
Executable file → Normal file
2
backend/app/workers/vehicle/vehicle_robot_2_researcher.py
Executable file → Normal file
2
backend/app/workers/vehicle/vehicle_robot_2_researcher.py
Executable file → Normal 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,
|
||||
|
||||
0
backend/app/workers/vehicle/vehicle_robot_4_vin_auditor.py
Executable file → Normal file
0
backend/app/workers/vehicle/vehicle_robot_4_vin_auditor.py
Executable file → Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -36,3 +36,6 @@ apscheduler
|
||||
pytest
|
||||
pytest-asyncio
|
||||
psycopg2-binary
|
||||
rich
|
||||
nvidia-ml-py
|
||||
psutil
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user