# πŸ—οΈ Logic Spec: Subscription Card Upgrade (FinanceMainView) **Date:** 2026-07-27 **Author:** System Architect (Architect Mode) **Status:** πŸ”΅ Blueprint β€” Ready for Code Mode Hand-off **Masterbook Ref:** Master Book 2.0 / Epic 3: Financial Motor / Subscription Packages --- ## 1. 🎯 Objective Transform the "πŸ“¦ Csomagok" card (Card 2) in [`FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue:118) from a dead display of the raw DB slug (`private_pro_v1`) into a highly functional dashboard widget showing: | # | Feature | Priority | |---|---------|----------| | 1 | Human-readable display name (e.g., "PrivΓ‘t Pro") | P0 | | 2 | Expiry/renewal date (e.g., "Γ‰rvΓ©nyes: 2026.12.31-ig") | P0 | | 3 | Vehicle usage capacity (e.g., "Kezelt jΓ‘rmΕ±vek: 3 / 5") | P1 | | 4 | Dynamic CTA button ("Csomag kezelΓ©se" / "Upgrade") | P1 | --- ## 2. πŸ” Gap Analysis ### 2.1 Current State β€” What Card 2 Shows Today [`FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue:118-154): ```ts const subscriptionPlanName = computed(() => { return authStore.user?.subscription_plan || 'β€”' }) ``` This displays the raw DB slug like `private_pro_v1` with no formatting. ### 2.2 What Data IS Available | Data Point | Source | Available in authStore? | Available via API? | |------------|--------|------------------------|-------------------| | `subscription_plan` (slug) | `identity.users.subscription_plan` | βœ… | `/auth/me` | | `max_vehicles` | Resolved from `SubscriptionTier.rules.allowances` | βœ… | `/auth/me` (line 259) | | `max_garages` | Resolved from `SubscriptionTier.rules.allowances` | βœ… | `/auth/me` (line 260) | | `display_name` | `SubscriptionTier.rules.display_name` JSONB | ❌ | Only via `GET /subscriptions/my` | | `subscription_expires_at` | `UserSubscription.valid_until` | ❌ NOT in `/auth/me` | `GET /subscriptions/my` | | `vehicleCount` | `vehicleStore.vehicles.length` | N/A (vehicleStore) | N/A | | `subscription_valid_until` | Org-level: `OrganizationSubscription.valid_until` | Partially (org data) | `GET /organizations/my` | ### 2.3 Key Gaps to Fill 1. **`subscription_expires_at` not in `/auth/me` response** β€” The backend `read_users_me()` already resolves `SubscriptionTier` for `max_vehicles`/`max_garages` but does NOT extract `valid_until` from the `UserSubscription`/`OrganizationSubscription` record. 2. **`subscription_display_name` not in `/auth/me` response** β€” The `SubscriptionTier.rules["display_name"]` JSONB field exists (e.g., `"display_name": "PrivΓ‘t Pro"`) but is never sent to the frontend via `/auth/me`. 3. **Card 2 doesn't use `vehicleStore`** β€” The vehicle count is already available in the `SubscriptionStatusWidget.vue` but not in `FinanceMainView.vue`. --- ## 3. πŸ“ Architecture: Data Flow (Current vs. Target) ```mermaid graph TD subgraph "Current Flow (BROKEN)" A1["/auth/me"] -->|subscription_plan slug| B1[authStore.user] B1 -->|raw slug 'private_pro_v1'| C1[FinanceMainView Card 2] end subgraph "Target Flow (UPGRADED)" A2["/auth/me"] -->|+ display_name + expires_at + max_vehicles| B2[authStore.user] A2 -->|+ subscription_tier_id| B2 B2 -->|display_name 'PrivΓ‘t Pro'| C2[FinanceMainView Card 2] B2 -->|expires_at '2026-12-31'| C2 B2 -->|max_vehicles 5| C2 D[vehicleStore.vehicles.length] -->|vehicleCount 3| C2 end ``` --- ## 4. πŸ› οΈ Implementation Blueprint ### PHASE 1: Backend β€” Enrich `/auth/me` Response **File:** [`backend/app/api/v1/endpoints/users.py`](backend/app/api/v1/endpoints/users.py) #### Step 1.1: Resolve `subscription_expires_at` and `subscription_display_name` In the `read_users_me()` function, after the P0 block at lines 206-253 (where `max_vehicles` and `max_garages` are resolved), add new resolution logic: **Location:** After line 253 (`max_garages = int(allowances.get("max_garages", 1))`), add: ```python # ── P0 Subscription Card Upgrade: resolve display_name and valid_until ── subscription_display_name = None subscription_valid_until = None subscription_tier_id = None if active_org_id is not None: # Org-level: get valid_until from OrganizationSubscription org_sub_full_stmt = ( select(OrganizationSubscription) .where( OrganizationSubscription.org_id == active_org_id, OrganizationSubscription.is_active == True ) .order_by(OrganizationSubscription.valid_from.desc()) .limit(1) ) org_sub_full = (await db.execute(org_sub_full_stmt)).scalar_one_or_none() if org_sub_full: subscription_valid_until = org_sub_full.valid_until subscription_tier_id = org_sub_full.tier_id else: # Personal mode: get valid_until from UserSubscription user_sub_full_stmt = ( select(UserSubscription) .where( UserSubscription.user_id == current_user.id, UserSubscription.is_active == True ) .order_by(UserSubscription.valid_from.desc()) .limit(1) ) user_sub_full = (await db.execute(user_sub_full_stmt)).scalar_one_or_none() if user_sub_full: subscription_valid_until = user_sub_full.valid_until subscription_tier_id = user_sub_full.tier_id # Resolve display_name from the already-fetched tier (reuse from lines 211-253) if tier and tier.rules: subscription_display_name = tier.rules.get("display_name", None) ``` #### Step 1.2: Add new fields to the response dict After line 260 (`response_data["max_garages"] = max_garages`), add: ```python response_data["subscription_expires_at"] = ( subscription_valid_until.isoformat() if subscription_valid_until else None ) response_data["subscription_display_name"] = subscription_display_name response_data["subscription_tier_id"] = subscription_tier_id ``` **Note:** The `_build_user_response()` helper at line 121-144 defines the base shape. We do NOT need to modify it β€” the new fields are injected after `_build_user_response()` returns, just like `max_vehicles` and `max_garages` already are. --- ### PHASE 2: Frontend β€” Update Type Definitions **File:** [`frontend_app/src/stores/auth.ts`](frontend_app/src/stores/auth.ts) #### Step 2.1: Extend `UserProfile` interface Add two new fields to the `UserProfile` interface (after line 66 `max_garages`): ```typescript /** P0 Subscription Card Upgrade: Human-readable plan name (e.g., "PrivΓ‘t Pro") */ subscription_display_name?: string | null /** P0 Subscription Card Upgrade: FK to system.subscription_tiers */ subscription_tier_id?: number | null ``` The `subscription_expires_at` field already exists at line 63. βœ… --- ### PHASE 3: Frontend β€” Rewrite Card 2 in FinanceMainView **File:** [`frontend_app/src/views/FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue) #### Step 3.1: Import `useVehicleStore` Add to the imports at line 228: ```typescript import { useVehicleStore } from '../stores/vehicle' ``` #### Step 3.2: Initialize vehicle store After line 234: ```typescript const vehicleStore = useVehicleStore() ``` #### Step 3.3: Replace the computed property Replace lines 249-252: ```typescript // ── Card 2: Subscription data (upgraded) ────────────────────────── const subscriptionDisplayName = computed(() => { // Priority: 1) display_name from backend, 2) humanized slug, 3) fallback const displayName = authStore.user?.subscription_display_name if (displayName) return displayName const plan = authStore.user?.subscription_plan if (!plan || plan === 'FREE') return t('subscription.free') // Simple slugβ†’human mapping as fallback const slugMap: Record = { 'private_pro_v1': 'PrivΓ‘t Pro', 'corp_premium_v1': 'CΓ©ges PrΓ©mium', 'corp_premium_plus_v1': 'CΓ©ges PrΓ©mium Plus', } return slugMap[plan] || plan }) const subscriptionExpiresAt = computed(() => { return authStore.user?.subscription_expires_at ?? null }) const formattedExpiry = computed(() => { const raw = subscriptionExpiresAt.value if (!raw) return null const d = new Date(raw) if (isNaN(d.getTime())) return null return `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}` }) const isExpired = computed(() => { const raw = subscriptionExpiresAt.value if (!raw) return false return new Date(raw) < new Date() }) const vehicleCount = computed(() => vehicleStore.vehicles?.length || 0) const maxVehicles = computed(() => authStore.user?.max_vehicles ?? 0) const vehiclePercent = computed(() => { if (!maxVehicles.value || maxVehicles.value <= 0) return 0 return Math.min(100, (vehicleCount.value / maxVehicles.value) * 100) }) // CTA routing depends on mode const ctaRoute = computed(() => { if (authStore.isCorporateMode && authStore.user?.active_organization_id) { return `/organization/${authStore.user.active_organization_id}/subscription` } return '/dashboard/subscription' }) const ctaLabel = computed(() => { if (isExpired.value) return t('subscription.renewNow') || 'ÚjΓ­tΓ‘s most' return t('subscription.managePlan') }) ``` #### Step 3.4: Replace Card 2 template (lines 118-154) Replace the entire Card 2 block with: ```vue
πŸ“¦ {{ t('subscription.title') }} {{ isExpired ? t('subscription.expired') : t('subscription.active') }}

{{ t('subscription.active') }}

{{ subscriptionDisplayName }}

{{ t('subscription.expiresAt') }}: {{ formattedExpiry }}

πŸš— {{ t('subscription.vehicles') }} {{ vehicleCount }} / {{ maxVehicles }}
{{ ctaLabel }}
``` #### Step 3.5: Add `onMounted` vehicle fetch In the existing `onMounted` block at line 263-265, add vehicle store fetch: ```typescript onMounted(() => { financeStore.fetchWalletBalance() // Ensure vehicles are loaded for usage display if (vehicleStore.vehicles.length === 0) { vehicleStore.fetchVehicles() } }) ``` --- ### PHASE 4: Frontend β€” I18n Keys **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) #### Step 4.1: Add new Hungarian keys In `hu.ts`, inside the `subscription:` block (after line 87): ```typescript renewNow: 'ÚjΓ­tΓ‘s most', upgradePlan: 'Csomag vΓ‘ltΓ‘sa', ``` #### Step 4.2: Add new English keys In `en.ts`, inside the `subscription:` block (at corresponding position): ```typescript renewNow: 'Renew Now', upgradePlan: 'Upgrade Plan', ``` --- ## 5. πŸ“‹ Files Changed Summary | # | File | Change Type | Lines Affected | |---|------|-------------|---------------| | 1 | [`backend/app/api/v1/endpoints/users.py`](backend/app/api/v1/endpoints/users.py) | Add `display_name` + `valid_until` resolution | ~+30 lines after L253 | | 2 | [`frontend_app/src/stores/auth.ts`](frontend_app/src/stores/auth.ts) | Add `subscription_display_name`, `subscription_tier_id` to `UserProfile` | ~+3 lines after L66 | | 3 | [`frontend_app/src/views/FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue) | Rewrite Card 2 template + computed properties | Replace L118-154, add computed after L252 | | 4 | [`frontend_app/src/i18n/hu.ts`](frontend_app/src/i18n/hu.ts) | Add `renewNow`, `upgradePlan` keys | ~+2 lines after L87 | | 5 | [`frontend_app/src/i18n/en.ts`](frontend_app/src/i18n/en.ts) | Add `renewNow`, `upgradePlan` keys | ~+2 lines at matching position | --- ## 6. ⚠️ Risk Assessment | Risk | Mitigation | |------|-----------| | `display_name` may be `null` in old tier records | Fallback chain: display_name β†’ slugβ†’human map β†’ "β€”" | | `valid_until` may be `None` for lifetime subscriptions | Conditionally render expiry block (`v-if="formattedExpiry"`) | | `vehicleStore.vehicles` might not be loaded yet | `onMounted` fetches vehicles if needed | | Corporate vs. Individual mode routing | `ctaRoute` computed handles both modes | --- ## 7. πŸ§ͺ Testing Checklist - [ ] Card 2 shows `display_name` (e.g., "PrivΓ‘t Pro") instead of raw slug - [ ] Expiry date displayed as `YYYY.MM.DD` format - [ ] "LejΓ‘rt" badge shown when subscription is expired - [ ] Vehicle usage bar shows correct `vehicleCount / maxVehicles` - [ ] CTA button navigates to correct route (personal or org subscription page) - [ ] CTA label changes to "ÚjΓ­tΓ‘s most" when expired - [ ] Fallback works for users without `display_name` in their tier - [ ] No regressions on Card 1 (Wallet) and other cards - [ ] I18n keys work in both `hu` and `en` locales --- ## 8. πŸ“Ž References - [`SubscriptionTier` model](backend/app/models/core_logic.py:68) β€” `system.subscription_tiers` - [`UserSubscription` model](backend/app/models/core_logic.py:174) β€” `finance.user_subscriptions` - [`OrganizationSubscription` model](backend/app/models/core_logic.py:143) β€” `finance.org_subscriptions` - [`SubscriptionService`](backend/app/services/subscription_service.py:74) β€” Full subscription resolution logic - [`SubscriptionStatusWidget`](frontend_app/src/components/dashboard/SubscriptionStatusWidget.vue) β€” Existing rich widget (dark theme) - [`SubscriptionInfoModal`](frontend_app/src/components/subscription/SubscriptionInfoModal.vue) β€” Modal with detailed info - [P0 Subscription JSONB Audit](docs/p0_subscription_jsonb_structural_audit_report.md) β€” Tier JSONB structure verified