fix(billing): complete billing module — fix tier change, platform support, merchant portal

- Fix admin tier change: resolve tier_code→tier_id in update_subscription(),
  delegate to billing_service.change_tier() for Stripe-connected subs
- Add platform support to admin tiers page: platform column, filter dropdown,
  platform selector in create/edit modal, platform_name in tier API response
- Filter used platforms in create subscription modal on merchant detail page
- Enrich merchant portal API responses with tier code, tier_name, platform_name
- Add eager-load of platform relationship in get_merchant_subscription()
- Remove stale store_name/store_code references from merchant templates
- Add merchant tier change endpoint (POST /change-tier) and tier selector UI
  replacing broken requestUpgrade() button
- Fix subscription detail link to use platform_id instead of sub.id

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 20:49:48 +01:00
parent 0b37274140
commit d1fe3584ff
54 changed files with 222 additions and 52 deletions

View File

@@ -59,8 +59,17 @@ def list_subscription_tiers(
"""List all subscription tiers."""
tiers = admin_subscription_service.get_tiers(db, include_inactive=include_inactive, platform_id=platform_id)
from app.modules.tenancy.models import Platform
platforms_map = {p.id: p.name for p in db.query(Platform).all()}
tiers_response = []
for t in tiers:
resp = SubscriptionTierResponse.model_validate(t)
resp.platform_name = platforms_map.get(t.platform_id) if t.platform_id else None
tiers_response.append(resp)
return SubscriptionTierListResponse(
tiers=[SubscriptionTierResponse.model_validate(t) for t in tiers],
tiers=tiers_response,
total=len(tiers),
)

View File

@@ -19,6 +19,7 @@ registration under /api/v1/merchants/billing/*).
import logging
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.api.deps import get_current_merchant_from_cookie_or_header
@@ -97,13 +98,15 @@ def list_merchant_subscriptions(
merchant = _get_user_merchant(db, current_user)
subscriptions = subscription_service.get_merchant_subscriptions(db, merchant.id)
return {
"subscriptions": [
MerchantSubscriptionResponse.model_validate(sub)
for sub in subscriptions
],
"total": len(subscriptions),
}
items = []
for sub in subscriptions:
data = MerchantSubscriptionResponse.model_validate(sub).model_dump()
data["tier"] = sub.tier.code if sub.tier else None
data["tier_name"] = sub.tier.name if sub.tier else None
data["platform_name"] = sub.platform.name if sub.platform else ""
items.append(data)
return {"subscriptions": items, "total": len(items)}
@router.get("/subscriptions/{platform_id}")
@@ -129,6 +132,11 @@ def get_merchant_subscription(
detail=f"No subscription found for platform {platform_id}",
)
sub_data = MerchantSubscriptionResponse.model_validate(subscription).model_dump()
sub_data["tier"] = subscription.tier.code if subscription.tier else None
sub_data["tier_name"] = subscription.tier.name if subscription.tier else None
sub_data["platform_name"] = subscription.platform.name if subscription.platform else ""
tier_info = None
if subscription.tier:
tier = subscription.tier
@@ -142,7 +150,7 @@ def get_merchant_subscription(
)
return {
"subscription": MerchantSubscriptionResponse.model_validate(subscription),
"subscription": sub_data,
"tier": tier_info,
}
@@ -180,6 +188,40 @@ def get_available_tiers(
}
class ChangeTierRequest(BaseModel):
"""Request for changing subscription tier."""
tier_code: str
is_annual: bool = False
@router.post("/subscriptions/{platform_id}/change-tier")
def change_subscription_tier(
request: Request,
tier_data: ChangeTierRequest,
platform_id: int = Path(..., description="Platform ID"),
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
db: Session = Depends(get_db),
):
"""
Change the subscription tier for a specific platform.
Handles both Stripe-connected and non-Stripe subscriptions.
"""
merchant = _get_user_merchant(db, current_user)
result = billing_service.change_tier(
db, merchant.id, platform_id, tier_data.tier_code, tier_data.is_annual
)
db.commit()
logger.info(
f"Merchant {merchant.id} ({merchant.name}) changed tier to "
f"{tier_data.tier_code} on platform={platform_id}"
)
return result
@router.post(
"/subscriptions/{platform_id}/checkout",
response_model=CheckoutResponse,