# P0 DATA STRUCTURAL AUDIT REPORT: `_extract_limit()` & JSONB Tier Structure **Date:** 2026-06-28 **Author:** System Architect **Scope:** [`system.subscription_tiers`](backend/app/models/core_logic.py:68), [`finance.org_subscriptions`](backend/app/models/core_logic.py:143), [`_extract_limit()`](backend/app/api/v1/endpoints/admin_organizations.py:397) **Status:** ✅ Structural audit complete — `_extract_limit` logic is **correct**, DB JSONB is **valid** --- ## 1. AUDIT: `system.subscription_tiers` (Base Tier Rules) ### Raw JSON Structure All three corporate tiers have their limits stored at **`rules["allowances"]`** (nested object, 2nd level): | Tier | ID | `jsonb_typeof(rules)` | `allowances.max_vehicles` | `allowances.max_garages` | |------|----|----------------------|--------------------------|--------------------------| | `private_pro_v1` | 14 | `object` ✅ | 3 | 1 | | `corp_premium_v1` | 16 | `object` ✅ | 20 | 3 | | `corp_premium_plus_v1` | 17 | `object` ✅ | 50 | 10 | ### Full JSON for `corp_premium_plus_v1` (id=17) ```json { "type": "corporate", "pricing": { "currency": "EUR", "credit_price": 6000, "yearly_price": 599.99, "monthly_price": 59.99 }, "duration": { "days": 30, "allow_stacking": true }, "ad_policy": { "show_ads": false, "ad_free_grace_days": 0, "max_daily_impressions": null }, "affiliate": { "referral_bonus_credits": 250, "commission_rate_percent": 20 }, "lifecycle": { "is_public": true }, "marketing": { "badge": "Plus", "subtitle": "Nagy flották prémium menedzsmentje.", "highlight_color": "#8B5CF6" }, "allowances": { "max_garages": 10, "max_vehicles": 50, "monthly_free_credits": 800 }, "display_name": "Ceiges Preimium Plus", "entitlements": ["SRV_DATA_EXPORT", "SRV_AI_UPLOAD", "SRV_ACCOUNTING_SYNC"], "pricing_zones": { "HU": { "currency": "HUF", "credit_price": 6000, "yearly_price": 239990, "monthly_price": 23990 }, "DEFAULT": { "currency": "EUR", "credit_price": 6000, "yearly_price": 599.99, "monthly_price": 59.99 } } } ``` ### Confirmed: `rules` is a proper JSONB **object**, NOT a string ```sql SELECT jsonb_typeof(rules) FROM system.subscription_tiers WHERE id = 17; -- Result: "object" ✅ (not "string") ``` --- ## 2. AUDIT: `finance.org_subscriptions` (Active Subs & Extra Allowances) | org_id | Sub ID | Tier | Tier ID | `extra_allowances` | |--------|--------|------|---------|-------------------| | 15 | 2 | `corp_premium_v1` | 16 | (not queried) | | 43 | 30 | `private_pro_v1` | 14 | `{}` | | 44 | 22 | `corp_premium_v1` | 16 | (not queried) | | 45 | 21 | `corp_premium_plus_v1` | 17 | (not queried) | | 49 | 20 | `corp_premium_v1` | 16 | (not queried) | For org_id=43: `extra_allowances` = `{}` (empty JSONB object) — no extra allowances purchased. --- ## 3. TEST SCRIPT: `_extract_limit()` Behavior ### Implementation (exact copy from [`admin_organizations.py:397`](backend/app/api/v1/endpoints/admin_organizations.py:397)) The function searches at **3 levels**: 1. **Level 1**: Direct keys at rules root (`rules["max_vehicles"]`) 2. **Level 2**: Nested under `rules["allowances"]` (`rules["allowances"]["max_vehicles"]`) 3. **Level 3**: Nested under `rules["limits"]` (`rules["limits"]["max_vehicles"]`) ### Test Results — ALL PASS ✅ | Tier | Vehicles | Branches | Users | Status | |------|----------|----------|-------|--------| | `corp_premium_plus_v1` | **50** ✅ | **10** ✅ | 0 | **PASS** | | `corp_premium_v1` | **20** ✅ | **3** ✅ | 0 | **PASS** | | `private_pro_v1` | **3** ✅ | **1** ✅ | 0 | **PASS** | ### Edge Cases | Input | Result | Expected | Status | |-------|--------|----------|--------| | Empty dict `{}` | 5 (default) | 5 | ✅ | | `None` | 5 (default) | 5 | ✅ | | String (not dict) | 5 (default) | 5 | ✅ | | Direct key match | 100 | 100 | ✅ | | `allowances.max_vehicles` | 50 | 50 | ✅ | | `limits.max_vehicles` | 25 | 25 | ✅ | ### Double-Encoding Simulation When the JSON is **double-encoded** (string stored inside JSONB instead of object): ``` Input: '"{\\"allowances\\": {\\"max_vehicles\\": 50}}"' → json.loads() returns str, not dict → _extract_limit(str, ...) returns default=0 ❌ ``` ✅ **Confirmed: Current DB data is NOT double-encoded.** (`jsonb_typeof = "object"`) --- ## 4. ROOT CAUSE ANALYSIS ### The `_extract_limit()` function is **NOT the problem** All tests confirm the function correctly extracts limits from the current JSONB structure. It fully supports the `allowances` nested format used by all tiers. ### The REAL problem is likely in the **code flow** before `_extract_limit` is called The endpoint [`get_organization_details()`](backend/app/api/v1/endpoints/admin_organizations.py:462) has **two code paths**: #### Path A (line 579-664) — Uses `finance.org_subscriptions` (NEW system) ```python active_subs = sub_result.scalars().all() # Query finance.org_subscriptions if active_subs: base_subs = [s for s in active_subs if s.tier and s.tier.type in (None, '', 'base')] primary_sub = base_subs_sorted[0] if base_subs_sorted else ... if primary_sub and primary_sub.tier: primary_rules = primary_sub.tier.rules or {} # ← THIS must work ``` #### Path B (line 665-691) — Uses `fleet.organizations.subscription_tier_id` (LEGACY) ```python elif org.subscription_tier: rules = org.subscription_tier.rules or {} # ← LEGACY path ``` ### Potential failure scenarios (outside `_extract_limit`): | Scenario | Symptom | Likelihood | |----------|---------|------------| | **Path A → `active_subs` is empty** (no `finance.org_subscriptions` row) | Falls to Path B | Medium | | **Path A → `primary_sub.tier` is `None`** (FK broken, tier missing) | Skips limit calc, `subscription_summary` stays `None` | Low | | **Path B → `org.subscription_tier` points to wrong tier** (old FK mismatch) | Wrong limits returned | Medium | | **`org.subscription_tier.rules` is `None`** (tier exists but rules column is NULL) | `{} or None` → `{}` → default | Low | --- ## 5. RECOMMENDATIONS ### ✅ Short-term: Verify runtime dataflow 1. **Test with exact org_id** that shows the 0 limit: ```sql SELECT os.id, os.org_id, os.tier_id, st.name, st.rules IS NOT NULL as has_rules FROM finance.org_subscriptions os JOIN system.subscription_tiers st ON os.tier_id = st.id WHERE os.org_id = AND os.is_active = true; ``` 2. **Check if the endpoint takes Path A or Path B** by inspecting logs/Debug mode. ### ✅ Long-term: Code improvements (separate ticket) 1. **Add logging** at key decision points in [`get_organization_details()`](backend/app/api/v1/endpoints/admin_organizations.py:579-614): - Log `active_subs` count - Log which path is taken (A vs B) - Log `primary_rules` keys and extracted limits - Log if `primary_sub.tier` is None 2. **Add defensive fallback** — if `_extract_limit` returns 0 but `rules` has `allowances`, log a warning: ```python base_vehicles = _extract_limit(primary_rules, ["max_vehicles", "max_assets"], fc_fallback) if base_vehicles == 0 and primary_rules.get("allowances", {}).get("max_vehicles"): logger.warning(f"_extract_limit returned 0 despite allowances.max_vehicles={primary_rules['allowances']['max_vehicles']}") ``` ### 🔍 Most Probable Root Cause The issue is likely that **the org(s) reporting 0 limits do NOT have an active row in `finance.org_subscriptions`** (Path A), and the **legacy Path B is failing** because `org.subscription_tier.rules` is `None` or the old FK points to a tier that doesn't have `allowances` populated. **Next step**: Identify the exact org_id(s) that show 0 and check their `finance.org_subscriptions` presence and `fleet.organizations.subscription_tier_id` value. --- ## Appendix: Test Script Location The standalone test script is at: [`tests/active/test_extract_limit_standalone.py`](tests/active/test_extract_limit_standalone.py) Run with: ```bash docker compose exec sf_api python3 /app/tests/test_extract_limit_standalone.py ```