P0 HOTFIX: Subscription asset_limit calculation fix
Root cause: Old code used rules.get('max_vehicles') which only searched
JSONB root level. Corporate tiers store limits under rules['allowances'].
Changes:
1. Added _extract_limit() helper (3-level search: direct → allowances → limits)
2. Replaced single-sub .limit(1) with multi-sub .all() for true aggregation
3. Fixed fallback path to use _extract_limit() instead of rules.get()
4. Added clamping: asset_limit = max(asset_limit, 1) right before
SubscriptionSummary creation on BOTH code paths
5. Added 'type' column to SubscriptionTier model (base vs addon)
This commit is contained in:
@@ -389,6 +389,64 @@ async def list_organizations(
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# P0 ARCHITECTURE UNIFICATION: _extract_limit() helper
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _extract_limit(
|
||||
rules: dict,
|
||||
keys: list[str],
|
||||
default: int = 0,
|
||||
) -> int:
|
||||
"""
|
||||
P0 ARCHITECTURE UNIFICATION: Egyetlen, robusztus függvény a limit értékek
|
||||
kinyerésére a SubscriptionTier.rules JSONB objektumból.
|
||||
|
||||
Két rétegben keres:
|
||||
1. Közvetlenül a rules objektumban: rules["max_vehicles"]
|
||||
2. A rules["allowances"] nested objektumban: rules["allowances"]["max_vehicles"]
|
||||
|
||||
Ez biztosítja, hogy a JSONB struktúra változásai (pl. 'allowances' nested object)
|
||||
ne törjék meg a limit számításokat.
|
||||
|
||||
Args:
|
||||
rules: A SubscriptionTier.rules JSONB dict-je.
|
||||
keys: A keresendő kulcsok listája (pl. ["max_vehicles", "max_assets"]).
|
||||
Az első találat értéke kerül visszaadásra.
|
||||
default: Alapértelmezett érték, ha egyik kulcs sem található.
|
||||
|
||||
Returns:
|
||||
int: A talált limit érték, vagy a default.
|
||||
"""
|
||||
if not rules or not isinstance(rules, dict):
|
||||
return default
|
||||
|
||||
# 1. szint: Közvetlen keresés a rules objektumban
|
||||
for key in keys:
|
||||
value = rules.get(key)
|
||||
if value is not None and isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
|
||||
# 2. szint: Keresés a rules["allowances"] nested objektumban
|
||||
allowances = rules.get("allowances")
|
||||
if allowances and isinstance(allowances, dict):
|
||||
for key in keys:
|
||||
value = allowances.get(key)
|
||||
if value is not None and isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
|
||||
# 3. szint: Keresés a rules["limits"] nested objektumban (alternatív név)
|
||||
limits = rules.get("limits")
|
||||
if limits and isinstance(limits, dict):
|
||||
for key in keys:
|
||||
value = limits.get(key)
|
||||
if value is not None and isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
|
||||
return default
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /{org_id}/details — Garázs részletes adatai (General Tab)
|
||||
# =============================================================================
|
||||
@@ -438,7 +496,9 @@ async def get_organization_details(
|
||||
detail=f"Szervezet nem található ID-vel: {org_id}",
|
||||
)
|
||||
|
||||
# 2. Aktív előfizetés lekérése (P0 ADD-ON: valid_until >= now() check)
|
||||
# 2. Aktív előfizetések lekérése (P0 ARCHITECTURE UNIFICATION: .all() instead of .limit(1))
|
||||
# Több subscription is lehet (1 Base + N Add-on). Minden aktív sort lekérünk,
|
||||
# majd aggregáljuk a limit értékeket.
|
||||
now = datetime.utcnow()
|
||||
sub_stmt = (
|
||||
select(OrganizationSubscription)
|
||||
@@ -449,10 +509,9 @@ async def get_organization_details(
|
||||
OrganizationSubscription.valid_until >= now,
|
||||
)
|
||||
.order_by(OrganizationSubscription.id.desc())
|
||||
.limit(1)
|
||||
)
|
||||
sub_result = await db.execute(sub_stmt)
|
||||
active_sub = sub_result.scalar_one_or_none()
|
||||
active_subs = sub_result.scalars().all()
|
||||
|
||||
# 2b. P0 ADD-ON: Valós járműszám lekérése a fleet táblából
|
||||
from app.models.fleet.vehicle import Vehicle # noqa: E402
|
||||
@@ -512,50 +571,118 @@ async def get_organization_details(
|
||||
contact_result = await db.execute(contact_stmt)
|
||||
contact = contact_result.scalar_one_or_none()
|
||||
|
||||
# 5. Subscription összefoglaló — P0 ADD-ON UPGRADE
|
||||
# Fix: explicit dict.get() instead of `or` (bugfix: 0 is falsy in Python)
|
||||
# Add: extra_allowances math (Base + Extra)
|
||||
# 5. Subscription összefoglaló — P0 ARCHITECTURE UNIFICATION
|
||||
# Unified limit extraction via _extract_limit() helper.
|
||||
# Multi-subscription aggregation: Base + Add-on tiers.
|
||||
# Path A and Path B are now ONE unified code path.
|
||||
subscription_summary = None
|
||||
if active_sub and active_sub.tier:
|
||||
rules = active_sub.tier.rules or {}
|
||||
fc = active_sub.tier.feature_capabilities or {}
|
||||
extra = active_sub.extra_allowances or {}
|
||||
if active_subs:
|
||||
# ── P0 ARCHITECTURE UNIFICATION: Aggregate across ALL active subscriptions ──
|
||||
# Separate Base subscriptions from Add-on subscriptions
|
||||
base_subs = [s for s in active_subs if s.tier and s.tier.type in (None, '', 'base')]
|
||||
addon_subs = [s for s in active_subs if s.tier and s.tier.type == 'addon']
|
||||
|
||||
# Base limits from tier rules (with explicit .get() — no `or` bug)
|
||||
base_vehicles = rules.get("max_vehicles", fc.get("max_vehicles", 1))
|
||||
base_branches = rules.get("max_branches", fc.get("max_branches", 0))
|
||||
base_users = rules.get("max_users", fc.get("max_users", 0))
|
||||
|
||||
# Extra allowances (add-ons)
|
||||
extra_vehicles = extra.get("extra_vehicles", 0) or 0
|
||||
extra_branches = extra.get("extra_branches", 0) or 0
|
||||
extra_users = extra.get("extra_users", 0) or 0
|
||||
|
||||
# Total = Base + Extra
|
||||
asset_limit = base_vehicles + extra_vehicles
|
||||
branch_limit = base_branches + extra_branches
|
||||
user_limit = base_users + extra_users
|
||||
|
||||
subscription_summary = SubscriptionSummary(
|
||||
tier_name=active_sub.tier.name,
|
||||
tier_level=active_sub.tier.tier_level,
|
||||
valid_from=active_sub.valid_from.isoformat() if active_sub.valid_from else None,
|
||||
expires_at=active_sub.valid_until.isoformat() if active_sub.valid_until else None,
|
||||
is_active=active_sub.is_active,
|
||||
asset_count=asset_count,
|
||||
asset_limit=asset_limit,
|
||||
branch_limit=branch_limit,
|
||||
user_limit=user_limit,
|
||||
extra_allowances=extra,
|
||||
# Use the highest-tier Base subscription for tier metadata
|
||||
# (sort by tier_level descending, pick the first)
|
||||
base_subs_sorted = sorted(
|
||||
base_subs,
|
||||
key=lambda s: s.tier.tier_level if s.tier else 0,
|
||||
reverse=True,
|
||||
)
|
||||
primary_sub = base_subs_sorted[0] if base_subs_sorted else (active_subs[0] if active_subs else None)
|
||||
|
||||
if primary_sub and primary_sub.tier:
|
||||
# ── Base limits from primary subscription (using _extract_limit) ──
|
||||
primary_rules = primary_sub.tier.rules or {}
|
||||
primary_fc = primary_sub.tier.feature_capabilities or {}
|
||||
|
||||
# vehicle_limit_keys: search in rules, then allowances, then feature_capabilities
|
||||
base_vehicles = _extract_limit(
|
||||
primary_rules,
|
||||
["max_vehicles", "max_assets"],
|
||||
_extract_limit(primary_fc, ["max_vehicles", "max_assets"], 1),
|
||||
)
|
||||
base_branches = _extract_limit(
|
||||
primary_rules,
|
||||
["max_branches", "max_garages"],
|
||||
_extract_limit(primary_fc, ["max_branches", "max_garages"], 0),
|
||||
)
|
||||
base_users = _extract_limit(
|
||||
primary_rules,
|
||||
["max_users", "max_members"],
|
||||
_extract_limit(primary_fc, ["max_users", "max_members"], 0),
|
||||
)
|
||||
|
||||
# ── Aggregate extra_allowances from ALL active subscriptions ──
|
||||
total_extra_vehicles = 0
|
||||
total_extra_branches = 0
|
||||
total_extra_users = 0
|
||||
aggregated_extra = {}
|
||||
|
||||
for sub in active_subs:
|
||||
extra = sub.extra_allowances or {}
|
||||
total_extra_vehicles += extra.get("extra_vehicles", 0) or 0
|
||||
total_extra_branches += extra.get("extra_branches", 0) or 0
|
||||
total_extra_users += extra.get("extra_users", 0) or 0
|
||||
# Merge extra_allowances dicts (later subs overwrite earlier ones for same keys)
|
||||
aggregated_extra.update(extra)
|
||||
|
||||
# ── Also aggregate add-on tier base limits ──
|
||||
for addon_sub in addon_subs:
|
||||
if addon_sub.tier:
|
||||
addon_rules = addon_sub.tier.rules or {}
|
||||
addon_fc = addon_sub.tier.feature_capabilities or {}
|
||||
base_vehicles += _extract_limit(
|
||||
addon_rules, ["max_vehicles", "max_assets"],
|
||||
_extract_limit(addon_fc, ["max_vehicles", "max_assets"], 0),
|
||||
)
|
||||
base_branches += _extract_limit(
|
||||
addon_rules, ["max_branches", "max_garages"],
|
||||
_extract_limit(addon_fc, ["max_branches", "max_garages"], 0),
|
||||
)
|
||||
base_users += _extract_limit(
|
||||
addon_rules, ["max_users", "max_members"],
|
||||
_extract_limit(addon_fc, ["max_users", "max_members"], 0),
|
||||
)
|
||||
|
||||
# Total = Base (primary + addon) + Extra (from all subscriptions)
|
||||
asset_limit = base_vehicles + total_extra_vehicles
|
||||
branch_limit = base_branches + total_extra_branches
|
||||
user_limit = base_users + total_extra_users
|
||||
|
||||
# P0 HOTFIX: Clamping — ensure asset_limit >= 1 even if all sums are 0
|
||||
asset_limit = max(asset_limit, 1)
|
||||
subscription_summary = SubscriptionSummary(
|
||||
tier_name=primary_sub.tier.name,
|
||||
tier_level=primary_sub.tier.tier_level,
|
||||
valid_from=primary_sub.valid_from.isoformat() if primary_sub.valid_from else None,
|
||||
expires_at=primary_sub.valid_until.isoformat() if primary_sub.valid_until else None,
|
||||
is_active=primary_sub.is_active,
|
||||
asset_count=asset_count,
|
||||
asset_limit=asset_limit,
|
||||
branch_limit=branch_limit,
|
||||
user_limit=user_limit,
|
||||
extra_allowances=aggregated_extra,
|
||||
)
|
||||
elif org.subscription_tier:
|
||||
# Fallback: Organization.subscription_tier_id kapcsolat (nincs OrganizationSubscription)
|
||||
rules = org.subscription_tier.rules or {}
|
||||
fc = org.subscription_tier.feature_capabilities or {}
|
||||
extra = {}
|
||||
|
||||
base_vehicles = rules.get("max_vehicles", fc.get("max_vehicles", 1))
|
||||
base_branches = rules.get("max_branches", fc.get("max_branches", 0))
|
||||
base_users = rules.get("max_users", fc.get("max_users", 0))
|
||||
base_vehicles = _extract_limit(
|
||||
rules, ["max_vehicles", "max_assets"],
|
||||
_extract_limit(fc, ["max_vehicles", "max_assets"], 1),
|
||||
)
|
||||
base_branches = _extract_limit(
|
||||
rules, ["max_branches", "max_garages"],
|
||||
_extract_limit(fc, ["max_branches", "max_garages"], 0),
|
||||
)
|
||||
base_users = _extract_limit(
|
||||
rules, ["max_users", "max_members"],
|
||||
_extract_limit(fc, ["max_users", "max_members"], 0),
|
||||
)
|
||||
# P0 HOTFIX: Clamping — ensure asset_limit >= 1 even for fallback path
|
||||
base_vehicles = max(base_vehicles, 1)
|
||||
|
||||
subscription_summary = SubscriptionSummary(
|
||||
tier_name=org.subscription_tier.name,
|
||||
|
||||
Reference in New Issue
Block a user