120 lines
4.3 KiB
Markdown
120 lines
4.3 KiB
Markdown
# 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
|
|
})
|
|
```
|