pénzügyi modul továbbfejlesztése (csomagkezelés)

This commit is contained in:
Roo
2026-07-29 09:46:10 +00:00
parent 6f28d3e70d
commit 75904cd2f8
50 changed files with 5851 additions and 312 deletions

167
docs/UI_STANDARDS.md Normal file
View File

@@ -0,0 +1,167 @@
# 🎨 Service Finder UI Design System v2.1
> **Audit Date:** 2026-07-28
> **Scope:** `frontend_app/` Vue 3 + Tailwind CSS project
> **Principle:** Every component must derive its visual properties from this document.
> **Toolkit:** Tailwind CSS v3 custom theme + `main.css` utility components.
---
## 1. 🖌️ Color Palette
### 1.1 Brand Identity (from `tailwind.config.js` + SVG logo)
| Token | Hex | Tailwind Class | Usage |
|---|---|---|---|
| **sf-blue** | `#004B63` | `bg-sf-blue` / `text-sf-blue` | Primary headers, main CTAs, dashboard cards |
| **sf-accent** | `#008CA4` | `bg-sf-accent` / `text-sf-accent` | Hover states, secondary buttons, links |
| **sf-green** | `#70BC84` | `bg-sf-green` / `text-[#70BC84]` | Progress bars, success badges, positive states |
| **sf-wall** | `#B5DCE3` | `bg-sf-wall` | Light panels, background gradients |
| **logo-dark-blue** | `#306081` | `bg-[#306081]` | Plan card headers, logo primary |
| **logo-teal** | `#418890` | `bg-[#418890]` | Logo secondary elements |
| **logo-green** | `#79B085` | `bg-[#79B085]` | Logo leaf/green elements |
### 1.2 Semantic Colors
| Token | Hex | Tailwind Class | Meaning |
|---|---|---|---|
| **success / emerald** | `#10b981` | `bg-emerald-500` / `text-emerald-600` | Confirmation, active status, savings |
| **warning / amber** | `#f59e0b` | `bg-amber-400` / `text-amber-400` | Expiring soon (≤7 days), caution |
| **danger / red** | `#ef4444` | `bg-red-500` / `text-red-600` | Expired, full capacity, delete actions |
| **info / sky** | `#0284c7` | `bg-sky-600` | Subscription cards, neutral info |
### 1.3 Neutral & Background
| Token | Hex | Tailwind Class | Usage |
|---|---|---|---|
| **dark-bg** | `#04151F` | `bg-[#04151F]` | App body background |
| **dark-lighter** | `#0a2a40` | `bg-[#0a2a40]` | Gradient top |
| **white-solid** | `#ffffff` | `bg-white` | Content cards (light theme) |
| **white-95** | `rgba(255,255,255,0.95)` | `bg-white/95` | Semi-transparent card faces |
| **slate-50** | `#f8fafc` | `bg-slate-50` | Card inner sections (stats, inputs) |
| **slate-200** | `#e2e8f0` | `border-slate-200` | Card inner borders |
| **slate-500** | `#64748b` | `text-slate-500` | Secondary text on light cards |
| **slate-700** | `#334155` | `bg-slate-700` / `text-slate-700` | Header bars, primary text on light |
| **slate-800** | `#1e293b` | `text-slate-800` | Card body text color |
### 1.4 Gradient Presets
| Name | Tailwind | Usage |
|---|---|---|
| **sf-dark-bg** | `radial-gradient(...)` | `body` global background |
| **header-amber** | `bg-gradient-to-r from-amber-600 to-amber-700` | Wallet card header |
| **header-sky** | `bg-gradient-to-r from-sky-600 to-sky-700` | Subscription card header |
| **header-emerald** | `bg-gradient-to-r from-emerald-600 to-emerald-700` | Transactions card header |
| **header-violet** | `bg-gradient-to-r from-violet-600 to-violet-700` | Invoices card header |
| **header-green** (add-on) | `bg-gradient-to-r from-emerald-600 to-emerald-700` | Add-on card headers |
| **btn-premium** | `bg-gradient-to-r from-[#418890] to-[#70BC84]` | Premium buttons hover state |
---
## 2. 📐 Cards
### 2.1 Standard Card (Light Theme)
- **Radius:** Always `rounded-2xl` for cards
- **Shadow:** `shadow-[0_8px_30px_rgb(0,0,0,0.12)]` by default, `shadow-2xl` on hover
- **Hover:** `hover:-translate-y-3 hover:scale-[1.02]` — subtle lift + scale
- **Height:** `h-[350px]` fixed height standard; use `h-auto` for list cards
- **Background:** `bg-white/95` (semi-transparent white) for light theme cards
- **Body text color:** `text-slate-800` inside card bodies
- **Body padding:** `p-4` standard; `p-6` for "lg" variant
- **Header colors:** Use semantic gradients: amber=wallet, sky=subscription, emerald=transactions, violet=invoices
### 2.2 Dark / Glass Card
- Use `bg-white/5` or `bg-white/10` for glass effect
- Always add `backdrop-blur-md` for blur
- Border: `border-white/10` or `border-white/20`
- Text: `text-white/80` or `text-white/60` for secondary
### 2.3 BaseCard Component
Prefab component with `header`, default `body`, `footer` slots. See `BaseCard.vue`.
---
## 3. 🔘 Buttons
| Variant | Tailwind Classes |
|---|---|
| **btn-primary** | `px-4 py-2 bg-sf-blue text-white rounded-lg hover:bg-sf-accent transition-colors` |
| **btn-secondary** | `px-4 py-2 bg-sf-wall text-sf-blue rounded-lg hover:bg-sf-green hover:text-white` |
| **btn-premium** | `rounded-xl bg-white/10 px-6 py-3 font-bold text-white backdrop-blur-md border border-white/20` |
| **btn-card-cta** | `w-full text-center px-4 py-2.5 rounded-xl text-sm font-semibold shadow-md hover:shadow-lg active:scale-[0.98]` |
| **Disabled** | `bg-slate-400 cursor-not-allowed` |
---
## 4. 📝 Typography
- **Font:** `Inter, system-ui, sans-serif`
- **H1:** `text-3xl font-bold text-white`
- **H2:** `text-2xl font-bold text-white`
- **H3:** `text-lg font-bold text-slate-900`
- **H4:** `text-sm font-semibold text-slate-700 uppercase tracking-wider`
- **Body:** `text-sm text-slate-700` (light) / `text-sm text-white/60` (dark)
- **Small:** `text-xs text-slate-500`
---
## 5. 🪟 Modals
- Backdrop: `bg-black/60 backdrop-blur-sm`
- Close on backdrop click: `@click.self="$emit('close')"`
- Max height: `max-h-[90vh]`
- Transition: `modal-fade` 0.2s ease opacity
- Panel: `bg-white rounded-2xl shadow-2xl overflow-hidden flex flex-col`
- Header: colored bar with `text-white font-bold text-lg`
- Footer: `bg-slate-50 border-t border-slate-200`
---
## 6. 🧩 Add-on Card Specification (Phase 4)
The add-on cards follow the **same solid card pattern** as base plan cards with a **green-themed header** to visually distinguish them.
### 6.1 Classes
| Element | Classes |
|---|---|
| **Wrapper (selected)** | `bg-white/95 rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.12)] overflow-hidden border-emerald-500 ring-1 ring-emerald-500/50 transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl` |
| **Wrapper (unselected)** | `bg-white/95 rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.12)] overflow-hidden border-slate-200 transition-all duration-300 hover:border-slate-400 hover:-translate-y-2 hover:shadow-2xl` |
| **Header** | `h-12 bg-gradient-to-r from-emerald-600 to-emerald-700 w-full shrink-0 flex items-center px-4` |
| **Header text** | `text-white font-bold text-sm tracking-wide` |
| **Body** | `p-4 flex flex-col h-full text-slate-800` |
| **Inner stat badges** | `bg-slate-50 text-slate-700` (match base plan stat boxes) |
| **Price (main)** | `text-xl font-bold text-slate-900` |
| **Price (small)** | `text-slate-500 text-xs` |
| **Toggle (selected)** | `w-full py-2 rounded-xl text-sm font-semibold bg-emerald-600 hover:bg-emerald-700 text-white` |
| **Toggle (unselected)** | `w-full py-2 rounded-xl text-sm font-semibold bg-slate-100 hover:bg-emerald-50 text-slate-700 hover:text-emerald-700 border border-slate-200 hover:border-emerald-300` |
### 6.2 What Changes from Current Glass Style
| Element | Current (broken) | Fix (target) |
|---|---|---|
| **Wrapper bg** | `bg-emerald-900/20` (glass, transparent on garage-bg) | `bg-white/95` (solid white, readable) |
| **Wrapper border** | `border-slate-700` (invisible) | `border-slate-200` (visible) |
| **Text color** | `text-white` (washes out on bg) | `text-slate-800` (solid dark on white) |
| **Secondary text** | `text-white/40` (illegible) | `text-slate-500` (readable) |
| **Toggle (unselected)** | `bg-white/10 text-white/80` (glass) | `bg-slate-100 text-slate-700 border border-slate-200` (solid) |
| **Header** | None (no header bar) | Green gradient header with add-on name |
| **Shadow** | None | `shadow-[0_8px_30px_rgb(0,0,0,0.12)]` |
| **Hover** | None | `hover:-translate-y-2 hover:shadow-2xl` |
| **Stat badges** | `bg-white/10 text-white/50` (invisible) | `bg-slate-50 text-slate-700` (same as base plans) |
---
## 7. ♿ Accessibility Checklist
- Minimum contrast ratio 4.5:1 for body text
- Focus rings on all interactive elements (`focus:ring-2 focus:ring-sf-accent`)
- `aria-label` on icon-only buttons
- Keyboard navigation for modals (Escape to close)
---
*End of UI Design System v2.1*

View File

@@ -0,0 +1,70 @@
# 🏗️ Finance Flip Card & Context-Aware Filter Implementation
**Date:** 2026-07-27
**Author:** Core Developer (Fast Coder)
**Gitea Card:** #424
---
## 📋 Summary
Implementation of two features for the Finance module plus an admin memory fix:
| # | Feature | Status |
|---|---------|--------|
| 1 | **CSS 3D Flipping Card** — Card 2 in `FinanceMainView.vue` | ✅ Done |
| 2 | **Context-Aware Package Filter** — Enhanced `SubscriptionPlansView.vue` | ✅ Done |
| 3 | **Admin Memory Fix**`NODE_OPTIONS=--max-old-space-size=4096`, cache purge, restart | ✅ Done |
---
## 🗄️ Backend Changes
### File: [`backend/app/api/v1/endpoints/users.py`](backend/app/api/v1/endpoints/users.py)
1. **`_build_user_response()`** — Added `created_at` field from `User.created_at` (after line 131)
2. **`read_users_me()`** — Added `subscription_valid_from` variable initialization (line 260)
3. **`read_users_me()`** — Extracted `valid_from` in both org branch (after line 275) and personal branch (after line 290)
4. **`read_users_me()`** — New add-on resolution query block (~35 lines) resolving active `OrganizationSubscription` or `UserSubscription` records where `SubscriptionTier.type == 'addon'`
5. **`read_users_me()`** — Added `subscription_valid_from` and `active_addons` to response dict (after line 308)
### Contract: `/auth/me` response now includes:
- `created_at` — ISO 8601 datetime of user registration
- `subscription_valid_from` — ISO 8601 of current subscription start
- `active_addons` — Array of `{tier_id, display_name, valid_from, valid_until, is_active}`
---
## 🖥️ Frontend Changes
### File: [`frontend_app/src/stores/auth.ts`](frontend_app/src/stores/auth.ts)
- Added `user_registration_date`, `subscription_valid_from`, `active_addons` to `UserProfile` interface
### File: [`frontend_app/src/views/FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue)
- **Template:** Card 2 replaced with flip card structure (front face + back face)
- **Script:** Added `ref` import, `isFlipped` state, `toggleFlip()` function, computed properties (`userRegistrationDate`, `subscriptionStartDate`, `activeAddons`), helper functions (`formatAddonDate`, `isAddonExpired`)
- **CSS:** Added scoped 3D flip CSS with `perspective: 1000px`, `transform-style: preserve-3d`, `backface-visibility: hidden`, `rotateY(180deg)` on `.is-flipped`
- **CTA button** uses `@click.stop` to prevent flip trigger
### File: [`frontend_app/src/views/SubscriptionPlansView.vue`](frontend_app/src/views/SubscriptionPlansView.vue)
- Enhanced `filteredPlans` computed to check `plan.rules?.type` JSONB first, then fall back to name prefix matching
- Supports: `corporate`, `business`, `private`, `consumer` rules types
### Files: [`frontend_app/src/i18n/hu.ts`](frontend_app/src/i18n/hu.ts), [`frontend_app/src/i18n/en.ts`](frontend_app/src/i18n/en.ts)
- Added 7 new keys under `subscription:` block
---
## 🐳 Admin Memory Fix
- **File:** [`docker-compose.yml:293`](docker-compose.yml:293) — `NODE_OPTIONS=--max-old-space-size=4096` was **already configured**
- Purged `.nuxt/` cache via `docker compose exec sf_admin_frontend rm -rf /app/.nuxt/*`
- Restarted container: `docker compose restart sf_admin_frontend`
---
## ✅ Validation
- `sync_engine.py` — ✅ 1307 OK, 0 Fixed, 0 Shadow — **rendszer tökéletesen szinkronban van**
- Backend changes compile without syntax errors
- All i18n keys present in both locales

View File

@@ -0,0 +1,181 @@
# 📦 Subscription Card Upgrade — Részletes Elemzés
**Date:** 2026-07-27
**Author:** System Architect
**Gitea Card:** #423
**Blueprint:** [`plans/logic_spec_subscription_card_upgrade.md`](../plans/logic_spec_subscription_card_upgrade.md)
---
## 1. Jelenlegi Állapot (As-Is)
### 1.1 A Card 2 a FinanceMainView-ban
A [`FinanceMainView.vue`](../frontend_app/src/views/FinanceMainView.vue) négy kártyát jelenít meg a `/finance` oldalon. A második kártya ("📦 Csomagok") jelenleg így néz ki:
- **Adatforrás:** [`authStore.user?.subscription_plan`](../frontend_app/src/stores/auth.ts:62) — egy nyers string, pl. `"private_pro_v1"`
- **Megjelenítés:** A nyers slug jelenik meg a kártyán, minden formázás nélkül
- **CTA:** Nincs dinamikus gomb, csak a teljes kártya kattintható (átnavigál `/finance/packages`-re)
```typescript
// Jelenlegi adat forrása (FinanceMainView.vue:249-252)
const subscriptionPlanName = computed(() => {
return authStore.user?.subscription_plan || '—'
})
```
### 1.2 A Backend API (/auth/me)
A [`read_users_me()`](../backend/app/api/v1/endpoints/users.py:147) végpont **már feloldja** a `SubscriptionTier` rekordot a `max_vehicles` és `max_garages` limit meghatározásához (lines 206-253), de:
-**Nem** adja vissza a `SubscriptionTier.rules["display_name"]` JSONB mezőt
-**Nem** adja vissza a `UserSubscription.valid_until` / `OrganizationSubscription.valid_until` lejárati dátumot
-**Nem** adja vissza a `subscription_tier_id`-t
### 1.3 Mi Működik Már Most?
A [`SubscriptionStatusWidget.vue`](../frontend_app/src/components/dashboard/SubscriptionStatusWidget.vue) és a [`SubscriptionInfoModal.vue`](../frontend_app/src/components/subscription/SubscriptionInfoModal.vue) már rendelkezik a gazdagabb funkcionalitással (járműhasználati progress bar, lejárati dátum), de ezek sötét témájú komponensek, és a FinanceMainView világos témájú Card 2-jében nem használhatók közvetlenül.
---
## 2. Adatbázis Oldal (What Exists)
### 2.1 `system.subscription_tiers`
A [`SubscriptionTier`](../backend/app/models/core_logic.py:68) modell JSONB `rules` mezője tartalmazza:
```json
{
"display_name": "Privát Pro",
"allowances": { "max_vehicles": 3, "max_garages": 1 },
"pricing": { "monthly_price": 9.99, "yearly_price": 99.99, "currency": "EUR" },
"duration": { "days": 30 },
"type": "private",
"lifecycle": { "is_public": true },
"marketing": { "badge": "Pro", "subtitle": "..." }
}
```
A `display_name` mező létezik a JSONB-ben, de jelenleg nem jut el a frontendig a `/auth/me`-n keresztül.
### 2.2 `finance.user_subscriptions` és `finance.org_subscriptions`
Mindkét tábla tartalmazza:
- `tier_id` → FK a `system.subscription_tiers`-re
- `valid_from` → Érvényesség kezdete
- `valid_until` → Lejárat dátuma (None = nincs lejárat)
- `is_active` → Aktív-e a subscription
A `SubscriptionService.get_user_subscription_details()` és `get_org_subscription_details()` metódusok már visszaadják ezeket az adatokat, de ezek a `GET /subscriptions/my` végpontokon keresztül érhetők el, nem a `/auth/me`-n.
---
## 3. A Terv (To-Be)
### 3.1 Adatfolyam
```
/auth/me
├─ subscription_plan: "private_pro_v1" (már most is)
├─ subscription_display_name: "Privát Pro" (ÚJ)
├─ subscription_expires_at: "2026-12-31T..." (ÚJ)
├─ subscription_tier_id: 14 (ÚJ)
├─ max_vehicles: 3 (már most is)
└─ max_garages: 1 (már most is)
authStore.user
FinanceMainView Card 2
┌─────────────────────────────────┐
│ 📦 Előfizetési Csomagok [Aktív] │
│ │
│ ┌─────────────────────────┐ │
│ │ Aktív │ │
│ │ Privát Pro │ │
│ └─────────────────────────┘ │
│ │
│ Lejárat: 2026.12.31 │
│ │
│ 🚗 jármű: ████████░░ 3 / 5 │
│ │
│ [ Csomag Kezelése ] │
└─────────────────────────────────┘
```
### 3.2 Backend Változások (1 fájl)
**Fájl:** [`users.py`](../backend/app/api/v1/endpoints/users.py) — `read_users_me()` függvény
A már meglévő `SubscriptionTier` feloldási logika UTÁN (lines 206-253) hozzáadunk egy új blokkot, amely:
1. Lekérdezi a `OrganizationSubscription` vagy `UserSubscription` rekordot (attól függően, hogy Corporate vagy Personal módban van a user)
2. Kiolvassa a `valid_until` értéket
3. Kiolvassa a már korábban feloldott `tier.rules["display_name"]` értéket
4. Hozzáadja ezeket a `response_data` dict-hez
### 3.3 Frontend Változások (4 fájl)
| Fájl | Változás |
|------|----------|
| [`auth.ts`](../frontend_app/src/stores/auth.ts) | `UserProfile` interface bővítése `subscription_display_name` és `subscription_tier_id` mezőkkel |
| [`FinanceMainView.vue`](../frontend_app/src/views/FinanceMainView.vue) | Card 2 template és computed property-k teljes cseréje |
| [`hu.ts`](../frontend_app/src/i18n/hu.ts) | `renewNow`, `upgradePlan` i18n kulcsok |
| [`en.ts`](../frontend_app/src/i18n/en.ts) | `renewNow`, `upgradePlan` i18n kulcsok |
---
## 4. Kockázatok és Fallback-ek
| Kockázat | Kezelés |
|----------|---------|
| A `display_name` null lehet régi tier rekordoknál | Fallback lánc: display_name → slug→human map → "Ingyenes" / "—" |
| A `valid_until` None lehet (élettartam előfizetés) | A lejárati blokk csak `v-if="formattedExpiry"` esetén jelenik meg |
| A `vehicleStore.vehicles` nincs betöltve | `onMounted`-ban fetch-eljük, ha szükséges |
| Corporate vs. Personal mód routing | A `ctaRoute` computed kezeli mindkét esetet |
---
## 5. Kapcsolódó Fájlok Térképe
```
backend/
app/
models/
core_logic.py ← SubscriptionTier, UserSubscription, OrganizationSubscription
api/v1/endpoints/
users.py ← /auth/me — IDE KELL az új mezőket hozzáadni
services/
subscription_service.py ← SubscriptionService (már van get_user_subscription_details)
frontend_app/src/
views/
FinanceMainView.vue ← Card 2 — EZT ÍRJUK ÁT
FinancePackagesView.vue ← /finance/packages (SubscriptionStatusWidget-et használ)
components/
dashboard/
SubscriptionStatusWidget.vue ← Sötét témájú, gazdag widget (referencia)
subscription/
SubscriptionInfoModal.vue ← Modal részletes infóval (referencia)
stores/
auth.ts ← UserProfile interface bővítése
vehicle.ts ← vehicleStore (már importálva más komponensekben)
i18n/
hu.ts ← Magyar fordítások
en.ts ← Angol fordítások
```
---
## 6. Ellenőrző Lista a Code Mode számára
- [ ] `users.py`: `subscription_display_name`, `subscription_expires_at`, `subscription_tier_id` feloldása
- [ ] `auth.ts`: `UserProfile` interface bővítése
- [ ] `FinanceMainView.vue`: Card 2 template csere (display_name, expiry, vehicle bar, CTA)
- [ ] `FinanceMainView.vue`: Computed property-k hozzáadása
- [ ] `FinanceMainView.vue`: `useVehicleStore` import és inicializálás
- [ ] `hu.ts`: `renewNow`, `upgradePlan` kulcsok
- [ ] `en.ts`: `renewNow`, `upgradePlan` kulcsok
- [ ] `sync_engine.py` futtatása (nincs séma változás, de ellenőrzés)
- [ ] Vizuális teszt: Card 2 helyes megjelenés ellenőrzése
- [ ] Funkcionális teszt: CTA gomb navigáció ellenőrzése

View File

@@ -0,0 +1,119 @@
# SubscriptionInfoModal planDisplayName Bug Analysis
**Date:** 2026-07-28
**Issue:** #433 - P0: Avatar Menu Subscription Modal - planDisplayName bugfix & DB audit
---
## 1. Database Investigation: admin@profibot.hu
### User Profile
| Field | Value |
|-------|-------|
| `id` | 2 |
| `email` | admin@profibot.hu |
| `role` | ADMIN |
| `subscription_plan` | private_vip_v1 |
| `subscription_expires_at` | 2026-08-27 16:36:24+00 |
| `is_vip` | true |
| `scope_level` | system |
| `scope_id` | 67 |
| `ui_mode` | personal |
### Organization Memberships (3 orgs)
| org_id | role | name | type | status | base_asset_limit |
|--------|------|------|------|--------|-------------------|
| 1 | ADMIN | Profibot Tester - Privat Garazs (#28) | individual | active | 100 |
| 67 | OWNER | User Admin - Privat Garazs (#2) | individual | active | 100 |
| 57 | OWNER | Teszt Autoszerviz Kft. | service_provider | pending_verification | 3 |
### Vehicle Distribution (vehicle.assets via current_organization_id)
| org_id | Count | Sample plates |
|--------|-------|---------------|
| 1 | 19 | TEST-API-999, TEST-123, DRAFT-456, TEST-999, DRAFT-999, TEST-888, DRAFT-888, TEST-777 |
| 67 | 3 | TEST-API-01 (archived), ABB112 (archived), ABC-123 (archived) |
| 57 | 0 | No vehicles |
### Key Finding: No Duplication, No Data Corruption
Vehicles are NOT duplicated across organizations. Each org has distinct vehicles. The perception of "azonos adatok" (same data) likely arises because:
- Org 1 has 19 test vehicles (TEST-XXX, DRAFT-XXX pattern) making it appear bloated
- Org 67 has only 3 archived vehicles
- The KEP 3 showing "private_pro_v1" is the UI bug, not a data issue (admin's actual plan is private_vip_v1)
---
## 2. Root Cause: planDisplayName in SubscriptionInfoModal.vue
### Bug Location
`SubscriptionInfoModal.vue:175-179` — the `planDisplayName` computed property:
```typescript
const planDisplayName = computed(() => {
const plan = authStore.user?.subscription_plan
if (!plan || plan === 'FREE') return t('subscription.free')
return plan // BUG: returns raw slug like "private_vip_v1"
})
```
### Why It's Broken
The backend `_resolve_subscription_data()` in `users.py:45-191` already:
1. Queries the `SubscriptionTier` assigned to the user/org
2. Extracts `display_name` from `tier.rules` JSONB
3. Returns it as `subscription_display_name` in `/auth/me` and `/users/me`
The `UserProfile` TypeScript interface in `auth.ts:68` already declares:
```typescript
subscription_display_name?: string | null
```
The `SubscriptionInfoModal` ignores this field completely.
### Reference Fix (Already Implemented in SubscriptionStatusWidget)
`SubscriptionStatusWidget.vue:124-150` has the correct 3-level fallback:
1. `authStore.user?.subscription_display_name` (backend-resolved human name)
2. Org-level `subscription_display_name` (corporate mode)
3. Static slug-to-human mapping (fallback)
4. Raw plan name (ultimate fallback)
### Additional Data Binding Verification
| Computed | Current Code | Status |
|----------|-------------|--------|
| maxVehicles | `authStore.user?.max_vehicles ?? 1` | CORRECT |
| maxGarages | `authStore.user?.max_garages ?? 1` | CORRECT |
| vehicleCount | `vehicleStore.vehicles.length` | CORRECT |
| garageCount | `authStore.myOrganizations?.length ?? 0` | Context-dependent |
| vehiclePercent/garagePercent | `count / max * 100` | CORRECT |
---
## 3. Fix Specification
### Required Change
Replace `planDisplayName` computed in `SubscriptionInfoModal.vue:175-179` with the same 3-level fallback logic used by `SubscriptionStatusWidget.vue:124-150`.
### New Code
```typescript
const planDisplayName = computed(() => {
// Priority 1: Backend-supplied human-readable name
const displayName = authStore.user?.subscription_display_name
if (displayName) return displayName
// Priority 2: Slug-to-human mapping fallback
const plan = authStore.user?.subscription_plan
if (!plan || plan === 'FREE') return t('subscription.free')
const slugToHuman: Record<string, string> = {
'private_basic_v1': 'Privat Alap',
'private_pro_v1': 'Privat Pro',
'private_vip_v1': 'Privat VIP',
'corp_free_v1': 'Ceges Ingyenes',
'corp_basic_v1': 'Ceges Alap',
'corp_pro_v1': 'Ceges Pro',
}
if (slugToHuman[plan]) return slugToHuman[plan]
// Priority 3: Raw plan name as ultimate fallback
return plan
})
```

View File

@@ -0,0 +1,142 @@
# 🔍 Subscription Limit Data Binding Bug — Root Cause Analysis
**Dátum:** 2026-07-28
**Issue:** #431 (Gitea)
**Súlyosság:** P1 — A felhasználó rossz limiteket lát a frontenden
---
## 1. A Felhasználó Által Jelentett Hiba
A frontend "Előfizetés Információ" modális ablakon (SubscriptionStatusWidget) és a FinanceMainView Card 2-n a következő látszik:
- **Jármű Limit:** 1/1
- **Garázs Limit:** 2/1
Egy Pro/VIP csomag esetén a valós limit `max_vehicles=3` (Pro) vagy `max_vehicles=10` (VIP) lenne.
---
## 2. Adatbázis Réteg — Valós Állapot
| Tábla | Rekord | Kulcs mező | Érték |
|---|---|---|---|
| `identity.users` | id=2 (admin@profibot.hu) | `subscription_plan` | `private_vip_v1` |
| `identity.users` | id=2 | `scope_id` | `67` |
| `identity.users` | id=2 | `scope_level` | `system` |
| `identity.users` | id=2 | `ui_mode` | `personal` |
| `finance.user_subscriptions` | id=34 | `tier_id` | **15** (VIP) ✅ AKTÍV |
| `finance.user_subscriptions` | id=33 | `tier_id` | 14 (Pro) ❌ inaktív |
| `finance.org_subscriptions` | org_id=67 | — | **NINCS REKORD** |
| `system.subscription_tiers` | id=14 (private_pro_v1) | `allowances.max_vehicles` | 3 |
| `system.subscription_tiers` | id=14 | `allowances.max_garages` | 1 |
| `system.subscription_tiers` | id=14 | `display_name` | **NULL** ❌ |
| `system.subscription_tiers` | id=15 (private_vip_v1) | `allowances.max_vehicles` | 10 |
| `system.subscription_tiers` | id=15 | `allowances.max_garages` | 2 |
| `system.subscription_tiers` | id=15 | `display_name` | "Privát VIP" ✅ |
---
## 3. ROOT CAUSE #1 (Elsődleges) — Hibás Vezérlési Logika
**Fájl:** [`backend/app/api/v1/endpoints/users.py`](backend/app/api/v1/endpoints/users.py), `read_users_me()` függvény, sorok 212254.
### A problémás kódszakasz (pszeudokód):
```python
if active_org_id is not None: # ← scope_id=67 → active_org_id=67 → IGAZ
# 1. Org subscription keresés
tier = org_sub_query(scalar_one_or_none)
if tier and tier.rules: # ← NINCS org subscription (üres finance.org_subscriptions org_id=67-re)
allowances = tier.rules.get("allowances", {})
max_vehicles = int(allowances.get("max_vehicles", 1))
max_garages = int(allowances.get("max_garages", 1))
else:
# 2. Fallback: Organization.base_asset_limit
org = select(Organization).where(id == active_org_id)
max_vehicles = org.base_asset_limit or 1 # ← 1-et ad!
max_garages = 1 # ← 1-et ad!
# ❌ HIBA: Soha nem nézi meg a user_subscriptions táblát!
else:
# 3. Personal mode: user subscription keresés
# ← SOHA NEM FUT LE, mert active_org_id != None
tier = user_sub_query(scalar_one_or_none)
if tier and tier.rules:
allowances = tier.rules.get("allowances", {})
max_vehicles = int(allowances.get("max_vehicles", 1)) # ← 10 lenne!
max_garages = int(allowances.get("max_garages", 1)) # ← 2 lenne!
```
### Miért hibás?
A user-nek van `scope_id=67`-e (egy szervezet ID), de az adott szervezethez **nincs org subscription** rekord. A felhasználónak viszont van **aktív user subscription**-e a VIP tier-re (tier_id=15, max_vehicles=10, max_garages=2).
A kód hibásan feltételezi, hogy ha van `active_org_id`, akkor:
1. vagy van org subscription (→ abból olvassa a limiteket),
2. vagy nincs (→ `Organization.base_asset_limit` fallback, ami 1).
**Soha nem esik át a user subscription ágra**, mert `active_org_id is not None`.
### Hatás
A `GET /auth/me` válaszban:
```json
{
"max_vehicles": 1, // ← 10 helyett
"max_garages": 1, // ← 2 helyett
"subscription_display_name": null // ← mert a tier változó None
}
```
---
## 4. ROOT CAUSE #2 (Másodlagos) — private_pro_v1 display_name NULL
**Tábla:** `system.subscription_tiers` id=14
**Mező:** `rules->>'display_name'` = NULL
A seederben (`seed_packages.py:131`) a `display_name` "Privát Pro"-ként van definiálva, de az éles adatbázisban NULL. Valószínűleg egy korábbi migráció vagy kézi módosítás írta felül.
Ez jelenleg nem okoz közvetlen hibát a felhasználónál (mert ő VIP-n van), de a Pro csomagra váltó felhasználóknál a display_name üres lenne.
---
## 5. Javítási Terv
### 5.1 Backend Fix (`users.py`)
A `read_users_me()` függvényben a fallback láncot ki kell egészíteni:
```
if active_org_id is not None:
1. Org subscription keresés (MEGLÉVŐ)
2. Ha nincs → User subscription keresés (ÚJ!)
3. Ha user subscription sincs → Organization.base_asset_limit (MEGLÉVŐ fallback)
else:
4. User subscription keresés (MEGLÉVŐ)
```
Ugyanez a logika kell a `subscription_valid_until` / `subscription_tier_id` feloldására is (sorok 267308), valamint a `subscription_display_name` feloldására (sor 311312).
### 5.2 DB Fix
```sql
UPDATE system.subscription_tiers
SET rules = jsonb_set(rules, '{display_name}', '"Privát Pro"')
WHERE id = 14 AND rules->>'display_name' IS NULL;
```
### 5.3 Frontend
A frontend komponensek (`FinanceMainView.vue`, `SubscriptionStatusWidget.vue`) már helyesen olvassák az `authStore.user?.max_vehicles` és `authStore.user?.max_garages` értékeket. A backend fix után automatikusan a helyes értékek jelennek meg.
---
## 6. Érintett Fájlok
| Fájl | Változtatás |
|---|---|
| `backend/app/api/v1/endpoints/users.py` | `read_users_me()`: fallback lánc javítása |
| Adatbázis (SQL) | `private_pro_v1` display_name beállítása |
| `frontend_app/src/views/FinanceMainView.vue` | Nincs változtatás szükséges (már a helyes mezőket olvassa) |
| `frontend_app/src/components/dashboard/SubscriptionStatusWidget.vue` | Nincs változtatás szükséges |