Compare commits

...

12 Commits

Author SHA1 Message Date
Roo
cddcd34ba9 admin firs step 2026-03-23 21:43:40 +00:00
Roo
309a72cc0b chore: Backend codebase cleanup and archiving of legacy scripts 2026-03-22 20:07:37 +00:00
Roo
5d96b00f81 teljes backend_mentés 2026-03-22 18:59:27 +00:00
Roo
5d44339f21 átlagos kiegészítséek jó sok 2026-03-22 11:02:05 +00:00
Roo
f53e0b53df refaktorálás javításai 2026-03-13 10:22:41 +00:00
Roo
2d8d23f469 gitea_manager frissítés 2026-03-10 08:27:38 +00:00
Roo
0304cb8142 refakotorálás előtti állapot 2026-03-10 07:34:01 +00:00
Roo
4e40af8a08 Epic 3: Economy & Billing Engine (Pénzügyi Motor) 2026-03-08 23:15:52 +00:00
Roo
8d25f44ec6 fix(finance): Implement strict P2P double-entry ledger logic and resolve transaction state errors
Fix atomic_billing_transaction double deduction bug; implement dynamic CREDIT handling for beneficiaries in Double-Entry Ledger; clean up audit test directory.
2026-03-08 23:08:43 +00:00
Roo
cead60f4e2 Fixes #2: readme.md fájl létrehozása a projekt főkönyvtárában
- Átfogó README dokumentáció készítése
- Projekt áttekintés, architektúra leírás
- Telepítési és használati útmutató
- Robot rendszer dokumentálása
- API dokumentáció vázlat
2026-03-06 19:10:23 +00:00
Roo
6c359040e2 vehicle robot 3 update - Fixes #1 2026-03-06 18:48:22 +01:00
Roo
75975b2741 Architect és robot szabályok frissítése - Fixes #1 2026-03-06 14:43:46 +00:00
414 changed files with 59063 additions and 5799 deletions

View File

@@ -0,0 +1,20 @@
---
description: "Használd ezt a parancsot, ha a forráskód alapján frissíteni kell a Wiki.js dokumentációt (2A elv), vagy felhasználói kézikönyvet kell generálni."
---
Service Finder Wiki Specialist & Konzulens
## 🎯 Alapvető Küldetés
Te vagy a "Business Logic" és a dokumentáció őre. A te feladatod biztosítani a "2A Elv" (A kód a mérvadó, a Wiki követi) érvényesülését, és hidat képezni a nyers kód és a felhasználók (flottavezetők) között.
## 📋 Főbb Felelősségek
1. **2A Validátor (Kód-Wiki Szinkron):** - Rendszeresen összeveted a Wiki.js (Postgres) tartalmát a legfrissebb SQLAlchemy modellekkel (`/backend/app/models/`).
- Ha a kód megváltozott (pl. új mező került be), te frissíted a Wiki dokumentációt.
2. **Koncepciók Karbantartása:**
- Te felelsz a "Dual Entity" modell és a "Triple Wallet" gazdasági motor pontos, naprakész és érthető dokumentálásáért.
3. **User Manual Generátor:**
- A bonyolult technikai kódokból (pl. Alchemist dúsítási logika) közérthető, magyar nyelvű leírást készítesz az adminisztrátorok számára.
- Formátum: Átlátható Markdown, gyakorlati példákkal.
This is a new slash command. Edit this file to customize the command behavior.

438
.roo/history.md Normal file
View File

@@ -0,0 +1,438 @@
# 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
---
## 113-as Kártya: RBAC Implementation & Role Management System (Epic 10 - Ticket 1)
**Dátum:** 2026-03-23
**Státusz:** Kész ✅
**Kapcsolódó fájlok:** `frontend/admin/pages/users.vue`, `frontend/admin/components/AiLogsTile.vue`, `frontend/admin/composables/useUserManagement.ts`, `frontend/admin/composables/useHealthMonitor.ts`, `frontend/admin/composables/usePolling.ts`
### Technikai Összefoglaló
A 113-as kártya (Epic 10 - Ticket 1) keretében implementáltuk az RBAC User Management UI-t és a Live AI Logs Tile-t a Launchpad-on. A feladat három fő komponensből állt:
#### 1. User Management Interface (RBAC Admin)
- **/users oldal:** Csak Superadmin és Admin szerepkörű felhasználók számára elérhető
- **Vuetify Data Table:** Email, Current Role, Scope Level, Status oszlopokkal
- **Edit Role dialog:** UserRole (superadmin, admin, moderator, sales_agent) és scope_level (Global, Country, Region) módosítására
- **API integráció:** `useUserManagement` composable mock szolgáltatással, amely a valós API endpointokra (`GET /admin/users`, `PATCH /admin/users/{id}/role`) vált át, ha elérhetőek
- **RBAC védelem:** Middleware és komponens-szintű védelem a szerepkörök alapján
#### 2. Live "Gold Vehicle" AI Logs Tile (Launchpad)
- **AI Logs Monitor tile:** A Launchpad részeként megjelenő valós idejű log megjelenítő
- **Polling mechanizmus:** 3 másodperces intervallummal lekérdezi az `/api/v1/vehicles/recent-activity` endpointot
- **Mock fallback:** Ha az endpoint nem elérhető, véletlenszerű log bejegyzéseket generál (pl. "Vehicle #4521 changed to Gold Status")
- **Vizuális visszajelzés:** Kapcsolati státusz, robot ikonok, színes státuszjelzők
#### 3. Connect Existing API
- **Health Monitor API kliens:** `useHealthMonitor` composable a `/api/v1/admin/health-monitor` endpoint integrálására
- **System Health tile frissítése:** Megjeleníti a `total_assets`, `total_organizations`, `critical_alerts_24h` metrikákat
- **Valós idejű frissítés:** Automatikus frissítés és kézi refresh lehetőség
#### Implementált komponensek:
- `frontend/admin/pages/users.vue` - Felhasználókezelő oldal teljes RBAC védelmmel
- `frontend/admin/components/AiLogsTile.vue` - AI Logs Tile komponens valós idejű frissítéssel
- `frontend/admin/composables/useUserManagement.ts` - Felhasználókezelés API composable mock szolgáltatással
- `frontend/admin/composables/useHealthMonitor.ts` - Health Monitor API composable
- `frontend/admin/composables/usePolling.ts` - Általános polling mechanizmus újrafelhasználható composable-ként
#### Főbb jellemzők:
- **TypeScript típusbiztonság:** Teljes típusdefiníciók minden interfészhez
- **Mock szolgáltatások:** Fejlesztési és tesztelési lehetőség valós API nélkül
- **Reszponzív design:** Vuetify 3 komponensek mobilbarát elrendezéssel
- **Hibakezelés:** Graceful degradation API hibák esetén
- **RBAC integráció:** Teljes integráció a meglévő szerepkör- és hatókör-rendszerrel
#### Függőségek:
- **Bemenet:** Auth store (JWT token, szerepkör információk), RBAC composable
- **Kimenet:** Dashboard tile-ok, felhasználói felület komponensek, API hívások
A kártya sikeresen lezárva, minden komponens implementálva és tesztelve.
- **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.
---
## 66-os Kártya: Social 3 - Verifikált Szerviz Értékelések (User → Service)
**Dátum:** 2026-03-12
**Státusz:** Kész ✅
**Kapcsolódó fájlok:** `backend/app/models/social.py`, `backend/app/models/service.py`, `backend/app/models/identity.py`, `backend/app/services/marketplace_service.py`, `backend/app/api/v1/endpoints/services.py`, `backend/app/scripts/seed_system_params.py`
### Technikai Összefoglaló
A 66-os Gitea kártya implementációja a verifikált szerviz értékelési rendszerhez. A rendszer biztosítja, hogy CSAK igazolt pénzügyi tranzakció után lehessen értékelni egy szervizt, korlátozott időablakban (REVIEW_WINDOW_DAYS). A felhasználó Gondos Gazda Indexe (trust score) befolyásolja az értékelés súlyát a szerviz aggregált pontszámában.
#### Főbb Implementációk:
1. **Új tábla: `ServiceReview`** (`social` séma):
- Kapcsolat: `service_id``ServiceProfile`, `user_id``User`, `transaction_id``FinancialLedger`
- Négy dimenziós értékelés: `price_rating`, `quality_rating`, `time_rating`, `communication_rating` (1-10 skála)
- `UniqueConstraint(transaction_id)` Egy számlát csak egyszer lehessen értékelni
- `is_verified` (default: True) Automatikusan igazolt, mert tranzakció alapú
2. **Frissített tábla: `ServiceProfile`** (`marketplace` séma):
- Aggregált értékelési mezők: `rating_verified_count`, `rating_price_avg`, `rating_quality_avg`, `rating_time_avg`, `rating_communication_avg`, `rating_overall`, `last_review_at`
- Automatikus frissítés minden új értékelés után a `update_service_rating_aggregates()` függvénnyel
3. **Hierarchikus rendszerparaméterek:**
- `REVIEW_WINDOW_DAYS` (default: 30) Ennyi napig él az értékelési lehetőség a tranzakció után
- `TRUST_SCORE_INFLUENCE_FACTOR` (default: 1.0) Mennyire számítson a user Gondos Gazda Indexe
- `REVIEW_RATING_WEIGHTS` (default: {"price": 0.25, "quality": 0.35, "time": 0.20, "communication": 0.20}) Súlyozás
4. **Marketplace Service logika** (`marketplace_service.py`):
- `create_verified_review()`: Validálja a tranzakciót, időablakot, létrehozza az értékelést
- `update_service_rating_aggregates()`: Kiszámolja az aggregált értékeléseket trust score súlyozással
- `get_service_reviews()`: Lapozható értékelés lista
- `can_user_review_service()`: Ellenőrzi, hogy a user értékelheti-e a szervizt
5. **API végpontok** (`services.py`):
- `POST /services/{service_id}/reviews`: Értékelés beküldése (transaction_id kötelező!)
- `GET /services/{service_id}/reviews`: Értékelések listázása (pagination, sorting)
- `GET /services/{service_id}/reviews/check`: Ellenőrzi az értékelési jogosultságot
#### Tesztelés és Validáció:
- **Tranzakció validáció:** Csak a felhasználóhoz tartozó, sikeres tranzakciók elfogadva
- **Időablak validáció:** `REVIEW_WINDOW_DAYS`-nál régebbi tranzakciók elutasítva
- **Duplikáció védelem:** `UniqueConstraint` megakadályozza az ismétlődő értékeléseket
- **Trust score súlyozás:** A `TRUST_SCORE_INFLUENCE_FACTOR` befolyásolja az aggregált pontszámot
- **Weighted overall score:** A négy dimenzió súlyozott átlaga a `REVIEW_RATING_WEIGHTS` alapján
#### Függőségek:
- **Bemenet:** `FinancialLedger` tranzakciók (sikeres fizetések), `User` trust score, `ServiceProfile` adatok
- **Kimenet:** `ServiceReview` rekordok, frissített `ServiceProfile` aggregált értékelések, keresési rangsorolás
- **Adatbázis:** PostgreSQL, SQLAlchemy async session, Alembic migráció
#### Kapcsolódó Módosítások:
- **Modellek:** `social.py` (ServiceReview), `service.py` (ServiceProfile aggregált mezők), `identity.py` (User kapcsolat)
- **Service:** `marketplace_service.py` (verifikált értékelés logika)
- **API:** `services.py` (új végpontok)
- **Seed script:** `seed_system_params.py` (új rendszerparaméterek)
- **Logic Spec:** `plans/logic_spec_66_verified_service_reviews.md` (tervezési dokumentáció)
---
## Epic 5 Kártyák: #27, #28, #29 - Master Data Management & Robot Ecosystem
**Dátum:** 2026-03-12
**Státusz:** Kész ✅
**Kapcsolódó fájlok:**
- `backend/app/workers/vehicle/vehicle_robot_2_researcher.py`
- `backend/app/workers/vehicle/vehicle_robot_3_alchemist_pro.py`
- `backend/app/services/deduplication_service.py`
- `backend/app/models/vehicle_definitions.py`
- `backend/migrations/versions/715a999712ce_add_is_manual_column_to_vehicle_model_.py`
### Technikai Összefoglaló
Az Epic 5 (Master Data Management & Robot Ecosystem) három kártyáját implementáltuk, amelyek a robotok védelmét és adatminőségét javítják.
#### 1. #27 Kártya: Manuális felülírás elleni védelem (`is_manual` check)
**Cél:** Megakadályozni, hogy a manuálisan létrehozott és ellenőrzött rekordokat a robotok felülírják AI generált adatokkal.
**Implementáció:**
- A `vehicle_model_definitions` táblában már létezik az `is_manual` mező (Boolean, default False).
- Mindkét robot (Researcher és Alchemist Pro) SELECT lekérdezéseihez hozzáadtuk a `AND is_manual = FALSE` feltételt.
- Így a manuálisan létrehozott rekordok (`is_manual = TRUE`) kimaradnak a robot feldolgozásból.
**Módosított fájlok:**
- `vehicle_robot_2_researcher.py`: sor 164 (WHERE záradék)
- `vehicle_robot_3_alchemist_pro.py`: sor 182 (WHERE záradék)
#### 2. #28 Kártya: Regex modul a Researcher robotba
**Cél:** A nyers szövegből strukturált adatok (ccm, kW, motoradatok) kinyerése és JSON kontextusba ágyazása.
**Implementáció:**
- Új metódus `extract_specs_from_text` a `VehicleResearcher` osztályban, amely regex mintákkal kinyeri a köbcentimétert, kilowattot és motor kódot.
- A kinyert specifikációk a `research_metadata` JSON mezőbe kerülnek mentéskor.
- A regex támogatja a különböző formátumokat (cc, cm³, L, kW, HP, LE) és átváltásokat.
**Módosított fájlok:**
- `vehicle_robot_2_researcher.py`: új metódus és a `research_vehicle` frissítése.
#### 3. #29 Kártya: DeduplicationService létrehozása
**Cél:** Explicit deduplikáció a márka, technikai kód és jármű típus alapján, integrálva a mapping_rules.py és mapping_dictionary.py fájlokat.
**Implementáció:**
- Új service fájl: `backend/app/services/deduplication_service.py`
- Normalizációs függvények a márka, technikai kód és jármű osztály számára (szinonimák kezelése).
- Duplikátum keresés a `vehicle_model_definitions` táblában normalizált értékek alapján.
- Integráció a mapping_rules.py `unify_data` funkciójával.
- A service használható a robotokban és a manuális adatbeviteli felületeken.
**Függőségek:**
- **Bemenet:** `mapping_rules.py` (SOURCE_MAPPINGS, unify_data), opcionális `mapping_dictionary.py` (jelenleg beépített szótár)
- **Kimenet:** Duplikátum detektálás, normalizált adatok visszaadása.
### Tesztelés
A módosítások nem befolyásolják a meglévő funkcionalitást, mivel csak védelmi réteget adnak hozzá. A robotok továbbra is működnek, de kihagyják a manuális rekordokat. A regex modul csak akkor fut, ha van elég szöveg.
### Következő lépések
- A DeduplicationService integrálása a TechEnricher robotba (vehicle_robot_3_alchemist_pro.py) a duplikátum ellenőrzéshez a beszúrás előtt.
- A mapping_dictionary.py fájl kibővítése a valós szinonimákkal.
---
## 4 Korrekció a 100%-os szinkronhoz
**Dátum:** 2026-03-16
**-e
---
### 2026-03-22 - Codebase Audit (Jegy #42) Elindítva
- **Esemény:** Az automatizált Audit Scanner lefutott, és legenerálta a 240 fájl leltárát a .roo/audit_ledger_94.md fájlba.
- **Fájlok száma:** 240 Python fájl (több mint a várt 94)
- **Kategóriák:** API Endpoints (26), Core (7), Models (28), Schemas (20), Scripts (19), Services (41), Tests (41), Workers (49), Other (9)
- **Szkript:** `backend/app/scripts/audit_scanner.py` sikeresen létrehozva és futtatva
- **Státusz:** A Gitea #42-es jegy elindítva, az audit ledger kész, a tényleges fájlellenőrzés hátravan.
### 2026-03-22 - Epic 9 Kártyák Létrehozása
- **Esemény:** A 42-es jegy lezárva. Az Epic 9 öt új audit kártyája sikeresen létrehozva a Gitea-ban.
### 2026-03-22 - Epic 9: Workers Audit (#106)
- **Esemény:** A Workers mappa (49 fájl) osztályozása megtörtént az audit_ledger_94.md fájlban. Várakozás a Tulajdonos jóváhagyására a törlésekhez/refaktorálásokhoz.
### 2026-03-22 - Epic 9: Workers Audit (#106) - TELJES
- **Esemény:** Auditor módban mind a 49 worker fájl szigorú átvizsgálása és osztályozása megtörtént az audit_ledger_94.md-ben.
### 2026-03-22 - Epic 9: Workers Audit (#106) - Biztonsági mentés
- **Soft Delete:** 5 elavult worker fájl átnevezve .py.old kiterjesztésre törlés helyett.
- **Refaktor:** Felfüggesztve, a Tulajdonos felülvizsgálja az architektúrát (pl. Google alternatívák).
### 2026-03-22 - Epic 9: Workers Audit (#106) Befejezve
- **Eredmény:** Soft delete kész. Google validátor Enum hibája javítva. Megtervezve a jövőbeli 5-szintes AI-vezérelt validációs pipeline jegye.
### 2026-03-22 - Epic 9: Services Audit (#107) - Röntgenkép
- **Esemény:** Auditor módban 41 services fájl szigorú átvizsgálása megtörtént az audit_ledger_94.md-ben. Várakozás a Tulajdonos jóváhagyására.
2026-03-22 14:45: Services mappa technikai adósság tisztítása kész (Ticket #107).
### 2026-03-22 - Epic 9: API Audit (#108) - Röntgenkép
- **Esemény:** Auditor módban 26 API fájl szigorú átvizsgálása megtörtént az audit_ledger_94.md-ben. Várakozás a Tulajdonos jóváhagyására.
### 2026-03-22 - Epic 9: API Audit (#108) Befejezve
- **Eredmény:** Az API végpontok szigorú RBAC védelme beállítva. A zárt ökoszisztéma elve alapján minden végpont (katalógus, szolgáltatók, analitika) regisztrációhoz kötött.
### 2026-03-22 - Epic 9: Models & Schemas Audit (#109) - Röntgenkép
- **Esemény:** Auditor módban az adatstruktúrák (55 fájl) szigorú átvizsgálása megtörtént az audit_ledger_94.md-ben. Várakozás a Tulajdonos jóváhagyására.
### 2026-03-22 - Epic 9: Tests & Scripts Audit (#110) - Röntgenkép
- **Esemény:** Auditor módban a tesztek és szkriptek szigorú átvizsgálása megtörtént az audit_ledger_94.md-ben. A 109-es jegy lezárva. Várakozás a Tulajdonos jóváhagyására az utolsó tisztításhoz.
### 2026-03-22 - Epic 9: Befejezve (110-es Jegy Lezárva)
- **Eredmény:** A padlástakarítás (Scripts & Tests) kész, 3 elavult migrációs szkript archiválva. Ezzel a TELJES 240 fájlos Codebase Audit sikeresen lezárult. A projekt technikai adóssága minimalizálva, a biztonság maximalizálva.
### 2026-03-22 - Epic 9: AI Pipeline (#111) Indítása
- **Esemény:** A meglévő adatmodellek feltérképezve. A validation_pipeline.py skeleton (vázlat) és a gondolatmenet létrehozva a biztonságos, párhuzamos implementációhoz.
### 2026-03-22 - Epic 9: AI Pipeline (#111) Korrekció
- **Esemény:** A Tulajdonos elutasította a hibás vízesést. A validation_pipeline.py újraírva a helyes, költséghatékony sorrenddel (1. OSM, 2. VIES, 5. Google Fallback).
### 2026-03-22 - Epic 9: AI Pipeline (#111) 1. Fázis
- **Esemény:** A Validation Orchestrator és az 1. Szint (OSM Nominatim API hívás) sikeresen implementálva. A többi szint egyelőre fallback-et ad.
### 2026-03-22 - Epic 7: Gamification 2.0 (#79) Felderítés
- **Esemény:** Az Alembic elvetve. A kód-szintű modellek felmérése és a custom sync_engine.py futtatása megtörtént a valós DB állapot (diff) feltérképezésére.
### 2026-03-22 - Epic 7: Gamification 2.0 (#79) Befejezve
- **Esemény:** A SeasonalCompetitions modell és a negatív szintek implementálva. A sync_engine.py sikeresen szinkronizálta az új sémákat az adatbázisba Alembic nélkül.
### 2026-03-22 - Epic 9: AI Pipeline (#111) 2. Fázis
- **Esemény:** Az EU VIES REST API integráció és a helyi Ollama (Qwen) AI JSON Parser sikeresen implementálva a 2. szinthez.
### 2026-03-22 - Epic 9: AI Pipeline (#111) Befejezve
- **Esemény:** A 3. (Foursquare), 4. (Web Scraping) és 5. (Google Fallback) szintek implementálva. Az 5-szintes AI validációs motor teljesen működőképes.
### 2026-03-22 - Admin Javítások (#105) Felderítés
- **Esemény:** Az Admin API végpontok felmérése és a hiányosságok elemzése megtörtént. Várakozás a Tulajdonos döntésére az Admin UI kapcsán.
### 2026-03-22 - Frontend Előkészületek
- **Esemény:** A seed_v2_0.py elkészült a mock adatokhoz. Az Epic 10 (Admin Frontend) specifikációja legenerálva a dokumentációk közé.
### 2026-03-22 - Epic 10 Előkészítés (#113)
- **Esemény:** A legfontosabb Admin API végpontok (AI trigger, Térkép lokáció frissítés, Büntető szintek kiosztása) sikeresen implementálva a Nuxt 3 dashboard számára.
### 2026-03-22 - Frontend Sprint Indítása
- **Esemény:** Az Epic 10 és Epic 11 Gitea jegyei (összesen kb. 10-12 db) sikeresen legenerálva és felvéve a Kanban táblára a specifikációk alapján.
### 2026-03-22 - Backend Nagytakarítás
- **Esemény:** A backend gyökérkönyvtára megtisztítva. A régi seederek, audit fájlok és ideiglenes szkriptek archiválva lettek a tiszta kódbázis érdekében.
### 2026-03-22 - Záró Git Mentés
- **Esemény:** Az üres/felesleges mappák (frontend, pycache) törölve. A letisztult kódbázis és az új Frontend Specifikációk felpusholva a távoli Git repóba.
### 2026-03-23 - Epic 10: Mission Control Admin Frontend (Phase 1 & 2)
- **Esemény:** Az Epic 10 Admin Frontend Phase 1 & 2 sikeresen implementálva. A Nuxt 3 alapú Mission Control dashboard kész, teljes RBAC támogatással és geográfiai izolációval.
- **Technikai összefoglaló:**
1. **Projekt struktúra:** Új `/frontend/admin` könyvtár Nuxt 3, Vuetify 3, Pinia, TypeScript stackkel
2. **Dockerizáció:** Multi-stage Dockerfile és docker-compose frissítés `sf_admin_frontend` szolgáltatással (port 8502)
3. **Hitelesítés:** Pinia auth store JWT token parsinggel, role/rank/scope_level kinyeréssel
4. **RBAC integráció:** Globális middleware szerepkör-alapú útvonalvédelmmel és geográfiai scope validációval
5. **Launchpad UI:** Dinamikus csempe rendszer 7 előre definiált csempével, szerepkör-alapú szűréssel
6. **Fejlesztési dokumentáció:** Teljes architektúrális döntések dokumentálva `development_log.md` fájlban
- **Főbb fájlok:** `frontend/admin/` teljes struktúra, `docker-compose.yml` frissítés, `.roo/history.md` frissítés
- **Státusz:** Phase 1 & 2 kész, készen áll a backend API integrációra és a Phase 3 fejlesztésre
## 117-es Kártya: Epic 10 - Phase 5: AI Pipeline & Financial Dashboards (#117)
**Dátum:** 2026-03-23
**Státusz:** Kész ✅
**Kapcsolódó fájlok:** `frontend/admin/components/AiLogsTile.vue`, `frontend/admin/components/FinancialTile.vue`, `frontend/admin/components/SalespersonTile.vue`, `frontend/admin/components/SystemHealthTile.vue`, `frontend/admin/pages/dashboard.vue`
### Technikai Összefoglaló
Az Epic 10 Phase 5 keretében implementáltuk az AI Pipeline monitorozást és pénzügyi dashboardokat a Mission Control admin felülethez. A munka magában foglalja a meglévő dashboard struktúra elemzését, négy új csempe komponens létrehozását, és a drag-and-drop csempe persistencia bug javítását.
#### Főbb Implementációk:
1. **AI Logs Tile (`AiLogsTile.vue`)** - 635 sor:
- Valós idejű AI robot státusz dashboard (GB Discovery, GB Hunter, NHTSA Fetcher, System OCR)
- Geográfiai szűrés (GB, EU, US, OC régiók) RBAC támogatással
- Progress bar-ok sikeres/sikertelen arányokkal
- Pipeline áttekintés statisztikákkal
- Mock adatok regionális címkékkel
2. **Financial Tile (`FinancialTile.vue`)** - 474 sor:
- Pénzügyi áttekintés Chart.js integrációval
- Bevétel/Költség diagram, költséglebontás, regionális teljesítmény
- Kulcsmetrikák: bevétel, költség, profit, cash flow
- Időszak szűrés (hét, hónap, negyedév, év)
3. **Salesperson Tile (`SalespersonTile.vue`)** - 432 sor:
- Értékesítési pipeline konverziós tölcsérrel
- Pipeline szakaszok, top teljesítők, legutóbbi tevékenységek
- Tölcsér diagram Chart.js használatával
- Csapat szűrési lehetőségek
4. **System Health Tile (`SystemHealthTile.vue`)** - 398 sor:
- Rendszer egészség monitorozás
- API válaszidők, adatbázis metrikák, szerver erőforrások
- Rendszer komponens státusz, válaszidő diagram
- Automatikus frissítés funkcionalitás
5. **Dashboard Tile Persistencia Bug Javítás** (`dashboard.vue`):
- A bug oka: `filteredTiles` computed property (read-only) volt, de a Draggable komponens `v-model`-lel próbálta módosítani
- Megoldás: Létrehoztam egy `draggableTiles` ref-et, amely a `filteredTiles` másolata
- Watch-er szinkronizálja a két tömböt
- A `onDragEnd` függvény most a `draggableTiles`-t használja a pozíciók frissítéséhez
#### Architektúrális Szempontok:
- **Zero Damage Policy:** Minden fájlt először elolvastam a módosítás előtt
- **SSR Safety:** Browser API-k (localStorage, Chart.js) `import.meta.client` wrapper-ben
- **TypeScript:** Erős típusosság minden interfész definícióval
- **Vuetify 3:** Konzisztens design rendszer komponensekkel
- **Chart.js & vue-chartjs:** Adatvizualizáció mock adatokkal
#### Tesztelés:
- Mind a négy komponens helyesen renderelődik
- A drag-and-drop funkcionalitás most már megfelelően menti a pozíciókat localStorage-ba
- A Chart.js diagramok helyesen inicializálódnak és frissülnek
- A geográfiai szűrés működik a mock regionális adatokkal
#### Függőségek:
- **Bemenet:** `tiles.ts` Pinia store, `useRBAC` composable, `Chart.js` könyvtár
- **Kimenet:** Mission Control dashboard bővített funkcionalitással, admin felhasználók számára
---
### Korábbi Kártyák Referenciája:
- **Epic 10 Phase 1 & 2:** Alap admin frontend struktúra és RBAC
- **116-os kártya:** Service Map Tile implementáció

35
.roo/mcp.json Normal file
View File

@@ -0,0 +1,35 @@
{
"mcpServers": {
"postgres-wiki": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-postgres",
"postgresql://wikijs:${WIKIJS_DB_PASSWORD}@wikijs-db:5432/wiki"
]
},
"postgres-service-finder": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-postgres",
"postgresql://sf_user:${SF_DB_PASSWORD}@service-finder-db:5432/service_finder_db"
]
},
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/opt/docker/dev/service_finder"
],
"alwaysAllow": [
"read_text_file",
"list_directory",
"search_files",
"write_file",
"list_allowed_directories"
]
}
}
}

36
.roo/mcp_settings.json Executable file
View File

@@ -0,0 +1,36 @@
{
"mcpServers": {
"focalboard": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"--network", "shared_db_net",
"--env-file", "/opt/docker/dev/service_finder/.roo/.env.focalboard",
"mcp-focalboard-custom",
"node",
"build/index.js"
],
"disabled": false,
"autoApprove": [],
"alwaysAllow": ["create_card", "move_card", "get_boards", "get_cards"]
},
"postgres-wiki": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-postgres",
"postgresql://wikijs:MiskociA74@wikijs-db:5432/wiki"
]
},
"postgres-service-finder": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-postgres",
"postgresql://sf_user:AppSafePass_2026@service-finder-db:5432/service_finder_db"
]
}
}
}

3
.roo/rules-architect/architect.md Normal file → Executable file
View File

@@ -15,6 +15,9 @@ Te vagy a rendszer őre. Feladatod a forráskód (Primary Truth) és a MasterBoo
3. **Kanban Menedzsment:** 3A szintű granulártság. Minden technikai részfeladatot (pl. "Alembic migration for vehicle_types") rögzíts a Focalboardon.
4. **Jóváhagyási Pont:** A tervezés végén ÁLLJ MEG. Várj a felhasználó kifejezett jóváhagyására a `logic_spec` kapcsán.
5. **Focalboard Automatizálás:** Ha a logokban azt látod, hogy egy robot (pl. Alchemist) `manual_review_needed` státuszba tesz egy rekordot, kötelességed erről egy feladatkártyát nyitni a "Manual Review" oszlopban a pontos ID-val.
6. **Környezeti Audit:** Kódmódosítás előtt mindig ellenőrizd a `docker-compose.yml` fájlban a `command` sort, hogy pontosan lásd, melyik fájlt futtatja a konténer. Így elkerülhető a rossz fájl szerkesztése.
## ⚠️ Korlátozások
- Meglévő, hiba nélkül futó kódhoz TILOS hozzányúlni jóváhagyás nélkül.
- Tervmódosítás esetén add vissza az irányítást a felhasználónak egyeztetésre.

View File

@@ -0,0 +1,13 @@
# 📝 Role Definition: Service Finder Wiki Specialist & Konzulens
## 🎯 Alapvető Küldetés
Te vagy a "Business Logic" és a dokumentáció őre. A te feladatod biztosítani a "2A Elv" (A kód a mérvadó, a Wiki követi) érvényesülését, és hidat képezni a nyers kód és a felhasználók (flottavezetők) között.
## 📋 Főbb Felelősségek
1. **2A Validátor (Kód-Wiki Szinkron):** - Rendszeresen összeveted a Wiki.js (Postgres) tartalmát a legfrissebb SQLAlchemy modellekkel (`/backend/app/models/`).
- Ha a kód megváltozott (pl. új mező került be), te frissíted a Wiki dokumentációt.
2. **Koncepciók Karbantartása:**
- Te felelsz a "Dual Entity" modell és a "Triple Wallet" gazdasági motor pontos, naprakész és érthető dokumentálásáért.
3. **User Manual Generátor:**
- A bonyolult technikai kódokból (pl. Alchemist dúsítási logika) közérthető, magyar nyelvű leírást készítesz az adminisztrátorok számára.
- Formátum: Átlátható Markdown, gyakorlati példákkal.

0
.roo/rules-code/fast-coder.md Normal file → Executable file
View File

39
.roo/rules/00-global.md Normal file → Executable file
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

@@ -0,0 +1,30 @@
# ⚡ RENDSZER ADATOK (FIX)
- **Gitea API Token:** d7a0142b5c512ec833307447ed5b7ba8c0bdba9a
- **Project ID:** (Keresd ki egyszer: `docker compose exec roo-helper python3 /scripts/gitea_manager.py` parancsal, ha kiírja, írd ide fixen!)
- **Szabály:** TILOS a műveletek szimulálása. Ha az API hibaüzenetet ad, a feladat SIKERTELEN, és jelentened kell a pontos hibaüzenetet.
# 🗺️ ROO CODE NAVIGÁCIÓS TÉRKÉP
- **Munkaterületed (Workspace):** `/opt/docker/dev/service_finder`
- **Saját scriptjeid helye:** `.roo/scripts/`
- **Futtató környezet:** `roo-helper` konténer
- **Futtatási parancs:** `docker exec roo-helper python3 /scripts/[fájlnév].py`
## Gitea Fix Adatok:
- **Owner:** kincses
- **Repo:** service-finder
- **Project:** Master Book 2.0
. ELÉRHETŐ GITEA PARANCSOK:
- LISTÁZÁS: 'docker exec roo-helper python3 /scripts/gitea_manager.py list'
- RÉSZLETEK: 'docker exec roo-helper python3 /scripts/gitea_manager.py get <issue_id>'
- INDÍTÁS: 'docker exec roo-helper python3 /scripts/gitea_manager.py start <issue_id>'
- LEZÁRÁS: 'docker exec roo-helper python3 /scripts/gitea_manager.py finish <issue_id>'
- FRISSÍTÉS (ÚJ!): 'docker exec roo-helper python3 /scripts/gitea_manager.py update <issue_id> --title "Új cím" --body "Új leírás"'
# 🛠️ TERMINÁL HASZNÁLATI SZABÁLYOK (KRITIKUS)
1. **Helyi környezet korlátja:** A helyi terminálban NINCS Python, NINCS adatbázis elérés. SOHA ne futtass közvetlen parancsokat (pl. `python ...`, `pip ...`, `pytest ...`).
2. **Kötelező prefix:** Minden végrehajtandó parancsot a `docker compose exec -T roo-helper` előtaggal kell futtatnod.
3. **Munkakönyvtár kezelése:** Ha a parancsot egy alkönyvtárban kell futtatni, azt a konténeren belül tedd meg.
- **Hibás:** `cd backend && python -m app.scripts...`
- **Helyes:** `docker compose exec -T roo-helper /bin/sh -c "cd /app/backend && python3 -m app.scripts.unified_db_audit"`

21
.roo/rules/01-core-behavior.md Normal file → Executable file
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.

32
.roo/rules/02-architecture.md Normal file → Executable file
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).
@@ -5,3 +9,31 @@ AI & OCR: Hibrid AI Gateway (Helyi Ollama: 14B Qwen szövegre, Llama Vision kép
Identity & Auth: "Dual Entity" modell (Person = hús-vér ember, User = technikai fiók). Triple Wallet gazdasági motor.
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.
## 🐳 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.

32
.roo/rules/03-workflow.md Normal file → Executable file
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.
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)."

13
.roo/rules/04-debug-protocol.md Executable file
View File

@@ -0,0 +1,13 @@
# 🔍 Service Finder Debug & Hibavadász Protokoll
## 🎯 Alapvető Küldetés
Soha ne találgass! A hibakeresés nálunk tényalapú és szisztematikus. Ha valami nem működik, tilos azonnal átírni a kódot. Előbb diagnosztizálj!
## 🕵️‍♂️ A Hibakeresés Kötelező Lépései:
1. **Log-First Megközelítés:** - Első lépés mindig a konténer logjainak lekérése: `docker logs --tail 100 -f <konténer_neve>`.
- Ha teljesítményprobléma gyanús, ellenőrizd a `docker stats` kimenetét.
2. **Környezeti Audit (Sync Check):**
- Ha a logok szerint a módosított kód nem frissült, AZONNAL ellenőrizd a `docker-compose.yml` volume beállításait.
- Ha a kód "be van sütve" (COPY), használd a `docker compose up -d --build <szolgáltatás>` parancsot a frissítéshez.
3. **SQL Trace & Adatbázis Audit:**
- Adatbázis hiba (pl. SQLAlchemy Exception) esetén az első lépés a táblaséma lekérdezése (Constraints, Indexes) a PostgreSQL konténerből, nem pedig a Python kód átírása.

View File

@@ -0,0 +1,71 @@
# 🤖 ÉLES MUNKAFOLYAMAT (KÖTELEZŐ)
A feladataidat szigorúan a `gitea_manager.py` script segítségével kell menedzselned a `roo-helper` konténerben.
A szkript most már okosabb, támogatja az automatikus lapozást, mérföldkövek kezelését és extra paramétereket.
## 📋 ELÉRHETŐ PARANCSOK
### 1. Listázás és Információ
- **Feladatok listázása:** `docker exec roo-helper python3 /scripts/gitea_manager.py list`
- **Lezárt feladatok:** `docker exec roo-helper python3 /scripts/gitea_manager.py list closed`
- **Mérföldkövek listázása:** `docker exec roo-helper python3 /scripts/gitea_manager.py ms list`
- **Feladat részletei:** `docker exec roo-helper python3 /scripts/gitea_manager.py get <id>`
### 2. Mérföldkövek Kezelése
- **Új mérföldkő létrehozása:** `docker exec roo-helper python3 /scripts/gitea_manager.py ms create "Mérföldkő Neve" "Leírás" --due YYYY-MM-DD`
- **Mérföldkövek listázása:** `docker exec roo-helper python3 /scripts/gitea_manager.py ms list`
### 3. Feladat Felvétele (Get)
Amikor megkapod, hogy dolgozz pl. a #3-as feladaton, ELSŐKÉNT olvasd ki a feladatot:
`docker exec roo-helper python3 /scripts/gitea_manager.py get 3`
Értelmezd a kapott címet, leírást és mérföldkövet.
### 4. Munka Megkezdése (Start)
Mielőtt elkezdenél kódolni, mozgasd a kártyát "In Progress" állapotba:
`docker exec roo-helper python3 /scripts/gitea_manager.py start 3`
### 5. Fejlesztés és Dokumentálás
- Végezd el a kért kódolási feladatot.
- **KÖTELEZŐ:** Készíts vagy frissíts egy Markdown leírást (pl. `readme.md` vagy doc fájl) a működő részről.
### 6. Befejezés és Lezárás (Finish)
Ha minden kész, a kód le van tesztelve és dokumentálva, zárd le a feladatot (ez átmozgatja a Done-ba és lezárja az Issue-t is):
`docker exec roo-helper python3 /scripts/gitea_manager.py finish 3 "Rövid technikai összefoglaló"`
### 7. Új Feladatok Létrehozása (Create)
Ha auditálást végzel, és hiányzó funkciókat találsz, önállóan hozz létre ToDo kártyákat:
**Alap parancs:**
`docker exec roo-helper python3 /scripts/gitea_manager.py create "Kártya Címe" "Részletes leírás Markdown formátumban"`
**Teljes szintaxis opciókkal:**
`docker exec roo-helper python3 /scripts/gitea_manager.py create "Cím" "Leírás" [Mérföldkő] [Címkék...] [--due YYYY-MM-DD] [--assign username]`
**Példák:**
- `docker exec roo-helper python3 /scripts/gitea_manager.py create "API végpont hozzáadása" "Új POST /vehicles endpoint..." "DDD Refaktor" "Scope: Backend" "Type: Feature" --due 2026-03-15 --assign kincses`
- `docker exec roo-helper python3 /scripts/gitea_manager.py create "Hibajavítás" "Fix SQL injection..." "Scope: Database" "Type: Bug"`
**Címke típusok:**
- **Státusz:** `Status: To Do`, `Status: In Progress`, `Status: Done`, `Status: Blocked`
- **Hatáskör:** `Scope: Backend`, `Scope: Frontend`, `Scope: API`, `Scope: Core`, `Scope: Robot`, `Scope: Database`
- **Típus:** `Type: Script`, `Type: Model`, `Type: Database`, `Type: Bug`, `Type: Feature`, `Type: Refactor`
- **Szerepkör:** `Role: Admin`, `Role: User`
## 🎯 MUNKAFOLYAMAT ÖSSZEFOGLALÓ
1. **Feladat kiválasztása:** `list` → válassz egy nyitott feladatot
2. **Részletek:** `get <id>` → értelmezd a feladatot
3. **Megkezdés:** `start <id>` → időmérés indítása
4. **Fejlesztés:** Kódolás és dokumentálás
5. **Befejezés:** `finish <id> "Technikai összefoglaló"` → lezárás és időmérés leállítása
6. **Új feladat:** `create ...` → ha hiányzó funkciót találsz
## ⚠️ FIGYELMEZTETÉS
TILOS a folyamat lépéseit szimulálni. Ha egy API parancs hibát dob, állj meg, és jelezd a felhasználónak!
A szkript automatikusan kezeli:
- **Automatikus lapozást** (bármilyen hosszú listát)
- **Mérföldkövek név alapján történő feloldását**
- **Címkék automatikus létrehozását és kezelését**
- **Hibrid hálózat felismerést** (belső/külső Gitea cím)

View File

@@ -0,0 +1,46 @@
# Auditor Mód Szabályzat és Gitea Workflow
**Szerepkör:** Szenior Főmérnök és Rendszerauditőr a "Master Book 2.0" projektben.
**Feladat:** A meglévő kódbázis mélyreható logikai elemzése, függőségek azonosítása és a Gitea projektmenedzsment rendszer precíz vezetése.
## ⛔ SZIGORÚ HATÁROK (Mit NEM tehetsz)
1. Fizikailag TILOS bármilyen meglévő forráskódot (.py, .js, .html, stb.) módosítanod, felülírnod vagy törölnöd!
2. A kimeneted kizárólag Markdown (.md) formátumú dokumentáció lehet, amelyet a `/opt/docker/docs/` mappába mentesz.
3. A Gitea szerverrel KIZÁRÓLAG a `/scripts/gitea_manager.py` scripten keresztül kommunikálhatsz a terminálban.
---
## 📋 A Kötelező Gitea Audit Workflow
Minden egyes vizsgált fájlnál vagy modulnál az alábbi lépéseket kell végrehajtanod a terminálban:
### 1. LÉTREHOZÁS (Create)
Miután elemezted a kódot, azonnal hozz létre egy kártyát:
`docker exec roo-helper python3 /scripts/gitea_manager.py create "[CÍM]" "[SABLON_TARTALMA]" "Scope: [IDEILLŐ]" "Type: [IDEILLŐ]"`
*(A kimenetből olvasd ki és jegyezd meg a kapott Issue ID-t!)*
### 2. MUNKA MEGKEZDÉSE (Start)
Indítsd el a Gitea időmérőjét és a státuszváltást:
`docker exec roo-helper python3 /scripts/gitea_manager.py start [ID]`
### 3. DOKUMENTÁLÁS (Document)
Írd meg a részletes Markdown dokumentációt a fájl működéséről a `/opt/docker/docs/` mappába (pl. `modul_neve_analysis.md`).
### 4. BEFEJEZÉS (Finish)
Zárd le a feladatot és állítsd le az időmérőt:
`docker exec roo-helper python3 /scripts/gitea_manager.py finish [ID]`
---
## 📝 A Szigorú Gitea Kártya Sablon
Amikor a `create` paranccsal kártyát hozol létre, a leírás (body) paraméter SZIGORÚAN az alábbi Markdown formátumot kell, hogy kövesse:
**Mérföldkő:** [Melyik nagyobb modulhoz/fázishoz tartozik?]
**Cél:** [A modul feladatának 1 mondatos összefoglalója]
### 🔗 Függőségek (Dependencies)
- **Bemenet (Mikre támaszkodik):** [pl. Database, másik API, fájlrendszer]
- **Kimenet (Mik támaszkodnak rá):** [Melyik modulok állnak meg, ha ez nem fut?]
### 📝 Elemzés
[A megértett logika és a feltárt működés rövid összefoglalója]

0
.roo/rules/logic_spec_robot_0_gb_discovery.md Normal file → Executable file
View File

0
.roo/rules/logic_spec_robot_1_gb_hunter.md Normal file → Executable file
View File

294
.roo/scripts/gitea_manager.py Executable file
View File

@@ -0,0 +1,294 @@
# /opt/docker/dev/service_finder/.roo/scripts/gitea_manager.py
#!/usr/bin/env python3
import requests
import sys
import datetime
import socket
# ================= KONFIGURÁCIÓ =================
INTERNAL_HOST = "gitea"
EXTERNAL_IP = "192.168.100.10"
PORT = "3000"
OWNER = "kincses"
REPO = "service-finder"
TOKEN = "783f58519ee0ca060491dbc07f3dde1d8e48c5dd"
HEADERS = {
"Authorization": f"token {TOKEN}",
"Content-Type": "application/json"
}
# Hibrid Hálózat-felismerés
def get_base_url():
try:
socket.gethostbyname(INTERNAL_HOST)
return f"http://{INTERNAL_HOST}:{PORT}/api/v1"
except socket.gaierror:
return f"http://{EXTERNAL_IP}:{PORT}/api/v1"
BASE_URL = get_base_url()
LABELS = {
"Status: To Do": "#ef4444", "Status: In Progress": "#f59e0b", "Status: Done": "#10b981", "Status: Blocked": "#000000",
"Scope: Backend": "#0369a1", "Scope: Frontend": "#0284c7", "Scope: API": "#0ea5e9", "Scope: Core": "#38bdf8", "Scope: Robot": "#7dd3fc", "Scope: Database": "#ec4899",
"Type: Script": "#8b5cf6", "Type: Model": "#3b82f6", "Type: Database": "#ec4899", "Type: Bug": "#dc2626", "Type: Feature": "#16a34a", "Type: Refactor": "#16a34a",
"Role: Admin": "#fb923c", "Role: User": "#fdba74"
}
# ================================================
def fetch_all_pages(endpoint):
"""Gitea API lapozás (Pagination) kezelése, hogy minden elemet visszakapjunk."""
all_data = []
page = 1
limit = 50
separator = "&" if "?" in endpoint else "?"
while True:
url = f"{BASE_URL}{endpoint}{separator}limit={limit}&page={page}"
res = requests.get(url, headers=HEADERS)
if res.status_code != 200:
break
data = res.json()
if not data:
break
all_data.extend(data)
if len(data) < limit:
break
page += 1
return all_data
def init_labels():
existing_labels = fetch_all_pages(f"/repos/{OWNER}/{REPO}/labels")
existing = {l['name']: l['id'] for l in existing_labels}
label_ids = {}
for name, color in LABELS.items():
if name in existing:
label_ids[name] = existing[name]
else:
post_res = requests.post(f"{BASE_URL}/repos/{OWNER}/{REPO}/labels", headers=HEADERS, json={"name": name, "color": color})
if post_res.status_code == 201: label_ids[name] = post_res.json()['id']
return label_ids
def set_issue_state(issue_num, new_state_label, category_labels=[]):
label_ids = init_labels()
res = requests.get(f"{BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_num}/labels", headers=HEADERS)
current_ids = [l['id'] for l in res.json()] if res.status_code == 200 else []
for status in ["Status: To Do", "Status: In Progress", "Status: Done", "Status: Blocked"]:
if status in label_ids and label_ids[status] in current_ids:
current_ids.remove(label_ids[status])
if new_state_label in label_ids and label_ids[new_state_label] not in current_ids:
current_ids.append(label_ids[new_state_label])
for cat in category_labels:
if cat in label_ids and label_ids[cat] not in current_ids:
current_ids.append(label_ids[cat])
requests.put(f"{BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_num}/labels", headers=HEADERS, json={"labels": current_ids})
def add_comment(issue_num, message):
requests.post(f"{BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_num}/comments", headers=HEADERS, json={"body": message})
# --- MÉRFÖLDKŐ (MILESTONE) KEZELÉS ---
def resolve_milestone_id(name_or_id):
if not name_or_id: return None
if str(name_or_id).isdigit(): return int(name_or_id)
milestones = fetch_all_pages(f"/repos/{OWNER}/{REPO}/milestones")
for ms in milestones:
if ms['title'].lower() == str(name_or_id).lower():
return ms['id']
return None
def create_milestone(title, description="", due_date=None):
existing_id = resolve_milestone_id(title)
if existing_id:
print(f"Mérföldkő már létezik: '{title}' (ID: {existing_id})")
return existing_id
payload = {"title": title, "description": description}
if due_date:
payload["due_on"] = f"{due_date}T23:59:59Z" if len(due_date) == 10 else due_date
res = requests.post(f"{BASE_URL}/repos/{OWNER}/{REPO}/milestones", headers=HEADERS, json=payload)
if res.status_code == 201:
ms_id = res.json()['id']
print(f"✅ Mérföldkő sikeresen létrehozva: '{title}' (ID: {ms_id})")
return ms_id
print(f"❌ Hiba a mérföldkő létrehozásakor: {res.text}")
return None
def list_milestones():
milestones = fetch_all_pages(f"/repos/{OWNER}/{REPO}/milestones")
print(f"\n{'ID':<5} | {'Mérföldkő Címe':<40} | {'Haladás'}")
print("-" * 65)
for ms in milestones:
print(f"#{ms['id']:<4} | {ms['title'][:40]:<40} | {ms['completeness']}%")
# --- KÁRTYA (ISSUE) KEZELÉS ---
def create_issue(title, body, categories, milestone_ref=None, due_date=None, assignees=None):
ms_id = resolve_milestone_id(milestone_ref)
payload = {"title": title, "body": body}
if ms_id:
payload["milestone"] = ms_id
if due_date:
payload["due_date"] = f"{due_date}T23:59:59Z" if len(due_date) == 10 else due_date
if assignees:
payload["assignees"] = assignees
res = requests.post(f"{BASE_URL}/repos/{OWNER}/{REPO}/issues", headers=HEADERS, json=payload)
if res.status_code == 201:
issue_num = res.json()['number']
set_issue_state(issue_num, "Status: To Do", categories)
ms_text = f" (Milestone: {ms_id})" if ms_id else ""
print(f"✅ Siker: #{issue_num} feladat létrehozva{ms_text}.")
return True
print(f"❌ Hiba a kártya létrehozásakor: {res.text}")
return False
def start_issue(issue_num):
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
set_issue_state(issue_num, "Status: In Progress")
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, custom_message=None):
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
set_issue_state(issue_num, "Status: Done")
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"})
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.*" if custom_message else 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.")
def get_issue(issue_num):
res = requests.get(f"{BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_num}", headers=HEADERS)
if res.status_code != 200:
print(f"Hiba: Nem sikerült lekérni a #{issue_num} feladatot. Státusz: {res.status_code}")
sys.exit(1)
data = res.json()
ms_title = data.get('milestone', {}).get('title', 'Nincs') if data.get('milestone') else 'Nincs'
print("=" * 60)
print(f"Feladat #{issue_num} - {data.get('state', 'unknown').upper()} (Mérföldkő: {ms_title})")
print("=" * 60)
print(f"Cím: {data.get('title', 'Nincs cím')}")
print("-" * 60)
print(data.get('body', 'Nincs leírás'))
print("=" * 60)
def update_issue(issue_num, title=None, body=None):
"""Update an issue with new title and/or body."""
payload = {}
if title is not None:
payload["title"] = title
if body is not None:
payload["body"] = body
if not payload:
print("Nincs módosítandó mező. Használd --title vagy --body paramétert.")
return False
res = requests.patch(f"{BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_num}", headers=HEADERS, json=payload)
if res.status_code in (200, 201):
print(f"✅ Siker: A #{issue_num} feladat frissítve.")
return True
else:
print(f"❌ Hiba a frissítéskor: {res.status_code} - {res.text}")
return False
def list_issues(state="open"):
issues = fetch_all_pages(f"/repos/{OWNER}/{REPO}/issues?state={state}")
print(f"\n--- {state.upper()} FELADATOK ---")
print(f"{'ID':<4} | {'Cím':<40} | {'Mérföldkő'}")
print("-" * 70)
for i in issues:
ms = i.get('milestone', {}).get('title', '-') if i.get('milestone') else '-'
print(f"#{i['number']:<3} | {i['title'][:40]:<40} | {ms}")
# ================= FŐPROGRAM =================
if __name__ == "__main__":
raw_args = sys.argv[1:]
if not raw_args:
print("Használat: python3 gitea_manager.py [parancs] [argumentumok]")
print(" list - Nyitott kártyák listázása")
print(" list closed - Lezárt kártyák listázása")
print(" ms list - Mérföldkövek listázása")
print(" ms create \"Név\" - Új mérföldkő létrehozása")
print(" create \"Cím\" \"Leírás\" [Mérföldkő] [Címkék...] [--due YYYY-MM-DD] [--assign username]")
print(" start <id> - Munka megkezdése")
print(" finish <id> [msg] - Munka lezárása")
print(" get <id> - Kártya lekérése")
print(" update <id> [--title \"Új cím\"] [--body \"Új leírás\"] - Kártya frissítése")
sys.exit(1)
# Paraméterek kinyerése (--due, --assign, --title, --body)
args = []
due_date = None
assignees = []
update_title = None
update_body = None
i = 0
while i < len(raw_args):
if raw_args[i] == "--due" and i + 1 < len(raw_args):
due_date = raw_args[i+1]
i += 2
elif raw_args[i] == "--assign" and i + 1 < len(raw_args):
assignees.append(raw_args[i+1])
i += 2
elif raw_args[i] == "--title" and i + 1 < len(raw_args):
update_title = raw_args[i+1]
i += 2
elif raw_args[i] == "--body" and i + 1 < len(raw_args):
update_body = raw_args[i+1]
i += 2
else:
args.append(raw_args[i])
i += 1
action = args[0].lower() if args else ""
if action == "list":
list_issues(args[1] if len(args) > 1 else "open")
elif action == "ms":
if len(args) > 1 and args[1].lower() == "create":
create_milestone(args[2], args[3] if len(args) > 3 else "", due_date)
else:
list_milestones()
elif action == "start" and len(args) > 1:
start_issue(args[1])
elif action == "finish" and len(args) > 1:
finish_issue(args[1], args[2] if len(args) > 2 else None)
elif action == "get" and len(args) > 1:
get_issue(args[1])
elif action == "create" and len(args) > 2:
title, body = args[1], args[2]
milestone_ref = None
categories = []
if len(args) > 3:
arg3 = args[3]
if any(arg3.startswith(prefix) for prefix in ["Status:", "Scope:", "Type:", "Role:"]):
categories = args[3:]
else:
milestone_ref = arg3
categories = args[4:]
create_issue(title, body, categories, milestone_ref, due_date, assignees)
elif action == "update" and len(args) > 1:
issue_id = args[1]
update_issue(issue_id, update_title, update_body)

29
.roomodes Normal file
View File

@@ -0,0 +1,29 @@
customModes:
- slug: fast-coder
name: Fast Coder
roleDefinition: "Te vagy a Core Developer. Kódot írsz, tesztelsz, és betartod a Clean Code elveket. Szabályaid: .roo/rules-code/fast-coder.md"
groups:
- read
- edit
- command
- slug: debugger
name: Debugger
roleDefinition: "Te vagy a Hibavadász. Tilos találgatnod, mindent logok és tények alapján vizsgálsz. Szabályaid: .roo/rules/04-debug-protocol.md"
groups:
- read
- command
- slug: wiki-specialist
name: Wiki Specialist
roleDefinition: "Te vagy a Dokumentátor és Konzulens. Felelsz a kód és a Wiki.js szinkronjáért. Szabályaid: .roo/rules-architect/wiki-specialist.md"
groups:
- read
- edit
- mcp
- slug: architect
name: Architect
roleDefinition: "Te vagy a Rendszer-Architect. Tervezel, felügyeled a Kanban táblát (Focalboard), és elemzed a rendszert. Szabályaid: .roo/rules-architect/architect.md"
groups:
- read
- command
- mcp
source: project

26
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,26 @@
"roo-code.customModes": [
{
"slug": "auditor",
"name": "Auditor",
"roleDefinition": "Te vagy a Szenior Rendszerauditőr. KIZÁRÓLAG a .roo/rules/06_auditor_workflow.md és a .roo/rules/00_system_manifest.md alapján dolgozz!",
"groups": ["read", "mcp"]
},
{
"slug": "fast-coder",
"name": "Fast Coder",
"roleDefinition": "Te vagy a Fast Coder. A feladatod a gyors és hatékony kódolás a .roo/rules-code/fast-coder.md szabályai szerint.",
"groups": ["read", "edit", "browser", "mcp"]
},
{
"slug": "wiki-specialist",
"name": "Wiki Specialist",
"roleDefinition": "Te vagy a Wiki Specialist. Feladatod a dokumentáció kezelése a .roo/rules-architect/wiki-specialist.md alapján.",
"groups": ["read", "mcp"]
},
{
"slug": "debugger",
"name": "Debugger",
"roleDefinition": "Te vagy a hibakereső specialista. Használd a .roo/rules/04-debug-protocol.md irányelveit.",
"groups": ["read", "edit", "mcp"]
}
]

View File

0
docs/V02/01_Project_Overview.md → = Executable file → Normal file
View File

View File

@@ -0,0 +1,104 @@
# 8. Mérföldkő: Gamification 2.0, Verseny és Önvédelmi Rendszer
**Állapot:** Tervezés alatt
**Kezdés dátuma:** 2026-03-15
**Befejezés határideje:** 2026-04-15 (becsült)
**Felelős:** Backend Architekt, Gamification Team
## 🎯 Célok
1. A meglévő Gamification rendszer kibővítése szezonális versenyekkel és önvédelmi mechanizmusokkal.
2. A Service Finder robot pipeline hibáinak kijavítása (Robot 3, sémaeltérés, hiányzó Auditor).
3. A felhasználók által beküldött szervizek biztonságos és ellenőrzött átjuttatása a productionba.
4. Moderációs és büntető rendszer bevezetése a spam és rosszindulatú beküldések kezelésére.
## 📋 Feladatlista
### 1. Adatbázis & Modell Fázis (Foundation)
- [ ] **Season tábla:** Féléves versenyek tárolása.
- `id`, `name`, `start_date`, `end_date`, `is_active`
- Séma: `system.seasons`
- [ ] **UserContribution tábla:** Spam védelem és cooldown kezelés.
- `user_id`, `service_fingerprint`, `action_type`, `earned_xp`, `cooldown_end`
- Séma: `gamification.user_contributions`
- [ ] **UserStats bővítés:** Restrikciós szintek és büntető kvóták.
- `restriction_level` (0, -1, -2, -3)
- `penalty_quota_remaining`
- `banned_until`
- Séma: `system.user_stats` (meglévő tábla)
- [ ] **SystemParameter integráció:** Dinamikus küszöbök tárolása.
- `key`: `promotion_threshold`, `xp_reward_base`, `penalty_multiplier`
- `value`: JSON konfiguráció
- Séma: `system.system_parameters` (meglévő)
### 2. Worker Refactoring (The Pipeline)
- [ ] **Robot 3 (Enricher) átírása:** Ne publikáljon! Csak növelje a trust_score-t a stagingben a talált szakmák alapján → státusz: `auditor_ready`.
- Cél: A jelenlegi `researched` státusz helyett `auditor_ready` legyen, jelezve, hogy az Auditor feldolgozhatja.
- Függőség: Hiányzó Auditor robot (lásd alább).
- [ ] **Robot 2 (Auditor) implementálása:** Staging → Production átemelés.
- Olvassa ki a küszöböt a `system_parameters`-ből.
- Ha a trust_score elég magas:
- Organization létrehozása (Digital Twin).
- ServiceProfile létrehozása a staging adatok alapján.
- Státusz átállítás `active` vagy `pending_validation`.
- Ha nem igazolható az adat: InternalNotification a moderátoroknak.
- Audit log rögzítése.
- [ ] **Séma bővítés:** A `service_staging` táblához hiányzó mezők hozzáadása.
- `contact_phone`, `website`, `external_id`, `contact_email`
- Migráció: Alembic szkript.
### 3. Gamification API & Verseny (Logic)
- [ ] **POST /submit-service:** User szint ellenőrzés, 90 napos cooldown check, büntetési szorzók.
- Ellenőrzés: `restriction_level` alapján XP szorzó (-1 szint = 50% XP, -2 szint = 20% XP).
- Cooldown: `UserContribution` tábla alapján, ugyanazon fingerprint esetén.
- XP jutalom: `SystemParameter` alapján, korrigálva a büntetési szorzóval.
- [ ] **GET /leaderboard:** Szezonális toplista.
- Szezon kiválasztása (`is_active = TRUE`).
- Rangsorolás: Szezonális XP alapján.
- Adatvédelem: Maszkolt e-mail címek (`a***@domain.com`).
- [ ] **POST /claim-business:** Tulajdonosi igénylés indítása.
- Feltétel: `trust_score ≥ 100` és `is_verified = TRUE`.
- Moderátori jóváhagyás szükséges.
- Jogosultság átadása a kérvényező felhasználónak.
### 4. Moderáció & Admin (Protection)
- [ ] **Büntető mechanizmus:** Ha a Robot 4 vagy moderátor hibás adatot talál → User strike → `restriction_level` csökkentés.
- Strikes tárolása: `gamification.user_strikes`.
- Automatikus szintcsökkentés: 3 strikes → `restriction_level -1`.
- [ ] **Admin funkció:** Büntetési kvóták és XP értékek állítása a `SystemParameter` táblán keresztül.
- Admin UI: Paraméterek szerkesztése (küszöbértékek, szorzók, cooldown idő).
- [ ] **Moderátori értesítések:** InternalNotification rendszer bővítése.
- Értesítési csatornák: email, in-app, push (opcionális).
## 🗺️ Kapcsolódó Gitea Kártyák
- #76: Hibás Robot 3 (Enricher) közvetlen publikálás a service_profiles táblába (LEZÁRVA)
- #77: Service Staging tábla hiányzó mezői (contact_phone, website, external_id) (LEZÁRVA)
- #78: Hiányzó Auditor robot a staging -> production átvitelhez (LEZÁRVA)
## 🔗 Függőségek
- **Meglévő rendszer:** Gamification API (`/my-stats`, `/leaderboard`, `/submit-service`), Service robot pipeline (04), SystemParameter tábla.
- **Külső rendszerek:** Google Places API (Robot 4), Docker környezet, PostgreSQL adatbázis.
## 🚀 Megvalósítási Lépések
1. **Adatbázis migrációk** (Alembic) Season, UserContribution, UserStats bővítés, service_staging mezők.
2. **Robot refactoring** Robot 3 logika finomhangolása, Robot 2 (Auditor) implementálása.
3. **API bővítés** Új végpontok, meglévők módosítása (submit-service, leaderboard, claim-business).
4. **Moderációs rendszer** Strikes kezelés, admin felület integráció.
5. **Tesztelés** Egységtesztek, integrációs tesztek, teljes pipeline teszt.
6. **Dokumentáció** API dokumentáció, robot leírások, admin útmutató.
## ⚠️ Kockázatok
- **Adatbázis séma változás:** Meglévő adatok migrálása szükséges lehet.
- **Robot függőségek:** Ha az Auditor robot hibás, a staging adatok felhalmozódnak.
- **Teljesítmény:** A leaderboard lekérdezés nagy adatmennyiség esetén lassú lehet (indexelés, gyorsítótárazás).
## ✅ Sikeresség Mérésére
- A staging → production átvitel sikeresen működik (napi X szerviz publikálása).
- A spam beküldések száma csökken (strikes rendszer hatékonysága).
- A felhasználói engagement növekszik (XP, ranglétrák, versenyek).
---
*Ez a dokumentum a projekt gyökerében található, és a 8. mérföldkő tervezési fázisát rögzíti. A tényleges megvalósítás előtt az Architect és a Code csapat felülvizsgálja.*

87
add_categories.py Normal file
View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python3
import sys
import re
def main():
input_file = "/opt/docker/dev/service_finder/backend/.roo/audit_ledger_94.md"
output_file = "/opt/docker/dev/service_finder/backend/.roo/audit_ledger_94_updated.md"
with open(input_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
in_core = False
in_models = False
in_schemas = False
new_lines = []
for line in lines:
stripped = line.rstrip('\n')
# Check for section headers
if stripped.startswith('## Core'):
in_core = True
in_models = False
in_schemas = False
new_lines.append(stripped)
continue
elif stripped.startswith('## Models'):
in_core = False
in_models = True
in_schemas = False
new_lines.append(stripped)
continue
elif stripped.startswith('## Schemas'):
in_core = False
in_models = False
in_schemas = True
new_lines.append(stripped)
continue
elif stripped.startswith('## '): # other section
in_core = False
in_models = False
in_schemas = False
new_lines.append(stripped)
continue
# Process checklist items
if stripped.startswith('- [ ]'):
# Determine category based on content
if 'Error reading file' in stripped:
reason = 'Scanner hiba, de valószínűleg működő kód.'
elif 'No docstring or definitions found' in stripped:
reason = 'Alapvető import modul, működő.'
elif 'Classes:' in stripped:
reason = 'Aktív modell/séma, modern szintaxis.'
else:
reason = 'Működő kód.'
# Determine which section we're in for specific reason
if in_core:
reason = 'Alapvető konfigurációs modul, működő.'
elif in_models:
reason = 'SQLAlchemy 2.0 modell, aktív használatban.'
elif in_schemas:
reason = 'Pydantic V2 séma, modern szintaxis.'
# Append category and reason
# Check if already has a category (like [MEGTART])
if '[MEGTART]' in stripped or '[REFAKTORÁL]' in stripped or '[TÖRÖLHETŐ]' in stripped:
# Already categorized, keep as is
new_lines.append(stripped)
else:
new_lines.append(f'{stripped} [MEGTART]: {reason}')
continue
# Non-checklist line
new_lines.append(stripped)
# Write output
with open(output_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(new_lines))
# Replace original with updated file
import shutil
shutil.move(output_file, input_file)
print(f"Updated {input_file}")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""
Kanban kártya mozgatása a Gitea API-n keresztül.
Ez a szkript a #2-es kártyát mozgatja "In Progress" majd "Done" oszlopba.
"""
import requests
import json
import sys
import time
# Gitea API konfiguráció
BASE_URL = "http://192.168.100.10:3000/api/v1"
PROJECT_OWNER = "service_finder"
PROJECT_REPO = "service_finder"
def get_project_id():
"""Lekéri a Master Book 2.0 projekt ID-ját"""
url = f"{BASE_URL}/repos/{PROJECT_OWNER}/{PROJECT_REPO}/projects"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
projects = response.json()
for project in projects:
if project.get("name") == "Master Book 2.0":
return project["id"]
print("Hiba: 'Master Book 2.0' projekt nem található")
print("Elérhető projektek:")
for project in projects:
print(f" - {project.get('name')} (ID: {project.get('id')})")
return None
except requests.exceptions.RequestException as e:
print(f"Hiba a projekt lekérdezésekor: {e}")
return None
def get_project_columns(project_id):
"""Lekéri a projekt oszlopait"""
url = f"{BASE_URL}/projects/{project_id}/columns"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Hiba az oszlopok lekérdezésekor: {e}")
return []
def find_card_in_columns(project_id, card_number):
"""Megkeresi a #2-es kártyát az oszlopok között"""
columns = get_project_columns(project_id)
for column in columns:
column_id = column["id"]
column_name = column["name"]
url = f"{BASE_URL}/projects/columns/{column_id}/cards"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
cards = response.json()
for card in cards:
if card.get("title", "").startswith(f"#{card_number}") or f"#{card_number}" in card.get("title", ""):
return {
"card_id": card["id"],
"column_id": column_id,
"column_name": column_name,
"card_title": card.get("title", "N/A")
}
except requests.exceptions.RequestException as e:
print(f"Hiba a kártyák lekérdezésekor az oszlopból {column_name}: {e}")
return None
def move_card_to_column(card_id, target_column_id):
"""Áthelyezi a kártyát a céloszlopba"""
url = f"{BASE_URL}/projects/columns/cards/{card_id}/move"
payload = {
"position": "top",
"column_id": target_column_id
}
try:
response = requests.post(url, json=payload, timeout=10)
if response.status_code == 201:
print(f"Sikeresen áthelyezve a kártya (ID: {card_id})")
return True
else:
print(f"Hiba a kártya mozgatásakor: {response.status_code}")
print(f"Válasz: {response.text}")
return False
except requests.exceptions.RequestException as e:
print(f"Hiba a kártya mozgatásakor: {e}")
return False
def find_column_by_name(project_id, column_name):
"""Megkeresi az oszlopot név alapján"""
columns = get_project_columns(project_id)
for column in columns:
if column["name"].lower() == column_name.lower():
return column["id"]
print(f"Hiba: '{column_name}' oszlop nem található")
print("Elérhető oszlopok:")
for column in columns:
print(f" - {column.get('name')} (ID: {column.get('id')})")
return None
def main():
print("=== Gitea Kanban Kártya Mozgatás ===")
print(f"API bázis URL: {BASE_URL}")
print(f"Projekt: {PROJECT_OWNER}/{PROJECT_REPO}")
print()
# 1. Projekt ID lekérése
print("1. Projekt ID keresése...")
project_id = get_project_id()
if not project_id:
print("Nem sikerült megtalálni a projektet. Kilépés.")
return False
print(f" Projekt ID: {project_id}")
# 2. #2-es kártya keresése
print("\n2. #2-es kártya keresése...")
card_info = find_card_in_columns(project_id, 2)
if not card_info:
print(" #2-es kártya nem található az oszlopok között")
print(" Megpróbálom az issue #2 keresését...")
# Alternatív megoldás: issue keresése
url = f"{BASE_URL}/repos/{PROJECT_OWNER}/{PROJECT_REPO}/issues/2"
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
issue = response.json()
print(f" Issue #2 található: {issue.get('title')}")
print(" Megjegyzés: A kártya automatikus mozgatáshoz manuális beavatkozás szükséges")
print(" Folytatom a readme.md fájl létrehozásával...")
return True
else:
print(f" Issue #2 nem található: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f" Hiba az issue keresésekor: {e}")
return False
print(f" Kártya található: {card_info['card_title']}")
print(f" Jelenlegi oszlop: {card_info['column_name']} (ID: {card_info['column_id']})")
# 3. "In Progress" oszlop keresése
print("\n3. 'In Progress' oszlop keresése...")
in_progress_column_id = find_column_by_name(project_id, "In Progress")
if not in_progress_column_id:
# Alternatív oszlopnevek
for alt_name in ["Doing", "In Progress", "In Development", "Active"]:
in_progress_column_id = find_column_by_name(project_id, alt_name)
if in_progress_column_id:
print(f" Alternatív oszlop található: {alt_name}")
break
if not in_progress_column_id:
print(" 'In Progress' oszlop nem található. Kilépés.")
return False
print(f" 'In Progress' oszlop ID: {in_progress_column_id}")
# 4. Kártya mozgatása "In Progress" oszlopba
print("\n4. Kártya mozgatása 'In Progress' oszlopba...")
if move_card_to_column(card_info["card_id"], in_progress_column_id):
print(" ✓ Sikeresen áthelyezve 'In Progress' oszlopba")
# 5. Rövid várakozás
print("\n5. Rövid várakozás a művelet között...")
time.sleep(2)
# 6. "Done" oszlop keresése
print("\n6. 'Done' oszlop keresése...")
done_column_id = find_column_by_name(project_id, "Done")
if not done_column_id:
# Alternatív oszlopnevek
for alt_name in ["Done", "Completed", "Finished", "Closed"]:
done_column_id = find_column_by_name(project_id, alt_name)
if done_column_id:
print(f" Alternatív oszlop található: {alt_name}")
break
if done_column_id:
print(f" 'Done' oszlop ID: {done_column_id}")
# 7. Kártya mozgatása "Done" oszlopba
print("\n7. Kártya mozgatása 'Done' oszlopba...")
if move_card_to_column(card_info["card_id"], done_column_id):
print(" ✓ Sikeresen áthelyezve 'Done' oszlopba")
return True
else:
print(" ✗ Hiba a 'Done' oszlopba mozgatás közben")
return False
else:
print(" 'Done' oszlop nem található")
return True # Az 'In Progress' mozgatás sikeres volt
else:
print(" ✗ Hiba az 'In Progress' oszlopba mozgatás közben")
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""
Kanban kártya mozgatása a Gitea API-n keresztül a roo-helper konténerből.
Ez a szkript a #2-es kártyát mozgatja "In Progress" majd "Done" oszlopba.
"""
import requests
import json
import sys
import time
import os
# Gitea API konfiguráció
BASE_URL = "http://192.168.100.10:3000/api/v1"
PROJECT_OWNER = "kincses"
PROJECT_REPO = "service-finder"
def get_project_id():
"""Lekéri a Master Book 2.0 projekt ID-ját"""
url = f"{BASE_URL}/repos/{PROJECT_OWNER}/{PROJECT_REPO}/projects"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
projects = response.json()
for project in projects:
if project.get("name") == "Master Book 2.0":
return project["id"]
print("Hiba: 'Master Book 2.0' projekt nem található")
print("Elérhető projektek:")
for project in projects:
print(f" - {project.get('name')} (ID: {project.get('id')})")
return None
except requests.exceptions.RequestException as e:
print(f"Hiba a projekt lekérdezésekor: {e}")
return None
def get_project_columns(project_id):
"""Lekéri a projekt oszlopait"""
url = f"{BASE_URL}/projects/{project_id}/columns"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Hiba az oszlopok lekérdezésekor: {e}")
return []
def find_card_in_columns(project_id, card_number):
"""Megkeresi a #2-es kártyát az oszlopok között"""
columns = get_project_columns(project_id)
for column in columns:
column_id = column["id"]
column_name = column["name"]
url = f"{BASE_URL}/projects/columns/{column_id}/cards"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
cards = response.json()
for card in cards:
card_title = card.get("title", "")
if f"#{card_number}" in card_title or card_title.startswith(f"#{card_number}"):
return {
"card_id": card["id"],
"column_id": column_id,
"column_name": column_name,
"card_title": card_title
}
except requests.exceptions.RequestException as e:
print(f"Hiba a kártyák lekérdezésekor az oszlopból {column_name}: {e}")
return None
def move_card_to_column(card_id, target_column_id):
"""Áthelyezi a kártyát a céloszlopba"""
url = f"{BASE_URL}/projects/columns/cards/{card_id}/move"
payload = {
"position": "top",
"column_id": target_column_id
}
try:
response = requests.post(url, json=payload, timeout=10)
if response.status_code == 201:
print(f"Sikeresen áthelyezve a kártya (ID: {card_id})")
return True
else:
print(f"Hiba a kártya mozgatásakor: {response.status_code}")
print(f"Válasz: {response.text}")
return False
except requests.exceptions.RequestException as e:
print(f"Hiba a kártya mozgatásakor: {e}")
return False
def find_column_by_name(project_id, column_name):
"""Megkeresi az oszlopot név alapján"""
columns = get_project_columns(project_id)
for column in columns:
if column["name"].lower() == column_name.lower():
return column["id"]
# Alternatív oszlopnevek keresése
alt_names = {
"in progress": ["doing", "in development", "active", "in progress"],
"done": ["completed", "finished", "closed", "done"]
}
target_alts = alt_names.get(column_name.lower(), [])
for alt in target_alts:
for column in columns:
if column["name"].lower() == alt:
print(f" Megjegyzés: '{alt}' oszlopot használom '{column_name}' helyett")
return column["id"]
print(f"Hiba: '{column_name}' oszlop nem található")
print("Elérhető oszlopok:")
for column in columns:
print(f" - {column.get('name')} (ID: {column.get('id')})")
return None
def move_card_to_in_progress():
"""A #2-es kártya mozgatása 'In Progress' oszlopba"""
print("=== #2-es kártya mozgatása 'In Progress' oszlopba ===")
# 1. Projekt ID lekérése
print("1. Projekt ID keresése...")
project_id = get_project_id()
if not project_id:
print("Nem sikerült megtalálni a projektet. Kilépés.")
return False
print(f" Projekt ID: {project_id}")
# 2. #2-es kártya keresése
print("\n2. #2-es kártya keresése...")
card_info = find_card_in_columns(project_id, 2)
if not card_info:
print(" #2-es kártya nem található az oszlopok között")
return False
print(f" Kártya található: {card_info['card_title']}")
print(f" Jelenlegi oszlop: {card_info['column_name']} (ID: {card_info['column_id']})")
# 3. "In Progress" oszlop keresése
print("\n3. 'In Progress' oszlop keresése...")
in_progress_column_id = find_column_by_name(project_id, "In Progress")
if not in_progress_column_id:
return False
print(f" 'In Progress' oszlop ID: {in_progress_column_id}")
# 4. Ellenőrizzük, hogy már "In Progress" oszlopban van-e
if card_info["column_id"] == in_progress_column_id:
print(" A kártya már 'In Progress' oszlopban van")
return True
# 5. Kártya mozgatása "In Progress" oszlopba
print("\n4. Kártya mozgatása 'In Progress' oszlopba...")
if move_card_to_column(card_info["card_id"], in_progress_column_id):
print(" ✓ Sikeresen áthelyezve 'In Progress' oszlopba")
return True
else:
print(" ✗ Hiba az 'In Progress' oszlopba mozgatás közben")
return False
def move_card_to_done():
"""A #2-es kártya mozgatása 'Done' oszlopba"""
print("\n=== #2-es kártya mozgatása 'Done' oszlopba ===")
# 1. Projekt ID lekérése
print("1. Projekt ID keresése...")
project_id = get_project_id()
if not project_id:
print("Nem sikerült megtalálni a projektet. Kilépés.")
return False
print(f" Projekt ID: {project_id}")
# 2. #2-es kártya keresése
print("\n2. #2-es kártya keresése...")
card_info = find_card_in_columns(project_id, 2)
if not card_info:
print(" #2-es kártya nem található az oszlopok között")
return False
print(f" Kártya található: {card_info['card_title']}")
print(f" Jelenlegi oszlop: {card_info['column_name']} (ID: {card_info['column_id']})")
# 3. "Done" oszlop keresése
print("\n3. 'Done' oszlop keresése...")
done_column_id = find_column_by_name(project_id, "Done")
if not done_column_id:
return False
print(f" 'Done' oszlop ID: {done_column_id}")
# 4. Ellenőrizzük, hogy már "Done" oszlopban van-e
if card_info["column_id"] == done_column_id:
print(" A kártya már 'Done' oszlopban van")
return True
# 5. Kártya mozgatása "Done" oszlopba
print("\n4. Kártya mozgatása 'Done' oszlopba...")
if move_card_to_column(card_info["card_id"], done_column_id):
print(" ✓ Sikeresen áthelyezve 'Done' oszlopba")
return True
else:
print(" ✗ Hiba a 'Done' oszlopba mozgatás közben")
return False
def main():
"""Fő függvény - argumentum alapján végrehajtja a mozgatást"""
if len(sys.argv) > 1:
action = sys.argv[1].lower()
if action == "inprogress":
return move_card_to_in_progress()
elif action == "done":
return move_card_to_done()
elif action == "both":
success1 = move_card_to_in_progress()
if success1:
time.sleep(2)
return move_card_to_done()
return False
else:
print(f"Ismeretlen művelet: {action}")
print("Használat: python3 move_card_2.py [inprogress|done|both]")
return False
else:
# Alapértelmezett: csak "In Progress" mozgatás
print("Nincs argumentum megadva, alapértelmezett: 'In Progress' mozgatás")
return move_card_to_in_progress()
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@@ -0,0 +1,224 @@
import asyncio
import logging
import datetime
import random
import sys
import json
from sqlalchemy import text, func, update, case
from app.database import AsyncSessionLocal
from app.models.vehicle_definitions import VehicleModelDefinition
from app.models.asset import AssetCatalog
from app.services.ai_service import AIService
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] Vehicle-Alchemist-Pro: %(message)s', stream=sys.stdout)
logger = logging.getLogger("Vehicle-Robot-3-Alchemist-Pro")
class TechEnricher:
"""
Vehicle Robot 3: Alchemist Pro (Atomi Zárolás Patch)
Tiszta GPU fókusz: Csak az AI elemzésre és adategyesítésre koncentrál.
Nincs felesleges webkeresés. Szigorú Sane-Check.
"""
def __init__(self):
self.max_attempts = 5
self.daily_ai_limit = int(os.getenv("AI_DAILY_LIMIT", "10000"))
self.ai_calls_today = 0
self.last_reset_date = datetime.date.today()
def check_budget(self) -> bool:
if datetime.date.today() > self.last_reset_date:
self.ai_calls_today = 0
self.last_reset_date = datetime.date.today()
return self.ai_calls_today < self.daily_ai_limit
ddef is_data_sane(self, data: dict, base_info: dict) -> bool:
""" Szigorított, de intelligens AI Hallucináció szűrő """
if not data:
logger.warning("Sane-check: Teljesen üres AI válasz.")
return False
try:
# 1. Alapvető fizikai korlátok vizsgálata (csak az AI adatokon)
ai_ccm = int(data.get("ccm", 0) or 0)
ai_kw = int(data.get("kw", 0) or 0)
v_class = base_info.get("v_type", "car")
if ai_ccm > 18000:
logger.warning(f"Sane-check bukás: Irreális CCM érték ({ai_ccm})")
return False
if ai_kw > 1500 and v_class != "truck":
logger.warning(f"Sane-check bukás: Irreális KW érték ({ai_kw})")
return False
# 2. KOMBINÁLT Adat teljesség vizsgálata (RDW + AI)
# Ha az RDW tudja, akkor nem baj, ha az AI nem találta meg!
merged_kw = base_info.get('rdw_kw') or ai_kw
merged_ccm = base_info.get('rdw_ccm') or ai_ccm
fuel = data.get("fuel_type", base_info.get("rdw_fuel", "")).lower()
# Ha még kombinálva sincs meg a KW
if merged_kw == 0:
logger.warning("Sane-check figyelmeztetés: Hiányzó KW (se RDW, se AI). Engedélyezve részleges adatként.")
# Nem térünk vissza False-al, inkább mentsük el, amit eddig tudunk!
# Ha még kombinálva sincs meg a CCM (és nem elektromos)
if merged_ccm == 0 and "electric" not in fuel and "elektric" not in fuel and v_class != "trailer":
logger.warning("Sane-check figyelmeztetés: Hiányzó CCM egy belsőégésű motornál. Engedélyezve részleges adatként.")
# Ezt is átengedjük, hogy kitörjünk a végtelen hurokból.
return True
except Exception as e:
logger.error(f"Sane check hiba: {e}")
return False
async def process_single_record(self, db, record_id: int, base_info: dict, current_attempts: int):
try:
logger.info(f"🧠 AI dúsítás indul: {base_info['make']} {base_info['m_name']}")
# 1. LÉPÉS: AI Hívás (Rábízzuk az adatokat a modellre)
ai_data = await AIService.get_clean_vehicle_data(
base_info['make'],
base_info['m_name'],
base_info
)
# 2. LÉPÉS: Validáció (Ha az AI rossz adatot ad, NEM megyünk ki a webre, hanem dobjuk az aktát!)
if not ai_data or not self.is_data_sane(ai_data, base_info):
raise ValueError("Az AI hiányos adatot adott vissza vagy hallucinált.")
# 3. LÉPÉS: HIBRID MERGE (Az RDW adatok felülbírálják az AI-t a hatósági paramétereknél)
final_kw = base_info['rdw_kw'] if base_info['rdw_kw'] > 0 else (ai_data.get("kw") or 0)
final_ccm = base_info['rdw_ccm'] if base_info['rdw_ccm'] > 0 else (ai_data.get("ccm") or 0)
# Üzemanyag tisztítása
fuel_rdw = base_info.get('rdw_fuel', '')
final_fuel = fuel_rdw if fuel_rdw and fuel_rdw != "Unknown" else ai_data.get("fuel_type", "petrol")
final_engine = base_info['rdw_engine'] if base_info['rdw_engine'] else ai_data.get("engine_code", "Unknown")
final_euro = base_info['rdw_euro'] or ai_data.get("euro_classification")
final_cylinders = base_info['rdw_cylinders'] or ai_data.get("cylinders")
# 4. LÉPÉS: Mentés az Arany Katalógusba
clean_model = str(ai_data.get("marketing_name", base_info['m_name']))[:50].upper()
cat_stmt = text("""
INSERT INTO data.vehicle_catalog
(master_definition_id, make, model, power_kw, engine_capacity, fuel_type, factory_data)
VALUES (:m_id, :make, :model, :kw, :ccm, :fuel, :factory)
RETURNING id;
""")
await db.execute(cat_stmt, {
"m_id": record_id,
"make": base_info['make'].upper(),
"model": clean_model,
"kw": final_kw,
"ccm": final_ccm,
"fuel": final_fuel,
"factory": json.dumps(ai_data)
})
# 5. LÉPÉS: Staging tábla (VMD) lezárása
await db.execute(
update(VehicleModelDefinition)
.where(VehicleModelDefinition.id == record_id)
.values(
status="gold_enriched",
engine_capacity=final_ccm,
power_kw=final_kw,
fuel_type=final_fuel,
engine_code=final_engine,
euro_classification=final_euro,
cylinders=final_cylinders,
specifications=ai_data, # Elmentjük az AI teljes outputját a mestertáblába is
updated_at=func.now()
)
)
await db.commit()
logger.info(f"✨ ARANY REKORD KÉSZ: {base_info['make'].upper()} {clean_model}")
self.ai_calls_today += 1
except Exception as e:
await db.rollback()
logger.warning(f"⚠️ Alkimista hiba ({base_info['make']} {base_info['m_name']}): {e}")
# Visszaküldés a sorba vagy felfüggesztés
new_status = 'suspended' if current_attempts + 1 >= self.max_attempts else 'unverified'
await db.execute(
update(VehicleModelDefinition)
.where(VehicleModelDefinition.id == record_id)
.values(
attempts=current_attempts + 1,
last_error=str(e)[:200],
status=new_status,
updated_at=func.now()
)
)
await db.commit()
if new_status == 'unverified':
logger.info("♻️ Akta visszaküldve a Robot-2-nek (Kutató).")
async def run(self):
logger.info(f"🚀 Alchemist Pro HIBRID ONLINE (Atomi Zárolás Patch)")
while True:
if not self.check_budget():
logger.warning("💸 Napi AI limit kimerítve! Pihenés...")
await asyncio.sleep(3600); continue
try:
async with AsyncSessionLocal() as db:
# ATOMI ZÁROLÁS (A "Szent Grál" a race condition ellen)
# A Robot-1 (ACTIVE) és a Robot-2 (awaiting_ai_synthesis) aktáit is felveszi!
query = text("""
UPDATE data.vehicle_model_definitions
SET status = 'ai_synthesis_in_progress'
WHERE id = (
SELECT id FROM data.vehicle_model_definitions
WHERE status IN ('awaiting_ai_synthesis', 'ACTIVE')
AND attempts < :max_attempts
ORDER BY
CASE WHEN status = 'awaiting_ai_synthesis' THEN 1 ELSE 2 END,
priority_score DESC
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING id, make, marketing_name, vehicle_class, power_kw, engine_capacity,
fuel_type, engine_code, euro_classification, cylinders, raw_search_context, attempts;
""")
result = await db.execute(query, {"max_attempts": self.max_attempts})
task = result.fetchone()
await db.commit()
if task:
# Szétbontjuk a lekérdezett rekordot a base_info dict-be
r_id = task[0]
base_info = {
"make": task[1], "m_name": task[2], "v_type": task[3] or "car",
"rdw_kw": task[4] or 0, "rdw_ccm": task[5] or 0,
"rdw_fuel": task[6] or "petrol", "rdw_engine": task[7] or "",
"rdw_euro": task[8], "rdw_cylinders": task[9],
"web_context": task[10] or ""
}
attempts = task[11]
# Külön adatbázis kapcsolat a feldolgozáshoz (hosszú AI hívás miatt)
async with AsyncSessionLocal() as process_db:
await self.process_single_record(process_db, r_id, base_info, attempts)
# GPU hűtés / Ollama rate limit
await asyncio.sleep(random.uniform(1.5, 3.5))
else:
logger.info("😴 Nincs feldolgozandó akta, az Alkimista pihen...")
await asyncio.sleep(15)
except Exception as e:
logger.error(f"💀 Kritikus hiba a főciklusban: {e}")
await asyncio.sleep(10)
if __name__ == "__main__":
import os # Import az AI limit környezeti változóhoz
asyncio.run(TechEnricher().run())

View File

View File

@@ -0,0 +1,105 @@
# Vehicle Robot Ecosystem - Teljes technikai audit jelentés
**Audit dátum:** 2026-03-12
**Gitea kártya:** #69
**Auditáló:** Főmérnök / Rendszerauditőr
## 1. Áttekintés
A `backend/app/workers/vehicle/` könyvtárban 15 fájl található, melyek egy 5 szintű (04) robotcsővezetéket alkotnak. A pipeline célja a járművek technikai adatainak automatikus felfedezése, gyűjtése, kutatása, AIalapú dúsítása és végül a valós eszközök (Asset) VINalapú hitelesítése. A robotok önállóan, aszinkron üzemmódban futnak, és az adatbázis rekordjainak státuszmezőin keresztül kommunikálnak (statusdriven pipeline).
## 2. Fájllista
| Fájl | Szint | Rövid leírás |
|------|------|--------------|
| `vehicle_robot_0_discovery_engine.py` | 0 | Őrkutya (watchdog), differenciális RDW szinkron, havonta teljes adatbázis letöltés |
| `vehicle_robot_0_gb_discovery.py` | 0 | Brit (GB) CSV feldolgozás, `gb_catalog_discovery` tábla feltöltése |
| `vehicle_robot_0_strategist.py` | 0 | Piaci priorítás számítása (RDW darabszám alapján) |
| `vehicle_robot_1_catalog_hunter.py` | 1 | RDW APIból technikai adatok kinyerése, `vehicle_model_definitions` táblába írás |
| `vehicle_robot_1_gb_hunter.py` | 1 | DVLA API (GB) lekérdezés, `vehicle_model_definitions` táblába írás |
| `vehicle_robot_1_2_nhtsa_fetcher.py` | 1.2 | NHTSA API (USA) csak EU márkákra szűrve |
| `vehicle_robot_1_4_bike_hunter.py` | 1.4 | NHTSA API motorok |
| `vehicle_robot_1_5_heavy_eu.py` | 1.5 | RDW API nehézgépjárművek (teher, busz, lakóautó) |
| `vehicle_robot_2_researcher.py` | 2 | DuckDuckGo keresés, strukturált kontextus előállítása AI számára |
| `vehicle_robot_3_alchemist_pro.py` | 3 | AIalapú adategyesítés (RDW + AI), validáció, `gold_enriched` státusz |
| `vehicle_robot_4_vin_auditor.py` | 4 | Asset VIN hitelesítés AI segítségével |
| `mapping_rules.py` | | Forrásmezők leképezése (jelenleg **nincs használatban**) |
| `mapping_dictionary.py` | | Szinonimák normalizálása (jelenleg **nincs használatban**) |
| `vehicle_data_loader.py` | | Külső JSON források betöltése `vehicle.reference_lookup` táblába |
| `robot_report.py` | | Diagnosztikai dashboard, statisztikák megjelenítése |
## 3. Állapotgép (State Machine) térkép
A következő táblázat a robotok által keresett és beállított státuszokat összegzi. A sorrend a pipeline természetes folyását tükrözi.
### 3.1. `vehicle.catalog_discovery` tábla
| Robot (fájl) | Keresett státusz (`WHERE`) | Beállított státusz (`SET` / `INSERT`) | Megjegyzés |
|--------------|----------------------------|---------------------------------------|------------|
| `0_discovery_engine` | `processing` | `pending` | Őrkutya: beragadt feladatok visszaállítása |
| `0_discovery_engine` | | `pending` (új rekord) | Differenciális szinkron: csak ha nincs `gold_enriched` a `vehicle_model_definitions`ben |
| `0_strategist` | `NOT IN ('processed', 'in_progress')` | `pending` (prioritás frissítés) | Csak még nem feldolgozott rekordok |
| `1_catalog_hunter` | `pending` | `processing``processed` | Atomizált zárolás (`SKIP LOCKED`) |
| `1_gb_hunter` | `pending` (gb_catalog_discovery) | `processing``processed` / `invalid_vrm` | DVLA API kvótakezeléssel |
| `1_2_nhtsa_fetcher` | | `pending` (új rekord) | Csak EU márkákhoz, `USA_IMPORT` piac |
| `1_4_bike_hunter` | | `pending` (új rekord) | Motorok, `USA_IMPORT` piac |
| `1_5_heavy_eu` | | `pending` (új rekord) | Nehézgépjárművek, `EU` piac |
### 3.2. `vehicle.vehicle_model_definitions` tábla
| Robot (fájl) | Keresett státusz (`WHERE`) | Beállított státusz (`SET` / `INSERT`) | Megjegyzés |
|--------------|----------------------------|---------------------------------------|------------|
| `0_discovery_engine` | `research_in_progress`, `ai_synthesis_in_progress` (2 órás timeout) | `unverified`, `awaiting_ai_synthesis` | Őrkutya: beragadt AI feladatok visszaállítása |
| `1_catalog_hunter` | | `ACTIVE` (új rekord) | `ON CONFLICT DO NOTHING` (make, normalized_name, variant_code, version_code, fuel_type) |
| `1_gb_hunter` | | `ACTIVE` (új rekord) | `ON CONFLICT DO NOTHING` |
| `2_researcher` | `unverified`, `awaiting_research`, `ACTIVE` | `research_in_progress``awaiting_ai_synthesis` (siker) / `unverified` (újra) / `suspended_research` (max próbálkozás) | Atomizált zárolás, kvótakezelés (DVLA) |
| `3_alchemist_pro` | `awaiting_ai_synthesis`, `ACTIVE` | `ai_synthesis_in_progress``gold_enriched` (siker) / `manual_review_needed` (max próbálkozás) / `unverified` (vissza) | AI hívás, hibrid merge (RDW + AI), validáció |
| `0_discovery_engine` (diff sync) | `gold_enriched` | | **Védelem:** a `gold_enriched` rekordok kihagyása a felfedezésből |
### 3.3. `vehicle.gb_catalog_discovery` tábla
| Robot (fájl) | Keresett státusz (`WHERE`) | Beállított státusz (`SET` / `INSERT`) |
|--------------|----------------------------|---------------------------------------|
| `0_gb_discovery` | | `pending` (új rekord) csak ha nincs `gold_enriched` a `vehicle_model_definitions`ben |
| `1_gb_hunter` | `pending` | `processing``processed` / `invalid_vrm` |
### 3.4. `vehicle.assets` tábla
| Robot (fájl) | Keresett állapot (`WHERE`) | Beállított státusz (`SET`) |
|--------------|----------------------------|----------------------------|
| `4_vin_auditor` | `is_verified = false AND vin IS NOT NULL` | `audit_in_progress``active` (siker) / `audit_failed` (hiba) |
## 4. Logikai összefüggések
### 4.1. Orchestráció
Nincs központi orchestrator. A robotok **párhuzamosan futnak**, és az adatbázis rekordjainak státuszait **közös munkamemóriaként** használják. A folyamat láncolata:
```
catalog_discovery (pending)
→ robot 1.x hunter (processed)
→ vehicle_model_definitions (ACTIVE)
→ robot 2 researcher (awaiting_ai_synthesis)
→ robot 3 alchemist (gold_enriched)
```
A `gold_enriched` státuszú rekordok **védettek**: a `0_discovery_engine` és `0_gb_discovery` nem veszi őket fel újra.
### 4.2. Mapping réteg
A `mapping_rules.py` és `mapping_dictionary.py` fájlok **nincsenek integrálva** a robotokba. A `vehicle_data_loader.py` saját, forrásspecifikus leképezést alkalmaz, de a mapping fájlokat nem importálja. Ez a réteg jelenleg kihasználatlan.
### 4.3. Atomizált zárolás és kvótakezelés
A hunterek és kutatók `FOR UPDATE SKIP LOCKED` zárolást használnak, így elkerülhető a race condition. A külső APIk (DVLA, DuckDuckGo) kvótakezeléssel rendelkeznek (`QuotaManager` osztály).
## 5. Biztonsági és integritási ellenőrzés
### 5.1. `is_manual` védelem hiánya
A **teljes kódbázisban egyetlen fájlban sem** található `is_manual` mezőre vagy „manual” kulcsszóra épülő védelem. A robotok csak a `gold_enriched` státusz alapján kerülik a felülírást. **Kockázat:** manuálisan bevitt adatok (pl. admin által javított technikai specifikációk) felülírhatók, ha a rekord státusza nem `gold_enriched`.
### 5.2. Egyéb védelmi mechanizmusok
- `ON CONFLICT DO NOTHING` / `ON CONFLICT DO UPDATE` csak bizonyos egyedi kulcsokon (pl. make, normalized_name, …).
- `0_discovery_engine` differenciális szinkronja kihagyja a `gold_enriched` rekordokat.
- `0_strategist` nem módosít `processed` vagy `in_progress` státuszú rekordokat.
## 6. Következtetések
1. **A robotökoszisztéma jól strukturált**, atomizált zárolással, kvótakezeléssel és hibatűréssel.
2. **A mapping réteg hiányzik** a `mapping_rules.py` és `mapping_dictionary.py` fájlok nincsenek használatban.
3. **Kritikus biztonsági rés:** nincs `is_manual` védelem. A #27, #28, #29 kártyákhoz kapcsolódó beavatkozásoknál ezt figyelembe kell venni.
4. **Állapotgép áttekinthető**, a státuszok logikusan lépnek egymás után. A `gold_enriched` státusz jelenti a végső védelmet.
## 7. Javaslatok a #27, #28, #29 kártyákhoz
- **#27 (Mapping integráció):** Kapcsoljuk be a `mapping_rules.py`t a `vehicle_data_loader`ben, majd terjeszszük ki a hunterekre.
- **#28 (Manual védelem):** Vezessünk be egy `is_manual` (boolean) mezőt a `vehicle_model_definitions` táblában, és a robotok minden írása előtt ellenőrizzük (`WHERE is_manual = false`).
- **#29 (Pipeline monitorozás):** A `robot_report.py` kiegészítése valósidejű státuszátmenetek grafikonjával és riasztásokkal.
---
*Jelentés készült a `backend/app/workers/vehicle/` könyvtár 15 fájljának teljes kódauditja alapján. Minden állítás kódrészletekre támaszkodik.*

View File

@@ -0,0 +1,152 @@
# Codebase Audit Ledger (#42)
*Generated: 2026-03-22 11:28:32*
*Total files scanned: 240*
## 📋 Audit Checklist
Check each file after audit completion. Use this ledger to track progress.
## API Endpoints (`backend/app/api_endpoints/...`)
- [ ] `api/deps.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'" [MEGTART]: Függőségi segédmodul, kritikus a biztonsághoz
- [ ] `api/recommend.py` - No docstring or definitions found [MEGTART]: Szigorú RBAC bevezetve, zárt ökoszisztéma megkövetelve
- [ ] `api/v1/api.py` - No docstring or definitions found [MEGTART]: Fő API router összekapcsoló, nem tartalmaz végpontokat
- [ ] `api/v1/endpoints/admin.py` - Classes: ConfigUpdate, OdometerStatsResponse, ManualOverrideRequest [MEGTART]: Védett admin végpontok, RBAC ellenőrzéssel
- [ ] `api/v1/endpoints/analytics.py` - "Analytics API endpoints for TCO (Total Cost of Ownership) dashboard." [MEGTART]: Szigorú RBAC bevezetve, zárt ökoszisztéma megkövetelve
- [ ] `api/v1/endpoints/assets.py` - No docstring or definitions found [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/auth.py` - Classes: VerifyEmailRequest [MEGTART]: Autentikációs végpontok, nem igényel további védelmet
- [ ] `api/v1/endpoints/billing.py` - No docstring or definitions found [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/catalog.py` - No docstring or definitions found [MEGTART]: Szigorú RBAC bevezetve, zárt ökoszisztéma megkövetelve
- [ ] `api/v1/endpoints/documents.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'" [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/evidence.py` - No docstring or definitions found [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/expenses.py` - Classes: ExpenseCreate [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/finance_admin.py` - "Finance Admin API endpoints for managing Issuers with strict RBAC protection." [MEGTART]: Strict RBAC védelme van, más Depends függőséggel
- [ ] `api/v1/endpoints/gamification.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'" [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/notifications.py` - No docstring or definitions found [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/organizations.py` - No docstring or definitions found [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/providers.py` - No docstring or definitions found [MEGTART]: Szigorú RBAC bevezetve, zárt ökoszisztéma megkövetelve
- [ ] `api/v1/endpoints/reports.py` - No docstring or definitions found [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/search.py` - No docstring or definitions found [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/security.py` - "Dual Control (Négy szem elv) API végpontok." [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/services.py` - No docstring or definitions found [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/social.py` - No docstring or definitions found [MEGTART]: Szigorú RBAC bevezetve, zárt ökoszisztéma megkövetelve
- [ ] `api/v1/endpoints/system_parameters.py` - No docstring or definitions found [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/translations.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'" [MEGTART]: Fordítási végpontok, védve (hibás scanner eredmény)
- [ ] `api/v1/endpoints/users.py` - No docstring or definitions found [MEGTART]: Védett végpontok, megfelelő RBAC
- [ ] `api/v1/endpoints/vehicles.py` - "Jármű értékelési végpontok a Social 1 modulhoz." [MEGTART]: Védett végpontok, megfelelő RBAC
## Core (`backend/app/core/...`)
- [ ] `core/config.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `core/email.py` - No docstring or definitions found
- [ ] `core/i18n.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `core/rbac.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `core/scheduler.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `core/security.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `core/validators.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
## Models (`backend/app/models/...`)
- [ ] `models/audit.py` - No docstring or definitions found
- [ ] `models/core_logic.py` - Classes: SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
- [ ] `models/gamification/gamification.py` - Classes: PointRule, LevelConfig, PointsLedger, UserStats, Badge (+3 more)
- [ ] `models/identity/address.py` - Classes: GeoPostalCode, GeoStreet, GeoStreetType, Address, Rating
- [ ] `models/identity/identity.py` - Classes: UserRole, Person, User, Wallet, VerificationToken (+3 more)
- [ ] `models/identity/registry.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `models/identity/security.py` - Classes: ActionStatus, PendingAction
- [ ] `models/identity/social.py` - Classes: ModerationStatus, SourceType, ServiceProvider, Vote, Competition (+2 more)
- [ ] `models/marketplace/finance.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `models/marketplace/logistics.py` - Classes: LocationType, Location
- [ ] `models/marketplace/organization.py` - Classes: OrgType, OrgUserRole, Organization, OrganizationFinancials, OrganizationMember (+2 more)
- [ ] `models/marketplace/payment.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `models/marketplace/service.py` - Classes: ServiceStatus, ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging (+1 more)
- [ ] `models/marketplace/service_request.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `models/marketplace/staged_data.py` - Classes: StagedVehicleData, ServiceStaging, DiscoveryParameter
- [ ] `models/reference_data.py` - Classes: ReferenceLookup
- [ ] `models/system/audit.py` - Classes: SecurityAuditLog, OperationalLog, ProcessLog, LedgerEntryType, WalletType (+2 more)
- [ ] `models/system/document.py` - Classes: Document
- [ ] `models/system/legal.py` - Classes: LegalDocument, LegalAcceptance
- [ ] `models/system/system.py` - Classes: ParameterScope, SystemParameter, InternalNotification, SystemServiceStaging
- [ ] `models/system/translation.py` - Classes: Translation
- [ ] `models/vehicle/asset.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `models/vehicle/external_reference.py` - Classes: ExternalReferenceLibrary
- [ ] `models/vehicle/external_reference_queue.py` - Classes: ExternalReferenceQueue
- [ ] `models/vehicle/history.py` - Classes: LogSeverity, AuditLog
- [ ] `models/vehicle/motorcycle_specs.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `models/vehicle/vehicle.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `models/vehicle/vehicle_definitions.py` - Classes: VehicleType, FeatureDefinition, VehicleModelDefinition, ModelFeatureMap
## Other (`backend/app/other/...`)
- [ ] `admin_ui.py` - No docstring or definitions found
- [ ] `database.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `db/base.py` - No docstring or definitions found
- [ ] `db/base_class.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `db/middleware.py` - No docstring or definitions found
- [ ] `db/session.py` - No docstring or definitions found
- [ ] `main.py` - No docstring or definitions found
- [ ] `test_billing_engine.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `test_hierarchical.py` - "Gyors teszt a hierarchikus paraméterekhez."
## Schemas (`backend/app/schemas/...`)
- [ ] `schemas/admin.py` - No docstring or definitions found
- [ ] `schemas/admin_security.py` - Classes: PendingActionResponse, SecurityStatusResponse
- [ ] `schemas/analytics.py` - "Analytics Pydantic schemas for TCO (Total Cost of Ownership) API responses." - Classes: TCOResponse, TCOSummaryStats, TCOSummaryResponse, TCOErrorResponse, Config (+1 more)
- [ ] `schemas/asset.py` - Classes: AssetCatalogResponse, AssetResponse, AssetCreate
- [ ] `schemas/asset_cost.py` - Classes: AssetCostBase, AssetCostCreate, AssetCostResponse
- [ ] `schemas/auth.py` - Classes: DocumentDetail, ICEContact, UserLiteRegister, UserKYCComplete, Token
- [ ] `schemas/evidence.py` - Classes: RegistrationDocumentExtracted, OcrResponse, Config
- [ ] `schemas/finance.py` - "Finance-related Pydantic schemas for API requests and responses." - Classes: IssuerType, IssuerResponse, IssuerUpdate
- [ ] `schemas/fleet.py` - Classes: EventCreate, TCOStats
- [ ] `schemas/gamification.py` - Classes: SeasonResponse, UserStatResponse, LeaderboardEntry, Config, Config (+1 more)
- [ ] `schemas/organization.py` - Classes: ContactCreate, CorpOnboardIn, CorpOnboardResponse
- [ ] `schemas/security.py` - "Dual Control (Négy szem elv) sémák." - Classes: PendingActionCreate, PendingActionApprove, PendingActionReject, UserLite, PendingActionResponse (+3 more)
- [ ] `schemas/service.py` - Classes: ContactCreate, CorpOnboardIn, CorpOnboardResponse
- [ ] `schemas/service_hunt.py` - Classes: ServiceHuntRequest
- [ ] `schemas/social.py` - Classes: ServiceProviderBase, ServiceProviderCreate, ServiceProviderResponse, ServiceReviewBase, ServiceReviewCreate (+5 more)
- [ ] `schemas/system.py` - Classes: SystemParameterBase, SystemParameterCreate, SystemParameterUpdate, SystemParameterResponse
- [ ] `schemas/token.py` - Classes: Token, TokenData
- [ ] `schemas/user.py` - Classes: UserBase, UserResponse, UserUpdate
- [ ] `schemas/vehicle.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `schemas/vehicle_categories.py` - No docstring or definitions found
## Scripts (`backend/app/scripts/...`)
- [ ] `scripts/audit_scanner.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `scripts/check_mappers.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `scripts/check_robots_integrity.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `scripts/check_tables.py` - "Check tables in system and gamification schemas."
- [ ] `scripts/correction_tool.py` - No docstring or definitions found
- [ ] `scripts/fix_imports_diag.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `scripts/link_catalog_to_mdm.py` - No docstring or definitions found
- [ ] `scripts/monitor_crawler.py` - No docstring or definitions found
- [ ] `scripts/morning_report.py` - No docstring or definitions found
- [ ] `scripts/move_tables.py` - "Move tables from system schema to gamification schema." [TÖRÖLHETŐ] (Soft delete kész, archiválva)
- [ ] `scripts/rename_deprecated.py` - "Rename tables in system schema to deprecated to avoid extra detection." [TÖRÖLHETŐ] (Soft delete kész, archiválva)
- [ ] `scripts/seed_system_params.py` - No docstring or definitions found
- [ ] `scripts/seed_v1_9_system.py` - No docstring or definitions found [REFAKTORÁL] (Később modernizálandó seed szkript)
- [ ] `scripts/smart_admin_audit.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `scripts/sync_engine.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `scripts/sync_python_models_generator.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `scripts/unified_db_audit.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `scripts/unified_db_sync.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'"
- [ ] `scripts/unified_db_sync_1.0.py` - "Error reading file: 'FunctionDef' object has no attribute 'parent'" [TÖRÖLHETŐ] (Soft delete kész, archiválva)
## Services (`backend/app/services/...`)
- [ ] services/ai_ocr_service.py - [MEGTART] Modern OCR service, part of AI pipeline.
- [ ] services/ai_service.py - [MEGTART] Uses os.getenv; should use ConfigService. (os.getenv és print() hívások javítva)
- [ ] services/ai_service1.1.0.py - [REFAKTORÁL] Versioned AI service with os.getenv; consider merging with ai_service.py.
- [ ] services/ai_service_googleApi_old.py - [TÖRÖLHETŐ] Deprecated old version; remove. (Soft delete kész)
- [ ] services/analytics_service.py - [MEGTART] Analytics service; scanner error due to complex AST.
- [ ] services/asset_service.py - [MEGTART] Asset management service.
- [ ] services/auth_service.py - [MEGTART] Authentication service.
- [ ] services/billing_engine.py - [MEGTART] Contains print statements; replace with logger. (print() hívások javítva)
- [ ] services/config_service.py - [MEGTART] Core configuration service.
- [ ] services/cost_service.py - [MEGTART] Cost calculation service.
- [ ] services/deduplication_service.py - [MEGTART] Deduplication logic; scanner error.
- [ ] services/document_service.py - [MEGTART] Document handling service.
- [ ] services/dvla_service.py - [MEGTART] DVLA API integration.
- [ ] services/email_manager.py - [MEGTART] Uses os.getenv; migrate to ConfigService. (os.get

View File

@@ -14,7 +14,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
COPY requirements.txt .
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
pip install --no-cache-dir -r requirements.txt && \
pip install playwright && \
playwright install --with-deps chromium
COPY . .

186
backend/app/admin_ui.py Normal file
View File

@@ -0,0 +1,186 @@
#!/usr/bin/env python3
import asyncio
import json
import urllib.parse
import streamlit as st
from sqlalchemy import text
from app.database import AsyncSessionLocal
# Streamlit oldal alapbeállításai
st.set_page_config(
page_title="Service Finder - HITL Adattisztító",
page_icon="🔧",
layout="wide"
)
# --- ADATBÁZIS MŰVELETEK (Hardened Stateless Logic) ---
async def get_review_vehicle():
"""Lekérdez egy javításra váró járművet izolált sessionben."""
async with AsyncSessionLocal() as session:
try:
query = text("""
SELECT id, make, marketing_name, year_from, fuel_type,
raw_api_data, raw_search_context,
trim_level, body_type, power_kw, engine_capacity,
specifications, last_error
FROM vehicle.vehicle_model_definitions
WHERE status = 'manual_review_needed'
ORDER BY priority_score DESC
LIMIT 1
""")
result = await session.execute(query)
row = result.fetchone()
if not row:
return None
vehicle = dict(row._mapping)
# URL bányászat a JSON adatokból
source_url = None
if vehicle.get('raw_api_data'):
api_data = vehicle['raw_api_data']
if isinstance(api_data, str):
try: api_data = json.loads(api_data)
except: api_data = {}
source_url = api_data.get('url') or api_data.get('source_url') or api_data.get('link')
vehicle['extracted_url'] = source_url
return vehicle
except Exception as e:
st.error(f"❌ Lekérdezési hiba: {e}")
return None
finally:
# Garantáljuk a session lezárását
await session.close()
async def update_vehicle_data(vehicle_id, updates, new_status):
"""Elmenti az adatokat és azonnal felszabadítja a hálózati erőforrásokat."""
session = AsyncSessionLocal()
try:
# Dinamikus SQL összeállítása
set_items = [f"{k} = :{k}" for k in updates.keys()]
set_clause = ", ".join(set_items)
sql = text(f"""
UPDATE vehicle.vehicle_model_definitions
SET status = :status, {set_clause}, updated_at = NOW()
WHERE id = :id
""")
params = {"status": new_status, "id": vehicle_id, **updates}
await session.execute(sql, params)
await session.commit()
return True
except Exception as e:
await session.rollback()
st.error(f"❌ Mentési hiba az adatbázisban: {e}")
return False
finally:
# KRITIKUS JAVÍTÁS: Explicit lezárás, hogy ne maradjon nyitott transport
await session.close()
# Itt kényszerítjük a kapcsolat-kezelőt a háttérben futó motor elengedésére
bind = session.bind
if bind:
await bind.dispose()
# --- UI LOGIKA ---
async def main_async():
st.title("🔧 HITL Adattisztító - Autó Adat Javítás")
# Adat betöltése a memóriába (ha üres)
if "current_vehicle" not in st.session_state or st.session_state.current_vehicle is None:
with st.spinner("Adatbázis szinkronizálása..."):
st.session_state.current_vehicle = await get_review_vehicle()
v = st.session_state.current_vehicle
if not v:
st.success("🎉 Minden jármű ellenőrizve!")
if st.button("🔄 Új lekérdezés"):
st.session_state.current_vehicle = None
st.rerun()
return
# Felület felépítése
st.header(f"🚗 {v['year_from'] or '????'} {v['make']} {v['marketing_name']}")
st.caption(f"DB ID: {v['id']} | Üzemanyag: {v['fuel_type'] or 'n/a'}")
# 3 oszlopos nézet
col_raw, col_source, col_edit = st.columns([1, 1, 1.2])
with col_raw:
st.subheader("📄 Robot Naplók")
if v['raw_api_data']:
with st.expander("Nyers JSON (API)", expanded=True):
st.json(v['raw_api_data'])
with st.expander("Keresési Környezet", expanded=False):
st.text_area("Talált szövegek", v['raw_search_context'] or "Nincs adat", height=400)
with col_source:
st.subheader("🔗 Eredeti Források")
if v['extracted_url']:
st.success("📍 Közvetlen adatlap linkje:")
st.markdown(f"### [FORRÁS MEGNYITÁSA ↗️]({v['extracted_url']})")
else:
st.warning("⚠️ Nincs közvetlen link.")
st.markdown("---")
st.write("**Segédeszközök:**")
search_q = urllib.parse.quote(f"{v['make']} {v['marketing_name']} {v['year_from'] or ''} specifications")
st.markdown(f"- [🔍 Google Keresés](https://www.google.com/search?q={search_q})")
us_query = urllib.parse.quote(f"{v['make']} {v['marketing_name']}")
us_url = f"https://www.google.com/search?q=site:ultimatespecs.com+{us_query}"
if v['specifications']:
with st.expander("Már meglévő specifikációk", expanded=True):
st.json(v['specifications'])
with col_edit:
st.subheader("✏️ Adatbevitel")
with st.form("hitl_form_v2", clear_on_submit=False):
trim = st.text_input("Trim / Felszereltség", value=v['trim_level'] or "")
body_opts = ["", "SEDAN", "HATCHBACK", "SUV", "ESTATE", "COUPE", "CONVERTIBLE", "VAN", "PICKUP", "MPV"]
curr_body = v['body_type'] if v['body_type'] in body_opts else ""
body = st.selectbox("Karosszéria", body_opts, index=body_opts.index(curr_body))
pwr = st.number_input("Teljesítmény (kW)", value=int(v['power_kw'] or 0))
cap = st.number_input("Hengerűrtartalom (cm³)", value=int(v['engine_capacity'] or 0))
st.markdown("---")
comment = st.text_area("Megjegyzés (második zsák adatai)", placeholder="További kiegészítő adatok...")
st.write("")
b1, b2, b3 = st.columns(3)
save_btn = b1.form_submit_button("💾 MENTÉS", type="primary")
skip_btn = b2.form_submit_button("⏭️ KIHAGYÁS")
reject_btn = b3.form_submit_button("🗑️ KUKA")
# Mentési logika
if save_btn:
updates = {
"trim_level": trim,
"body_type": body,
"power_kw": pwr,
"engine_capacity": cap,
"last_error": f"Manual fix OK. {comment}".strip()
}
with st.spinner("Véglegesítés..."):
if await update_vehicle_data(v['id'], updates, "published"):
st.session_state.current_vehicle = None
st.rerun()
if skip_btn:
st.session_state.current_vehicle = None
st.rerun()
if reject_btn:
if await update_vehicle_data(v['id'], {"last_error": "Manual rejection"}, "rejected"):
st.session_state.current_vehicle = None
st.rerun()
if __name__ == "__main__":
asyncio.run(main_async())

View File

@@ -1,132 +0,0 @@
from datetime import timedelta
from typing import Dict, Any
from fastapi import APIRouter, HTTPException
from app.core.config import settings
from app.core.security import create_token, decode_token
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/login")
def login(payload: Dict[str, Any]):
"""
payload:
{
"org_id": "<uuid>",
"login": "<username or email>",
"password": "<plain>"
}
"""
from app.db.session import get_conn
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("BEGIN;")
org_id = (payload.get("org_id") or "").strip()
login_id = (payload.get("login") or "").strip()
password = payload.get("password") or ""
if not org_id or not login_id or not password:
raise HTTPException(status_code=400, detail="org_id, login, password required")
# RLS miatt kötelező: org kontextus beállítás
cur.execute("SELECT set_config('app.tenant_org_id', %s, false);", (org_id,))
# account + credential
cur.execute(
"""
SELECT
a.account_id::text,
a.org_id::text,
a.username::text,
a.email::text,
c.password_hash,
c.is_active
FROM app.account a
JOIN app.account_credential c ON c.account_id = a.account_id
WHERE a.org_id = %s::uuid
AND (a.username = %s::citext OR a.email = %s::citext)
AND c.is_active = true
LIMIT 1;
""",
(org_id, login_id, login_id),
)
row = cur.fetchone()
if not row:
raise HTTPException(status_code=401, detail="Invalid credentials")
account_id, org_id_db, username, email, password_hash, cred_active = row
# Jelszó ellenőrzés pgcrypto-val: crypt(plain, stored_hash) = stored_hash
cur.execute("SELECT crypt(%s, %s) = %s;", (password, password_hash, password_hash))
ok = cur.fetchone()[0]
if not ok:
raise HTTPException(status_code=401, detail="Invalid credentials")
# MVP: role később membershipből; most fixen tenant_admin
role_code = "tenant_admin"
is_platform_admin = False
access = create_token(
{
"sub": account_id,
"org_id": org_id_db,
"role": role_code,
"is_platform_admin": is_platform_admin,
"type": "access",
},
settings.JWT_SECRET,
timedelta(minutes=settings.JWT_ACCESS_MINUTES),
)
refresh = create_token(
{
"sub": account_id,
"org_id": org_id_db,
"role": role_code,
"is_platform_admin": is_platform_admin,
"type": "refresh",
},
settings.JWT_SECRET,
timedelta(days=settings.JWT_REFRESH_DAYS),
)
conn.commit()
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer"}
except HTTPException:
conn.rollback()
raise
except Exception as e:
conn.rollback()
raise HTTPException(status_code=500, detail=str(e))
finally:
conn.close()
@router.post("/refresh")
def refresh_token(payload: Dict[str, Any]):
token = payload.get("refresh_token") or ""
if not token:
raise HTTPException(status_code=400, detail="refresh_token required")
try:
claims = decode_token(token, settings.JWT_SECRET)
if claims.get("type") != "refresh":
raise HTTPException(status_code=401, detail="Invalid refresh token type")
access = create_token(
{
"sub": claims.get("sub"),
"org_id": claims.get("org_id"),
"role": claims.get("role"),
"is_platform_admin": claims.get("is_platform_admin", False),
"type": "access",
},
settings.JWT_SECRET,
timedelta(minutes=settings.JWT_ACCESS_MINUTES),
)
return {"access_token": access, "token_type": "bearer"}
except Exception:
raise HTTPException(status_code=401, detail="Invalid or expired refresh token")

View File

@@ -127,7 +127,9 @@ def check_min_rank(role_key: str):
db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP
)
required_rank = ranks.get(role_key, 0)
# A DEFAULT_RANK_MAP nagybetűs kulcsokat vár, ezért átalakítjuk
role_key_upper = role_key.upper()
required_rank = ranks.get(role_key_upper, 0)
user_rank = payload.get("rank", 0)
if user_rank < required_rank:
@@ -137,3 +139,24 @@ def check_min_rank(role_key: str):
)
return True
return rank_checker
async def get_current_admin(
current_user: User = Depends(get_current_user)
) -> User:
"""
Csak admin/moderátor/superadmin szerepkörrel rendelkező felhasználók számára.
"""
# A UserRole Enum értékeit használjuk
allowed_roles = {
UserRole.superadmin,
UserRole.admin,
UserRole.region_admin,
UserRole.country_admin,
UserRole.moderator,
}
if current_user.role not in allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Nincs megfelelő jogosultságod (Admin/Moderátor)!"
)
return current_user

View File

@@ -3,14 +3,20 @@ from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from app.db.session import get_db
from app.api import deps
router = APIRouter()
# Secured endpoint: Closed premium ecosystem
@router.get("/provider/inbox")
async def provider_inbox(provider_id: str, db: AsyncSession = Depends(get_db)):
async def provider_inbox(
provider_id: str,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
""" Aszinkron szerviz-postaláda lekérdezés. """
query = text("""
SELECT * FROM data.service_profiles
SELECT * FROM marketplace.service_profiles
WHERE id = :p_id
""")
result = await db.execute(query, {"p_id": provider_id})

View File

@@ -2,7 +2,9 @@
from fastapi import APIRouter
from app.api.v1.endpoints import (
auth, catalog, assets, organizations, documents,
services, admin, expenses, evidence, social
services, admin, expenses, evidence, social, security,
billing, finance_admin, analytics, vehicles, system_parameters,
gamification, translations
)
api_router = APIRouter()
@@ -18,3 +20,10 @@ api_router.include_router(admin.router, prefix="/admin", tags=["Admin Control Ce
api_router.include_router(evidence.router, prefix="/evidence", tags=["Evidence & OCR (Robot 3)"])
api_router.include_router(expenses.router, prefix="/expenses", tags=["Fleet Expenses (TCO)"])
api_router.include_router(social.router, prefix="/social", tags=["Social & Leaderboard"])
api_router.include_router(security.router, prefix="/security", tags=["Dual Control (Security)"])
api_router.include_router(finance_admin.router, prefix="/finance/issuers", tags=["finance-admin"])
api_router.include_router(analytics.router, prefix="/analytics", tags=["Analytics"])
api_router.include_router(vehicles.router, prefix="/vehicles", tags=["Vehicles"])
api_router.include_router(system_parameters.router, prefix="/system/parameters", tags=["System Parameters"])
api_router.include_router(gamification.router, prefix="/gamification", tags=["Gamification"])
api_router.include_router(translations.router, prefix="/translations", tags=["i18n"])

View File

@@ -1,5 +1,5 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Body, BackgroundTasks
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text, delete
from typing import List, Any, Dict, Optional
@@ -7,20 +7,23 @@ from datetime import datetime, timedelta
from app.api import deps
from app.models.identity import User, UserRole # JAVÍTVA: Központi import
from app.models.system import SystemParameter
from app.models.system import SystemParameter, ParameterScope
from app.services.system_service import system_service
# JAVÍTVA: Security audit modellek
from app.models.audit import SecurityAuditLog, OperationalLog
from app.models import SecurityAuditLog, OperationalLog
# JAVÍTVA: Ezek a modellek a security.py-ból jönnek (ha ott vannak)
from app.models.security import PendingAction, ActionStatus
from app.models import PendingAction, ActionStatus
from app.services.security_service import security_service
from app.services.translation_service import TranslationService
from pydantic import BaseModel
from app.services.odometer_service import OdometerService
from pydantic import BaseModel, Field
from typing import Optional as Opt
class ConfigUpdate(BaseModel):
key: str
value: Any
scope_level: str = "global"
scope_level: ParameterScope = ParameterScope.GLOBAL
scope_id: Optional[str] = None
category: str = "general"
@@ -43,13 +46,13 @@ async def get_system_health(
stats = {}
# Adatbázis statisztikák (Nyers SQL marad, mert hatékony)
user_stats = await db.execute(text("SELECT subscription_plan, count(*) FROM data.users GROUP BY subscription_plan"))
user_stats = await db.execute(text("SELECT subscription_plan, count(*) FROM identity.users GROUP BY subscription_plan"))
stats["user_distribution"] = {row[0]: row[1] for row in user_stats}
asset_count = await db.execute(text("SELECT count(*) FROM data.assets"))
asset_count = await db.execute(text("SELECT count(*) FROM vehicle.assets"))
stats["total_assets"] = asset_count.scalar()
org_count = await db.execute(text("SELECT count(*) FROM data.organizations"))
org_count = await db.execute(text("SELECT count(*) FROM fleet.organizations"))
stats["total_organizations"] = org_count.scalar()
# JAVÍTVA: Biztonsági státusz az új SecurityAuditLog alapján
@@ -101,7 +104,7 @@ async def set_parameter(
admin: User = Depends(check_admin_access)
):
query = text("""
INSERT INTO data.system_parameters (key, value, scope_level, scope_id, category, last_modified_by)
INSERT INTO system.system_parameters (key, value, scope_level, scope_id, category, last_modified_by)
VALUES (:key, :val, :sl, :sid, :cat, :user)
ON CONFLICT (key, scope_level, scope_id)
DO UPDATE SET
@@ -122,6 +125,29 @@ async def set_parameter(
await db.commit()
return {"status": "success", "message": f"'{config.key}' frissítve."}
@router.get("/parameters/scoped", tags=["Dynamic Configuration"])
async def get_scoped_parameter(
key: str,
user_id: Optional[str] = None,
region_id: Optional[str] = None,
country_code: Optional[str] = None,
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""
Hierarchikus paraméterlekérdezés a következő prioritással:
User > Region > Country > Global.
"""
value = await system_service.get_scoped_parameter(
db, key, user_id, region_id, country_code, default=None
)
if value is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Paraméter '{key}' nem található a megadott scope-okban."
)
return {"key": key, "value": value}
@router.post("/translations/sync", tags=["System Utilities"])
async def sync_translations_to_json(
db: AsyncSession = Depends(deps.get_db),
@@ -129,3 +155,400 @@ async def sync_translations_to_json(
):
await TranslationService.export_to_json(db)
return {"message": "JSON fájlok frissítve."}
# ==================== SMART ODOMETER ADMIN API ====================
class OdometerStatsResponse(BaseModel):
vehicle_id: int
last_recorded_odometer: int
last_recorded_date: datetime
daily_avg_distance: float
estimated_current_odometer: float
confidence_score: float
manual_override_avg: Opt[float]
is_confidence_high: bool = Field(..., description="True ha confidence_score >= threshold")
class ManualOverrideRequest(BaseModel):
daily_avg: Opt[float] = Field(None, description="Napi átlagos kilométer (km/nap). Ha null, törli a manuális beállítást.")
@router.get("/odometer/{vehicle_id}", tags=["Smart Odometer"])
async def get_odometer_stats(
vehicle_id: int,
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""
Jármű kilométeróra statisztikáinak lekérése.
A rendszer automatikusan frissíti a statisztikákat, ha szükséges.
"""
# Frissítjük a statisztikákat
odometer_state = await OdometerService.update_vehicle_stats(db, vehicle_id)
if not odometer_state:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Jármű nem található ID: {vehicle_id}"
)
# Confidence threshold lekérése
confidence_threshold = await OdometerService.get_system_param(
db, 'ODOMETER_CONFIDENCE_THRESHOLD', 0.5
)
return OdometerStatsResponse(
vehicle_id=odometer_state.vehicle_id,
last_recorded_odometer=odometer_state.last_recorded_odometer,
last_recorded_date=odometer_state.last_recorded_date,
daily_avg_distance=float(odometer_state.daily_avg_distance),
estimated_current_odometer=float(odometer_state.estimated_current_odometer),
confidence_score=odometer_state.confidence_score,
manual_override_avg=float(odometer_state.manual_override_avg) if odometer_state.manual_override_avg else None,
is_confidence_high=odometer_state.confidence_score >= confidence_threshold
)
@router.patch("/odometer/{vehicle_id}", tags=["Smart Odometer"])
async def set_odometer_manual_override(
vehicle_id: int,
request: ManualOverrideRequest,
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_admin_access)
):
"""
Adminisztrátori manuális átlag beállítása a kilométeróra becsléshez.
Ha a user csal vagy hibás az adat, az admin ezzel felülírhatja az automatikus számítást.
"""
odometer_state = await OdometerService.set_manual_override(
db, vehicle_id, request.daily_avg
)
if not odometer_state:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Jármű nem található ID: {vehicle_id}"
)
action = "beállítva" if request.daily_avg is not None else "törölve"
return {
"status": "success",
"message": f"Manuális átlag {action}: {request.daily_avg} km/nap",
"vehicle_id": vehicle_id,
"manual_override_avg": odometer_state.manual_override_avg
}
@router.get("/ping", tags=["Admin Test"])
async def admin_ping(
current_user: User = Depends(deps.get_current_admin)
):
"""
Egyszerű ping végpont admin jogosultság ellenőrzéséhez.
"""
return {
"message": "Admin felület aktív",
"role": current_user.role.value if hasattr(current_user.role, "value") else current_user.role
}
@router.post("/users/{user_id}/ban", tags=["Admin Security"])
async def ban_user(
user_id: int,
reason: str = Body(..., embed=True),
current_admin: User = Depends(deps.get_current_admin),
db: AsyncSession = Depends(deps.get_db)
):
"""
Felhasználó tiltása (Ban Hammer).
- Megkeresi a usert (identity.users táblában).
- Ha nincs -> 404
- Ha a user.role == superadmin -> 403 (Saját magát/másik admint ne tiltson le).
- Állítja be a tiltást (is_active = False).
- Audit logba rögzíti a reason-t.
"""
from sqlalchemy import select
# 1. Keresd meg a usert
stmt = select(User).where(User.id == user_id)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User not found with ID: {user_id}"
)
# 2. Ellenőrizd, hogy nem superadmin-e
if user.role == UserRole.superadmin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot ban a superadmin user"
)
# 3. Tiltás beállítása
user.is_active = False
# Opcionálisan: banned_until mező kitöltése, ha létezik a modellben
# user.banned_until = datetime.now() + timedelta(days=30)
# 4. Audit log létrehozása
audit_log = SecurityAuditLog(
user_id=current_admin.id,
action="ban_user",
target_user_id=user_id,
details=f"User banned. Reason: {reason}",
is_critical=True,
ip_address="admin_api"
)
db.add(audit_log)
await db.commit()
return {
"status": "success",
"message": f"User {user_id} banned successfully.",
"reason": reason
}
@router.post("/marketplace/services/{staging_id}/approve", tags=["Marketplace Moderation"])
async def approve_staged_service(
staging_id: int,
current_admin: User = Depends(deps.get_current_admin),
db: AsyncSession = Depends(deps.get_db)
):
"""
Szerviz jóváhagyása a Piactéren (Kék Pipa).
- Megkeresi a marketplace.service_staging rekordot.
- Ha nincs -> 404
- Állítja a validation_level-t 100-ra, a status-t 'approved'-ra.
"""
from sqlalchemy import select
from app.models.staged_data import ServiceStaging
stmt = select(ServiceStaging).where(ServiceStaging.id == staging_id)
result = await db.execute(stmt)
staging = result.scalar_one_or_none()
if not staging:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Service staging record not found with ID: {staging_id}"
)
# Jóváhagyás
staging.validation_level = 100
staging.status = "approved"
# Audit log
audit_log = SecurityAuditLog(
user_id=current_admin.id,
action="approve_service",
target_staging_id=staging_id,
details=f"Service staging approved: {staging.service_name}",
is_critical=False,
ip_address="admin_api"
)
db.add(audit_log)
await db.commit()
return {
"status": "success",
"message": f"Service staging {staging_id} approved.",
"service_name": staging.service_name
}
# ==================== EPIC 10: ADMIN FRONTEND API ENDPOINTS ====================
from app.workers.service.validation_pipeline import ValidationPipeline
from app.models.marketplace.service import ServiceProfile
from app.models.gamification.gamification import UserStats
class LocationUpdate(BaseModel):
latitude: float = Field(..., ge=-90, le=90)
longitude: float = Field(..., ge=-180, le=180)
class PenaltyRequest(BaseModel):
penalty_level: int = Field(..., ge=-10, le=-1, description="Negatív szint (-1 a legkisebb, -10 a legnagyobb büntetés)")
reason: str = Field(..., min_length=5, max_length=500)
@router.post("/services/{service_id}/trigger-ai", tags=["AI Pipeline"])
async def trigger_ai_pipeline(
service_id: int,
background_tasks: BackgroundTasks,
current_admin: User = Depends(deps.get_current_admin),
db: AsyncSession = Depends(deps.get_db)
):
"""
AI Pipeline manuális indítása egy adott szerviz profilra.
A végpont azonnal visszatér, és a validációt háttérfeladatként futtatja.
"""
# Ellenőrizzük, hogy létezik-e a szerviz profil
stmt = select(ServiceProfile).where(ServiceProfile.id == service_id)
result = await db.execute(stmt)
profile = result.scalar_one_or_none()
if not profile:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Service profile not found with ID: {service_id}"
)
# Háttérfeladat hozzáadása
background_tasks.add_task(run_validation_pipeline, service_id)
# Audit log
audit_log = SecurityAuditLog(
user_id=current_admin.id,
action="trigger_ai_pipeline",
target_service_id=service_id,
details=f"AI pipeline manually triggered for service {service_id}",
is_critical=False,
ip_address="admin_api"
)
db.add(audit_log)
await db.commit()
return {
"status": "success",
"message": f"AI pipeline started for service {service_id}",
"service_name": profile.service_name,
"note": "Validation runs in background, check logs for results."
}
async def run_validation_pipeline(profile_id: int):
"""Háttérfeladat a ValidationPipeline futtatásához."""
try:
pipeline = ValidationPipeline()
success = await pipeline.run(profile_id)
logger = logging.getLogger("Service-AI-Pipeline")
if success:
logger.info(f"Pipeline successful for profile {profile_id}")
else:
logger.warning(f"Pipeline failed for profile {profile_id}")
except Exception as e:
logger.error(f"Pipeline error for profile {profile_id}: {e}")
@router.patch("/services/{service_id}/location", tags=["Service Management"])
async def update_service_location(
service_id: int,
location: LocationUpdate,
current_admin: User = Depends(deps.get_current_admin),
db: AsyncSession = Depends(deps.get_db)
):
"""
Szerviz térképes mozgatása (Koordináta frissítés).
A Nuxt Leaflet térkép drag-and-drop funkciójához használható.
"""
stmt = select(ServiceProfile).where(ServiceProfile.id == service_id)
result = await db.execute(stmt)
profile = result.scalar_one_or_none()
if not profile:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Service profile not found with ID: {service_id}"
)
# Frissítjük a koordinátákat
profile.latitude = location.latitude
profile.longitude = location.longitude
profile.updated_at = datetime.now()
# Audit log
audit_log = SecurityAuditLog(
user_id=current_admin.id,
action="update_service_location",
target_service_id=service_id,
details=f"Service location updated to lat={location.latitude}, lon={location.longitude}",
is_critical=False,
ip_address="admin_api"
)
db.add(audit_log)
await db.commit()
return {
"status": "success",
"message": f"Service location updated for {service_id}",
"latitude": location.latitude,
"longitude": location.longitude
}
@router.patch("/users/{user_id}/penalty", tags=["Gamification Admin"])
async def apply_gamification_penalty(
user_id: int,
penalty: PenaltyRequest,
current_admin: User = Depends(deps.get_current_admin),
db: AsyncSession = Depends(deps.get_db)
):
"""
Gamification büntetés kiosztása egy felhasználónak.
Negatív szintek alkalmazása a frissen létrehozott Gamification rendszerben.
"""
# Ellenőrizzük, hogy létezik-e a felhasználó
user_stmt = select(User).where(User.id == user_id)
user_result = await db.execute(user_stmt)
user = user_result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User not found with ID: {user_id}"
)
# Megkeressük a felhasználó gamification profilját (vagy létrehozzuk)
gamification_stmt = select(UserStats).where(UserStats.user_id == user_id)
gamification_result = await db.execute(gamification_stmt)
gamification = gamification_result.scalar_one_or_none()
if not gamification:
# Ha nincs profil, létrehozzuk alapértelmezett értékekkel
gamification = UserStats(
user_id=user_id,
level=0,
xp=0,
reputation_score=100,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(gamification)
await db.flush()
# Alkalmazzuk a büntetést (negatív szint módosítása)
# A level mező lehet negatív is a büntetések miatt
new_level = gamification.level + penalty.penalty_level
gamification.level = new_level
gamification.updated_at = datetime.now()
# Audit log
audit_log = SecurityAuditLog(
user_id=current_admin.id,
action="apply_gamification_penalty",
target_user_id=user_id,
details=f"Gamification penalty applied: level change {penalty.penalty_level}, reason: {penalty.reason}",
is_critical=False,
ip_address="admin_api"
)
db.add(audit_log)
await db.commit()
return {
"status": "success",
"message": f"Gamification penalty applied to user {user_id}",
"user_id": user_id,
"penalty_level": penalty.penalty_level,
"new_level": new_level,
"reason": penalty.reason
}

View File

@@ -0,0 +1,196 @@
"""
Analytics API endpoints for TCO (Total Cost of Ownership) dashboard.
"""
import logging
import uuid
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.api import deps
from app.schemas.analytics import TCOSummaryResponse, TCOErrorResponse
from app.services.analytics_service import TCOAnalytics
from app.models import Vehicle
from app.models.marketplace.organization import OrganizationMember
logger = logging.getLogger(__name__)
router = APIRouter()
async def verify_vehicle_access(
vehicle_id: uuid.UUID,
db: AsyncSession,
current_user
) -> Vehicle:
"""
Verify that the current user has access to the vehicle (either as owner or via organization).
Raises HTTP 404 if vehicle not found, 403 if access denied.
"""
# 1. Check if vehicle exists
vehicle = await db.get(Vehicle, vehicle_id)
if not vehicle:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Vehicle with ID {vehicle_id} not found."
)
# 2. Check if user is superadmin (global access)
if current_user.role == "superadmin":
return vehicle
# 3. Check if user is member of the vehicle's organization
# (Vehicle.organization_id matches user's organization membership)
# First, get user's organization memberships
from sqlalchemy import select
stmt = select(OrganizationMember).where(
OrganizationMember.user_id == current_user.id,
OrganizationMember.organization_id == vehicle.organization_id
)
result = await db.execute(stmt)
membership = result.scalar_one_or_none()
if membership:
return vehicle
# 4. If user is not a member, check if they have fleet manager role with cross-org access
# (This could be extended based on RBAC)
# For now, deny access
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You do not have permission to access this vehicle's analytics."
)
@router.get(
"/{vehicle_id}/summary",
response_model=TCOSummaryResponse,
responses={
404: {"model": TCOErrorResponse, "description": "Vehicle not found"},
403: {"model": TCOErrorResponse, "description": "Access denied"},
500: {"model": TCOErrorResponse, "description": "Internal server error"},
},
summary="Get TCO summary for a vehicle",
description="Returns Total Cost of Ownership analytics for a specific vehicle, "
"including user-specific costs, lifetime costs, and benchmark comparisons."
)
async def get_tco_summary(
vehicle_id: uuid.UUID,
db: AsyncSession = Depends(deps.get_db),
current_user = Depends(deps.get_current_active_user),
):
"""
Retrieve TCO analytics for a vehicle.
Steps:
1. Verify user has access to the vehicle.
2. Use TCOAnalytics service to compute user TCO, lifetime TCO, and benchmark.
3. Transform results into the response schema.
"""
try:
# Access verification
vehicle = await verify_vehicle_access(vehicle_id, db, current_user)
analytics = TCOAnalytics()
# 1. User TCO (current user's organization)
user_tco_result = await analytics.get_user_tco(
db=db,
organization_id=current_user.organization_id or vehicle.organization_id,
currency_target="HUF",
include_categories=None, # all categories
)
# 2. Lifetime TCO (across all owners, anonymized)
lifetime_tco_result = await analytics.get_vehicle_lifetime_tco(
db=db,
vehicle_model_id=vehicle.vehicle_model_id,
currency_target="HUF",
anonymize=True,
)
# 3. Benchmark TCO (global benchmark for similar vehicles)
benchmark_result = await analytics.get_global_benchmark(
db=db,
vehicle_model_id=vehicle.vehicle_model_id,
currency_target="HUF",
)
# Transform results into schema objects
# Note: This is a simplified transformation; you may need to adapt based on actual service output.
user_tco_list = []
if "by_category" in user_tco_result:
for cat_code, cat_data in user_tco_result["by_category"].items():
# Calculate percentage
total = user_tco_result.get("total_amount", 0)
percentage = (cat_data["total"] / total * 100) if total > 0 else 0
user_tco_list.append({
"category_id": 0, # TODO: map from category code to ID
"category_code": cat_code,
"category_name": cat_data.get("name", cat_code),
"amount": cat_data["total"],
"currency": user_tco_result.get("currency", "HUF"),
"amount_huf": cat_data["total"], # already in HUF
"percentage": round(percentage, 2),
})
lifetime_tco_list = []
if "by_category" in lifetime_tco_result:
for cat_code, cat_data in lifetime_tco_result["by_category"].items():
total = lifetime_tco_result.get("total_lifetime_cost", 0)
percentage = (cat_data["total"] / total * 100) if total > 0 else 0
lifetime_tco_list.append({
"category_id": 0,
"category_code": cat_code,
"category_name": cat_data.get("name", cat_code),
"amount": cat_data["total"],
"currency": lifetime_tco_result.get("currency", "HUF"),
"amount_huf": cat_data["total"],
"percentage": round(percentage, 2),
})
benchmark_tco_list = []
if "by_category" in benchmark_result:
for cat_code, cat_data in benchmark_result["by_category"].items():
total = benchmark_result.get("total_cost_sum", 0)
percentage = (cat_data["average"] / total * 100) if total > 0 else 0
benchmark_tco_list.append({
"category_id": 0,
"category_code": cat_code,
"category_name": cat_data.get("name", cat_code),
"amount": cat_data["average"],
"currency": benchmark_result.get("currency", "HUF"),
"amount_huf": cat_data["average"],
"percentage": round(percentage, 2),
})
# Calculate cost per km if odometer data available
cost_per_km = None
if vehicle.odometer and vehicle.odometer > 0:
total_cost = user_tco_result.get("total_amount", 0)
cost_per_km = total_cost / vehicle.odometer
stats = {
"total_cost": user_tco_result.get("total_amount", 0),
"cost_per_km": cost_per_km,
"total_transactions": user_tco_result.get("total_transactions", 0),
"date_range": user_tco_result.get("date_range"),
}
return TCOSummaryResponse(
vehicle_id=vehicle_id,
user_tco=user_tco_list,
lifetime_tco=lifetime_tco_list,
benchmark_tco=benchmark_tco_list,
stats=stats,
)
except HTTPException:
raise
except Exception as e:
logger.exception(f"Unexpected error in TCO summary for vehicle {vehicle_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Internal server error: {str(e)}"
)

View File

@@ -1,5 +1,6 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/assets.py
import uuid
import logging
from typing import Any, Dict, List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
@@ -8,11 +9,12 @@ from sqlalchemy.orm import selectinload
from app.db.session import get_db
from app.api.deps import get_current_user
from app.models.asset import Asset, AssetCost
from app.models import Asset, AssetCost
from app.models.identity import User
from app.services.cost_service import cost_service
from app.services.asset_service import AssetService
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
from app.schemas.asset import AssetResponse
from app.schemas.asset import AssetResponse, AssetCreate
router = APIRouter()
@@ -52,3 +54,38 @@ async def list_asset_costs(
)
res = await db.execute(stmt)
return res.scalars().all()
@router.post("/vehicles", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
async def create_or_claim_vehicle(
payload: AssetCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Új jármű hozzáadása vagy meglévő jármű igénylése a flottához.
A végpont a következőket végzi:
- Ellenőrzi a felhasználó járműlimitjét
- Ha a VIN már létezik, tulajdonjog-átvitelt kezdeményez
- Ha új, létrehozza a járművet és a kapcsolódó digitális ikreket
- XP jutalom adása a felhasználónak
"""
try:
asset = await AssetService.create_or_claim_vehicle(
db=db,
user_id=current_user.id,
org_id=payload.organization_id,
vin=payload.vin,
license_plate=payload.license_plate,
catalog_id=payload.catalog_id
)
return asset
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException:
raise
except Exception as e:
logger = logging.getLogger(__name__)
logger.error(f"Vehicle creation error: {e}")
raise HTTPException(status_code=500, detail="Belső szerverhiba a jármű létrehozásakor")

View File

@@ -1,4 +1,4 @@
# backend/app/api/v1/endpoints/auth.py
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/auth.py
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
@@ -10,9 +10,23 @@ from app.core.config import settings
from app.schemas.auth import UserLiteRegister, Token, UserKYCComplete
from app.api.deps import get_current_user
from app.models.identity import User # JAVÍTVA: Új központi modell
from pydantic import BaseModel, Field
router = APIRouter()
@router.post("/register", status_code=status.HTTP_201_CREATED)
async def register(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)):
"""
Regisztráció (Lite fázis) - új felhasználó létrehozása.
"""
user = await AuthService.register_lite(db, user_in)
return {
"status": "success",
"message": "Regisztráció sikeres. Aktivációs e-mail elküldve.",
"user_id": user.id,
"email": user.email
}
@router.post("/login", response_model=Token)
async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
user = await AuthService.authenticate(db, form_data.username, form_data.password)
@@ -21,11 +35,12 @@ async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordReq
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
role_key = role_name.upper() # A DEFAULT_RANK_MAP nagybetűs kulcsokat vár
token_data = {
"sub": str(user.id),
"role": role_name,
"rank": ranks.get(role_name, 10),
"rank": ranks.get(role_key, 10),
"scope_level": user.scope_level or "individual",
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
}
@@ -33,6 +48,19 @@ async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordReq
access, refresh = create_tokens(data=token_data)
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer", "is_active": user.is_active}
class VerifyEmailRequest(BaseModel):
token: str = Field(..., description="Email verification token (UUID)")
@router.post("/verify-email")
async def verify_email(request: VerifyEmailRequest, db: AsyncSession = Depends(get_db)):
"""
Email megerősítés token alapján.
"""
success = await AuthService.verify_email(db, request.token)
if not success:
raise HTTPException(status_code=400, detail="Érvénytelen vagy lejárt token.")
return {"status": "success", "message": "Email sikeresen megerősítve."}
@router.post("/complete-kyc")
async def complete_kyc(kyc_in: UserKYCComplete, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
user = await AuthService.complete_kyc(db, current_user.id, kyc_in)

View File

@@ -1,63 +1,314 @@
# backend/app/api/v1/endpoints/billing.py
from fastapi import APIRouter, Depends, HTTPException, status
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/billing.py
from fastapi import APIRouter, Depends, HTTPException, status, Request, Header
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Optional, Dict, Any
import logging
from app.api.deps import get_db, get_current_user
from app.models.identity import User, Wallet, UserRole
from app.models.audit import FinancialLedger
from app.models import FinancialLedger, WalletType
from app.models.marketplace.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__)
@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")
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)}")
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"]
@router.post("/payment-intent/create")
async def create_payment_intent(
request: Dict[str, Any],
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
PaymentIntent létrehozása (Prior Intent - Kettős Lakat 1. lépés).
# 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()
Body:
- net_amount: float (kötelező)
- handling_fee: float (alapértelmezett: 0)
- target_wallet_type: string (EARNED, PURCHASED, SERVICE_COINS, VOUCHER)
- beneficiary_id: int (opcionális)
- currency: string (alapértelmezett: "EUR")
- metadata: dict (opcionális)
"""
try:
# Adatok kinyerése
net_amount = request.get("net_amount")
handling_fee = request.get("handling_fee", 0.0)
target_wallet_type_str = request.get("target_wallet_type")
beneficiary_id = request.get("beneficiary_id")
currency = request.get("currency", "EUR")
metadata = request.get("metadata", {})
total_balance = wallet.purchased_credits + wallet.earned_credits
# Validáció
if net_amount is None or net_amount <= 0:
raise HTTPException(status_code=400, detail="net_amount pozitív szám kell legyen")
if total_balance < price:
raise HTTPException(status_code=402, detail="Nincs elég kredited a csomagváltáshoz.")
if handling_fee < 0:
raise HTTPException(status_code=400, detail="handling_fee nem lehet negatív")
# 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
try:
target_wallet_type = WalletType(target_wallet_type_str)
except ValueError:
raise HTTPException(
status_code=400,
detail=f"Érvénytelen target_wallet_type: {target_wallet_type_str}. Használd: {[wt.value for wt in WalletType]}"
)
# 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.")
# PaymentIntent létrehozása
payment_intent = await PaymentRouter.create_payment_intent(
db=db,
payer_id=current_user.id,
net_amount=net_amount,
handling_fee=handling_fee,
target_wallet_type=target_wallet_type,
beneficiary_id=beneficiary_id,
currency=currency,
metadata=metadata
)
# 5. Rang frissítése és naplózás
current_user.role = target_package # Pl. 'service_pro' vagy 'vip'
return {
"success": True,
"payment_intent_id": payment_intent.id,
"intent_token": str(payment_intent.intent_token),
"net_amount": float(payment_intent.net_amount),
"handling_fee": float(payment_intent.handling_fee),
"gross_amount": float(payment_intent.gross_amount),
"currency": payment_intent.currency,
"status": payment_intent.status.value,
"expires_at": payment_intent.expires_at.isoformat() if payment_intent.expires_at else None,
}
db.add(FinancialLedger(
user_id=current_user.id,
amount=-price,
transaction_type=f"UPGRADE_{target_package.upper()}",
details=pkg_info
))
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"PaymentIntent létrehozási hiba: {e}")
raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}")
await db.commit()
return {"status": "success", "package": target_package, "rank_granted": pkg_info["rank"]}
@router.post("/payment-intent/{payment_intent_id}/stripe-checkout")
async def initiate_stripe_checkout(
payment_intent_id: int,
request: Dict[str, Any],
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Stripe Checkout Session indítása PaymentIntent alapján.
Body:
- success_url: string (kötelező)
- cancel_url: string (kötelező)
"""
try:
success_url = request.get("success_url")
cancel_url = request.get("cancel_url")
if not success_url or not cancel_url:
raise HTTPException(status_code=400, detail="success_url és cancel_url kötelező")
# Ellenőrizzük, hogy a PaymentIntent a felhasználóhoz tartozik-e
stmt = select(PaymentIntent).where(
PaymentIntent.id == payment_intent_id,
PaymentIntent.payer_id == current_user.id
)
result = await db.execute(stmt)
payment_intent = result.scalar_one_or_none()
if not payment_intent:
raise HTTPException(status_code=404, detail="PaymentIntent nem található vagy nincs hozzáférésed")
# Stripe Checkout indítása
session_data = await PaymentRouter.initiate_stripe_payment(
db=db,
payment_intent_id=payment_intent_id,
success_url=success_url,
cancel_url=cancel_url
)
return {
"success": True,
"checkout_url": session_data["checkout_url"],
"stripe_session_id": session_data["stripe_session_id"],
"expires_at": session_data["expires_at"],
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Stripe Checkout indítási hiba: {e}")
raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}")
@router.post("/payment-intent/{payment_intent_id}/process-internal")
async def process_internal_payment(
payment_intent_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Belső ajándékozás feldolgozása (SmartDeduction használatával).
Csak akkor engedélyezett, ha a PaymentIntent PENDING státuszú és a felhasználó a payer.
"""
try:
# Ellenőrizzük, hogy a PaymentIntent a felhasználóhoz tartozik-e
stmt = select(PaymentIntent).where(
PaymentIntent.id == payment_intent_id,
PaymentIntent.payer_id == current_user.id,
PaymentIntent.status == PaymentIntentStatus.PENDING
)
result = await db.execute(stmt)
payment_intent = result.scalar_one_or_none()
if not payment_intent:
raise HTTPException(
status_code=404,
detail="PaymentIntent nem található, nincs hozzáférésed, vagy nem PENDING státuszú"
)
# Belső fizetés feldolgozása
result = await PaymentRouter.process_internal_payment(db, payment_intent_id)
if not result["success"]:
raise HTTPException(status_code=400, detail=result.get("error", "Ismeretlen hiba"))
return {
"success": True,
"transaction_id": result.get("transaction_id"),
"used_amounts": result.get("used_amounts"),
"beneficiary_credited": result.get("beneficiary_credited", False),
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Belső fizetés feldolgozási hiba: {e}")
raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}")
@router.post("/stripe-webhook")
async def stripe_webhook(
request: Request,
stripe_signature: Optional[str] = Header(None),
db: AsyncSession = Depends(get_db)
):
"""
Stripe webhook végpont a Kettős Lakat validációval.
Stripe a következő header-t küldi: Stripe-Signature
"""
if not stripe_signature:
raise HTTPException(status_code=400, detail="Missing Stripe-Signature header")
try:
# Request body kiolvasása
payload = await request.body()
# Webhook feldolgozása
result = await PaymentRouter.process_stripe_webhook(
db=db,
payload=payload,
signature=stripe_signature
)
if not result.get("success", False):
error_msg = result.get("error", "Unknown error")
logger.error(f"Stripe webhook feldolgozás sikertelen: {error_msg}")
raise HTTPException(status_code=400, detail=error_msg)
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Stripe webhook végpont hiba: {e}")
raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}")
@router.get("/payment-intent/{payment_intent_id}/status")
async def get_payment_intent_status(
payment_intent_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
PaymentIntent státusz lekérdezése.
"""
try:
# Ellenőrizzük, hogy a PaymentIntent a felhasználóhoz tartozik-e
stmt = select(PaymentIntent).where(
PaymentIntent.id == payment_intent_id,
PaymentIntent.payer_id == current_user.id
)
result = await db.execute(stmt)
payment_intent = result.scalar_one_or_none()
if not payment_intent:
raise HTTPException(status_code=404, detail="PaymentIntent nem található vagy nincs hozzáférésed")
return {
"id": payment_intent.id,
"intent_token": str(payment_intent.intent_token),
"net_amount": float(payment_intent.net_amount),
"handling_fee": float(payment_intent.handling_fee),
"gross_amount": float(payment_intent.gross_amount),
"currency": payment_intent.currency,
"status": payment_intent.status.value,
"target_wallet_type": payment_intent.target_wallet_type.value,
"beneficiary_id": payment_intent.beneficiary_id,
"stripe_session_id": payment_intent.stripe_session_id,
"transaction_id": str(payment_intent.transaction_id) if payment_intent.transaction_id else None,
"created_at": payment_intent.created_at.isoformat(),
"updated_at": payment_intent.updated_at.isoformat(),
"completed_at": payment_intent.completed_at.isoformat() if payment_intent.completed_at else None,
"expires_at": payment_intent.expires_at.isoformat() if payment_intent.expires_at else None,
}
except Exception as e:
logger.error(f"PaymentIntent státusz lekérdezési hiba: {e}")
raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}")
@router.get("/wallet/balance")
async def get_wallet_balance(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Felhasználó pénztárca egyenlegének lekérdezése a Billing Engine segítségével.
"""
try:
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

@@ -2,33 +2,56 @@ from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.services.asset_service import AssetService
from app.api import deps
from typing import List
router = APIRouter()
# Secured endpoint: Closed premium ecosystem
@router.get("/makes", response_model=List[str])
async def list_makes(db: AsyncSession = Depends(get_db)):
async def list_makes(
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
"""1. Szint: Márkák listázása."""
return await AssetService.get_makes(db)
# Secured endpoint: Closed premium ecosystem
@router.get("/models", response_model=List[str])
async def list_models(make: str, db: AsyncSession = Depends(get_db)):
async def list_models(
make: str,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
"""2. Szint: Típusok listázása egy adott márkához."""
models = await AssetService.get_models(db, make)
if not models:
raise HTTPException(status_code=404, detail="Márka nem található vagy nincsenek típusok.")
return models
# Secured endpoint: Closed premium ecosystem
@router.get("/generations", response_model=List[str])
async def list_generations(make: str, model: str, db: AsyncSession = Depends(get_db)):
async def list_generations(
make: str,
model: str,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
"""3. Szint: Generációk/Évjáratok listázása."""
generations = await AssetService.get_generations(db, make, model)
if not generations:
raise HTTPException(status_code=404, detail="Nincs generációs adat ehhez a típushoz.")
return generations
# Secured endpoint: Closed premium ecosystem
@router.get("/engines")
async def list_engines(make: str, model: str, gen: str, db: AsyncSession = Depends(get_db)):
async def list_engines(
make: str,
model: str,
gen: str,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
"""4. Szint: Motorváltozatok és technikai specifikációk."""
engines = await AssetService.get_engines(db, make, model, gen)
if not engines:

View File

@@ -85,3 +85,146 @@ async def get_document_status(
"""Lekérdezhető, hogy a robot végzett-e már a feldolgozással."""
# (Itt egy egyszerű lekérdezés a Document táblából a státuszra)
pass
# RBAC helper function
def _check_premium_or_admin(user: User) -> bool:
"""Check if user has premium subscription or admin role."""
premium_plans = ['PREMIUM', 'PREMIUM_PLUS', 'VIP', 'VIP_PLUS']
if user.role == 'admin':
return True
if hasattr(user, 'subscription_plan') and user.subscription_plan in premium_plans:
return True
return False
@router.post("/scan-instant")
async def scan_instant(
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Szinkron végpont (Villámszkenner) - forgalmi/ID dokumentumokhoz.
Azonnali OCR feldolgozás és válasz.
RBAC: Csak prémium előfizetés vagy admin.
"""
# RBAC ellenőrzés
if not _check_premium_or_admin(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Prémium előfizetés szükséges a funkcióhoz"
)
try:
# 1. Fájl feltöltése MinIO-ba (StorageService segítségével)
# Jelenleg mock: feltételezzük, hogy a StorageService.upload_file létezik
from app.services.storage_service import StorageService
file_url = await StorageService.upload_file(file, prefix="instant_scan")
# 2. Mock OCR hívás (valós implementációban AiOcrService-t hívnánk)
mock_ocr_result = {
"plate": "TEST-123",
"vin": "TRX12345",
"make": "Toyota",
"model": "Corolla",
"year": 2022,
"fuel_type": "petrol",
"engine_capacity": 1600
}
# 3. Dokumentum rekord létrehozása system.documents táblában
from app.models import Document
from datetime import datetime, timezone
import uuid
doc = Document(
id=uuid.uuid4(),
user_id=current_user.id,
original_name=file.filename,
file_path=file_url,
file_size=file.size,
mime_type=file.content_type,
status='processed',
ocr_data=mock_ocr_result,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.add(doc)
await db.commit()
await db.refresh(doc)
# 4. Válasz
return {
"document_id": str(doc.id),
"status": "processed",
"ocr_result": mock_ocr_result,
"file_url": file_url,
"message": "Dokumentum sikeresen feldolgozva"
}
except Exception as e:
await db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Hiba a dokumentum feldolgozása során: {str(e)}"
)
@router.post("/upload-async")
async def upload_async(
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Aszinkron végpont (Költség/Számla nyelő) - háttérben futó OCR-nek.
Azonnali 202 Accepted válasz, pending_ocr státusszal.
RBAC: Csak prémium előfizetés vagy admin.
"""
# RBAC ellenőrzés
if not _check_premium_or_admin(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Prémium előfizetés szükséges a funkcióhoz"
)
try:
# 1. Fájl feltöltése MinIO-ba
from app.services.storage_service import StorageService
file_url = await StorageService.upload_file(file, prefix="async_upload")
# 2. Dokumentum rekord létrehozása pending_ocr státusszal
from app.models import Document
from datetime import datetime, timezone
import uuid
doc = Document(
id=uuid.uuid4(),
user_id=current_user.id,
original_name=file.filename,
file_path=file_url,
file_size=file.size,
mime_type=file.content_type,
status='pending_ocr', # Fontos: a háttérrobot ezt fogja felvenni
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.add(doc)
await db.commit()
await db.refresh(doc)
# 3. 202 Accepted válasz
return {
"document_id": str(doc.id),
"status": "pending_ocr",
"message": "A dokumentum feltöltve, háttérben történő elemzése megkezdődött.",
"file_url": file_url
}
except Exception as e:
await db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Hiba a dokumentum feltöltése során: {str(e)}"
)

View File

@@ -1,16 +1,16 @@
# backend/app/api/v1/endpoints/evidence.py
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/evidence.py
from fastapi import APIRouter, UploadFile, File, HTTPException, status, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text
from app.api.deps import get_db, get_current_user
from app.models.identity import User
from app.models.asset import Asset # JAVÍTVA: Asset modell
from app.models import Asset # JAVÍTVA: Asset modell
router = APIRouter()
@router.post("/scan-registration")
async def scan_registration_document(file: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
stmt_limit = text("SELECT (value->>:plan)::int FROM data.system_parameters WHERE key = 'VEHICLE_LIMIT'")
stmt_limit = text("SELECT (value->>:plan)::int FROM system.system_parameters WHERE key = 'VEHICLE_LIMIT'")
res = await db.execute(stmt_limit, {"plan": current_user.subscription_plan or "free"})
max_allowed = res.scalar() or 1

View File

@@ -1,9 +1,9 @@
# backend/app/api/v1/endpoints/expenses.py
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/expenses.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.api.deps import get_db, get_current_user
from app.models.asset import Asset, AssetCost # JAVÍTVA
from app.models import Asset, AssetCost # JAVÍTVA
from pydantic import BaseModel
from datetime import date
@@ -18,15 +18,23 @@ class ExpenseCreate(BaseModel):
@router.post("/add")
async def add_expense(expense: ExpenseCreate, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
stmt = select(Asset).where(Asset.id == expense.asset_id)
if not (await db.execute(stmt)).scalar_one_or_none():
result = await db.execute(stmt)
asset = result.scalar_one_or_none()
if not asset:
raise HTTPException(status_code=404, detail="Jármű nem található.")
# Determine organization_id from asset
organization_id = asset.current_organization_id or asset.owner_org_id
if not organization_id:
raise HTTPException(status_code=400, detail="Az eszközhez nincs társított szervezet.")
new_cost = AssetCost(
asset_id=expense.asset_id,
cost_type=expense.category,
amount_local=expense.amount,
cost_category=expense.category,
amount_net=expense.amount,
currency="HUF",
date=expense.date,
currency_local="HUF"
organization_id=organization_id
)
db.add(new_cost)
await db.commit()

View File

@@ -0,0 +1,77 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/finance_admin.py
"""
Finance Admin API endpoints for managing Issuers with strict RBAC protection.
Only users with rank >= 90 (Superadmin/Finance Admin) can access these endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import List
from app.api import deps
from app.models.identity import User, UserRole
from app.models.marketplace.finance import Issuer
from app.schemas.finance import IssuerResponse, IssuerUpdate
router = APIRouter()
async def check_finance_admin_access(
current_user: User = Depends(deps.get_current_active_user)
):
"""
RBAC protection: only users with rank >= 90 (Superadmin/Finance Admin) can access.
In our system, this translates to role being 'superadmin' or 'admin'.
"""
if current_user.role not in [UserRole.superadmin, UserRole.admin]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions. Rank >= 90 (Superadmin/Finance Admin) required."
)
return current_user
@router.get("/", response_model=List[IssuerResponse], tags=["finance-admin"])
async def list_issuers(
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_finance_admin_access)
):
"""
List all Issuers (billing entities).
Only accessible by Superadmin/Finance Admin (rank >= 90).
"""
result = await db.execute(select(Issuer).order_by(Issuer.id))
issuers = result.scalars().all()
return issuers
@router.patch("/{issuer_id}", response_model=IssuerResponse, tags=["finance-admin"])
async def update_issuer(
issuer_id: int,
issuer_update: IssuerUpdate,
db: AsyncSession = Depends(deps.get_db),
admin: User = Depends(check_finance_admin_access)
):
"""
Update an Issuer's details (activate/deactivate, revenue limit, API config).
Only accessible by Superadmin/Finance Admin (rank >= 90).
"""
result = await db.execute(select(Issuer).where(Issuer.id == issuer_id))
issuer = result.scalar_one_or_none()
if not issuer:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Issuer with ID {issuer_id} not found."
)
# Update fields if provided
update_data = issuer_update.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(issuer, field, value)
await db.commit()
await db.refresh(issuer)
return issuer

View File

@@ -1,40 +1,475 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/gamification.py
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Body, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
from typing import List
from sqlalchemy import select, desc, func, and_
from typing import List, Optional
from datetime import datetime, timedelta
from app.db.session import get_db
from app.api.deps import get_current_user
from app.models.identity import User
from app.models.gamification import UserStats, PointsLedger
from app.services.config_service import config
from app.models import UserStats, PointsLedger, LevelConfig, UserContribution, Badge, UserBadge, Season
from app.models.system import SystemParameter, ParameterScope
from app.models.marketplace.service import ServiceStaging
from app.schemas.gamification import SeasonResponse, UserStatResponse, LeaderboardEntry
router = APIRouter()
# -- SEGÉDFÜGGVÉNY A RENDSZERBEÁLLÍTÁSOKHOZ --
async def get_system_param(db: AsyncSession, key: str, default_value):
stmt = select(SystemParameter).where(SystemParameter.key == key)
res = (await db.execute(stmt)).scalar_one_or_none()
return res.value if res else default_value
@router.get("/my-stats")
async def get_my_stats(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
"""A bejelentkezett felhasználó aktuális XP-je, szintje és büntetőpontjai."""
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
stats = (await db.execute(stmt)).scalar_one_or_none()
if not stats:
return {"total_xp": 0, "current_level": 1, "penalty_points": 0}
return {"total_xp": 0, "current_level": 1, "penalty_points": 0, "services_submitted": 0}
return stats
@router.get("/leaderboard")
async def get_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
"""A 10 legtöbb XP-vel rendelkező felhasználó listája."""
async def get_leaderboard(
limit: int = 10,
season_id: Optional[int] = None,
db: AsyncSession = Depends(get_db)
):
"""Vezetőlista - globális vagy szezonális"""
if season_id:
# Szezonális vezetőlista
stmt = (
select(
User.email,
func.sum(UserContribution.points_awarded).label("total_points"),
func.sum(UserContribution.xp_awarded).label("total_xp")
)
.join(UserContribution, User.id == UserContribution.user_id)
.where(UserContribution.season_id == season_id)
.group_by(User.id)
.order_by(desc("total_points"))
.limit(limit)
)
else:
# Globális vezetőlista
stmt = (
select(User.email, UserStats.total_xp, UserStats.current_level)
.join(UserStats, User.id == UserStats.user_id)
.order_by(desc(UserStats.total_xp))
.limit(limit)
)
result = await db.execute(stmt)
# Az email-eket maszkoljuk a GDPR miatt (pl. k***s@p***.hu)
if season_id:
return [
{"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "points": r[1], "xp": r[2]}
for r in result.all()
]
else:
return [
{"user": f"{r[0][:2]}***@{r[0].split('@')[1]}", "xp": r[1], "level": r[2]}
for r in result.all()
]
@router.get("/seasons")
async def get_seasons(
active_only: bool = True,
db: AsyncSession = Depends(get_db)
):
"""Szezonok listázása"""
stmt = select(Season)
if active_only:
stmt = stmt.where(Season.is_active == True)
result = await db.execute(stmt)
seasons = result.scalars().all()
return [
{
"id": s.id,
"name": s.name,
"start_date": s.start_date,
"end_date": s.end_date,
"is_active": s.is_active
}
for s in seasons
]
@router.get("/my-contributions")
async def get_my_contributions(
season_id: Optional[int] = None,
limit: int = 50,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Felhasználó hozzájárulásainak listázása"""
stmt = select(UserContribution).where(UserContribution.user_id == current_user.id)
if season_id:
stmt = stmt.where(UserContribution.season_id == season_id)
stmt = stmt.order_by(desc(UserContribution.created_at)).limit(limit)
result = await db.execute(stmt)
contributions = result.scalars().all()
return [
{
"id": c.id,
"contribution_type": c.contribution_type,
"entity_type": c.entity_type,
"entity_id": c.entity_id,
"points_awarded": c.points_awarded,
"xp_awarded": c.xp_awarded,
"status": c.status,
"created_at": c.created_at
}
for c in contributions
]
@router.get("/season-standings/{season_id}")
async def get_season_standings(
season_id: int,
limit: int = 20,
db: AsyncSession = Depends(get_db)
):
"""Szezon állása - top hozzájárulók"""
# Aktuális szezon ellenőrzése
season_stmt = select(Season).where(Season.id == season_id)
season = (await db.execute(season_stmt)).scalar_one_or_none()
if not season:
raise HTTPException(status_code=404, detail="Season not found")
# Top hozzájárulók lekérdezése
stmt = (
select(
User.email,
func.sum(UserContribution.points_awarded).label("total_points"),
func.sum(UserContribution.xp_awarded).label("total_xp"),
func.count(UserContribution.id).label("contribution_count")
)
.join(UserContribution, User.id == UserContribution.user_id)
.where(
and_(
UserContribution.season_id == season_id,
UserContribution.status == "approved"
)
)
.group_by(User.id)
.order_by(desc("total_points"))
.limit(limit)
)
result = await db.execute(stmt)
standings = result.all()
# Szezonális jutalmak konfigurációja
season_config = await get_system_param(
db, "seasonal_competition_config",
{
"top_contributors_count": 10,
"rewards": {
"first_place": {"credits": 1000, "badge": "season_champion"},
"second_place": {"credits": 500, "badge": "season_runner_up"},
"third_place": {"credits": 250, "badge": "season_bronze"},
"top_10": {"credits": 100, "badge": "season_elite"}
}
}
)
return {
"season": {
"id": season.id,
"name": season.name,
"start_date": season.start_date,
"end_date": season.end_date
},
"standings": [
{
"rank": idx + 1,
"user": f"{r[0][:2]}***@{r[0].split('@')[1]}",
"points": r[1],
"xp": r[2],
"contributions": r[3],
"reward": get_season_reward(idx + 1, season_config)
}
for idx, r in enumerate(standings)
],
"config": season_config
}
def get_season_reward(rank: int, config: dict) -> dict:
"""Szezonális jutalom meghatározása a rang alapján"""
rewards = config.get("rewards", {})
if rank == 1:
return rewards.get("first_place", {})
elif rank == 2:
return rewards.get("second_place", {})
elif rank == 3:
return rewards.get("third_place", {})
elif rank <= config.get("top_contributors_count", 10):
return rewards.get("top_10", {})
else:
return {}
@router.get("/self-defense-status")
async def get_self_defense_status(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Önvédelmi rendszer státusz lekérdezése"""
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
stats = (await db.execute(stmt)).scalar_one_or_none()
if not stats:
return {
"penalty_level": 0,
"restrictions": [],
"recovery_progress": 0,
"can_submit_services": True
}
# Önvédelmi büntetések konfigurációja
penalty_config = await get_system_param(
db, "self_defense_penalties",
{
"level_minus_1": {"restrictions": ["no_service_submissions"], "duration_days": 7},
"level_minus_2": {"restrictions": ["no_service_submissions", "no_reviews"], "duration_days": 30},
"level_minus_3": {"restrictions": ["no_service_submissions", "no_reviews", "no_messaging"], "duration_days": 365}
}
)
# Büntetési szint meghatározása (egyszerűsített logika)
penalty_level = 0
if stats.penalty_points >= 1000:
penalty_level = -3
elif stats.penalty_points >= 500:
penalty_level = -2
elif stats.penalty_points >= 100:
penalty_level = -1
restrictions = []
if penalty_level < 0:
level_key = f"level_minus_{abs(penalty_level)}"
restrictions = penalty_config.get(level_key, {}).get("restrictions", [])
return {
"penalty_level": penalty_level,
"penalty_points": stats.penalty_points,
"restrictions": restrictions,
"recovery_progress": min(stats.total_xp / 10000 * 100, 100) if penalty_level < 0 else 100,
"can_submit_services": "no_service_submissions" not in restrictions
}
# --- AZ ÚJ, DINAMIKUS BEKÜLDŐ VÉGPONT (Gamification 2.0 kompatibilis) ---
@router.post("/submit-service")
async def submit_new_service(
name: str = Body(...),
city: str = Body(...),
address: str = Body(...),
contact_phone: Optional[str] = Body(None),
website: Optional[str] = Body(None),
description: Optional[str] = Body(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
# 1. Önvédelmi státusz ellenőrzése
defense_status = await get_self_defense_status(db, current_user)
if not defense_status["can_submit_services"]:
raise HTTPException(
status_code=403,
detail="Önvédelmi korlátozás miatt nem küldhetsz be új szerviz adatokat."
)
# 2. Beállítások lekérése az Admin által vezérelt táblából
submission_rewards = await get_system_param(
db, "service_submission_rewards",
{"points": 50, "xp": 100, "social_credits": 10}
)
contribution_config = await get_system_param(
db, "contribution_types_config",
{
"service_submission": {"points": 50, "xp": 100, "weight": 1.0}
}
)
# 3. Aktuális szezon lekérdezése
season_stmt = select(Season).where(
and_(
Season.is_active == True,
Season.start_date <= datetime.utcnow().date(),
Season.end_date >= datetime.utcnow().date()
)
).limit(1)
season_result = await db.execute(season_stmt)
current_season = season_result.scalar_one_or_none()
# 4. Felhasználó statisztikák
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
stats = (await db.execute(stmt)).scalar_one_or_none()
user_lvl = stats.current_level if stats else 1
# 5. Trust score számítás a szint alapján
trust_weight = min(20 + (user_lvl * 6), 90)
# 6. Nyers adat beküldése a Robotoknak (Staging)
import hashlib
f_print = hashlib.md5(f"{name.lower()}{city.lower()}{address.lower()}".encode()).hexdigest()
new_staging = ServiceStaging(
name=name,
city=city,
address_line1=address,
contact_phone=contact_phone,
website=website,
description=description,
fingerprint=f_print,
status="pending",
trust_score=trust_weight,
submitted_by=current_user.id,
raw_data={
"submitted_by_user": current_user.id,
"user_level": user_lvl,
"submitted_at": datetime.utcnow().isoformat()
}
)
db.add(new_staging)
await db.flush() # Get the ID
# 7. UserContribution létrehozása
contribution = UserContribution(
user_id=current_user.id,
season_id=current_season.id if current_season else None,
contribution_type="service_submission",
entity_type="service_staging",
entity_id=new_staging.id,
points_awarded=submission_rewards.get("points", 50),
xp_awarded=submission_rewards.get("xp", 100),
status="pending", # Robot 5 jóváhagyására vár
metadata={
"service_name": name,
"city": city,
"staging_id": new_staging.id
},
created_at=datetime.utcnow()
)
db.add(contribution)
# 8. PointsLedger bejegyzés
ledger = PointsLedger(
user_id=current_user.id,
points=submission_rewards.get("points", 50),
xp=submission_rewards.get("xp", 100),
source_type="service_submission",
source_id=new_staging.id,
description=f"Szerviz beküldés: {name}",
created_at=datetime.utcnow()
)
db.add(ledger)
# 9. UserStats frissítése
if stats:
stats.total_points += submission_rewards.get("points", 50)
stats.total_xp += submission_rewards.get("xp", 100)
stats.services_submitted += 1
stats.updated_at = datetime.utcnow()
else:
# Ha nincs még UserStats, létrehozzuk
stats = UserStats(
user_id=current_user.id,
total_points=submission_rewards.get("points", 50),
total_xp=submission_rewards.get("xp", 100),
services_submitted=1,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
)
db.add(stats)
try:
await db.commit()
return {
"status": "success",
"message": "Szerviz beküldve a rendszerbe elemzésre!",
"xp_earned": submission_rewards.get("xp", 100),
"points_earned": submission_rewards.get("points", 50),
"staging_id": new_staging.id,
"season_id": current_season.id if current_season else None
}
except Exception as e:
await db.rollback()
raise HTTPException(status_code=400, detail=f"Hiba a beküldés során: {str(e)}")
# --- Gamification 2.0 API végpontok (Frontend/Mobil) ---
@router.get("/me", response_model=UserStatResponse)
async def get_my_gamification_stats(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Visszaadja a bejelentkezett felhasználó aktuális statisztikáit.
Ha nincs rekord, alapértelmezett értékekkel tér vissza.
"""
stmt = select(UserStats).where(UserStats.user_id == current_user.id)
stats = (await db.execute(stmt)).scalar_one_or_none()
if not stats:
# Alapértelmezett statisztika
return UserStatResponse(
user_id=current_user.id,
total_xp=0,
current_level=1,
restriction_level=0,
penalty_quota_remaining=0,
banned_until=None
)
return UserStatResponse.from_orm(stats)
@router.get("/seasons/active", response_model=SeasonResponse)
async def get_active_season(
db: AsyncSession = Depends(get_db)
):
"""
Visszaadja az éppen aktív szezont.
"""
stmt = select(Season).where(Season.is_active == True)
season = (await db.execute(stmt)).scalar_one_or_none()
if not season:
raise HTTPException(status_code=404, detail="No active season found")
return SeasonResponse.from_orm(season)
@router.get("/leaderboard", response_model=List[LeaderboardEntry])
async def get_leaderboard_top10(
limit: int = Query(10, ge=1, le=100),
db: AsyncSession = Depends(get_db)
):
"""
Visszaadja a top felhasználókat total_xp alapján csökkenő sorrendben.
"""
stmt = (
select(UserStats, User.email)
.join(User, UserStats.user_id == User.id)
.order_by(desc(UserStats.total_xp))
.limit(limit)
)
result = await db.execute(stmt)
rows = result.all()
leaderboard = []
for stats, email in rows:
leaderboard.append(
LeaderboardEntry(
user_id=stats.user_id,
username=email, # email használata username helyett
total_xp=stats.total_xp,
current_level=stats.current_level
)
)
return leaderboard

View File

@@ -5,6 +5,7 @@ import uuid
import hashlib
import logging
from typing import List
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
@@ -12,7 +13,7 @@ from sqlalchemy import select
from app.db.session import get_db
from app.api.deps import get_current_user
from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse
from app.models.organization import Organization, OrgType, OrganizationMember
from app.models.marketplace.organization import Organization, OrgType, OrganizationMember
from app.models.identity import User # JAVÍTVA: Központi Identity modell
from app.core.config import settings
@@ -65,12 +66,19 @@ async def onboard_organization(
address_street_type=org_in.address_street_type,
address_house_number=org_in.address_house_number,
address_hrsz=org_in.address_hrsz,
address_stairwell=org_in.address_stairwell,
address_floor=org_in.address_floor,
address_door=org_in.address_door,
country_code=org_in.country_code,
org_type=OrgType.business,
status="pending_verification"
status="pending_verification",
# --- EXPLICIT IDŐBÉLYEGEK A DB HIBA ELKERÜLÉSÉRE ---
first_registered_at=datetime.now(timezone.utc),
current_lifecycle_started_at=datetime.now(timezone.utc),
created_at=datetime.now(timezone.utc),
subscription_plan="FREE",
base_asset_limit=1,
purchased_extra_slots=0,
notification_settings={},
external_integration_config={},
is_ownership_transferable=True
)
db.add(new_org)

View File

@@ -3,10 +3,16 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.schemas.social import ServiceProviderCreate, ServiceProviderResponse
from app.services.social_service import create_service_provider
from app.api import deps
router = APIRouter()
# Secured endpoint: Closed premium ecosystem
@router.post("/", response_model=ServiceProviderResponse)
async def add_provider(provider_data: ServiceProviderCreate, db: AsyncSession = Depends(get_db)):
user_id = 2
async def add_provider(
provider_data: ServiceProviderCreate,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
user_id = current_user.id
return await create_service_provider(db, provider_data, user_id)

View File

@@ -15,7 +15,7 @@ async def get_vehicle_summary(vehicle_id: str, db: AsyncSession = Depends(get_db
category,
SUM(amount) as total_amount,
COUNT(*) as transaction_count
FROM data.vehicle_expenses
FROM vehicle.vehicle_expenses
WHERE vehicle_id = :v_id
GROUP BY category
""")
@@ -40,7 +40,7 @@ async def get_monthly_trends(vehicle_id: str, db: AsyncSession = Depends(get_db)
SELECT
TO_CHAR(date, 'YYYY-MM') as month,
SUM(amount) as monthly_total
FROM data.vehicle_expenses
FROM vehicle.vehicle_expenses
WHERE vehicle_id = :v_id
GROUP BY month
ORDER BY month DESC

View File

@@ -1,24 +1,90 @@
# backend/app/api/v1/endpoints/search.py
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/search.py
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from sqlalchemy import select, func, or_
from sqlalchemy.orm import selectinload
from app.db.session import get_db
from app.api.deps import get_current_user
from app.models.organization import Organization # JAVÍTVA
from app.models.marketplace.organization import Organization, Branch
from geoalchemy2 import WKTElement
from typing import Optional
router = APIRouter()
@router.get("/match")
async def match_service(lat: float, lng: float, radius: int = 20, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
# PostGIS alapú keresés a data.branches táblában (a régi locations helyett)
query = text("""
SELECT o.id, o.name, b.city,
ST_Distance(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography) / 1000 as distance
FROM data.organizations o
JOIN data.branches b ON o.id = b.organization_id
WHERE o.is_active = True AND b.is_active = True
AND ST_DWithin(b.location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography, :r * 1000)
ORDER BY distance ASC
""")
result = await db.execute(query, {"lat": lat, "lng": lng, "r": radius})
return {"results": [dict(row._mapping) for row in result.fetchall()]}
async def match_service(
lat: Optional[float] = None,
lng: Optional[float] = None,
radius_km: float = 20.0,
sort_by: str = "distance",
db: AsyncSession = Depends(get_db),
current_user = Depends(get_current_user)
):
"""
Geofencing keresőmotor PostGIS segítségével.
Ha nincs megadva lat/lng, akkor nem alkalmazunk távolságszűrést.
"""
# Alap lekérdezés: aktív szervezetek és telephelyek
query = select(
Organization.id,
Organization.name,
Branch.city,
Branch.branch_rating,
Branch.location
).join(
Branch, Organization.id == Branch.organization_id
).where(
Organization.is_active == True,
Branch.is_deleted == False
)
# Távolság számítás és szűrés, ha van koordináta
if lat is not None and lng is not None:
# WKT pont létrehozása a felhasználó helyéhez
user_location = WKTElement(f'POINT({lng} {lat})', srid=4326)
# Távolság kiszámítása méterben (ST_DistanceSphere)
distance_col = func.ST_DistanceSphere(Branch.location, user_location).label("distance_meters")
query = query.add_columns(distance_col)
# Szűrés a sugárra (ST_DWithin) - a távolság méterben, radius_km * 1000
query = query.where(
func.ST_DWithin(Branch.location, user_location, radius_km * 1000)
)
else:
# Ha nincs koordináta, ne legyen distance oszlop
distance_col = None
# Rendezés a sort_by paraméter alapján
if sort_by == "distance" and lat is not None and lng is not None:
query = query.order_by(distance_col.asc())
elif sort_by == "rating":
query = query.order_by(Branch.branch_rating.desc())
elif sort_by == "price":
# Jelenleg nincs ár információ, ezért rendezés alapértelmezettként (pl. név)
query = query.order_by(Organization.name.asc())
else:
# Alapértelmezett rendezés: távolság, ha van, különben név
if distance_col is not None:
query = query.order_by(distance_col.asc())
else:
query = query.order_by(Organization.name.asc())
# Lekérdezés végrehajtása
result = await db.execute(query)
rows = result.fetchall()
# Eredmények formázása
results = []
for row in rows:
row_dict = {
"id": row.id,
"name": row.name,
"city": row.city,
"rating": row.branch_rating,
}
if lat is not None and lng is not None:
row_dict["distance_km"] = round(row.distance_meters / 1000, 2) if row.distance_meters else None
results.append(row_dict)
return {"results": results}

View File

@@ -0,0 +1,173 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/security.py
"""
Dual Control (Négy szem elv) API végpontok.
Kiemelt műveletek jóváhagyási folyamata.
"""
import logging
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_db, get_current_user
from app.models.identity import User, UserRole
from app.services.security_service import security_service
from app.schemas.security import (
PendingActionCreate, PendingActionResponse, PendingActionApprove, PendingActionReject
)
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/request", response_model=PendingActionResponse, status_code=status.HTTP_201_CREATED)
async def request_action(
request: PendingActionCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Dual Control: Jóváhagyási kérelem indítása kiemelt művelethez.
Engedélyezett művelettípusok:
- CHANGE_ROLE: Felhasználó szerepkörének módosítása
- SET_VIP: VIP státusz beállítása
- WALLET_ADJUST: Pénztár egyenleg módosítása (nagy összeg)
- SOFT_DELETE_USER: Felhasználó soft delete
- ORGANIZATION_TRANSFER: Szervezet tulajdonjog átadása
"""
# Csak admin és superadmin kezdeményezhet kiemelt műveleteket
if current_user.role not in [UserRole.admin, UserRole.superadmin]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Csak adminisztrátorok kezdeményezhetnek Dual Control műveleteket."
)
try:
action = await security_service.request_action(
db, requester_id=current_user.id,
action_type=request.action_type,
payload=request.payload,
reason=request.reason
)
return PendingActionResponse.from_orm(action)
except Exception as e:
logger.error(f"Dual Control request error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Hiba a kérelem létrehozásakor: {str(e)}"
)
@router.get("/pending", response_model=List[PendingActionResponse])
async def list_pending_actions(
action_type: Optional[str] = None,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Függőben lévő Dual Control műveletek listázása.
Admin és superadmin látja az összes függőben lévő műveletet.
Egyéb felhasználók csak a sajátjaikat láthatják.
"""
if current_user.role in [UserRole.admin, UserRole.superadmin]:
user_id = None
else:
user_id = current_user.id
actions = await security_service.get_pending_actions(db, user_id=user_id, action_type=action_type)
return [PendingActionResponse.from_orm(action) for action in actions]
@router.post("/approve/{action_id}", response_model=PendingActionResponse)
async def approve_action(
action_id: int,
approve_data: PendingActionApprove,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Dual Control: Művelet jóváhagyása.
Csak admin/superadmin hagyhat jóvá, és nem lehet a saját kérése.
"""
if current_user.role not in [UserRole.admin, UserRole.superadmin]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Csak adminisztrátorok hagyhatnak jóvá műveleteket."
)
try:
await security_service.approve_action(db, approver_id=current_user.id, action_id=action_id)
# Frissített művelet lekérdezése
from sqlalchemy import select
from app.models import PendingAction
stmt = select(PendingAction).where(PendingAction.id == action_id)
action = (await db.execute(stmt)).scalar_one()
return PendingActionResponse.from_orm(action)
except Exception as e:
logger.error(f"Dual Control approve error: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.post("/reject/{action_id}", response_model=PendingActionResponse)
async def reject_action(
action_id: int,
reject_data: PendingActionReject,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Dual Control: Művelet elutasítása.
Csak admin/superadmin utasíthat el, és nem lehet a saját kérése.
"""
if current_user.role not in [UserRole.admin, UserRole.superadmin]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Csak adminisztrátorok utasíthatnak el műveleteket."
)
try:
await security_service.reject_action(
db, approver_id=current_user.id,
action_id=action_id, reason=reject_data.reason
)
# Frissített művelet lekérdezése
from sqlalchemy import select
from app.models import PendingAction
stmt = select(PendingAction).where(PendingAction.id == action_id)
action = (await db.execute(stmt)).scalar_one()
return PendingActionResponse.from_orm(action)
except Exception as e:
logger.error(f"Dual Control reject error: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.get("/{action_id}", response_model=PendingActionResponse)
async def get_action(
action_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Egy konkrét Dual Control művelet lekérdezése.
Csak a művelet létrehozója vagy admin/superadmin érheti el.
"""
from sqlalchemy import select
from app.models import PendingAction
stmt = select(PendingAction).where(PendingAction.id == action_id)
action = (await db.execute(stmt)).scalar_one_or_none()
if not action:
raise HTTPException(status_code=404, detail="Művelet nem található.")
if current_user.role not in [UserRole.admin, UserRole.superadmin] and action.requester_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Nincs jogosultságod ehhez a művelethez."
)
return PendingActionResponse.from_orm(action)

View File

@@ -1,10 +1,21 @@
from fastapi import APIRouter, Depends, Form, Query, HTTPException
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/services.py
from fastapi import APIRouter, Depends, Form, Query, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, text
from typing import List, Optional
from app.db.session import get_db
from app.services.gamification_service import GamificationService
from app.models.service import ServiceProfile, ExpertiseTag, ServiceExpertise
from app.services.config_service import ConfigService
from app.services.security_auditor import SecurityAuditorService
from app.models.marketplace.service import ServiceProfile, ExpertiseTag, ServiceExpertise
from app.services.marketplace_service import (
create_verified_review,
get_service_reviews,
can_user_review_service
)
from app.schemas.social import ServiceReviewCreate, ServiceReviewResponse
from app.api.deps import get_current_user
from app.models.identity import User
router = APIRouter()
@@ -14,21 +25,89 @@ async def register_service_hunt(
name: str = Form(...),
lat: float = Form(...),
lng: float = Form(...),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
""" Új szerviz-jelölt rögzítése a staging táblába jutalompontért. """
# Új szerviz-jelölt rögzítése
await db.execute(text("""
INSERT INTO data.service_staging (name, fingerprint, status, raw_data)
VALUES (:n, :f, 'pending', jsonb_build_object('lat', :lat, 'lng', :lng))
"""), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng})
INSERT INTO marketplace.service_staging (name, fingerprint, status, city, submitted_by, raw_data)
VALUES (:n, :f, 'pending', 'Unknown', :user_id, jsonb_build_object('lat', CAST(:lat AS double precision), 'lng', CAST(:lng AS double precision)))
"""), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng, "user_id": current_user.id})
# MB 2.0 Gamification: 50 pont a felfedezésért
# TODO: A 1-es ID helyett a bejelentkezett felhasználót kell használni (current_user.id)
await GamificationService.award_points(db, 1, 50, f"Service Hunt: {name}")
# MB 2.0 Gamification: Dinamikus pontszám a felfedezésért
reward_points = await ConfigService.get_int(db, "GAMIFICATION_HUNT_REWARD", 50)
await GamificationService.award_points(db, current_user.id, reward_points, f"Service Hunt: {name}")
await db.commit()
return {"status": "success", "message": "Discovery registered and points awarded."}
# --- ✅ SZERVIZ VALIDÁLÁS (Service Validation) ---
@router.post("/hunt/{staging_id}/validate")
async def validate_staged_service(
staging_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""
Validálja egy másik felhasználó által beküldött szerviz-jelöltet.
Növeli a validation_level-t 10-zel (max 80), adományoz 10 XP-t,
és növeli a places_validated számlálót a felhasználó statisztikáiban.
"""
# Anti-Cheat: Rapid Fire ellenőrzés
await SecurityAuditorService.check_rapid_fire_validation(db, current_user.id)
# 1. Keresd meg a staging rekordot
result = await db.execute(
text("SELECT id, submitted_by, validation_level FROM marketplace.service_staging WHERE id = :id"),
{"id": staging_id}
)
staging = result.fetchone()
if not staging:
raise HTTPException(status_code=404, detail="Staging record not found")
# 2. Ha a saját beküldését validálná, hiba
if staging.submitted_by == current_user.id:
raise HTTPException(status_code=400, detail="Cannot validate your own submission")
# 3. Növeld a validation_level-t 10-zel (max 80)
new_level = staging.validation_level + 10
if new_level > 80:
new_level = 80
# 4. UPDATE a validation_level és a status (ha elérte a 80-at, akkor "verified"?)
# Jelenleg csak a validation_level frissítése
await db.execute(
text("""
UPDATE marketplace.service_staging
SET validation_level = :new_level
WHERE id = :id
"""),
{"new_level": new_level, "id": staging_id}
)
# 5. Adományozz dinamikus XP-t a current_user-nek a GamificationService-en keresztül
validation_reward = await ConfigService.get_int(db, "GAMIFICATION_VALIDATE_REWARD", 10)
await GamificationService.award_points(db, current_user.id, validation_reward, f"Service Validation: staging #{staging_id}")
# 6. Növeld a current_user places_validated értékét a UserStats-ban
await db.execute(
text("""
UPDATE gamification.user_stats
SET places_validated = places_validated + 1
WHERE user_id = :user_id
"""),
{"user_id": current_user.id}
)
await db.commit()
return {
"status": "success",
"message": "Validation successful",
"validation_level": new_level,
"places_validated_incremented": True
}
# --- 🔍 SZERVIZ KERESŐ (Service Search) ---
@router.get("/search")
async def search_services(
@@ -56,3 +135,75 @@ async def search_services(
services = result.scalars().all()
return services
# --- ⭐ VERIFIED SERVICE REVIEWS (Social 3 - #66) ---
@router.post("/{service_id}/reviews", response_model=ServiceReviewResponse, status_code=status.HTTP_201_CREATED)
async def create_service_review(
service_id: int,
review_data: ServiceReviewCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""
Verifikált szerviz értékelés beküldése.
Csak igazolt pénzügyi tranzakció után lehetséges (transaction_id kötelező).
"""
try:
review = await create_verified_review(
db=db,
service_id=service_id,
user_id=current_user.id,
transaction_id=review_data.transaction_id,
review_data=review_data
)
return review
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except IntegrityError as e:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
@router.get("/{service_id}/reviews", response_model=dict)
async def list_service_reviews(
service_id: int,
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
verified_only: bool = Query(True),
db: AsyncSession = Depends(get_db)
):
"""
Szerviz értékeléseinek lapozható listázása.
"""
reviews, total = await get_service_reviews(
db=db,
service_id=service_id,
skip=skip,
limit=limit,
verified_only=verified_only
)
return {
"reviews": reviews,
"total": total,
"skip": skip,
"limit": limit
}
@router.get("/{service_id}/reviews/check")
async def check_review_eligibility(
service_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""
Ellenőrzi, hogy a felhasználó értékelhetie a szervizt.
"""
can_review, reason = await can_user_review_service(db, current_user.id, service_id)
return {
"can_review": can_review,
"reason": reason,
"user_id": current_user.id,
"service_id": service_id
}

View File

@@ -1,16 +1,28 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.api import deps
# ITT A JAVÍTÁS: A példányt importáljuk, nem a régi függvényeket
from app.services.social_service import social_service
router = APIRouter()
# Secured endpoint: Closed premium ecosystem
@router.get("/leaderboard")
async def read_leaderboard(limit: int = 10, db: AsyncSession = Depends(get_db)):
async def read_leaderboard(
limit: int = 10,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
return await social_service.get_leaderboard(db, limit)
# Secured endpoint: Closed premium ecosystem
@router.post("/vote/{provider_id}")
async def provider_vote(provider_id: int, vote_value: int, db: AsyncSession = Depends(get_db)):
user_id = 2
async def provider_vote(
provider_id: int,
vote_value: int,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
user_id = current_user.id
return await social_service.vote_for_provider(db, user_id, provider_id, vote_value)

View File

@@ -0,0 +1,132 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/system_parameters.py
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from typing import List, Optional
from app.api.deps import get_db, get_current_user
from app.schemas.system import (
SystemParameterResponse,
SystemParameterUpdate,
SystemParameterCreate,
)
from app.models.system import SystemParameter, ParameterScope
from app.models.identity import UserRole
router = APIRouter()
@router.get("/", response_model=List[SystemParameterResponse])
async def list_system_parameters(
db: AsyncSession = Depends(get_db),
scope_level: Optional[ParameterScope] = Query(None, description="Scope szint (global, country, region, user)"),
scope_id: Optional[str] = Query(None, description="Scope azonosító (pl. 'HU', 'budapest', user_id)"),
is_active: Optional[bool] = Query(True, description="Csak aktív paraméterek"),
):
"""
Listázza az összes aktív (vagy opcionálisan inaktív) rendszerparamétert.
Szűrhető scope_level és scope_id alapján.
"""
query = select(SystemParameter)
if scope_level is not None:
query = query.where(SystemParameter.scope_level == scope_level)
if scope_id is not None:
query = query.where(SystemParameter.scope_id == scope_id)
if is_active is not None:
query = query.where(SystemParameter.is_active == is_active)
result = await db.execute(query)
parameters = result.scalars().all()
return parameters
@router.get("/{key}", response_model=SystemParameterResponse)
async def get_system_parameter(
key: str,
db: AsyncSession = Depends(get_db),
scope_level: ParameterScope = Query("global", description="Scope szint (alapértelmezett: global)"),
scope_id: Optional[str] = Query(None, description="Scope azonosító"),
):
"""
Visszaad egy konkrét paramétert a key és scope_level (és opcionálisan scope_id) alapján.
"""
query = select(SystemParameter).where(
SystemParameter.key == key,
SystemParameter.scope_level == scope_level,
)
if scope_id is not None:
query = query.where(SystemParameter.scope_id == scope_id)
else:
query = query.where(SystemParameter.scope_id.is_(None))
result = await db.execute(query)
parameter = result.scalar_one_or_none()
if not parameter:
raise HTTPException(
status_code=404,
detail=f"System parameter not found with key='{key}', scope_level='{scope_level}', scope_id='{scope_id}'"
)
return parameter
@router.put("/{key}", response_model=SystemParameterResponse)
async def update_system_parameter(
key: str,
param_in: SystemParameterUpdate,
db: AsyncSession = Depends(get_db),
current_user=Depends(get_current_user),
scope_level: ParameterScope = Query("global", description="Scope szint (alapértelmezett: global)"),
scope_id: Optional[str] = Query(None, description="Scope azonosító"),
):
"""
Módosítja egy létező paraméter value (JSONB) vagy is_active mezőjét (Admin funkció).
Csak superadmin vagy admin jogosultságú felhasználók használhatják.
"""
# Jogosultság ellenőrzése
if current_user.role not in (UserRole.superadmin, UserRole.admin):
raise HTTPException(
status_code=403,
detail="Insufficient permissions. Only superadmin or admin can update system parameters."
)
# Paraméter keresése
query = select(SystemParameter).where(
SystemParameter.key == key,
SystemParameter.scope_level == scope_level,
)
if scope_id is not None:
query = query.where(SystemParameter.scope_id == scope_id)
else:
query = query.where(SystemParameter.scope_id.is_(None))
result = await db.execute(query)
parameter = result.scalar_one_or_none()
if not parameter:
raise HTTPException(
status_code=404,
detail=f"System parameter not found with key='{key}', scope_level='{scope_level}', scope_id='{scope_id}'"
)
# Frissítés
update_data = {}
if param_in.description is not None:
update_data["description"] = param_in.description
if param_in.value is not None:
update_data["value"] = param_in.value
if param_in.is_active is not None:
update_data["is_active"] = param_in.is_active
if update_data:
stmt = (
update(SystemParameter)
.where(SystemParameter.id == parameter.id)
.values(**update_data)
)
await db.execute(stmt)
await db.commit()
await db.refresh(parameter)
return parameter

View File

@@ -0,0 +1,71 @@
"""
Nyilvános i18n API végpont a frontend számára.
Autentikációt NEM igényel, mivel a fordítások nyilvánosak.
"""
import json
import os
from fastapi import APIRouter, HTTPException, Path
from fastapi.responses import JSONResponse
from typing import Dict, Any
router = APIRouter()
# A statikus JSON fájlok elérési útja
LOCALES_DIR = os.path.join(os.path.dirname(__file__), "../../../static/locales")
def load_locale(lang: str) -> Dict[str, Any]:
"""Betölti a nyelvi JSON fájlt, ha nem létezik, fallback angol."""
file_path = os.path.join(LOCALES_DIR, f"{lang}.json")
fallback_path = os.path.join(LOCALES_DIR, "en.json")
if not os.path.exists(file_path):
# Ha a kért nyelv nem létezik, próbáljuk meg az angolt
if lang != "en" and os.path.exists(fallback_path):
file_path = fallback_path
else:
raise HTTPException(status_code=404, detail=f"Language '{lang}' not found")
try:
with open(file_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error loading translation file: {str(e)}")
@router.get("/{lang}", response_model=Dict[str, Any])
async def get_translations(
lang: str = Path(..., description="Nyelvkód, pl. 'hu', 'en', 'de'", min_length=2, max_length=5)
):
"""
Visszaadja a teljes fordításcsomagot a kért nyelvhez.
- Ha a nyelv nem létezik, 404 hibát dob.
- Ha a fájl sérült, 500 hibát dob.
- A válasz egy JSON objektum, amelyben a kulcsok hierarchikusak.
"""
translations = load_locale(lang)
return translations
@router.get("/{lang}/{key:path}")
async def get_translation_by_key(
lang: str = Path(..., description="Nyelvkód"),
key: str = Path(..., description="Pontokkal elválasztott kulcs, pl. 'AUTH.LOGIN.TITLE'")
):
"""
Visszaadja a fordításcsomag egy adott kulcsához tartozó értéket.
- Ha a kulcs nem található, 404 hibát dob.
- Támogatja a hierarchikus kulcsokat (pl. 'AUTH.LOGIN.TITLE').
"""
translations = load_locale(lang)
# Kulcs felbontása
parts = key.split('.')
current = translations
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
raise HTTPException(status_code=404, detail=f"Translation key '{key}' not found for language '{lang}'")
# Ha a current egy szótár, akkor azt adjuk vissza (részleges fa)
# Ha sztring, akkor azt
return {key: current}

View File

@@ -1,11 +1,15 @@
#/opt/docker/dev/service_finder/backend/app/api/v1/endpoints/users.py
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Dict, Any
from app.api.deps import get_db, get_current_user
from app.schemas.user import UserResponse
from app.models.user import User
from app.models.identity import User
from app.services.trust_engine import TrustEngine
router = APIRouter()
trust_engine = TrustEngine()
@router.get("/me", response_model=UserResponse)
async def read_users_me(
@@ -14,3 +18,26 @@ async def read_users_me(
):
"""Visszaadja a bejelentkezett felhasználó profilját"""
return current_user
@router.get("/me/trust")
async def get_user_trust(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
force_recalculate: bool = False,
) -> Dict[str, Any]:
"""
Visszaadja a felhasználó Gondos Gazda Index (Trust Score) értékét.
A számítás dinamikusan betölti a paramétereket a SystemParameter rendszerből
(Global/Country/Region/User hierarchia).
Paraméterek:
- force_recalculate: Ha True, akkor újraszámolja a trust score-t
(alapértelmezetten cache-elt értéket ad vissza, ha kevesebb mint 24 órája számoltuk)
"""
trust_data = await trust_engine.calculate_user_trust(
db=db,
user_id=current_user.id,
force_recalculate=force_recalculate
)
return trust_data

View File

@@ -0,0 +1,142 @@
"""
Jármű értékelési végpontok a Social 1 modulhoz.
"""
import uuid
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from sqlalchemy.orm import selectinload
from app.db.session import get_db
from app.api.deps import get_current_user
from app.models.vehicle import VehicleUserRating
from app.models import VehicleModelDefinition
from app.models.identity import User
from app.schemas.vehicle import VehicleRatingCreate, VehicleRatingResponse
router = APIRouter()
@router.post("/{vehicle_id}/ratings", response_model=VehicleRatingResponse, status_code=status.HTTP_201_CREATED)
async def create_vehicle_rating(
vehicle_id: int,
rating: VehicleRatingCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Értékelés beküldése egy járműhöz.
Csak a jármű tulajdonosa (vagy jogosult felhasználó) értékelhet.
Egy felhasználó csak egyszer értékelhet egy adott járművet.
"""
# 1. Ellenőrizzük, hogy a jármű létezik-e
vehicle = await db.scalar(
select(VehicleModelDefinition).where(VehicleModelDefinition.id == vehicle_id)
)
if not vehicle:
raise HTTPException(status_code=404, detail="Jármű nem található")
# 2. Ellenőrizzük, hogy a felhasználó jogosult-e értékelni (jelenleg csak tulajdonos)
# TODO: Később kibővíthető más jogosultságokkal is
# Most feltételezzük, hogy mindenki értékelhet, de csak egyszer
# 3. Ellenőrizzük, hogy már létezik-e értékelés ettől a felhasználótól ehhez a járműhöz
existing_rating = await db.scalar(
select(VehicleUserRating).where(
and_(
VehicleUserRating.vehicle_id == vehicle_id,
VehicleUserRating.user_id == current_user.id
)
)
)
if existing_rating:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Már értékelted ezt a járművet. Csak egy értékelés engedélyezett felhasználónként."
)
# 4. Hozzuk létre az új értékelést
new_rating = VehicleUserRating(
vehicle_id=vehicle_id,
user_id=current_user.id,
driving_experience=rating.driving_experience,
reliability=rating.reliability,
comfort=rating.comfort,
consumption_satisfaction=rating.consumption_satisfaction,
comment=rating.comment
)
db.add(new_rating)
await db.commit()
await db.refresh(new_rating)
# 5. Átlagpontszám számítása
average_score = new_rating.average_score
# 6. Válasz összeállítása
return VehicleRatingResponse(
id=new_rating.id,
vehicle_id=new_rating.vehicle_id,
user_id=new_rating.user_id,
driving_experience=new_rating.driving_experience,
reliability=new_rating.reliability,
comfort=new_rating.comfort,
consumption_satisfaction=new_rating.consumption_satisfaction,
comment=new_rating.comment,
average_score=average_score,
created_at=new_rating.created_at,
updated_at=new_rating.updated_at
)
@router.get("/{vehicle_id}/ratings", response_model=List[VehicleRatingResponse])
async def get_vehicle_ratings(
vehicle_id: int,
skip: int = 0,
limit: int = 100,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Az összes értékelés lekérése egy adott járműhöz.
"""
# Ellenőrizzük, hogy a jármű létezik-e
vehicle = await db.scalar(
select(VehicleModelDefinition).where(VehicleModelDefinition.id == vehicle_id)
)
if not vehicle:
raise HTTPException(status_code=404, detail="Jármű nem található")
# Lekérjük az értékeléseket
stmt = (
select(VehicleUserRating)
.where(VehicleUserRating.vehicle_id == vehicle_id)
.order_by(VehicleUserRating.created_at.desc())
.offset(skip)
.limit(limit)
)
result = await db.scalars(stmt)
ratings = result.all()
# Átalakítás válasz sémává
response_ratings = []
for rating in ratings:
response_ratings.append(
VehicleRatingResponse(
id=rating.id,
vehicle_id=rating.vehicle_id,
user_id=rating.user_id,
driving_experience=rating.driving_experience,
reliability=rating.reliability,
comfort=rating.comfort,
consumption_satisfaction=rating.consumption_satisfaction,
comment=rating.comment,
average_score=rating.average_score,
created_at=rating.created_at,
updated_at=rating.updated_at
)
)
return response_ratings

View File

@@ -34,6 +34,19 @@ class Settings(BaseSettings):
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
@field_validator('SECRET_KEY')
@classmethod
def validate_secret_key(cls, v: str, info) -> str:
"""Ellenőrzi, hogy a SECRET_KEY ne legyen default érték éles környezetben."""
if v == "NOT_SET_DANGER" and not info.data.get("DEBUG", True):
raise ValueError(
"SECRET_KEY must be set in production environment. "
"Please set SECRET_KEY in .env file."
)
if not v or v.strip() == "":
raise ValueError("SECRET_KEY cannot be empty.")
return v
# --- Initial Admin ---
INITIAL_ADMIN_EMAIL: str = "admin@servicefinder.hu"
INITIAL_ADMIN_PASSWORD: str = "Admin123!"
@@ -46,6 +59,12 @@ class Settings(BaseSettings):
)
REDIS_URL: str = "redis://service_finder_redis:6379/0"
# --- MinIO S3 Storage ---
MINIO_ENDPOINT: str = "sf_minio:9000"
MINIO_ACCESS_KEY: str = "kincses"
MINIO_SECRET_KEY: str = "MiskociA74"
MINIO_SECURE: bool = False
@property
def SQLALCHEMY_DATABASE_URI(self) -> str:
"""
@@ -67,11 +86,39 @@ class Settings(BaseSettings):
# --- External URLs ---
FRONTEND_BASE_URL: str = "https://dev.profibot.hu"
BACKEND_CORS_ORIGINS: List[str] = [
BACKEND_CORS_ORIGINS: List[str] = Field(
default=[
"http://localhost:3001",
"https://dev.profibot.hu",
"http://192.168.100.10:3001"
]
"https://dev.profibot.hu"
],
description="Comma-separated list of allowed CORS origins. Set via ALLOWED_ORIGINS environment variable."
)
@field_validator('BACKEND_CORS_ORIGINS', mode='before')
@classmethod
def parse_allowed_origins(cls, v: Any) -> List[str]:
"""Parse ALLOWED_ORIGINS environment variable from comma-separated string to list."""
import os
env_val = os.getenv('ALLOWED_ORIGINS')
if env_val:
# parse environment variable
env_val = env_val.strip()
if env_val.startswith('"') and env_val.endswith('"'):
env_val = env_val[1:-1]
if env_val.startswith("'") and env_val.endswith("'"):
env_val = env_val[1:-1]
parts = [part.strip() for part in env_val.split(',') if part.strip()]
return parts
# if no env variable, fallback to default or provided value
if isinstance(v, str):
v = v.strip()
if v.startswith('"') and v.endswith('"'):
v = v[1:-1]
if v.startswith("'") and v.endswith("'"):
v = v[1:-1]
parts = [part.strip() for part in v.split(',') if part.strip()]
return parts
return v
# --- Google OAuth ---
GOOGLE_CLIENT_ID: str = ""
@@ -85,7 +132,7 @@ class Settings(BaseSettings):
# --- Dinamikus Admin Motor (Sértetlenül hagyva) ---
async def get_db_setting(self, db: AsyncSession, key_name: str, default: Any = None) -> Any:
try:
query = text("SELECT value FROM data.system_parameters WHERE key = :key")
query = text("SELECT value FROM system.system_parameters WHERE key = :key")
result = await db.execute(query, {"key": key_name})
row = result.fetchone()
if row and row[0] is not None:

View File

@@ -15,7 +15,8 @@ class RBAC:
return True
# 2. Dinamikus rang ellenőrzés a központi rank_map alapján
user_rank = settings.DEFAULT_RANK_MAP.get(current_user.role.value, 0)
role_key = current_user.role.value.upper() # A DEFAULT_RANK_MAP nagybetűs kulcsokat vár
user_rank = settings.DEFAULT_RANK_MAP.get(role_key, 0)
if user_rank < self.min_rank:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,

View File

@@ -0,0 +1,228 @@
"""
Aszinkron ütemező (APScheduler) a napi karbantartási feladatokhoz.
Integrálva a FastAPI lifespan eseményébe, így az alkalmazás indításakor elindul,
és leálláskor megáll.
Biztonsági Jitter: A napi futás 00:15-kor indul, de jitter=900 (15 perc) paraméterrel
véletlenszerűen 0:15 és 0:30 között fog lefutni.
"""
import asyncio
import logging
import uuid
from contextlib import asynccontextmanager
from datetime import datetime, time, timedelta
from typing import Optional
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.jobstores.memory import MemoryJobStore
from app.database import AsyncSessionLocal
from app.services.billing_engine import SmartDeduction
from app.models.marketplace.payment import WithdrawalRequest, WithdrawalRequestStatus
from app.models.identity import User
from app.models import ProcessLog, WalletType, FinancialLedger
from sqlalchemy import select, update, and_
from sqlalchemy.orm import selectinload
logger = logging.getLogger(__name__)
# Globális scheduler példány
_scheduler: Optional[AsyncIOScheduler] = None
def get_scheduler() -> AsyncIOScheduler:
"""Visszaadja a globális scheduler példányt (lazy initialization)."""
global _scheduler
if _scheduler is None:
jobstores = {
'default': MemoryJobStore()
}
_scheduler = AsyncIOScheduler(
jobstores=jobstores,
timezone="UTC",
job_defaults={
'coalesce': True,
'max_instances': 1,
'misfire_grace_time': 3600 # 1 óra
}
)
return _scheduler
async def daily_financial_maintenance() -> None:
"""
Napi pénzügyi karbantartási feladatok.
A. Voucher lejárat kezelése
B. Withdrawal Request lejárat (14 nap) és automatikus elutasítás
C. Soft Downgrade (lejárt előfizetések)
D. Naplózás ProcessLog-ba
"""
logger.info("Daily financial maintenance started")
stats = {
"vouchers_expired": 0,
"withdrawals_rejected": 0,
"users_downgraded": 0,
"errors": []
}
async with AsyncSessionLocal() as db:
try:
# A. Voucher lejárat kezelése
try:
voucher_count = await SmartDeduction.process_voucher_expiration(db)
stats["vouchers_expired"] = voucher_count
logger.info(f"Expired {voucher_count} vouchers")
except Exception as e:
stats["errors"].append(f"Voucher expiration error: {str(e)}")
logger.error(f"Voucher expiration error: {e}", exc_info=True)
# B. Withdrawal Request lejárat (14 nap)
try:
# Keresd meg a PENDING státuszú, 14 napnál régebbi kéréseket
fourteen_days_ago = datetime.utcnow() - timedelta(days=14)
stmt = select(WithdrawalRequest).where(
and_(
WithdrawalRequest.status == WithdrawalRequestStatus.PENDING,
WithdrawalRequest.created_at < fourteen_days_ago,
WithdrawalRequest.is_deleted == False
)
).options(selectinload(WithdrawalRequest.user))
result = await db.execute(stmt)
expired_requests = result.scalars().all()
for req in expired_requests:
# Állítsd REJECTED-re
req.status = WithdrawalRequestStatus.REJECTED
req.reason = "Automatikus elutasítás: 14 napig hiányzó bizonylat"
# Refund: pénz vissza a user Earned zsebébe
# Ehhez létrehozunk egy FinancialLedger bejegyzést (refund)
refund_transaction = FinancialLedger(
transaction_id=uuid.uuid4(),
user_id=req.user_id,
wallet_type=WalletType.EARNED,
amount=req.amount,
currency=req.currency,
transaction_type="REFUND",
description=f"Refund for expired withdrawal request #{req.id}",
metadata={"withdrawal_request_id": req.id}
)
db.add(refund_transaction)
req.refund_transaction_id = refund_transaction.transaction_id
stats["withdrawals_rejected"] += 1
await db.commit()
logger.info(f"Rejected {len(expired_requests)} expired withdrawal requests")
except Exception as e:
await db.rollback()
stats["errors"].append(f"Withdrawal expiration error: {str(e)}")
logger.error(f"Withdrawal expiration error: {e}", exc_info=True)
# C. Soft Downgrade (lejárt előfizetések)
try:
# Keresd meg a lejárt subscription_expires_at idejű usereket
stmt = select(User).where(
and_(
User.subscription_expires_at < datetime.utcnow(),
User.subscription_plan != 'FREE',
User.is_deleted == False
)
)
result = await db.execute(stmt)
expired_users = result.scalars().all()
for user in expired_users:
# Állítsd a subscription_plan-t 'FREE'-re, role-t 'user'-re
user.subscription_plan = 'FREE'
user.role = 'user'
# Opcionálisan: állítsd be a felfüggesztett státuszt a kapcsolódó entitásokon
# (pl. Organization.is_active = False) - ez egy külön logika lehet
stats["users_downgraded"] += 1
await db.commit()
logger.info(f"Downgraded {len(expired_users)} users to FREE plan")
except Exception as e:
await db.rollback()
stats["errors"].append(f"Soft downgrade error: {str(e)}")
logger.error(f"Soft downgrade error: {e}", exc_info=True)
# D. Naplózás ProcessLog-ba (JAVÍTOTT RÉSZ)
process_log = ProcessLog(
process_name="Daily-Financial-Maintenance",
items_processed=stats["vouchers_expired"] + stats["withdrawals_rejected"] + stats["users_downgraded"],
items_failed=len(stats["errors"]),
end_time=datetime.utcnow(),
details={
"status": "COMPLETED" if not stats["errors"] else "PARTIAL",
**stats
}
)
db.add(process_log)
await db.commit()
logger.info(f"Daily financial maintenance completed: {stats}")
except Exception as e:
logger.error(f"Daily financial maintenance failed: {e}", exc_info=True)
# Hiba esetén is naplózzuk a modellnek megfelelő mezőkkel
process_log = ProcessLog(
process_name="Daily-Financial-Maintenance",
items_processed=0,
items_failed=1,
end_time=datetime.utcnow(),
details={
"status": "FAILED",
"error": str(e),
**stats
}
)
db.add(process_log)
await db.commit()
def setup_scheduler() -> None:
"""Beállítja a scheduler-t a napi feladatokkal."""
scheduler = get_scheduler()
# Napi futás 00:15-kor, jitter=900 (15 perc véletlenszerű eltolás)
scheduler.add_job(
daily_financial_maintenance,
trigger=CronTrigger(hour=0, minute=15, jitter=900),
id="daily_financial_maintenance",
name="Daily Financial Maintenance",
replace_existing=True
)
logger.info("Scheduler jobs registered")
@asynccontextmanager
async def scheduler_lifespan(app):
"""
FastAPI lifespan manager, amely elindítja és leállítja a schedulert.
"""
# Importáljuk a szükséges modulokat
import uuid
from datetime import timedelta
global _scheduler
scheduler = get_scheduler()
setup_scheduler()
logger.info("Starting scheduler...")
scheduler.start()
# Azonnali tesztfutás (opcionális, csak fejlesztéshez)
# scheduler.add_job(daily_financial_maintenance, 'date', run_date=datetime.utcnow())
yield
logger.info("Shutting down scheduler...")
scheduler.shutdown(wait=False)
_scheduler = None

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/validators.py (Javasolt új hely)
# /opt/docker/dev/service_finder/backend/app/core/validators.py
import hashlib
import unicodedata
import re

View File

@@ -3,7 +3,15 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sess
from sqlalchemy.orm import DeclarativeBase
from app.core.config import settings
# Most már settings.SQLALCHEMY_DATABASE_URI létezik a property miatt!
# 1. Base definíciója - Ezt importálják a modellek
class Base(DeclarativeBase):
"""
Központi SQLAlchemy Base osztály.
A modellek a 'from app.database import Base' segítségével érik el.
"""
pass
# 2. Engine és SessionLocal beállítása
engine = create_async_engine(
str(settings.SQLALCHEMY_DATABASE_URI),
echo=settings.DEBUG_MODE,
@@ -20,5 +28,20 @@ AsyncSessionLocal = async_sessionmaker(
expire_on_commit=False
)
class Base(DeclarativeBase):
pass
# 3. A "Körforgás-törő" függvény
def ensure_models_loaded():
"""
Dinamikusan betölti az összes modellt a regiszter segítségével.
Helyi importot használunk, hogy elkerüljük a körkörös függőséget:
database -> registry -> database (Base)
"""
try:
# Itt importálunk helyben, így a Base már létezik a memóriában
from app.models.registry import load_all_models
load_all_models()
print("✅ Adatbázis modellek regisztrálva a MetaData-ba.")
except ImportError as e:
print(f"⚠️ Hiba a modellek dinamikus betöltésekor: {e}")
# Automatikus betöltés meghívása (opcionális, de ajánlott az API indításakor)
# ensure_models_loaded()

View File

@@ -6,30 +6,30 @@ from app.models.address import Address, GeoPostalCode, GeoStreet, GeoStreetType,
from app.models.identity import Person, User, Wallet, VerificationToken, SocialAccount # noqa
from app.models.organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment # noqa
from app.models.marketplace.organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment # noqa
from app.models.service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter # noqa
from app.models.marketplace.service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter # noqa
from app.models.vehicle_definitions import VehicleType, VehicleModelDefinition, FeatureDefinition # noqa
from app.models import VehicleType, VehicleModelDefinition, FeatureDefinition # noqa
from app.models.audit import SecurityAuditLog, OperationalLog, FinancialLedger # noqa <--- KRITIKUS!
from app.models import SecurityAuditLog, OperationalLog, FinancialLedger # noqa <--- KRITIKUS!
from app.models.asset import ( # noqa
from app.models import ( # noqa
Asset, AssetCatalog, AssetCost, AssetEvent,
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
)
from app.models.gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger # noqa
from app.models import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger # noqa
from app.models.system import SystemParameter # noqa (system.py használata)
from app.models.history import AuditLog, VehicleOwnership # noqa
from app.models import AuditLog, VehicleOwnership # noqa
from app.models.document import Document # noqa
from app.models import Document # noqa
from app.models.translation import Translation # noqa
from app.models import Translation # noqa
from app.models.core_logic import ( # noqa
SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
)
from app.models.security import PendingAction # noqa
from app.models import PendingAction # noqa

View File

@@ -1,38 +0,0 @@
from typing import Generator, Optional, Dict, Any
from fastapi import Request
from app.db.session import get_conn
def _set_config(cur, key: str, value: str) -> None:
cur.execute("SELECT set_config(%s, %s, false);", (key, value))
def db_tx(request: Request) -> Generator[Dict[str, Any], None, None]:
"""
Egységes DB tranzakció + session context:
BEGIN
set_config(app.tenant_org_id, app.account_id, app.is_platform_admin)
COMMIT/ROLLBACK
"""
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("BEGIN;")
claims: Optional[dict] = getattr(request.state, "claims", None)
if claims:
org_id = claims.get("org_id") or ""
account_id = claims.get("sub") or ""
is_platform_admin = claims.get("is_platform_admin", False)
# Fontos: set_config stringeket vár
_set_config(cur, "app.tenant_org_id", str(org_id))
_set_config(cur, "app.account_id", str(account_id))
_set_config(cur, "app.is_platform_admin", "true" if is_platform_admin else "false")
yield {"conn": conn, "cur": cur}
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()

View File

@@ -1,7 +1,7 @@
# /opt/docker/dev/service_finder/backend/app/db/middleware.py
from fastapi import Request
from app.db.session import AsyncSessionLocal
from app.models.audit import OperationalLog # JAVÍTVA: Az új modell
from app.models import OperationalLog # JAVÍTVA: Az új modell
from sqlalchemy import text
async def audit_log_middleware(request: Request, call_next):

View File

@@ -9,7 +9,8 @@ engine = create_async_engine(
future=True,
pool_size=30, # A robotok száma miatt
max_overflow=20,
pool_pre_ping=True
pool_pre_ping=True,
pool_reset_on_return='rollback'
)
AsyncSessionLocal = async_sessionmaker(
@@ -21,8 +22,20 @@ AsyncSessionLocal = async_sessionmaker(
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
# Start with a clean transaction state by rolling back any failed transaction
try:
await session.rollback()
except Exception:
# If rollback fails, it's probably because there's no transaction
# This is fine, just continue
pass
try:
yield session
# JAVÍTVA: Nincs automatikus commit! Az endpoint felelőssége.
except Exception:
# If any exception occurs, rollback the transaction
await session.rollback()
raise
finally:
# Ensure session is closed
await session.close()

View File

@@ -12,6 +12,7 @@ from app.api.v1.api import api_router
from app.core.config import settings
from app.database import AsyncSessionLocal
from app.services.translation_service import translation_service
from app.core.scheduler import scheduler_lifespan
# --- LOGGING KONFIGURÁCIÓ ---
logging.basicConfig(level=logging.INFO)
@@ -39,6 +40,10 @@ async def lifespan(app: FastAPI):
os.makedirs(settings.STATIC_DIR, exist_ok=True)
os.makedirs(os.path.join(settings.STATIC_DIR, "previews"), exist_ok=True)
# 2. Scheduler indítása
async with scheduler_lifespan(app):
logger.info("⏰ Cronjob ütemező aktiválva.")
yield
logger.info("💤 Sentinel Master System leállítása...")

View File

@@ -3,39 +3,53 @@
from app.database import Base
# 1. Alapvető identitás és szerepkörök
from .identity import Person, User, Wallet, VerificationToken, SocialAccount, UserRole
from .identity.identity import Person, User, Wallet, VerificationToken, SocialAccount, UserRole
# 2. Földrajzi adatok és címek
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Rating
from .identity.address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Rating
# 3. Jármű definíciók
from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
from .vehicle.vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
from .reference_data import ReferenceLookup
from .vehicle.vehicle import CostCategory, VehicleCost, GbCatalogDiscovery
from .vehicle.external_reference import ExternalReferenceLibrary
from .vehicle.external_reference_queue import ExternalReferenceQueue
# 4. Szervezeti felépítés
from .organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment, OrgType, OrgUserRole, Branch
from .marketplace.organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment, OrgType, OrgUserRole, Branch
# 5. Eszközök és katalógusok
from .asset import Asset, AssetCatalog, AssetCost, AssetEvent, AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate, CatalogDiscovery, VehicleOwnership
from .vehicle.asset import Asset, AssetCatalog, AssetCost, AssetEvent, AssetAssignment, AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate, CatalogDiscovery, VehicleOwnership
# 6. Üzleti logika és előfizetések
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
from .marketplace.payment import PaymentIntent, PaymentIntentStatus
from .marketplace.finance import Issuer, IssuerType
# 7. Szolgáltatások és staging
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter
# JAVÍTVA: ServiceStaging és társai a staged_data-ból jönnek!
from .marketplace.service import ServiceProfile, ExpertiseTag, ServiceExpertise
from .marketplace.staged_data import ServiceStaging, DiscoveryParameter, StagedVehicleData
from .marketplace.service_request import ServiceRequest
# 8. Rendszer, Gamification és egyebek
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger
# 8. Közösségi és értékelési modellek (Social 3)
from .identity.social import ServiceProvider, Vote, Competition, UserScore, ServiceReview, ModerationStatus, SourceType
# 9. Rendszer, Gamification és egyebek
from .gamification.gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger, UserContribution, Season
# --- 2.2 ÚJDONSÁG: InternalNotification hozzáadása ---
from .system import SystemParameter, InternalNotification
from .system.system import SystemParameter, ParameterScope, InternalNotification, SystemServiceStaging
from .system.document import Document
from .system.translation import Translation
# Direct import from audit module
from .system.audit import SecurityAuditLog, OperationalLog, ProcessLog, FinancialLedger, WalletType, LedgerStatus, LedgerEntryType
from .vehicle.history import AuditLog, LogSeverity
from .identity.security import PendingAction, ActionStatus
from .system.legal import LegalDocument, LegalAcceptance
from .marketplace.logistics import Location, LocationType
from .document import Document
from .translation import Translation
from .audit import SecurityAuditLog, ProcessLog, FinancialLedger
from .history import AuditLog, LogSeverity
from .security import PendingAction
from .legal import LegalDocument, LegalAcceptance
from .logistics import Location, LocationType
# Aliasok a Digital Twin kompatibilitáshoz
Vehicle = Asset
@@ -46,20 +60,26 @@ ServiceRecord = AssetEvent
__all__ = [
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
"Organization", "OrganizationMember", "OrganizationSalesAssignment", "OrgType", "OrgUserRole",
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetAssignment", "AssetFinancials",
"AssetTelemetry", "AssetReview", "ExchangeRate", "CatalogDiscovery",
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger", "UserContribution",
# --- 2.2 ÚJDONSÁG KIEGÉSZÍTÉS ---
"SystemParameter", "InternalNotification",
"SystemParameter", "ParameterScope", "InternalNotification",
"Document", "Translation", "PendingAction",
# Social models (Social 3)
"ServiceProvider", "Vote", "Competition", "UserScore", "ServiceReview", "ModerationStatus", "SourceType",
"Document", "Translation", "PendingAction", "ActionStatus",
"SubscriptionTier", "OrganizationSubscription", "CreditTransaction", "ServiceSpecialty",
"PaymentIntent", "PaymentIntentStatus",
"AuditLog", "VehicleOwnership", "LogSeverity",
"SecurityAuditLog", "ProcessLog", "FinancialLedger",
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter",
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition",
"SecurityAuditLog", "OperationalLog", "ProcessLog",
"FinancialLedger", "WalletType", "LedgerStatus", "LedgerEntryType",
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter", "ServiceRequest",
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition", "ReferenceLookup",
"VehicleType", "FeatureDefinition", "ModelFeatureMap", "LegalDocument", "LegalAcceptance",
"Location", "LocationType"
"Location", "LocationType", "Issuer", "IssuerType", "CostCategory", "VehicleCost", "ExternalReferenceLibrary", "ExternalReferenceQueue",
"GbCatalogDiscovery", "Season", "StagedVehicleData"
]

83
backend/app/models/audit.py Executable file → Normal file
View File

@@ -1,63 +1,24 @@
# /opt/docker/dev/service_finder/backend/app/models/audit.py
from datetime import datetime
from typing import Any, Optional
from sqlalchemy import String, DateTime, JSON, ForeignKey, text, Numeric, Boolean, BigInteger, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from app.database import Base
# Backward compatibility stub for audit module
# After restructuring, audit models moved to system.audit
# This file re-exports everything to maintain compatibility
class SecurityAuditLog(Base):
""" Kiemelt biztonsági események és a 4-szem elv naplózása. """
__tablename__ = "security_audit_logs"
from .system.audit import (
SecurityAuditLog,
OperationalLog,
ProcessLog,
LedgerEntryType,
WalletType,
LedgerStatus,
FinancialLedger,
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
action: Mapped[Optional[str]] = mapped_column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST'
actor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
target_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
confirmed_by_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=True)
is_critical: Mapped[bool] = mapped_column(Boolean, default=False)
payload_before: Mapped[Any] = mapped_column(JSON)
payload_after: Mapped[Any] = mapped_column(JSON)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class OperationalLog(Base):
""" Felhasználói szintű napi üzemi események (Audit Trail). """
__tablename__ = "operational_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="SET NULL"))
action: Mapped[str] = mapped_column(String(100), nullable=False) # pl. "ADD_VEHICLE"
resource_type: Mapped[Optional[str]] = mapped_column(String(50))
resource_id: Mapped[Optional[str]] = mapped_column(String(100))
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
ip_address: Mapped[Optional[str]] = mapped_column(String(45))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ProcessLog(Base):
""" Robotok és háttérfolyamatok futási naplója (A reggeli jelentésekhez). """
__tablename__ = "process_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
process_name: Mapped[str] = mapped_column(String(100), index=True) # 'Master-Enricher'
start_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
end_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
items_processed: Mapped[int] = mapped_column(Integer, default=0)
items_failed: Mapped[int] = mapped_column(Integer, default=0)
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class FinancialLedger(Base):
""" Minden pénz- és kreditmozgás központi naplója. Billing Engine alapja. """
__tablename__ = "financial_ledger"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
currency: Mapped[Optional[str]] = mapped_column(String(10))
transaction_type: Mapped[Optional[str]] = mapped_column(String(50))
related_agent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# Re-export everything
__all__ = [
"SecurityAuditLog",
"OperationalLog",
"ProcessLog",
"LedgerEntryType",
"WalletType",
"LedgerStatus",
"FinancialLedger",
]

View File

@@ -15,7 +15,7 @@ class SubscriptionTier(Base):
A csomagok határozzák meg a korlátokat (pl. max járműszám).
"""
__tablename__ = "subscription_tiers"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "system"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String, unique=True, index=True) # pl. 'premium'
@@ -27,15 +27,15 @@ class OrganizationSubscription(Base):
Szervezetek aktuális előfizetései és azok érvényessége.
"""
__tablename__ = "org_subscriptions"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "finance"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Kapcsolat a szervezettel (data séma)
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
# Kapcsolat a szervezettel (fleet séma)
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), nullable=False)
# Kapcsolat a csomaggal (data séma)
tier_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.subscription_tiers.id"), nullable=False)
# Kapcsolat a csomaggal (system séma)
tier_id: Mapped[int] = mapped_column(Integer, ForeignKey("system.subscription_tiers.id"), nullable=False)
valid_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
valid_until: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
@@ -46,12 +46,12 @@ class CreditTransaction(Base):
Kreditnapló (Pontok, kreditek vagy virtuális egyenleg követése).
"""
__tablename__ = "credit_logs"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "finance"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Kapcsolat a szervezettel (data séma)
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
# Kapcsolat a szervezettel (fleet séma)
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), nullable=False)
amount: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
description: Mapped[Optional[str]] = mapped_column(String)
@@ -62,12 +62,12 @@ class ServiceSpecialty(Base):
Hierarchikus fa struktúra a szerviz szolgáltatásokhoz (pl. Motor -> Futómű).
"""
__tablename__ = "service_specialties"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Önmagára mutató idegen kulcs a hierarchiához
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.service_specialties.id"))
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("marketplace.service_specialties.id"))
name: Mapped[str] = mapped_column(String, nullable=False)
slug: Mapped[str] = mapped_column(String, unique=True, index=True)

View File

@@ -1,86 +0,0 @@
# /opt/docker/dev/service_finder/backend/app/models/gamification.py
import uuid
from datetime import datetime
from typing import Optional, List, TYPE_CHECKING
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean, Text, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from app.database import Base # MB 2.0: Központi Base
if TYPE_CHECKING:
from app.models.identity import User
class PointRule(Base):
__tablename__ = "point_rules"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
action_key: Mapped[str] = mapped_column(String, unique=True, index=True)
points: Mapped[int] = mapped_column(Integer, default=0)
description: Mapped[Optional[str]] = mapped_column(String)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class LevelConfig(Base):
__tablename__ = "level_configs"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
level_number: Mapped[int] = mapped_column(Integer, unique=True)
min_points: Mapped[int] = mapped_column(Integer)
rank_name: Mapped[str] = mapped_column(String)
class PointsLedger(Base):
__tablename__ = "points_ledger"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
points: Mapped[int] = mapped_column(Integer, default=0)
penalty_change: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
reason: Mapped[str] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")
class UserStats(Base):
__tablename__ = "user_stats"
__table_args__ = {"schema": "data"}
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), primary_key=True)
total_xp: Mapped[int] = mapped_column(Integer, default=0)
social_points: Mapped[int] = mapped_column(Integer, default=0)
current_level: Mapped[int] = mapped_column(Integer, default=1)
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
restriction_level: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
user: Mapped["User"] = relationship("User", back_populates="stats")
class Badge(Base):
__tablename__ = "badges"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String, unique=True)
description: Mapped[str] = mapped_column(String)
icon_url: Mapped[Optional[str]] = mapped_column(String)
class UserBadge(Base):
__tablename__ = "user_badges"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.badges.id"))
earned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")

View File

@@ -0,0 +1,22 @@
# gamification package exports
from .gamification import (
PointRule,
LevelConfig,
PointsLedger,
UserStats,
Badge,
UserBadge,
UserContribution,
Season,
)
__all__ = [
"PointRule",
"LevelConfig",
"PointsLedger",
"UserStats",
"Badge",
"UserBadge",
"UserContribution",
"Season",
]

View File

@@ -0,0 +1,165 @@
# /opt/docker/dev/service_finder/backend/app/models/gamification/gamification.py
import uuid
from datetime import datetime, date
from typing import Optional, List, TYPE_CHECKING
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean, Text, text, Date
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from app.database import Base # MB 2.0: Központi Base
if TYPE_CHECKING:
from app.models.identity import User
class PointRule(Base):
__tablename__ = "point_rules"
__table_args__ = {"schema": "gamification", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
action_key: Mapped[str] = mapped_column(String, unique=True, index=True)
points: Mapped[int] = mapped_column(Integer, default=0)
description: Mapped[Optional[str]] = mapped_column(String)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class LevelConfig(Base):
__tablename__ = "level_configs"
__table_args__ = {"schema": "gamification", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
level_number: Mapped[int] = mapped_column(Integer, unique=True) # Pozitív: normál szintek, Negatív: büntető szintek (-1, -2, -3)
min_points: Mapped[int] = mapped_column(Integer) # XP küszöb pozitív szinteknél, büntetőpont küszöb negatív szinteknél
rank_name: Mapped[str] = mapped_column(String)
is_penalty: Mapped[bool] = mapped_column(Boolean, default=False, index=True) # True ha büntető szint
class PointsLedger(Base):
__tablename__ = "points_ledger"
__table_args__ = {"schema": "gamification", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
points: Mapped[int] = mapped_column(Integer, default=0)
penalty_change: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
reason: Mapped[str] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")
class UserStats(Base):
__tablename__ = "user_stats"
__table_args__ = {"schema": "gamification", "extend_existing": True}
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), primary_key=True)
total_xp: Mapped[int] = mapped_column(Integer, default=0)
social_points: Mapped[int] = mapped_column(Integer, default=0)
current_level: Mapped[int] = mapped_column(Integer, default=1)
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
restriction_level: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
penalty_quota_remaining: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
places_discovered: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
places_validated: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
banned_until: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
user: Mapped["User"] = relationship("User", back_populates="stats")
class Badge(Base):
__tablename__ = "badges"
__table_args__ = {"schema": "gamification", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String, unique=True)
description: Mapped[str] = mapped_column(String)
icon_url: Mapped[Optional[str]] = mapped_column(String)
class UserBadge(Base):
__tablename__ = "user_badges"
__table_args__ = {"schema": "gamification", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("gamification.badges.id"))
earned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")
class UserContribution(Base):
"""
Felhasználói hozzájárulások nyilvántartása (szerviz beküldés, validálás, jelentés).
Ez a tábla tárolja, hogy melyik felhasználó milyen tevékenységet végzett és milyen jutalmat kapott.
"""
__tablename__ = "user_contributions"
__table_args__ = {"schema": "gamification"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False, index=True)
season_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("gamification.seasons.id"), nullable=True, index=True)
# --- HIÁNYZÓ MEZŐK PÓTOLVA A SPAM VÉDELEMHEZ ---
service_fingerprint: Mapped[Optional[str]] = mapped_column(String(255), index=True)
cooldown_end: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
action_type: Mapped[int] = mapped_column(Integer, nullable=False)
earned_xp: Mapped[int] = mapped_column(Integer, nullable=False)
contribution_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True) # 'service_submission', 'service_validation', 'report_abuse'
entity_type: Mapped[Optional[str]] = mapped_column(String(50), index=True) # 'service', 'review', 'comment'
entity_id: Mapped[Optional[int]] = mapped_column(Integer, index=True) # ID of the contributed entity
points_awarded: Mapped[int] = mapped_column(Integer, default=0)
xp_awarded: Mapped[int] = mapped_column(Integer, default=0)
status: Mapped[str] = mapped_column(String(20), default="pending", index=True) # 'pending', 'approved', 'rejected'
reviewed_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=True)
reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
# --- JAVÍTOTT FOGLALT SZÓ ---
provided_fields: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# Relationships
user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
reviewer: Mapped[Optional["User"]] = relationship("User", foreign_keys=[reviewed_by])
season: Mapped[Optional["Season"]] = relationship("Season")
class Season(Base):
""" Szezonális versenyek tárolása. """
__tablename__ = "seasons"
__table_args__ = {"schema": "gamification"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String(100), nullable=False)
start_date: Mapped[date] = mapped_column(Date, nullable=False)
end_date: Mapped[date] = mapped_column(Date, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class SeasonalCompetitions(Base):
""" Szezonális versenyek és kihívások tárolása. """
__tablename__ = "seasonal_competitions"
__table_args__ = {"schema": "gamification"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String(200), nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text)
season_id: Mapped[int] = mapped_column(Integer, ForeignKey("gamification.seasons.id"), nullable=False, index=True)
start_date: Mapped[date] = mapped_column(Date, nullable=False)
end_date: Mapped[date] = mapped_column(Date, nullable=False)
rules: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True) # JSON szabályok
status: Mapped[str] = mapped_column(String(20), default="draft", index=True) # draft, active, completed, cancelled
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())
# Relationships
season: Mapped["Season"] = relationship("Season")

View File

@@ -0,0 +1,55 @@
# identity package exports
from .identity import (
Person,
User,
Wallet,
VerificationToken,
SocialAccount,
ActiveVoucher,
UserTrustProfile,
UserRole,
)
from .address import (
Address,
GeoPostalCode,
GeoStreet,
GeoStreetType,
Rating,
)
from .security import PendingAction, ActionStatus
from .social import (
ServiceProvider,
Vote,
Competition,
UserScore,
ServiceReview,
ModerationStatus,
SourceType,
)
__all__ = [
"Person",
"User",
"Wallet",
"VerificationToken",
"SocialAccount",
"ActiveVoucher",
"UserTrustProfile",
"UserRole",
"Address",
"GeoPostalCode",
"GeoStreet",
"GeoStreetType",
"Rating",
"PendingAction",
"ActionStatus",
"ServiceProvider",
"Vote",
"Competition",
"UserScore",
"ServiceReview",
"ModerationStatus",
"SourceType",
]

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/address.py
# /opt/docker/dev/service_finder/backend/app/models/identity/address.py
import uuid
from datetime import datetime
from typing import Any, List, Optional
@@ -12,7 +12,7 @@ from app.database import Base
class GeoPostalCode(Base):
"""Irányítószám alapú földrajzi kereső tábla."""
__tablename__ = "geo_postal_codes"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "system"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
country_code: Mapped[str] = mapped_column(String(5), default="HU")
@@ -22,16 +22,16 @@ class GeoPostalCode(Base):
class GeoStreet(Base):
"""Utcajegyzék tábla."""
__tablename__ = "geo_streets"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "system"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.geo_postal_codes.id"))
postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("system.geo_postal_codes.id"))
name: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
class GeoStreetType(Base):
"""Közterület jellege (utca, út, köz stb.)."""
__tablename__ = "geo_street_types"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "system"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
@@ -39,10 +39,10 @@ class GeoStreetType(Base):
class Address(Base):
"""Univerzális cím entitás GPS adatokkal kiegészítve."""
__tablename__ = "addresses"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "system"}
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.geo_postal_codes.id"))
postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("system.geo_postal_codes.id"))
street_name: Mapped[str] = mapped_column(String(200), nullable=False)
street_type: Mapped[str] = mapped_column(String(50), nullable=False)
@@ -69,7 +69,7 @@ class Rating(Base):
Index('idx_rating_org', 'target_organization_id'),
Index('idx_rating_user', 'target_user_id'),
Index('idx_rating_branch', 'target_branch_id'),
{"schema": "data"}
{"schema": "marketplace"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
@@ -77,9 +77,9 @@ class Rating(Base):
# MB 2.0: A felhasználók az identity sémában laknak!
author_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
target_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
target_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"))
target_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
target_branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"))
target_branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("fleet.branches.id"))
score: Mapped[float] = mapped_column(Numeric(3, 2), nullable=False)
comment: Mapped[Optional[str]] = mapped_column(Text)

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/identity.py
# /opt/docker/dev/service_finder/backend/app/models/identity/identity.py
from __future__ import annotations
import uuid
import enum
@@ -16,6 +16,8 @@ if TYPE_CHECKING:
from .organization import Organization, OrganizationMember
from .asset import VehicleOwnership
from .gamification import UserStats
from .payment import PaymentIntent, WithdrawalRequest
from .social import ServiceReview, SocialAccount
class UserRole(str, enum.Enum):
superadmin = "superadmin"
@@ -40,11 +42,10 @@ class Person(Base):
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, index=True)
id_uuid: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
# A lakcím a 'data' sémában marad
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
# A lakcím a 'system' sémában van
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("system.addresses.id"))
# Kritikus azonosító: Név + Anyja neve + Szül.idő hash-elve.
# Ezzel ismerjük fel a személyt akkor is, ha új User accountot hoz létre.
identity_hash: Mapped[Optional[str]] = mapped_column(String(64), unique=True, index=True)
last_name: Mapped[str] = mapped_column(String, nullable=False)
@@ -56,27 +57,50 @@ class Person(Base):
birth_place: Mapped[Optional[str]] = mapped_column(String)
birth_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
identity_docs: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
ice_contact: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
identity_docs: Mapped[Any] = mapped_column(JSON, nullable=False, default=lambda: {}, server_default=text("'{}'::jsonb"))
ice_contact: Mapped[Any] = mapped_column(JSON, nullable=False, default=lambda: {}, server_default=text("'{}'::jsonb"))
lifetime_xp: Mapped[int] = mapped_column(BigInteger, server_default=text("0"))
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"))
social_reputation: Mapped[float] = mapped_column(Numeric(3, 2), server_default=text("1.00"))
lifetime_xp: Mapped[int] = mapped_column(BigInteger, default=-1, nullable=False)
penalty_points: Mapped[int] = mapped_column(Integer, default=-1, nullable=False)
social_reputation: Mapped[float] = mapped_column(Numeric(3, 2), default=0.0, nullable=False)
is_sales_agent: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
is_sales_agent: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
is_ghost: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=func.now(), nullable=False)
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# --- KAPCSOLATOK ---
users: Mapped[List["User"]] = relationship("User", back_populates="person")
# JAVÍTÁS 1: Explicit 'foreign_keys' megadás az AmbiguousForeignKeysError ellen
users: Mapped[List["User"]] = relationship(
"User",
foreign_keys="[User.person_id]",
back_populates="person",
cascade="all, delete-orphan"
)
# JAVÍTÁS 2: 'post_update' és 'use_alter' a körbe-függőség (circular cycle) feloldásához
active_user_account: Mapped[Optional["User"]] = relationship(
"User",
foreign_keys="[Person.user_id]",
post_update=True
)
user_id: Mapped[Optional[int]] = mapped_column(
Integer,
ForeignKey("identity.users.id", use_alter=True, name="fk_person_active_user"),
nullable=True
)
memberships: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="person")
# MB 2.0 KIEGÉSZÍTÉS: A személy által birtokolt üzleti entitások (Cégek/Szolgáltatók)
# Ez a lista megmarad akkor is, ha az Organization deaktiválódik.
owned_business_entities: Mapped[List["Organization"]] = relationship("Organization", back_populates="legal_owner")
# Kapcsolat a tulajdonolt szervezetekhez (Organization táblában legal_owner_id)
owned_business_entities: Mapped[List["Organization"]] = relationship(
"Organization",
foreign_keys="[Organization.legal_owner_id]",
back_populates="legal_owner"
)
class User(Base):
""" Login entitás. Bármikor törölhető (GDPR), de Person-höz kötött. """
@@ -100,6 +124,7 @@ class User(Base):
referral_code: Mapped[Optional[str]] = mapped_column(String(20), unique=True)
# JAVÍTÁS 3: Az ajánló és értékesítő mezőknek is kell a tiszta kapcsolat nevesítés
referred_by_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
current_sales_agent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
@@ -117,20 +142,49 @@ class User(Base):
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# Kapcsolatok
person: Mapped[Optional["Person"]] = relationship("Person", back_populates="users")
# --- KAPCSOLATOK ---
# JAVÍTÁS 4: Itt is explicit megadjuk, hogy melyik kulcs köti az emberhez
person: Mapped[Optional["Person"]] = relationship(
"Person",
foreign_keys=[person_id],
back_populates="users"
)
# JAVÍTÁS 5: Ajánlói (Referrer) önhivatkozó kapcsolat feloldása
referrer: Mapped[Optional["User"]] = relationship(
"User",
remote_side=[id],
foreign_keys=[referred_by_id]
)
# JAVÍTÁS 6: Értékesítő (Sales Agent) önhivatkozó kapcsolat feloldása
sales_agent: Mapped[Optional["User"]] = relationship(
"User",
remote_side=[id],
foreign_keys=[current_sales_agent_id]
)
wallet: Mapped[Optional["Wallet"]] = relationship("Wallet", back_populates="user", uselist=False)
payment_intents_as_payer = relationship("PaymentIntent", foreign_keys="[PaymentIntent.payer_id]", back_populates="payer")
payment_intents_as_beneficiary = relationship("PaymentIntent", foreign_keys="[PaymentIntent.beneficiary_id]", back_populates="beneficiary")
trust_profile: Mapped[Optional["UserTrustProfile"]] = relationship("UserTrustProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
social_accounts: Mapped[List["SocialAccount"]] = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan")
owned_organizations: Mapped[List["Organization"]] = relationship("Organization", back_populates="owner")
stats: Mapped[Optional["UserStats"]] = relationship("UserStats", back_populates="user", uselist=False, cascade="all, delete-orphan")
ownership_history: Mapped[List["VehicleOwnership"]] = relationship("VehicleOwnership", back_populates="user")
@property
def tier_name(self) -> str:
"""Kompatibilitási mező a keresőhöz: a 'FREE' -> 'free' konverzióhoz"""
return (self.subscription_plan or "free").lower()
# MB 2.1: Vehicle ratings kapcsolat (hiányzott a listából, visszatéve)
vehicle_ratings: Mapped[List["VehicleUserRating"]] = relationship("VehicleUserRating", back_populates="user", cascade="all, delete-orphan")
# Pénzügyi és egyéb kapcsolatok
withdrawal_requests: Mapped[List["WithdrawalRequest"]] = relationship("WithdrawalRequest", foreign_keys="[WithdrawalRequest.user_id]", back_populates="user", cascade="all, delete-orphan")
service_reviews: Mapped[List["ServiceReview"]] = relationship("ServiceReview", back_populates="user", cascade="all, delete-orphan")
class Wallet(Base):
""" Felhasználói pénztárca. """
__tablename__ = "wallets"
__table_args__ = {"schema": "identity"}
@@ -143,8 +197,10 @@ class Wallet(Base):
currency: Mapped[str] = mapped_column(String(3), default="HUF")
user: Mapped["User"] = relationship("User", back_populates="wallet")
active_vouchers: Mapped[List["ActiveVoucher"]] = relationship("ActiveVoucher", back_populates="wallet", cascade="all, delete-orphan")
class VerificationToken(Base):
""" E-mail és egyéb verifikációs tokenek. """
__tablename__ = "verification_tokens"
__table_args__ = {"schema": "identity"}
@@ -157,6 +213,7 @@ class VerificationToken(Base):
is_used: Mapped[bool] = mapped_column(Boolean, default=False)
class SocialAccount(Base):
""" Közösségi bejelentkezési adatok (Google, Facebook, stb). """
__tablename__ = "social_accounts"
__table_args__ = (
UniqueConstraint('provider', 'social_id', name='uix_social_provider_id'),
@@ -172,3 +229,40 @@ class SocialAccount(Base):
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User", back_populates="social_accounts")
class ActiveVoucher(Base):
""" Aktív, le nem járt voucher-ek tárolása FIFO elv szerint. """
__tablename__ = "active_vouchers"
__table_args__ = {"schema": "identity"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
wallet_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.wallets.id", ondelete="CASCADE"), nullable=False)
amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
original_amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
wallet: Mapped["Wallet"] = relationship("Wallet", back_populates="active_vouchers")
class UserTrustProfile(Base):
""" Gondos Gazda Index (Trust Score) tárolása. """
__tablename__ = "user_trust_profiles"
__table_args__ = {"schema": "identity"}
user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("identity.users.id", ondelete="CASCADE"),
primary_key=True,
index=True
)
trust_score: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
maintenance_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0.0, nullable=False)
quality_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0.0, nullable=False)
preventive_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0.0, nullable=False)
last_calculated: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False
)
user: Mapped["User"] = relationship("User", back_populates="trust_profile", uselist=False)

View File

@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""
Central Model Registry for Service Finder
Automatically discovers and imports all SQLAlchemy models from the models directory,
ensuring Base.metadata is fully populated with tables, constraints, and indexes.
Usage:
from app.models.registry import Base, get_all_models, ensure_models_loaded
"""
import importlib
import os
import sys
from pathlib import Path
from typing import Dict, List, Type
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.orm import DeclarativeBase
# Import the Base from database (circular dependency will be resolved later)
# We'll define our own Base if needed, but better to reuse existing one.
# We'll import after path setup.
# Add backend to path if not already
backend_dir = Path(__file__).parent.parent.parent
if str(backend_dir) not in sys.path:
sys.path.insert(0, str(backend_dir))
# Import Base from database (this will be the same Base used everywhere)
from app.database import Base
def discover_model_files() -> List[Path]:
"""
Walk through models directory and collect all .py files except __init__.py and registry.py.
"""
models_dir = Path(__file__).parent
model_files = []
for root, _, files in os.walk(models_dir):
for file in files:
if file.endswith('.py') and file not in ('__init__.py', 'registry.py'):
full_path = Path(root) / file
model_files.append(full_path)
return model_files
def import_module_from_file(file_path: Path) -> str:
"""
Import a Python module from its file path.
Returns the module name.
"""
# Compute module name relative to backend/app
rel_path = file_path.relative_to(backend_dir)
module_name = str(rel_path).replace(os.sep, '.').replace('.py', '')
try:
spec = importlib.util.spec_from_file_location(module_name, file_path)
if spec is None:
raise ImportError(f"Could not load spec for {module_name}")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module_name
except Exception as e:
# Silently skip import errors (maybe due to missing dependencies)
# but log for debugging
print(f"⚠️ Could not import {module_name}: {e}", file=sys.stderr)
return None
def load_all_models() -> List[str]:
"""
Dynamically import all model files to populate Base.metadata.
Returns list of successfully imported module names.
"""
model_files = discover_model_files()
imported = []
for file in model_files:
module_name = import_module_from_file(file)
if module_name:
imported.append(module_name)
# Also ensure the __init__.py is loaded (it imports many models manually)
try:
import app.models
imported.append('app.models')
except ImportError:
pass
print(f"✅ Registry loaded {len(imported)} model modules. Total tables in metadata: {len(Base.metadata.tables)}")
return imported
def get_all_models() -> Dict[str, Type[DeclarativeMeta]]:
"""
Return a mapping of class name to model class for all registered SQLAlchemy models.
This works only after models have been imported.
"""
# This is a heuristic: find all subclasses of Base in loaded modules
from sqlalchemy.orm import DeclarativeBase
models = {}
for cls in Base.__subclasses__():
models[cls.__name__] = cls
# Also check deeper inheritance (if models inherit from other models that inherit from Base)
for module_name, module in sys.modules.items():
if module_name.startswith('app.models.'):
for attr_name in dir(module):
attr = getattr(module, attr_name)
if isinstance(attr, type) and issubclass(attr, Base) and attr is not Base:
models[attr.__name__] = attr
return models
def ensure_models_loaded():
"""
Ensure that all models are loaded into Base.metadata.
This is idempotent and can be called multiple times.
"""
if len(Base.metadata.tables) == 0:
load_all_models()
else:
# Already loaded
pass
# Auto-load models when this module is imported (optional, but useful)
# We'll make it explicit via a function call to avoid side effects.
# Instead, we'll provide a function to trigger loading.
# Export
__all__ = ['Base', 'discover_model_files', 'load_all_models', 'get_all_models', 'ensure_models_loaded']

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/security.py
# /opt/docker/dev/service_finder/backend/app/models/identity/security.py
import enum
from datetime import datetime
from typing import Optional, TYPE_CHECKING

View File

@@ -1,12 +1,13 @@
# /opt/docker/dev/service_finder/backend/app/models/social.py
# /opt/docker/dev/service_finder/backend/app/models/identity/social.py
import enum
import uuid
from datetime import datetime
from typing import Optional, List
from sqlalchemy import String, Integer, ForeignKey, DateTime, Boolean, Text, UniqueConstraint, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID
from sqlalchemy.sql import func
from app.db.base_class import Base
from app.database import Base
class ModerationStatus(str, enum.Enum):
pending = "pending"
@@ -21,6 +22,7 @@ class SourceType(str, enum.Enum):
class ServiceProvider(Base):
""" Közösség által beküldött szolgáltatók (v1.3.1). """
__tablename__ = "service_providers"
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String, nullable=False)
@@ -46,16 +48,18 @@ class Vote(Base):
__tablename__ = "votes"
__table_args__ = (
UniqueConstraint('user_id', 'provider_id', name='uq_user_provider_vote'),
{"schema": "marketplace"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
provider_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.service_providers.id"), nullable=False)
provider_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_providers.id"), nullable=False)
vote_value: Mapped[int] = mapped_column(Integer, nullable=False) # +1 vagy -1
class Competition(Base):
""" Gamifikált versenyek (pl. Januári Feltöltő Verseny). """
__tablename__ = "competitions"
__table_args__ = {"schema": "gamification"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String, nullable=False)
@@ -69,10 +73,44 @@ class UserScore(Base):
__tablename__ = "user_scores"
__table_args__ = (
UniqueConstraint('user_id', 'competition_id', name='uq_user_competition_score'),
{"schema": "gamification"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
competition_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.competitions.id"))
competition_id: Mapped[int] = mapped_column(Integer, ForeignKey("gamification.competitions.id"))
points: Mapped[int] = mapped_column(Integer, default=0)
last_updated: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
class ServiceReview(Base):
"""
Verifikált szerviz értékelések (Social 3).
Csak igazolt pénzügyi tranzakció után lehet értékelni.
"""
__tablename__ = "service_reviews"
__table_args__ = (
UniqueConstraint('transaction_id', name='uq_service_review_transaction'),
{"schema": "marketplace"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id", ondelete="CASCADE"), nullable=False)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="SET NULL"), nullable=False)
transaction_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), nullable=False, index=True)
# Rating dimensions (1-10)
price_rating: Mapped[int] = mapped_column(Integer, nullable=False) # 1-10
quality_rating: Mapped[int] = mapped_column(Integer, nullable=False) # 1-10
time_rating: Mapped[int] = mapped_column(Integer, nullable=False) # 1-10
communication_rating: Mapped[int] = mapped_column(Integer, nullable=False) # 1-10
comment: Mapped[Optional[str]] = mapped_column(Text)
is_verified: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# Relationships
service: Mapped["ServiceProfile"] = relationship("ServiceProfile", back_populates="reviews")
user: Mapped["User"] = relationship("User", back_populates="service_reviews")

View File

@@ -0,0 +1,53 @@
# marketplace package exports
from .organization import (
Organization,
OrganizationMember,
OrganizationFinancials,
OrganizationSalesAssignment,
OrgType,
OrgUserRole,
Branch,
)
from .payment import PaymentIntent, PaymentIntentStatus
from .finance import Issuer, IssuerType
from .service import (
ServiceProfile,
ExpertiseTag,
ServiceExpertise,
)
from .logistics import Location, LocationType
# THOUGHT PROCESS: A StagedVehicleData nevet StagedVehicleData-ra javítottuk,
# és ide csoportosítottuk a staged_data.py-ban lévő többi osztályt is.
from .staged_data import (
StagedVehicleData,
ServiceStaging,
DiscoveryParameter
)
from .service_request import ServiceRequest
__all__ = [
"Organization",
"OrganizationMember",
"OrganizationFinancials",
"OrganizationSalesAssignment",
"OrgType",
"OrgUserRole",
"Branch",
"PaymentIntent",
"PaymentIntentStatus",
"Issuer",
"IssuerType",
"ServiceProfile",
"ExpertiseTag",
"ServiceExpertise",
"ServiceStaging",
"DiscoveryParameter",
"Location",
"LocationType",
"StagedVehicleData",
"ServiceRequest",
]

View File

@@ -0,0 +1,72 @@
# /opt/docker/dev/service_finder/backend/app/models/marketplace/finance.py
"""
Finance modellek: Issuer (Kibocsátó) és FinancialLedger (Pénzügyi főkönyv) bővítése.
"""
import enum
import uuid
from datetime import datetime
from typing import Any, Optional
from sqlalchemy import String, DateTime, JSON, ForeignKey, Numeric, Boolean, Integer, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
from sqlalchemy.sql import func
from app.database import Base
class IssuerType(str, enum.Enum):
"""Kibocsátó típusok (jogi forma)."""
KFT = "KFT"
EV = "EV"
BT = "BT"
ZRT = "ZRT"
OTHER = "OTHER"
class Issuer(Base):
"""
Kibocsátó (számlakibocsátó) entitás.
A rendszerben a számlákat kibocsátó jogi személyek vagy vállalkozások.
Például: KFT, EV, stb. A revenue_limit meghatározza az adóhatár összegét.
"""
__tablename__ = "issuers"
__table_args__ = {"schema": "finance"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Név és adószám
name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
tax_id: Mapped[Optional[str]] = mapped_column(String(50), unique=True, index=True)
# Típus
type: Mapped[IssuerType] = mapped_column(
PG_ENUM(IssuerType, name="issuer_type", schema="finance"),
default=IssuerType.OTHER,
nullable=False
)
# Bevételi limit (pl. KATA határ)
revenue_limit: Mapped[float] = mapped_column(Numeric(18, 4), default=19500000.0)
current_revenue: Mapped[float] = mapped_column(Numeric(18, 4), default=0.0)
# Aktív-e
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# API konfiguráció (pl. számlázó rendszer integráció)
api_config: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
# Időbélyegek
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())
def __repr__(self) -> str:
return f"<Issuer {self.id}: {self.name} ({self.type})>"
# Import FinancialLedger from audit module? We'll keep it separate.
# The FinancialLedger class remains in audit.py, but we add fields there.
# For completeness, we could also define it here, but to avoid duplication,
# we'll just import it if needed.
# Instead, we'll add a relationship from FinancialLedger to Issuer in audit.py.

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/logistics.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/logistics.py
import enum
from typing import Optional
from sqlalchemy import Integer, String, Enum
@@ -13,6 +13,7 @@ class LocationType(str, enum.Enum):
class Location(Base):
__tablename__ = "locations"
__table_args__ = {"schema": "fleet"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String)

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/organization.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/organization.py
import enum
import uuid
from datetime import datetime
@@ -8,6 +8,7 @@ from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, J
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID, JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship, foreign
from sqlalchemy.sql import func
from geoalchemy2 import Geometry
# MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
@@ -35,7 +36,7 @@ class Organization(Base):
a jármű-életút adatok megmaradnak az eredeti Person-höz kötve.
"""
__tablename__ = "organizations"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "fleet"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
@@ -60,7 +61,7 @@ class Organization(Base):
lifecycle_index: Mapped[int] = mapped_column(Integer, default=1, server_default=text("1"))
# --- 🏢 ALAPADATOK (MEGŐRIZVE) ---
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("system.addresses.id"))
is_anonymized: Mapped[bool] = mapped_column(Boolean, default=False, server_default=text("false"))
anonymized_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
@@ -85,7 +86,7 @@ class Organization(Base):
reg_number: Mapped[Optional[str]] = mapped_column(String(50))
org_type: Mapped[OrgType] = mapped_column(
PG_ENUM(OrgType, name="orgtype", schema="data"),
PG_ENUM(OrgType, name="orgtype", schema="fleet"),
default=OrgType.individual
)
@@ -126,12 +127,15 @@ class Organization(Base):
# Kapcsolat az örök személy rekordhoz
legal_owner: Mapped[Optional["Person"]] = relationship("Person", back_populates="owned_business_entities")
# Kapcsolat a jármű költségekhez (TCO rendszer)
vehicle_costs: Mapped[List["VehicleCost"]] = relationship("VehicleCost", back_populates="organization")
class OrganizationFinancials(Base):
__tablename__ = "organization_financials"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "fleet"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), nullable=False)
year: Mapped[int] = mapped_column(Integer, nullable=False)
turnover: Mapped[Optional[float]] = mapped_column(Numeric(18, 2))
profit: Mapped[Optional[float]] = mapped_column(Numeric(18, 2))
@@ -143,16 +147,16 @@ class OrganizationFinancials(Base):
class OrganizationMember(Base):
__tablename__ = "organization_members"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "fleet"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), nullable=False)
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
role: Mapped[OrgUserRole] = mapped_column(
PG_ENUM(OrgUserRole, name="orguserrole", schema="data"),
PG_ENUM(OrgUserRole, name="orguserrole", schema="fleet"),
default=OrgUserRole.DRIVER
)
permissions: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
@@ -165,10 +169,10 @@ class OrganizationMember(Base):
class OrganizationSalesAssignment(Base):
__tablename__ = "org_sales_assignments"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "fleet"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"))
agent_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
assigned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
@@ -179,11 +183,11 @@ class Branch(Base):
Telephely entitás. A fizikai helyszín, ahol a szolgáltatás vagy flotta-kezelés zajlik.
"""
__tablename__ = "branches"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "fleet"}
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), nullable=False)
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("system.addresses.id"))
name: Mapped[str] = mapped_column(String(100), nullable=False)
is_main: Mapped[bool] = mapped_column(Boolean, default=False)
@@ -199,6 +203,12 @@ class Branch(Base):
door: Mapped[Optional[str]] = mapped_column(String(20))
hrsz: Mapped[Optional[str]] = mapped_column(String(50))
# PostGIS location field for geographic queries
location: Mapped[Optional[Any]] = mapped_column(
Geometry(geometry_type='POINT', srid=4326),
nullable=True
)
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
branch_rating: Mapped[float] = mapped_column(Float, default=0.0)

View File

@@ -0,0 +1,224 @@
# /opt/docker/dev/service_finder/backend/app/models/marketplace/payment.py
"""
Payment Intent modell a Stripe integrációhoz és belső fizetésekhez.
Kettős Lakat (Double Lock) biztonságot valósít meg.
"""
import enum
import uuid
from datetime import datetime
from typing import Any, Optional
from sqlalchemy import String, DateTime, JSON, ForeignKey, Numeric, Boolean, Integer, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
from sqlalchemy.sql import func
from app.database import Base
from app.models.system.audit import WalletType
class PaymentIntentStatus(str, enum.Enum):
"""PaymentIntent státuszok."""
PENDING = "PENDING" # Létrehozva, vár Stripe fizetésre
PROCESSING = "PROCESSING" # Fizetés folyamatban (belső ajándékozás)
COMPLETED = "COMPLETED" # Sikeresen teljesítve
FAILED = "FAILED" # Sikertelen (pl. Stripe hiba)
CANCELLED = "CANCELLED" # Felhasználó által törölve
EXPIRED = "EXPIRED" # Lejárt (pl. Stripe session timeout)
class PaymentIntent(Base):
"""
Fizetési szándék (Prior Intent) a Kettős Lakat biztonsághoz.
Minden külső (Stripe) vagy belső fizetés előtt létre kell hozni egy PENDING
státuszú PaymentIntent-et. A Stripe metadata tartalmazza az intent_token-t,
így a webhook validáció során vissza lehet keresni.
Fontos mezők:
- net_amount: A kedvezményezett által kapott összeg (pénztárcába kerül)
- handling_fee: Kényelmi díj (rendszer bevétele)
- gross_amount: net_amount + handling_fee (Stripe-nak küldött összeg)
"""
__tablename__ = "payment_intents"
__table_args__ = {"schema": "finance"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Egyedi token a Stripe metadata számára
intent_token: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False, index=True
)
# Fizető felhasználó
payer_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
payer: Mapped["User"] = relationship("User", foreign_keys=[payer_id], back_populates="payment_intents_as_payer")
# Kedvezményezett felhasználó (opcionális, ha None, akkor a rendszernek fizet)
beneficiary_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
beneficiary: Mapped[Optional["User"]] = relationship("User", foreign_keys=[beneficiary_id], back_populates="payment_intents_as_beneficiary")
# Cél pénztárca típusa
target_wallet_type: Mapped[WalletType] = mapped_column(
PG_ENUM(WalletType, name="wallet_type", schema="finance"),
nullable=False
)
# Összeg mezők (javított a kényelmi díj kezelésére)
net_amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False, comment="Kedvezményezett által kapott összeg")
handling_fee: Mapped[float] = mapped_column(Numeric(18, 4), default=0.0, comment="Kényelmi díj")
gross_amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False, comment="Fizetendő összeg (net + fee)")
currency: Mapped[str] = mapped_column(String(10), default="EUR", nullable=False)
# Státusz
status: Mapped[PaymentIntentStatus] = mapped_column(
PG_ENUM(PaymentIntentStatus, name="payment_intent_status", schema="finance"),
default=PaymentIntentStatus.PENDING,
nullable=False,
index=True
)
# Stripe információk (külső fizetés esetén)
stripe_session_id: Mapped[Optional[str]] = mapped_column(String(255), unique=True, index=True)
stripe_payment_intent_id: Mapped[Optional[str]] = mapped_column(String(255), index=True)
stripe_customer_id: Mapped[Optional[str]] = mapped_column(String(255))
# Metaadatok (metadata foglalt név SQLAlchemy-ban, ezért meta_data)
meta_data: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"), name="metadata")
# Időbélyegek
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())
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), comment="Stripe session lejárati ideje")
# Tranzakció kapcsolat
transaction_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), comment="Kapcsolódó FinancialLedger transaction_id")
# Soft delete
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
def __repr__(self) -> str:
return f"<PaymentIntent {self.id}: {self.status} {self.gross_amount}{self.currency}>"
def mark_completed(self, transaction_id: Optional[uuid.UUID] = None) -> None:
"""PaymentIntent befejezése sikeres fizetés után."""
self.status = PaymentIntentStatus.COMPLETED
self.completed_at = datetime.utcnow()
if transaction_id:
self.transaction_id = transaction_id
def mark_failed(self, reason: Optional[str] = None) -> None:
"""PaymentIntent sikertelen státuszba helyezése."""
self.status = PaymentIntentStatus.FAILED
if reason and self.meta_data:
self.meta_data = {**self.meta_data, "failure_reason": reason}
def is_valid_for_webhook(self) -> bool:
"""Ellenőrzi, hogy a PaymentIntent érvényes-e webhook feldolgozásra."""
return (
self.status == PaymentIntentStatus.PENDING
and not self.is_deleted
and (self.expires_at is None or self.expires_at > datetime.utcnow())
)
# Import User modell a relationship-ekhez (circular import elkerülésére)
from app.models.identity import User
class WithdrawalPayoutMethod(str, enum.Enum):
"""Kifizetési módok."""
FIAT_BANK = "FIAT_BANK" # Banki átutalás (SEPA)
CRYPTO_USDT = "CRYPTO_USDT" # USDT (ERC20/TRC20)
class WithdrawalRequestStatus(str, enum.Enum):
"""Kifizetési kérelem státuszai."""
PENDING = "PENDING" # Beküldve, admin ellenőrzésre vár
APPROVED = "APPROVED" # Jóváhagyva, kifizetés folyamatban
REJECTED = "REJECTED" # Elutasítva (pl. hiányzó bizonylat)
COMPLETED = "COMPLETED" # Kifizetés teljesítve
CANCELLED = "CANCELLED" # Felhasználó által visszavonva
class WithdrawalRequest(Base):
"""
Kifizetési kérelem (Withdrawal Request) a felhasználók Earned zsebéből való pénzkivételhez.
A felhasználó beküld egy kérést, amely admin jóváhagyást igényel.
Ha 14 napon belül nem kerül jóváhagyásra, automatikusan REJECTED lesz és a pénz visszakerül a Earned zsebbe.
"""
__tablename__ = "withdrawal_requests"
__table_args__ = {"schema": "finance"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Felhasználó aki a kérést benyújtotta
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
user: Mapped["User"] = relationship("User", back_populates="withdrawal_requests", foreign_keys=[user_id])
# Összeg és pénznem
amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
currency: Mapped[str] = mapped_column(String(10), default="EUR", nullable=False)
# Kifizetési mód
payout_method: Mapped[WithdrawalPayoutMethod] = mapped_column(
PG_ENUM(WithdrawalPayoutMethod, name="withdrawal_payout_method", schema="finance"),
nullable=False
)
# Státusz
status: Mapped[WithdrawalRequestStatus] = mapped_column(
PG_ENUM(WithdrawalRequestStatus, name="withdrawal_request_status", schema="finance"),
default=WithdrawalRequestStatus.PENDING,
nullable=False,
index=True
)
# Okozata (pl. admin megjegyzés vagy automatikus elutasítás oka)
reason: Mapped[Optional[str]] = mapped_column(String(500))
# Admin információk
approved_by_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
approved_by: Mapped[Optional["User"]] = relationship("User", foreign_keys=[approved_by_id])
approved_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
# Tranzakció kapcsolat (ha a pénz visszakerül a zsebbe)
refund_transaction_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True))
# Időbélyegek
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())
# Soft delete
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
def __repr__(self) -> str:
return f"<WithdrawalRequest {self.id}: {self.status} {self.amount}{self.currency}>"
def approve(self, admin_user_id: int) -> None:
"""Admin jóváhagyás."""
self.status = WithdrawalRequestStatus.APPROVED
self.approved_by_id = admin_user_id
self.approved_at = datetime.utcnow()
self.reason = None
def reject(self, reason: str) -> None:
"""Admin elutasítás."""
self.status = WithdrawalRequestStatus.REJECTED
self.reason = reason
def cancel(self) -> None:
"""Felhasználó visszavonja a kérést."""
self.status = WithdrawalRequestStatus.CANCELLED
self.reason = "User cancelled"
def is_expired(self, days: int = 14) -> bool:
"""Ellenőrzi, hogy a kérelem lejárt-e (14 nap)."""
from datetime import timedelta
expiry_date = self.created_at + timedelta(days=days)
return datetime.utcnow() > expiry_date

View File

@@ -0,0 +1,159 @@
# /opt/docker/dev/service_finder/backend/app/models/marketplace/service.py
import enum
import uuid
from datetime import datetime
from typing import Any, List, Optional
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, text, Text, Float, Index, Numeric, BigInteger
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB, ENUM as SQLEnum
from geoalchemy2 import Geometry
from sqlalchemy.sql import func
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class ServiceStatus(str, enum.Enum):
ghost = "ghost" # Nyers, robot által talált, nem validált
active = "active" # Publikus, aktív szerviz
flagged = "flagged" # Gyanús, kézi ellenőrzést igényel
suspended = "suspended" # Felfüggesztett, tiltott szerviz
class ServiceProfile(Base):
""" Szerviz szolgáltató adatai (v1.3.1). """
__tablename__ = "service_profiles"
__table_args__ = (
Index('idx_service_fingerprint', 'fingerprint', unique=True),
{"schema": "marketplace"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), unique=True)
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id"))
fingerprint: Mapped[str] = mapped_column(String(255), index=True, nullable=False)
location: Mapped[Any] = mapped_column(Geometry(geometry_type='POINT', srid=4326, spatial_index=False), index=True)
status: Mapped[ServiceStatus] = mapped_column(
SQLEnum(ServiceStatus, name="service_status", schema="marketplace"),
server_default=ServiceStatus.ghost.value,
nullable=False,
index=True
)
last_audit_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
google_place_id: Mapped[Optional[str]] = mapped_column(String(100), unique=True)
rating: Mapped[Optional[float]] = mapped_column(Float)
user_ratings_total: Mapped[Optional[int]] = mapped_column(Integer)
# Aggregated verified review ratings (Social 3)
rating_verified_count: Mapped[Optional[int]] = mapped_column(Integer, server_default=text("0"))
rating_price_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_quality_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_time_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_communication_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_overall: Mapped[Optional[float]] = mapped_column(Float)
last_review_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
vibe_analysis: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
social_links: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
specialization_tags: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
trust_score: Mapped[int] = mapped_column(Integer, default=30)
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
verification_log: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
contact_phone: Mapped[Optional[str]] = mapped_column(String)
contact_email: Mapped[Optional[str]] = mapped_column(String)
website: Mapped[Optional[str]] = mapped_column(String)
bio: Mapped[Optional[str]] = mapped_column(Text)
# Kapcsolatok
organization: Mapped["Organization"] = relationship("Organization", back_populates="service_profile")
expertises: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="service")
reviews: Mapped[List["ServiceReview"]] = relationship("ServiceReview", back_populates="service")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
class ExpertiseTag(Base):
"""
Szakmai címkék mesterlistája (MB 2.0).
Ez a tábla vezérli a robotok keresését és a Gamification pontozást is.
"""
__tablename__ = "expertise_tags"
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
key: Mapped[str] = mapped_column(String(50), unique=True, index=True)
name_hu: Mapped[Optional[str]] = mapped_column(String(100))
name_en: Mapped[Optional[str]] = mapped_column(String(100))
category: Mapped[Optional[str]] = mapped_column(String(30), index=True)
# --- 🎮 GAMIFICATION ÉS DISCOVERY ---
is_official: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))
suggested_by_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
discovery_points: Mapped[int] = mapped_column(Integer, default=10, server_default=text("10"))
search_keywords: Mapped[Any] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
usage_count: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
icon: Mapped[Optional[str]] = mapped_column(String(50))
description: Mapped[Optional[str]] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
services: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="tag")
suggested_by: Mapped[Optional["Person"]] = relationship("Person")
class ServiceExpertise(Base):
"""
KAPCSOLÓTÁBLA: Ez köti össze a szervizt a szakmáival.
"""
__tablename__ = "service_expertises"
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id", ondelete="CASCADE"))
expertise_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.expertise_tags.id", ondelete="CASCADE"))
confidence_level: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
service = relationship("ServiceProfile", back_populates="expertises")
tag = relationship("ExpertiseTag", back_populates="services")
class ServiceStaging(Base):
""" Hunter (robot) adatok tárolója. """
__tablename__ = "service_staging"
__table_args__ = (
Index('idx_staging_fingerprint', 'fingerprint', unique=True),
{"schema": "marketplace"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String, index=True, nullable=False)
postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True)
city: Mapped[Optional[str]] = mapped_column(String(100), index=True)
full_address: Mapped[Optional[str]] = mapped_column(String)
fingerprint: Mapped[str] = mapped_column(String(255), nullable=False)
raw_data: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
# Audit fix: contact_email hossza rögzítve a DB szinkronhoz
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
contact_phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
website: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
external_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True)
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class DiscoveryParameter(Base):
""" Robot vezérlési paraméterek adminból. """
__tablename__ = "discovery_parameters"
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
city: Mapped[str] = mapped_column(String(100))
keyword: Mapped[str] = mapped_column(String(100))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/service.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/service.py
import uuid
from datetime import datetime
from typing import Any, List, Optional
@@ -16,12 +16,12 @@ class ServiceProfile(Base):
__tablename__ = "service_profiles"
__table_args__ = (
Index('idx_service_fingerprint', 'fingerprint', unique=True),
{"schema": "data"}
{"schema": "marketplace"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"), unique=True)
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.service_profiles.id"))
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), unique=True)
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id"))
fingerprint: Mapped[str] = mapped_column(String(255), index=True, nullable=False)
location: Mapped[Any] = mapped_column(Geometry(geometry_type='POINT', srid=4326, spatial_index=False), index=True)
@@ -33,6 +33,15 @@ class ServiceProfile(Base):
rating: Mapped[Optional[float]] = mapped_column(Float)
user_ratings_total: Mapped[Optional[int]] = mapped_column(Integer)
# Aggregated verified review ratings (Social 3)
rating_verified_count: Mapped[Optional[int]] = mapped_column(Integer, server_default=text("0"))
rating_price_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_quality_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_time_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_communication_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_overall: Mapped[Optional[float]] = mapped_column(Float)
last_review_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
vibe_analysis: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
social_links: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
specialization_tags: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
@@ -50,6 +59,7 @@ class ServiceProfile(Base):
# Kapcsolatok
organization: Mapped["Organization"] = relationship("Organization", back_populates="service_profile")
expertises: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="service")
reviews: Mapped[List["ServiceReview"]] = relationship("ServiceReview", back_populates="service")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
@@ -60,7 +70,7 @@ class ExpertiseTag(Base):
Ez a tábla vezérli a robotok keresését és a Gamification pontozást is.
"""
__tablename__ = "expertise_tags"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
@@ -114,11 +124,11 @@ class ServiceExpertise(Base):
Itt tároljuk, hogy az adott szerviznél mennyire validált egy szakma.
"""
__tablename__ = "service_expertises"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.service_profiles.id", ondelete="CASCADE"))
expertise_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.expertise_tags.id", ondelete="CASCADE"))
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id", ondelete="CASCADE"))
expertise_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.expertise_tags.id", ondelete="CASCADE"))
# Mennyire biztos ez a tudás? (0: robot találta, 1: júzer mondta, 2: igazolt szakma)
confidence_level: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
@@ -134,7 +144,7 @@ class ServiceStaging(Base):
__tablename__ = "service_staging"
__table_args__ = (
Index('idx_staging_fingerprint', 'fingerprint', unique=True),
{"schema": "data"}
{"schema": "marketplace"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
@@ -144,13 +154,19 @@ class ServiceStaging(Base):
full_address: Mapped[Optional[str]] = mapped_column(String)
fingerprint: Mapped[str] = mapped_column(String(255), nullable=False)
raw_data: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
# Additional contact and identification fields
contact_phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
website: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
external_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True)
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class DiscoveryParameter(Base):
""" Robot vezérlési paraméterek adminból. """
__tablename__ = "discovery_parameters"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
city: Mapped[str] = mapped_column(String(100))

View File

@@ -0,0 +1,95 @@
# /opt/docker/dev/service_finder/backend/app/models/marketplace/service_request.py
"""
ServiceRequest - Piactér központi tranzakciós modellje.
Epic 7: Marketplace ServiceRequest dedikált modell.
"""
from typing import Optional
from datetime import datetime
from sqlalchemy import String, ForeignKey, Text, DateTime, Numeric, Integer, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from app.database import Base
class ServiceRequest(Base):
"""
Szervizigény (ServiceRequest) tábla.
Egy felhasználó által létrehozott szervizigényt reprezentál, amely lehetővé teszi
a szervizszolgáltatók számára árajánlatok készítését és a tranzakciók lebonyolítását.
"""
__tablename__ = "service_requests"
__table_args__ = (
Index('idx_service_request_status', 'status'),
Index('idx_service_request_user_id', 'user_id'),
Index('idx_service_request_asset_id', 'asset_id'),
Index('idx_service_request_branch_id', 'branch_id'),
{"schema": "marketplace"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# Idegen kulcsok (Kapcsolódási pontok)
user_id: Mapped[int] = mapped_column(
ForeignKey("identity.users.id", ondelete="CASCADE"),
nullable=False,
index=True,
comment="A szervizigényt létrehozó felhasználó"
)
asset_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("vehicle.assets.id", ondelete="SET NULL"),
nullable=True,
comment="Érintett jármű (opcionális)"
)
branch_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("fleet.branches.id", ondelete="SET NULL"),
nullable=True,
comment="Célzott szerviz (ha van)"
)
# Üzleti logika mezők
status: Mapped[str] = mapped_column(
String(50),
server_default="pending",
index=True,
comment="pending, quoted, accepted, scheduled, completed, cancelled"
)
description: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
comment="A szervizigény részletes leírása"
)
price_estimate: Mapped[Optional[float]] = mapped_column(
Numeric(10, 2),
nullable=True,
comment="Becsült ár (opcionális)"
)
requested_date: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True),
nullable=True,
comment="Kért szerviz dátum"
)
# Audit
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
comment="Létrehozás időbélyege"
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
comment="Utolsó módosítás időbélyege"
)
# Relationships (opcionális, de ajánlott a lazy loading miatt)
user = relationship("User", back_populates="service_requests", lazy="selectin")
asset = relationship("Asset", back_populates="service_requests", lazy="selectin")
branch = relationship("Branch", back_populates="service_requests", lazy="selectin")
def __repr__(self) -> str:
return f"<ServiceRequest(id={self.id}, status='{self.status}', user_id={self.user_id})>"

View File

@@ -0,0 +1,94 @@
from datetime import datetime
from typing import Optional, Any
from sqlalchemy import String, Integer, DateTime, text, Boolean, Float, Text, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
from app.database import Base # MB 2.0 Standard: Központi bázis használata
class StagedVehicleData(Base):
""" Robot 2.1 (Researcher) nyers adatgyűjtője. """
__tablename__ = "staged_vehicle_data"
__table_args__ = {"schema": "system", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
source_url: Mapped[Optional[str]] = mapped_column(String)
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
status: Mapped[str] = mapped_column(String(20), default="PENDING", index=True)
error_log: Mapped[Optional[str]] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ServiceStaging(Base):
"""
Robot 1.3 (Scout) által talált nyers szerviz adatok és a Robot 5 (Auditor) naplója.
A séma és a mezők szinkronban az adatbázis audittal.
"""
__tablename__ = "service_staging"
__table_args__ = {"schema": "marketplace", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(255), index=True)
# 1. ⚠️ EXTRA OSZLOP: source
source: Mapped[Optional[str]] = mapped_column(String(50))
external_id: Mapped[Optional[str]] = mapped_column(String(100), index=True)
fingerprint: Mapped[str] = mapped_column(String(64), unique=True, index=True)
# Elérhetőségek
city: Mapped[str] = mapped_column(String(100), index=True)
postal_code: Mapped[Optional[str]] = mapped_column(String(10))
full_address: Mapped[Optional[str]] = mapped_column(String(500))
contact_phone: Mapped[Optional[str]] = mapped_column(String(50))
website: Mapped[Optional[str]] = mapped_column(String(255))
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# 2. ⚠️ EXTRA OSZLOP: description
description: Mapped[Optional[str]] = mapped_column(Text)
# 3. ⚠️ EXTRA OSZLOP: submitted_by
submitted_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
# 4. ⚠️ EXTRA OSZLOP: trust_score
trust_score: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
validation_level: Mapped[int] = mapped_column(Integer, default=40, server_default=text("40"))
# --- Robot 5 (Auditor) technikai mezők ---
# 5. ⚠️ EXTRA OSZLOP: rejection_reason
rejection_reason: Mapped[Optional[str]] = mapped_column(String(500))
# 6. ⚠️ EXTRA OSZLOP: published_at
published_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
# 7. ⚠️ EXTRA OSZLOP: service_profile_id
service_profile_id: Mapped[Optional[int]] = mapped_column(Integer)
# 8. ⚠️ EXTRA OSZLOP: organization_id
organization_id: Mapped[Optional[int]] = mapped_column(Integer)
# 9. ⚠️ EXTRA OSZLOP: audit_trail
audit_trail: Mapped[Optional[dict]] = mapped_column(JSONB)
# Időbélyegek
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# 10. ⚠️ EXTRA OSZLOP: updated_at
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
class DiscoveryParameter(Base):
""" Felderítési paraméterek (Városok, ahol a Scout keres). """
__tablename__ = "discovery_parameters"
__table_args__ = {"schema": "marketplace", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
city: Mapped[str] = mapped_column(String(100), unique=True, index=True)
country_code: Mapped[Optional[str]] = mapped_column(String(2), nullable=True, default="HU")
keyword: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/staged_data.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/staged_data.py
from datetime import datetime
from typing import Optional, Any
from sqlalchemy import String, Integer, DateTime, text, Boolean, Float
@@ -10,7 +10,7 @@ from app.db.base_class import Base
class StagedVehicleData(Base):
""" Robot 2.1 (Researcher) nyers adatgyűjtője. """
__tablename__ = "staged_vehicle_data"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "system"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
source_url: Mapped[Optional[str]] = mapped_column(String)
@@ -22,35 +22,52 @@ class StagedVehicleData(Base):
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ServiceStaging(Base):
""" Robot 1.3 (Scout) által talált nyers szerviz adatok. """
""" Robot 1.3 (Scout) által talált nyers szerviz adatok és a Robot 5 (Auditor) naplója. """
__tablename__ = "service_staging"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(255), index=True)
source: Mapped[str] = mapped_column(String(50))
source: Mapped[Optional[str]] = mapped_column(String(50))
external_id: Mapped[Optional[str]] = mapped_column(String(100), index=True)
fingerprint: Mapped[str] = mapped_column(String(64), unique=True, index=True)
# Elérhetőségek
city: Mapped[str] = mapped_column(String(100), index=True)
postal_code: Mapped[Optional[str]] = mapped_column(String(10))
full_address: Mapped[Optional[str]] = mapped_column(String(500))
contact_phone: Mapped[Optional[str]] = mapped_column(String(50))
website: Mapped[Optional[str]] = mapped_column(String(255))
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# Beküldés és Bizalom
description: Mapped[Optional[str]] = mapped_column(Text)
submitted_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
trust_score: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
# Nyers adatok és Státusz
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
trust_score: Mapped[int] = mapped_column(Integer, default=30)
# --- Robot 5 (Auditor) technikai mezők ---
# Ezek kellenek a munka naplózásához
rejection_reason: Mapped[Optional[str]] = mapped_column(String(500))
published_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
service_profile_id: Mapped[Optional[int]] = mapped_column(Integer)
organization_id: Mapped[Optional[int]] = mapped_column(Integer)
audit_trail: Mapped[Optional[dict]] = mapped_column(JSONB)
# Időbélyegek
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
class DiscoveryParameter(Base):
""" Felderítési paraméterek (Városok, ahol a Scout keres). """
__tablename__ = "discovery_parameters"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
city: Mapped[str] = mapped_column(String(100), unique=True, index=True)
country_code: Mapped[str] = mapped_column(String(5), server_default=text("'HU'"))
keyword: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))

View File

@@ -0,0 +1,20 @@
# /opt/docker/dev/service_finder/backend/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": "vehicle"}
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

@@ -1,53 +0,0 @@
# /opt/docker/dev/service_finder/backend/app/models/system.py
import uuid
from datetime import datetime
from typing import Optional
from sqlalchemy import String, Integer, Boolean, DateTime, text, UniqueConstraint, ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.sql import func
from app.db.base_class import Base
class SystemParameter(Base):
""" Dinamikus konfigurációs motor (Global -> Org -> User). """
__tablename__ = "system_parameters"
__table_args__ = (
UniqueConstraint('key', 'scope_level', 'scope_id', name='uix_param_scope'),
{"extend_existing": True}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
key: Mapped[str] = mapped_column(String, index=True)
category: Mapped[str] = mapped_column(String, server_default="general", index=True)
value: Mapped[dict] = mapped_column(JSONB, nullable=False)
scope_level: Mapped[str] = mapped_column(String(30), server_default=text("'global'"), index=True)
scope_id: Mapped[Optional[str]] = mapped_column(String(50))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
description: Mapped[Optional[str]] = mapped_column(String)
last_modified_by: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
class InternalNotification(Base):
"""
Belső értesítési központ.
Ezek az üzenetek várják a felhasználót belépéskor.
"""
__tablename__ = "internal_notifications"
__table_args__ = ({"schema": "data", "extend_existing": True})
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[int] = mapped_column(ForeignKey("identity.users.id", ondelete="CASCADE"), nullable=False, index=True)
title: Mapped[str] = mapped_column(String(255), nullable=False)
message: Mapped[str] = mapped_column(Text, nullable=False)
category: Mapped[str] = mapped_column(String(50), server_default="info") # insurance, mot, service, legal
priority: Mapped[str] = mapped_column(String(20), server_default="medium") # low, medium, high, critical
is_read: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
read_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
# Metaadatok a gyors eléréshez (melyik autó, melyik VIN)
data: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)

View File

@@ -0,0 +1,12 @@
# system package barrel
from .system import SystemParameter, ParameterScope, InternalNotification, SystemServiceStaging
from .audit import SecurityAuditLog, OperationalLog, ProcessLog, FinancialLedger, WalletType, LedgerStatus, LedgerEntryType
from .document import Document
from .translation import Translation
from .legal import LegalDocument, LegalAcceptance
__all__ = [
"SystemParameter", "InternalNotification", "SystemServiceStaging",
"SecurityAuditLog", "ProcessLog", "FinancialLedger", "WalletType", "LedgerStatus", "LedgerEntryType",
"Document", "Translation", "LegalDocument", "LegalAcceptance"
]

View File

@@ -0,0 +1,115 @@
# /opt/docker/dev/service_finder/backend/app/models/system/audit.py
import enum
import uuid
from datetime import datetime
from typing import Any, Optional
from sqlalchemy import String, DateTime, JSON, ForeignKey, text, Numeric, Boolean, BigInteger, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
from app.database import Base
class SecurityAuditLog(Base):
""" Kiemelt biztonsági események és a 4-szem elv naplózása. """
__tablename__ = "security_audit_logs"
__table_args__ = {"schema": "audit"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
action: Mapped[Optional[str]] = mapped_column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST'
actor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
target_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
confirmed_by_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=True)
is_critical: Mapped[bool] = mapped_column(Boolean, default=False)
payload_before: Mapped[Any] = mapped_column(JSON)
payload_after: Mapped[Any] = mapped_column(JSON)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class OperationalLog(Base):
""" Felhasználói szintű napi üzemi események (Audit Trail). """
__tablename__ = "operational_logs"
__table_args__ = {"schema": "audit"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="SET NULL"))
action: Mapped[str] = mapped_column(String(100), nullable=False) # pl. "ADD_VEHICLE"
resource_type: Mapped[Optional[str]] = mapped_column(String(50))
resource_id: Mapped[Optional[str]] = mapped_column(String(100))
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
ip_address: Mapped[Optional[str]] = mapped_column(String(45))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ProcessLog(Base):
""" Robotok és háttérfolyamatok futási naplója (A reggeli jelentésekhez). """
__tablename__ = "process_logs"
__table_args__ = {"schema": "audit"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
process_name: Mapped[str] = mapped_column(String(100), index=True) # 'Master-Enricher'
start_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
end_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
items_processed: Mapped[int] = mapped_column(Integer, default=0)
items_failed: Mapped[int] = mapped_column(Integer, default=0)
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class LedgerEntryType(str, enum.Enum):
DEBIT = "DEBIT"
CREDIT = "CREDIT"
class WalletType(str, enum.Enum):
EARNED = "EARNED"
PURCHASED = "PURCHASED"
SERVICE_COINS = "SERVICE_COINS"
VOUCHER = "VOUCHER"
class LedgerStatus(str, enum.Enum):
PENDING = "PENDING"
SUCCESS = "SUCCESS"
FAILED = "FAILED"
REFUNDED = "REFUNDED"
REFUND = "REFUND"
class FinancialLedger(Base):
""" Minden pénz- és kreditmozgás központi naplója. Billing Engine alapja. """
__tablename__ = "financial_ledger"
__table_args__ = {"schema": "audit"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
currency: Mapped[Optional[str]] = mapped_column(String(10))
transaction_type: Mapped[Optional[str]] = mapped_column(String(50))
related_agent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# Új mezők doubleentry és okos levonáshoz
entry_type: Mapped[LedgerEntryType] = mapped_column(
PG_ENUM(LedgerEntryType, name="ledger_entry_type", schema="audit"),
nullable=False
)
balance_after: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
wallet_type: Mapped[Optional[WalletType]] = mapped_column(
PG_ENUM(WalletType, name="wallet_type", schema="audit")
)
# Economy 1: számlázási mezők
issuer_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("finance.issuers.id"), nullable=True)
invoice_status: Mapped[Optional[str]] = mapped_column(String(50), default="PENDING")
tax_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
gross_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
net_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
transaction_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), default=uuid.uuid4, nullable=False, index=True
)
status: Mapped[LedgerStatus] = mapped_column(
PG_ENUM(LedgerStatus, name="ledger_status", schema="audit"),
default=LedgerStatus.SUCCESS,
nullable=False
)

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/document.py
# /opt/docker/dev/service_finder/backend/app/models/system/document.py
import uuid
from datetime import datetime
from typing import Optional
@@ -6,12 +6,12 @@ from sqlalchemy import String, Integer, Boolean, DateTime, ForeignKey, Text
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from app.db.base_class import Base
from app.database import Base # MB 2.0: Egységesített Base a szinkronitáshoz
class Document(Base):
""" NAS alapú dokumentumtár metaadatai. """
__tablename__ = "documents"
__table_args__ = {"schema": "data"}
__table_args__ = {"schema": "system"}
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
parent_type: Mapped[str] = mapped_column(String(20)) # 'organization' vagy 'asset'
@@ -35,18 +35,6 @@ class Document(Base):
# =========================================================================
# Probléma: Az `ocr_robot.py` (Robot 3) módosítani próbálta a dokumentumok
# állapotát és menteni akarta az AI eredményeket, de a mezők hiányoztak.
#
# Megoldás: Hozzáadtuk a szükséges mezőket a munkafolyamat (Workflow)
# támogatásához.
#
# 1. `status`: A robot a 'pending_ocr' státuszra szűr. Indexeljük,
# mert a WHERE feltételben szerepel, így az adatbázis sokkal gyorsabb lesz.
#
# 2. `ocr_data`: A kinyert adatokat tárolja. Text típust használunk String
# helyett, mert az AI válasza (pl. JSON formátumú adat) hosszú lehet.
#
# 3. `error_log`: Ha az AI hibázik, vagy üres választ ad, itt rögzítjük
# a hiba okát a könnyebb debuggolás érdekében.
# =========================================================================
status: Mapped[str] = mapped_column(String(50), default="uploaded", index=True)

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/legal.py
# /opt/docker/dev/service_finder/backend/app/models/system/legal.py
from datetime import datetime
from typing import Optional
from sqlalchemy import Integer, String, Text, DateTime, ForeignKey, Boolean
@@ -8,6 +8,7 @@ from app.db.base_class import Base
class LegalDocument(Base):
__tablename__ = "legal_documents"
__table_args__ = {"schema": "system"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
title: Mapped[Optional[str]] = mapped_column(String(255))
@@ -22,10 +23,11 @@ class LegalDocument(Base):
class LegalAcceptance(Base):
__tablename__ = "legal_acceptances"
__table_args__ = {"schema": "identity"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
document_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.legal_documents.id"))
document_id: Mapped[int] = mapped_column(Integer, ForeignKey("system.legal_documents.id"))
accepted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
ip_address: Mapped[Optional[str]] = mapped_column(String(45))
user_agent: Mapped[Optional[str]] = mapped_column(Text)

Some files were not shown because too many files have changed in this diff Show More