admin_users_personels

This commit is contained in:
Roo
2026-06-29 14:11:15 +00:00
parent 383e5511b6
commit df4a0f07ff
237 changed files with 17849 additions and 2934 deletions

View File

@@ -0,0 +1,189 @@
# 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 = <problematic_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
```