pénzügyi modul továbbfejlesztése (csomagkezelés)

This commit is contained in:
Roo
2026-07-29 09:46:10 +00:00
parent 6f28d3e70d
commit 75904cd2f8
50 changed files with 5851 additions and 312 deletions

View File

@@ -139,28 +139,61 @@ async def get_public_subscriptions(
**Szabály:**
- Csak azokat a csomagokat adja vissza, ahol a `rules.lifecycle.is_public` = true
(JSONB mező alapján szűrve). Ha a mező hiányzik, alapértelmezés szerint publikus.
- Defense-in-Depth: A `rules.type` mező alapján szűrjük a csomagokat a felhasználó
aktuális kontextusa szerint. Ha a user aktív szervezete `individual` típusú (vagy
nincs aktív szervezet), csak `private`/`consumer` típusú csomagokat adunk vissza.
Ha a szervezet business típusú, csak `corporate`/`business` típusú csomagokat.
- Minden csomaghoz tartozik egy `resolved_pricing` mező, amely a felhasználó
országkódja alapján feloldott árazást tartalmazza.
- A frontend tovább szűrhet a `name` mező alapján (private_ vs corp_ előtag).
"""
# 1. Determine user's country code
country_code = await get_user_country_code(current_user, db)
# 2. Fetch all public tiers
# 2. Determine if user is in corporate context (defense-in-depth)
active_org_id = getattr(current_user, "active_organization_id", None)
is_corporate = False
if active_org_id:
org_type_stmt = select(Organization.org_type).where(
Organization.id == active_org_id,
Organization.is_deleted == False,
)
org_type_result = await db.execute(org_type_stmt)
org_type = org_type_result.scalar_one_or_none()
if org_type and org_type != "individual":
is_corporate = True
# 3. Fetch all public tiers (filter out non-public at SQL level)
stmt = (
select(SubscriptionTier)
.where(
# Only show tiers where lifecycle.is_public is NOT explicitly 'false'
# Using SQLAlchemy JSONB path query (same approach as admin_packages.py line 134)
~SubscriptionTier.rules["lifecycle"]["is_public"].as_string().in_(["false"])
)
.order_by(SubscriptionTier.id)
)
result = await db.execute(stmt)
tiers = result.scalars().all()
# 3. Build response with resolved pricing
# 4. Build response with resolved pricing and type filtering
response: List[PublicSubscriptionTierResponse] = []
for t in tiers:
rules = t.rules or {}
if not rules.get("lifecycle", {}).get("is_public", True):
continue
# Defense-in-Depth: Filter by target audience type
rules_type = rules.get("type", None)
if rules_type:
if is_corporate:
# In corporate context: only show corporate/business packages
if rules_type not in ("corporate", "business"):
continue
else:
# In private context: only show private/consumer packages
if rules_type in ("corporate", "business"):
continue
# Resolve pricing for this user's country
resolved = resolve_pricing(rules, country_code)
resolved_pricing = None
@@ -253,12 +286,57 @@ async def get_my_subscription(
user_tier = await SubscriptionService.get_user_tier(db, user_id)
feature_flags = await SubscriptionService.get_user_feature_flags(db, user_id)
# ── P0 PENDING DOWNGRADE FIELDS (Issue #429) ──
# Check if there's a pending downgrade on the current subscription
has_pending_downgrade = False
pending_tier_name = None
pending_effective_date = None
if subscription_data:
pending_sub = None
if source == "organization" and active_org_id:
pending_stmt = select(OrganizationSubscription).options(
selectinload(OrganizationSubscription.pending_tier)
).where(
OrganizationSubscription.org_id == active_org_id,
OrganizationSubscription.is_active == True,
OrganizationSubscription.pending_tier_id.is_not(None),
).order_by(OrganizationSubscription.id.desc()).limit(1)
pending_result = await db.execute(pending_stmt)
pending_sub = pending_result.scalar_one_or_none()
elif source == "user":
pending_stmt = select(UserSubscription).options(
selectinload(UserSubscription.pending_tier)
).where(
UserSubscription.user_id == user_id,
UserSubscription.is_active == True,
UserSubscription.pending_tier_id.is_not(None),
).order_by(UserSubscription.id.desc()).limit(1)
pending_result = await db.execute(pending_stmt)
pending_sub = pending_result.scalar_one_or_none()
if pending_sub and pending_sub.pending_tier:
has_pending_downgrade = True
pt = pending_sub.pending_tier
pending_tier_name = (
pt.rules.get("display_name", pt.name)
if pt.rules else pt.name
)
pending_effective_date = (
pending_sub.valid_until.isoformat()
if pending_sub.valid_until else None
)
return {
"subscription": subscription_data,
"tier": user_tier,
"features": feature_flags.get("features", {}),
"expires_at": feature_flags.get("expires_at"),
"source": source,
# ── P0 Pending downgrade fields ──
"has_pending_downgrade": has_pending_downgrade,
"pending_tier_name": pending_tier_name,
"pending_effective_date": pending_effective_date,
}