admin frontend elkezdése, járművek tisztázása, frontend fejleszts

This commit is contained in:
Roo
2026-06-15 18:52:38 +00:00
parent ef8df9608c
commit 213ba3b0f1
80 changed files with 11615 additions and 2304 deletions

View File

@@ -0,0 +1,445 @@
# Költség/Billing Rendszer Elemzés & Módosítási Javaslat (v2)
## Premium szintű költségbontás — Subscription-alapú kategória láthatóság + Km tracking
**Készült:** 2026.06.12.
**Verzió:** v2.0 (User feedback alapján átdolgozva)
**Státusz:** Jóváhagyásra vár
---
## 1. Jelenlegi Állapot Elemzés
### 1.1 Előfizetési Architektúra
| Komponens | Tábla | Mező | Státusz |
|-----------|-------|------|---------|
| [`User`](backend/app/models/identity/identity.py:122) | `identity.users` | `subscription_plan: str` (default="FREE") | ✅ Megvan |
| [`User`](backend/app/models/identity/identity.py:122) | `identity.users` | `subscription_expires_at: datetime` | ✅ Megvan |
| [`User`](backend/app/models/identity/identity.py:122) | `identity.users` | `ui_mode: str` ("personal"/"business") | ✅ Megvan |
| [`Organization`](backend/app/models/marketplace/organization.py:35) | `marketplace.organizations` | `subscription_plan: str` (default="FREE") | ✅ Megvan |
| [`SubscriptionTier`](backend/app/models/core_logic.py:12) | `system.subscription_tiers` | `name: str`, `rules: JSONB` | ✅ Megvan |
| [`subscription_service.py`](backend/app/services/) | **Hiányzó fájl** | Gitea #239 | ❌ **Nincs meg** |
| [`subscription_worker.py`](backend/app/workers/system/subscription_worker.py:32) | Cron robot | Auto-downgrade expired → FREE | ✅ Megvan |
### 1.2 Költség Modellek
#### [`AssetCost`](backend/app/models/vehicle/asset.py:239) (`vehicle.asset_costs`) — **Modern/Aktív tábla**
- **Használja:** [`POST /expenses/`](backend/app/api/v1/endpoints/expenses.py:12), [`CostService.record_cost()`](backend/app/services/cost_service.py:23)
- **Mezői:** `id` (UUID), `asset_id`, `organization_id`, `category_id`, `amount_net`, `currency`, `date`, `invoice_number`, `data` (JSONB)
- **`data` JSONB tárol:** `mileage_at_cost`, `description`
- **⚠️ Nincs subscription check** — bármely felhasználó bármilyen adatot rögzíthet
#### [`CostCategory`](backend/app/models/vehicle/vehicle.py:19) (`vehicle.cost_categories`)
- **Meglévő mezők:** `id`, `parent_id` (hierarchia), `code`, `name`, `description`, `is_system`, `visibility` ("both"/"b2b"), `accounting_code`
- **Jelenlegi visibility map:** FUEL, MAINTENANCE, SERVICE, TIRE, INSURANCE, PARKING, TOLL, OTHER → `"both"` ; FINANCE, ADMIN → `"b2b"`
- **❌ Nincs `subscription_tier` mező** — nem lehet megkülönböztetni FREE és PREMIUM kategóriákat
#### [`Asset.current_mileage`](backend/app/models/vehicle/asset.py:111) — **Meglévő km mező**
- `current_mileage: Mapped[int] = mapped_column(Integer, default=0, index=True)` — Az Asset modellen
- **❌ Probléma:** Nincs automatizmus, ami frissítené amikor költséget rögzítenek
- **❌ Probléma:** Nincs history tábla a km állás változásainak követésére
#### [`AssetTelemetry`](backend/app/models/vehicle/asset.py:336) (`vehicle.asset_telemetry`)
- 1:1 kapcsolat Asset-tel, `current_mileage: Mapped[int]` mezővel
- **Technikai adósság:** Duplikálja az `Asset.current_mileage` mezőt
#### [`VehicleOdometerState`](backend/app/models/vehicle/vehicle.py:111) (`vehicle.vehicle_odometer_states`)
- Legacy tábla — `vehicle_model_definitions.id`-hez kötődik (nem az Asset UUID-hez)
- Tárol: `last_recorded_odometer`, `daily_avg_distance`, `estimated_current_odometer`
- **Nem használható** az új Asset-alapú architektúrában
### 1.3 API Végpontok Állapota
| Végpont | Fájl | Subscription Check | Megjegyzés |
|---------|------|-------------------|------------|
| `POST /expenses/` | [`expenses.py:12`](backend/app/api/v1/endpoints/expenses.py:12) | ❌ | Csak DRAFT limit check van |
| `GET /{vehicle_id}/summary` | [`analytics.py:66`](backend/app/api/v1/endpoints/analytics.py:66) | ❌ | TCO summary |
| `GET /dashboard` | [`analytics.py:199`](backend/app/api/v1/endpoints/analytics.py:199) | ❌ | Mock data |
| `POST /upgrade` | [`billing.py:20`](backend/app/api/v1/endpoints/billing.py:20) | ✅ | Megvan |
### 1.4 Hiányosságok (Gap Analysis)
| # | Hiányosság | Leírás | Súlyosság |
|---|-----------|--------|-----------|
| 1 | **Nincs subscription gating** | A `POST /expenses/` nem ellenőrzi a subscription_plan-t | 🔴 Kritikus |
| 2 | **Hiányzó `subscription_service.py`** | Gitea #239 — entitlement mátrix hiányzik | 🔴 Blokkoló |
| 3 | **Nincs `CostCategory.min_tier` mező** | Nem lehet megkülönböztetni FREE/PREMIUM kategóriákat | 🟡 Magas |
| 4 | **Km tracking nincs centralizálva** | `Asset.current_mileage` létezik, de nincs automatikus frissítés | 🟡 Magas |
| 5 | **Nincs OdometerLog history** | Nincs audit trail a km állás változásokról | 🟡 Közepes |
| 6 | **Párhuzamos telemetry tábla** | `AssetTelemetry` duplikálja `Asset.current_mileage`-t | 🟠 Alacsony |
---
## 2. Módosítási Javaslat (Logic Spec v2)
### 2.1 CostCategory Bővítés — `min_tier` mező
**Nem kell új `UserCostCategory` modell!** A meglévő [`CostCategory`](backend/app/models/vehicle/vehicle.py:19) kap egy új mezőt:
```python
class CostCategory(Base):
# ... meglévő mezők ...
# ÚJ: Minimum előfizetési szint a kategória használatához
min_tier: Mapped[str] = mapped_column(
String(20),
default="free",
server_default="'free'"
)
```
**CostCategory seed adat — kategória szintek:**
| Kategória CSoport | `code` értékek | `visibility` | `min_tier` | Leírás |
|-------------------|---------------|-------------|-----------|--------|
| **Alap (FREE)** | FUEL, MAINTENANCE, SERVICE, TIRE, INSURANCE, OTHER | `both` | `free` | Mindenki számára elérhető |
| **Kiterjesztett (PREMIUM)** | PARKING, TOLL, FINANCE, ADMIN | `both`/`b2b` | `premium` | Csak PREMIUM+ tagoknak |
| **Vállalati (VIP)** | *(speciális account kódok)* | `b2b` | `vip` | Csak VIP ügyfeleknek |
### 2.2 Odometer Rendszer — Centralizált Km Tracking
#### 2.2.1 Új modell: [`OdometerReading`](backend/app/models/vehicle/asset.py)
```python
class OdometerReading(Base):
"""
Kilométeróra állások history audit trail.
Minden esemény (költség, szerviz, telemetria) itt rögzíti a km állást.
Csak APPEND-only — soha nem törlődik, csak új rekord kerül hozzá.
"""
__tablename__ = "odometer_readings"
__table_args__ = {"schema": "vehicle"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("vehicle.assets.id", ondelete="CASCADE"),
index=True,
nullable=False
)
mileage: Mapped[int] = mapped_column(Integer, nullable=False) # km állás
recorded_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False
)
source: Mapped[str] = mapped_column(String(50), nullable=False)
# source lehet: 'cost_entry', 'manual', 'telemetry', 'import', 'service_event'
source_id: Mapped[Optional[str]] = mapped_column(String(100))
# source_id lehet: a kapcsolódó AssetCost UUID-ja, vagy esemény ID
asset: Mapped["Asset"] = relationship("Asset", back_populates="odometer_readings")
```
#### 2.2.2 [`Asset`](backend/app/models/vehicle/asset.py:69) — Bővítés
```python
class Asset(Base):
# ... meglévő mezők ...
# MEGLÉVŐ — marad, de automatikusan frissül:
current_mileage: Mapped[int] = mapped_column(Integer, default=0, index=True)
# ÚJ kapcsolat:
odometer_readings: Mapped[List["OdometerReading"]] = relationship(
"OdometerReading", back_populates="asset"
)
```
#### 2.2.3 [`CostService.record_cost()`](backend/app/services/cost_service.py:23) — Km frissítés
```python
class CostService:
async def record_cost(self, db: AsyncSession, cost_in: AssetCostCreate, user_id: int):
# 1. Meglévő logika (OCR, XP, currency conversion) ...
mileage = cost_in.data.get("mileage_at_cost") if cost_in.data else None
# 2. NEW: OdometerReading log + Asset.current_mileage frissítés
if mileage is not None:
# a) Logold a km állást
reading = OdometerReading(
asset_id=cost_in.asset_id,
mileage=mileage,
source="cost_entry",
source_id=str(new_cost.id)
)
db.add(reading)
# b) Frissítsd az Asset.current_mileage mezőt (ha nagyobb)
asset = await db.get(Asset, cost_in.asset_id)
if asset and mileage > asset.current_mileage:
asset.current_mileage = mileage
# 3. Meglévő logika folytatása ...
```
### 2.3 `subscription_service.py` — Entitlement Mátrix (Gitea #239)
```python
"""
subscription_service.py — Gitea #239
Feature entitlement mátrix subscription szintek szerint.
"""
from enum import Enum
class Feature(str, Enum):
EXTENDED_CATEGORIES = "extended_categories" # PARKING, TOLL, FINANCE, ADMIN
DOCUMENT_ATTACHMENT = "document_attachment"
VAT_TRACKING = "vat_tracking"
TCO_BREAKDOWN = "tco_breakdown"
EXPORT_CSV = "export_csv"
ANALYTICS_DASHBOARD = "analytics_dashboard"
FEATURE_ENTITLEMENTS = {
"FREE": [
Feature.TCO_BREAKDOWN, # Basic összesített TCO
],
"PREMIUM": [
Feature.EXTENDED_CATEGORIES, # Kiterjesztett kategóriák
Feature.DOCUMENT_ATTACHMENT,
Feature.VAT_TRACKING,
Feature.TCO_BREAKDOWN, # Részletes bontás
Feature.EXPORT_CSV,
Feature.ANALYTICS_DASHBOARD,
],
"VIP": [
Feature.EXTENDED_CATEGORIES,
Feature.DOCUMENT_ATTACHMENT,
Feature.VAT_TRACKING,
Feature.TCO_BREAKDOWN,
Feature.EXPORT_CSV,
Feature.ANALYTICS_DASHBOARD,
],
}
async def check_feature_access(db, user_id, feature):
"""Ellenőrzi a feature elérést a user subscription alapján."""
user = await db.get(User, user_id)
if not user:
return False
return feature in FEATURE_ENTITLEMENTS.get(user.subscription_plan, [])
async def require_feature(db, user_id, feature):
"""403-as hibát dob ha nincs hozzáférés."""
if not await check_feature_access(db, user_id, feature):
raise HTTPException(
status_code=403,
detail=f"Feature '{feature.value}' requires PREMIUM subscription"
)
async def get_available_categories(db, user_id):
"""Visszaadja a user számára elérhető CostCategory listát."""
user = await db.get(User, user_id)
plan = user.subscription_plan if user else "FREE"
query = """
SELECT * FROM vehicle.cost_categories
WHERE min_tier IN ('free', :plan)
ORDER BY parent_id NULLS FIRST, sort_order
"""
result = await db.execute(text(query), {"plan": plan.lower()})
return result.mappings().all()
```
### 2.4 Végpont Módosítások
#### 2.4.1 [`POST /expenses/`](backend/app/api/v1/endpoints/expenses.py:12)
```python
@router.post("/", status_code=201)
async def create_expense(
expense: AssetCostCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
# 1. Ellenőrizd, hogy a kategória elérhető-e a user számára
category = await db.get(CostCategory, expense.category_id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
if category.min_tier != "free" and current_user.subscription_plan == "FREE":
raise HTTPException(
status_code=403,
detail=f"Category '{category.name}' requires PREMIUM subscription"
)
# 2. PREMIUM+ dokumentum csatolás check
if expense.document_id:
await require_feature(db, current_user.id, Feature.DOCUMENT_ATTACHMENT)
# 3. Létrehozás
new_cost = AssetCost(
asset_id=expense.asset_id,
organization_id=organization_id,
category_id=expense.category_id,
amount_net=expense.amount_net,
currency=expense.currency,
date=expense.date,
invoice_number=expense.invoice_number,
data=expense.data
)
db.add(new_cost)
await db.flush() # Kell az ID-hoz
# 4. Odometer frissítés (ha van mileage_at_cost)
if expense.data and expense.data.get("mileage_at_cost"):
mileage = int(expense.data["mileage_at_cost"])
reading = OdometerReading(
asset_id=expense.asset_id,
mileage=mileage,
source="cost_entry",
source_id=str(new_cost.id)
)
db.add(reading)
asset = await db.get(Asset, expense.asset_id)
if asset and mileage > asset.current_mileage:
asset.current_mileage = mileage
await db.commit()
await db.refresh(new_cost)
```
#### 2.4.2 `GET /cost-categories/` — ÚJ végpont
```python
@router.get("/cost-categories/")
async def list_cost_categories(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Visszaadja a user számára elérhető költségkategóriákat."""
categories = await get_available_categories(db, current_user.id)
return categories
```
### 2.5 [`AssetCost`](backend/app/models/vehicle/asset.py:239) — Bővítés (minimális)
Csak a `document_id` mező kerül hozzáadásra (az Evidence Storeba mutató hivatkozás).
A többi funkció (VAT, notes) a meglévő `data` JSONB-ben tárolható, és csak PREMIUM-nak elérhető a frontenden.
```python
class AssetCost(Base):
# ... meglévő mezők ...
# ÚJ (opcionális, PREMIUM feature):
document_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True))
```
### 2.6 Admin Kontroll (SystemParameter)
| Paraméter | Scope | Default | Leírás |
|-----------|-------|---------|--------|
| `COST_EXTENDED_CATEGORIES_ENABLED` | GLOBAL | `true` | Kiterjesztett kategóriák bekapcsolása |
| `COST_VAT_TRACKING_ENABLED` | GLOBAL | `false` | ÁFA követés bekapcsolása |
| `COST_ODOMETER_REQUIRED` | GLOBAL | `true` | Km állás kötelező legyen-e |
---
## 3. Folyamatábra
```mermaid
flowchart TD
subgraph REQUEST["POST /expenses/"]
A[Bejövő kérés] --> B{Van category_id?}
B -->|Igen| C[CostCategory lekérése]
C --> D{min_tier == free?}
D -->|Igen| E[Engedélyezve]
D -->|Nem| F{User subscription?}
F -->|FREE| G[403 - Premium feature]
F -->|PREMIUM+| H[Engedélyezve]
E --> I[AssetCost létrehozása]
H --> I
I --> J{Van mileage_at_cost?}
J -->|Igen| K[OdometerReading log]
K --> L{ mileage > Asset.current_mileage ?}
L -->|Igen| M[Asset.current_mileage frissítés]
L -->|Nem| N[Nincs frissítés]
M --> O[Commit]
J -->|Nem| O
N --> O
end
subgraph READ["GET /cost-categories/"]
P[User subscription] --> Q{subscription_plan}
Q -->|FREE| R[Csak free kategóriák]
Q -->|PREMIUM+| S[Free + premium kategóriák]
end
```
---
## 4. Implementációs Terv
### 4.1 Lépések
| # | Lépés | Érintett fájlok | Függőség |
|---|-------|-----------------|----------|
| 1 | **`subscription_service.py`** létrehozása | `backend/app/services/subscription_service.py` | — |
| 2 | **`CostCategory.min_tier`** mező hozzáadása | [`backend/app/models/vehicle/vehicle.py:19`](backend/app/models/vehicle/vehicle.py:19) | #1 |
| 3 | **`OdometerReading`** modell létrehozása | [`backend/app/models/vehicle/asset.py`](backend/app/models/vehicle/asset.py) | — |
| 4 | **`Asset`** kapcsolat + `AssetCost.document_id` | [`backend/app/models/vehicle/asset.py:69`](backend/app/models/vehicle/asset.py:69) | #3 |
| 5 | **Pydantic sémák bővítése** | [`backend/app/schemas/asset_cost.py`](backend/app/schemas/asset_cost.py) | #2, #4 |
| 6 | **`CostService.record_cost()`** — km frissítés | [`backend/app/services/cost_service.py:23`](backend/app/services/cost_service.py:23) | #3 |
| 7 | **`POST /expenses/`** — subscription gating + km log | [`backend/app/api/v1/endpoints/expenses.py:12`](backend/app/api/v1/endpoints/expenses.py:12) | #1, #2, #6 |
| 8 | **`GET /cost-categories/`** — új végpont | [`backend/app/api/v1/endpoints/expenses.py`](backend/app/api/v1/endpoints/expenses.py) | #2 |
| 9 | **Seed script**`min_tier` értékek beállítása | `backend/scripts/seed_cost_category_tiers.py` | #2 |
| 10 | **Sync engine + tesztek** | `backend/app/scripts/sync_engine.py` | #2-#5 |
### 4.2 Adatbázis Szinkron
```bash
# Modellek módosítása után:
docker compose exec sf_api python3 /app/backend/app/scripts/sync_engine.py
```
### 4.3 Seed Script — Kategória szintek beállítása
```python
# backend/scripts/seed_cost_category_tiers.py
"""CostCategory min_tier mező seedelése."""
TIER_MAP = {
# FREE kategóriák (mindenki számára)
"FUEL": "free",
"MAINTENANCE": "free",
"SERVICE": "free",
"TIRE": "free",
"INSURANCE": "free",
"OTHER": "free",
# PREMIUM kategóriák (csak előfizetőknek)
"PARKING": "premium",
"TOLL": "premium",
"FINANCE": "premium",
"ADMIN": "premium",
# VIP kategóriák (speciális)
# ...
}
```
---
## 5. Összefoglalás
### Mit változtat a terv:
1. **FREE felhasználók:** Csak az alábbi kategóriákba rögzíthetnek költséget: FUEL, MAINTENANCE, SERVICE, TIRE, INSURANCE, OTHER. A km állást továbbra is megadhatják, és az automatikusan frissíti az `Asset.current_mileage` mezőt.
2. **PREMIUM+ felhasználók:** Minden kategória elérhető (FUEL, MAINTENANCE, SERVICE, TIRE, INSURANCE, PARKING, TOLL, FINANCE, ADMIN, OTHER). Plusz: dokumentum csatolás, ÁFA követés, részletes TCO bontás.
3. **Km tracking:** Az `OdometerReading` tábla minden költségrögzítéskor naplózza a km állást. Az `Asset.current_mileage` automatikusan frissül (ha az új érték nagyobb). Ez folyamatos, megbízható audit trail-et biztosít.
### Amit NEM kell megvalósítani (v2 törlések):
-`UserCostCategory` modell — a felhasználók nem hoznak létre saját kategóriákat
-`CostTag` / `AssetCostTag` — nem kell külön címke rendszer
-`AssetCost.user_category_id` — kategória kiválasztás nem szükséges
### Blokkoló:
- **Gitea #239 (`subscription_service.py`)** — ezt mindenképp létre kell hozni az entitlement mátrixhoz
---
**Jóváhagyásra vár:** Kérem a fenti v2 terv áttekintését. Ha elfogadásra kerül, létrehozom a Gitea kártyákat és megkezdjük az implementációt.