pénzügyi modul továbbfejlesztése (csomagkezelés)
This commit is contained in:
@@ -42,6 +42,155 @@ class NetworkResponse(BaseModel):
|
||||
level3: List[NetworkMemberL2L3]
|
||||
|
||||
|
||||
async def _resolve_subscription_data(
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
active_org_id: Optional[int] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
P0 BUGFIX (2026-07-28): Unified subscription resolution shared by /auth/me and /users/me.
|
||||
|
||||
Resolves the user's real subscription limits (max_vehicles, max_garages),
|
||||
display_name, expiry, auto-renewal fields, and active add-ons from the
|
||||
assigned subscription_tier JSONB rules.
|
||||
|
||||
Resolution order:
|
||||
1. Org-level subscription (OrganizationSubscription) if active_org_id is set
|
||||
2. User-level subscription (UserSubscription) fallback
|
||||
3. Organization.base_asset_limit as ultimate fallback
|
||||
|
||||
Returns a dict with all subscription-related fields ready to merge into
|
||||
the user response.
|
||||
"""
|
||||
from app.models.core_logic import SubscriptionTier, OrganizationSubscription, UserSubscription
|
||||
from app.models.marketplace.organization import Organization
|
||||
|
||||
tier: Optional[SubscriptionTier] = None
|
||||
subscription_record = None
|
||||
|
||||
# Step 1: Try org-level subscription first
|
||||
if active_org_id is not None:
|
||||
org_sub_stmt = (
|
||||
select(OrganizationSubscription)
|
||||
.where(
|
||||
OrganizationSubscription.org_id == active_org_id,
|
||||
OrganizationSubscription.is_active == True,
|
||||
)
|
||||
.order_by(OrganizationSubscription.valid_from.desc())
|
||||
.limit(1)
|
||||
)
|
||||
subscription_record = (await db.execute(org_sub_stmt)).scalar_one_or_none()
|
||||
if subscription_record and subscription_record.tier_id:
|
||||
tier = await db.get(SubscriptionTier, subscription_record.tier_id)
|
||||
|
||||
# Step 2: Fall back to user-level subscription (runs even when active_org_id is set)
|
||||
if subscription_record is None:
|
||||
user_sub_stmt = (
|
||||
select(UserSubscription)
|
||||
.where(
|
||||
UserSubscription.user_id == user.id,
|
||||
UserSubscription.is_active == True,
|
||||
)
|
||||
.order_by(UserSubscription.valid_from.desc())
|
||||
.limit(1)
|
||||
)
|
||||
subscription_record = (await db.execute(user_sub_stmt)).scalar_one_or_none()
|
||||
if subscription_record and subscription_record.tier_id:
|
||||
tier = await db.get(SubscriptionTier, subscription_record.tier_id)
|
||||
|
||||
# Step 3: Extract limits from the resolved tier or fall back
|
||||
max_vehicles = 1
|
||||
max_garages = 1
|
||||
if tier and tier.rules:
|
||||
allowances = tier.rules.get("allowances", {})
|
||||
max_vehicles = int(allowances.get("max_vehicles", 1))
|
||||
max_garages = int(allowances.get("max_garages", 1))
|
||||
elif active_org_id is not None:
|
||||
org_stmt = select(Organization).where(Organization.id == active_org_id)
|
||||
org = (await db.execute(org_stmt)).scalar_one_or_none()
|
||||
if org:
|
||||
max_vehicles = org.base_asset_limit or 1
|
||||
max_garages = 1
|
||||
|
||||
# Step 4: Extract metadata from the resolved subscription record
|
||||
subscription_display_name: Optional[str] = None
|
||||
subscription_valid_until: Optional[datetime] = None
|
||||
subscription_tier_id: Optional[int] = None
|
||||
subscription_valid_from: Optional[datetime] = None
|
||||
subscription_auto_renew: bool = False
|
||||
subscription_next_renewal_date: Optional[datetime] = None
|
||||
subscription_wallet_auto_deduct: bool = False
|
||||
|
||||
if subscription_record is not None:
|
||||
subscription_valid_until = subscription_record.valid_until
|
||||
subscription_tier_id = subscription_record.tier_id
|
||||
subscription_valid_from = subscription_record.valid_from
|
||||
subscription_auto_renew = getattr(subscription_record, 'auto_renew', False)
|
||||
subscription_next_renewal_date = getattr(subscription_record, 'next_renewal_date', None)
|
||||
subscription_wallet_auto_deduct = getattr(subscription_record, 'wallet_auto_deduct', False)
|
||||
|
||||
# Resolve display_name from the resolved tier
|
||||
if tier and tier.rules:
|
||||
subscription_display_name = tier.rules.get("display_name", None)
|
||||
|
||||
# Step 5: Resolve active add-on subscriptions
|
||||
active_addons: List[Dict[str, Any]] = []
|
||||
if active_org_id is not None:
|
||||
addon_stmt = (
|
||||
select(OrganizationSubscription, SubscriptionTier)
|
||||
.join(SubscriptionTier, OrganizationSubscription.tier_id == SubscriptionTier.id)
|
||||
.where(
|
||||
OrganizationSubscription.org_id == active_org_id,
|
||||
OrganizationSubscription.is_active == True,
|
||||
SubscriptionTier.type == 'addon',
|
||||
)
|
||||
.order_by(OrganizationSubscription.valid_from.desc())
|
||||
)
|
||||
addon_result = await db.execute(addon_stmt)
|
||||
for sub, tier_row in addon_result:
|
||||
active_addons.append({
|
||||
"tier_id": tier_row.id,
|
||||
"display_name": tier_row.rules.get("display_name", tier_row.name) if tier_row.rules else tier_row.name,
|
||||
"valid_from": sub.valid_from.isoformat() if sub.valid_from else None,
|
||||
"valid_until": sub.valid_until.isoformat() if sub.valid_until else None,
|
||||
"is_active": sub.is_active,
|
||||
})
|
||||
else:
|
||||
addon_stmt = (
|
||||
select(UserSubscription, SubscriptionTier)
|
||||
.join(SubscriptionTier, UserSubscription.tier_id == SubscriptionTier.id)
|
||||
.where(
|
||||
UserSubscription.user_id == user.id,
|
||||
UserSubscription.is_active == True,
|
||||
SubscriptionTier.type == 'addon',
|
||||
)
|
||||
.order_by(UserSubscription.valid_from.desc())
|
||||
)
|
||||
addon_result = await db.execute(addon_stmt)
|
||||
for sub, tier_row in addon_result:
|
||||
active_addons.append({
|
||||
"tier_id": tier_row.id,
|
||||
"display_name": tier_row.rules.get("display_name", tier_row.name) if tier_row.rules else tier_row.name,
|
||||
"valid_from": sub.valid_from.isoformat() if sub.valid_from else None,
|
||||
"valid_until": sub.valid_until.isoformat() if sub.valid_until else None,
|
||||
"is_active": sub.is_active,
|
||||
})
|
||||
|
||||
return {
|
||||
"max_vehicles": max_vehicles,
|
||||
"max_garages": max_garages,
|
||||
"subscription_display_name": subscription_display_name,
|
||||
"subscription_expires_at": subscription_valid_until.isoformat() if subscription_valid_until else None,
|
||||
"subscription_tier_id": subscription_tier_id,
|
||||
"subscription_valid_from": subscription_valid_from.isoformat() if subscription_valid_from else None,
|
||||
"subscription_auto_renew": subscription_auto_renew,
|
||||
"subscription_next_renewal_date": subscription_next_renewal_date.isoformat() if subscription_next_renewal_date else None,
|
||||
"subscription_wallet_auto_deduct": subscription_wallet_auto_deduct,
|
||||
"active_addons": active_addons,
|
||||
"subscription_plan": user.subscription_plan,
|
||||
}
|
||||
|
||||
|
||||
async def _build_user_response(user: User, active_org_id: Optional[int] = None, db: Optional[AsyncSession] = None) -> dict:
|
||||
"""
|
||||
Segédfüggvény a UserResponse dict előállításához.
|
||||
@@ -113,10 +262,26 @@ async def _build_user_response(user: User, active_org_id: Optional[int] = None,
|
||||
if db is not None:
|
||||
pass
|
||||
|
||||
# ── P0: Resolve subscription limits ──
|
||||
# Default fallback values
|
||||
max_vehicles = 1
|
||||
max_garages = 1
|
||||
# ── P0: Resolve subscription limits via shared function ──
|
||||
# P0 BUGFIX (2026-07-28): Previously hardcoded max_vehicles=1 / max_garages=1.
|
||||
# Now uses _resolve_subscription_data() which queries the actual subscription_tier
|
||||
# JSONB rules. This fixes both /auth/me and /users/me endpoints in one place.
|
||||
if db is not None:
|
||||
sub_data = await _resolve_subscription_data(db, user, active_org_id)
|
||||
else:
|
||||
sub_data = {
|
||||
"max_vehicles": 1,
|
||||
"max_garages": 1,
|
||||
"subscription_display_name": None,
|
||||
"subscription_expires_at": None,
|
||||
"subscription_tier_id": None,
|
||||
"subscription_valid_from": None,
|
||||
"subscription_auto_renew": False,
|
||||
"subscription_next_renewal_date": None,
|
||||
"subscription_wallet_auto_deduct": False,
|
||||
"active_addons": [],
|
||||
"subscription_plan": user.subscription_plan,
|
||||
}
|
||||
|
||||
return {
|
||||
"id": user.id,
|
||||
@@ -127,9 +292,19 @@ async def _build_user_response(user: User, active_org_id: Optional[int] = None,
|
||||
"region_code": user.region_code,
|
||||
"person_id": user.person_id,
|
||||
"role": role_key,
|
||||
"subscription_plan": user.subscription_plan,
|
||||
"max_vehicles": max_vehicles,
|
||||
"max_garages": max_garages,
|
||||
"subscription_plan": sub_data["subscription_plan"],
|
||||
"max_vehicles": sub_data["max_vehicles"],
|
||||
"max_garages": sub_data["max_garages"],
|
||||
"subscription_display_name": sub_data["subscription_display_name"],
|
||||
"subscription_expires_at": sub_data["subscription_expires_at"],
|
||||
"subscription_tier_id": sub_data["subscription_tier_id"],
|
||||
"subscription_valid_from": sub_data["subscription_valid_from"],
|
||||
"subscription_auto_renew": sub_data["subscription_auto_renew"],
|
||||
"subscription_next_renewal_date": sub_data["subscription_next_renewal_date"],
|
||||
"subscription_wallet_auto_deduct": sub_data["subscription_wallet_auto_deduct"],
|
||||
"active_addons": sub_data["active_addons"],
|
||||
"user_registration_date": user.created_at.isoformat() if user.created_at else None,
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None,
|
||||
"scope_level": user.scope_level or "individual",
|
||||
"scope_id": str(active_org_id) if active_org_id else None,
|
||||
"ui_mode": user.ui_mode or "personal",
|
||||
@@ -204,60 +379,21 @@ async def read_users_me(
|
||||
is_last_admin = await AuthService.check_is_last_admin(db, current_user.id)
|
||||
|
||||
# ── P0: Resolve real subscription limits from the assigned tier ──
|
||||
from app.models.core_logic import SubscriptionTier, OrganizationSubscription, UserSubscription
|
||||
max_vehicles = 1
|
||||
max_garages = 1
|
||||
|
||||
if active_org_id is not None:
|
||||
# Try org-level subscription first
|
||||
org_sub_stmt = (
|
||||
select(SubscriptionTier)
|
||||
.select_from(OrganizationSubscription)
|
||||
.join(SubscriptionTier, OrganizationSubscription.tier_id == SubscriptionTier.id)
|
||||
.where(
|
||||
OrganizationSubscription.org_id == active_org_id,
|
||||
OrganizationSubscription.is_active == True
|
||||
)
|
||||
.order_by(OrganizationSubscription.valid_from.desc())
|
||||
.limit(1)
|
||||
)
|
||||
tier = (await db.execute(org_sub_stmt)).scalar_one_or_none()
|
||||
if tier and tier.rules:
|
||||
allowances = tier.rules.get("allowances", {})
|
||||
max_vehicles = int(allowances.get("max_vehicles", 1))
|
||||
max_garages = int(allowances.get("max_garages", 1))
|
||||
else:
|
||||
# Fallback: read base_asset_limit from Organization record
|
||||
org_stmt = select(Organization).where(Organization.id == active_org_id)
|
||||
org = (await db.execute(org_stmt)).scalar_one_or_none()
|
||||
if org:
|
||||
max_vehicles = org.base_asset_limit or 1
|
||||
max_garages = 1 # No base_garage_limit column, default to 1
|
||||
else:
|
||||
# Personal mode: try user-level subscription
|
||||
user_sub_stmt = (
|
||||
select(SubscriptionTier)
|
||||
.select_from(UserSubscription)
|
||||
.join(SubscriptionTier, UserSubscription.tier_id == SubscriptionTier.id)
|
||||
.where(
|
||||
UserSubscription.user_id == current_user.id,
|
||||
UserSubscription.is_active == True
|
||||
)
|
||||
.order_by(UserSubscription.valid_from.desc())
|
||||
.limit(1)
|
||||
)
|
||||
tier = (await db.execute(user_sub_stmt)).scalar_one_or_none()
|
||||
if tier and tier.rules:
|
||||
allowances = tier.rules.get("allowances", {})
|
||||
max_vehicles = int(allowances.get("max_vehicles", 1))
|
||||
max_garages = int(allowances.get("max_garages", 1))
|
||||
# P0 BUGFIX (2026-07-28): Uses the shared _resolve_subscription_data() function
|
||||
# instead of duplicating the subscription resolution logic inline.
|
||||
# This fixes both /auth/me and /users/me in one place.
|
||||
#
|
||||
# _build_user_response() already calls _resolve_subscription_data() internally,
|
||||
# so _build_user_response now returns all subscription fields (max_vehicles,
|
||||
# subscription_display_name, etc.) in the base response. We just need to
|
||||
# override is_last_admin and org_capabilities post-build.
|
||||
sub_data = await _resolve_subscription_data(db, current_user, active_org_id)
|
||||
|
||||
# ── RBAC Phase 3: Build base response ──
|
||||
# P0 Phase 6: Pass db so system_capabilities gets populated from DB
|
||||
response_data = await _build_user_response(current_user, active_org_id, db)
|
||||
response_data["is_last_admin"] = is_last_admin
|
||||
response_data["max_vehicles"] = max_vehicles
|
||||
response_data["max_garages"] = max_garages
|
||||
# All subscription fields are already in response_data from _build_user_response
|
||||
|
||||
# ── RBAC Phase 3: Resolve org_capabilities ──
|
||||
# Get all organizations the user is a member of
|
||||
|
||||
Reference in New Issue
Block a user