feat(admin): extend /admin/plan-limits to manage plan_billing fields
Task 30 of self-serve signup Phase 2. Super-admins can now manage Stripe IDs, display names, prices, and public/archived flags via the existing admin plan-limits endpoints. - GET /admin/plan-limits now outer-joins plan_billing and returns merged PlanLimitWithBillingResponse rows. Plans without a plan_billing row return None for the billing fields. - PUT /admin/plan-limits accepts the new optional billing fields and upserts plan_billing in the same transaction. If no plan_billing row exists for the plan and the body includes any billing field, a row is created (display_name defaults to plan.capitalize() when omitted; display_name is never NULLed out on an existing row). - After commit, the handler queries account_ids on the affected plan and calls BillingService.invalidate_billing_cache(account_ids). This is a no-op stub today (logs only) — there's no in-process billing cache yet. TODO comment marks the wire-up point. - 3 new integration tests cover GET-with-billing-present, PUT creating a plan_billing row, and the invalidation hook being awaited with a list of account_ids. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""Single billing service module. Stripe is the only impl — no provider
|
||||
abstraction. Account row is canonical local state; Stripe is canonical
|
||||
remote state; the webhook handler bridges the two."""
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
import stripe
|
||||
@@ -17,8 +18,32 @@ from app.models.subscription import Subscription
|
||||
|
||||
TRIAL_DAYS = 14
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BillingService:
|
||||
@staticmethod
|
||||
async def invalidate_billing_cache(account_ids) -> None:
|
||||
"""No-op stub for future in-process billing cache invalidation.
|
||||
|
||||
Today there is no `app.state.billing_cache` — `BillingService.get_billing_state`
|
||||
always reads fresh from the DB. Call sites that mutate plan/feature data
|
||||
invoke this hook so that wiring is in place when an in-process cache is
|
||||
added later. Until then, this just logs.
|
||||
|
||||
TODO: when an in-process billing cache (e.g. `app.state.billing_cache`)
|
||||
is introduced, evict entries for the given account_ids here.
|
||||
"""
|
||||
try:
|
||||
count = len(list(account_ids))
|
||||
except TypeError:
|
||||
count = -1
|
||||
logger.debug(
|
||||
"BillingService.invalidate_billing_cache called for %d account(s) "
|
||||
"(no-op stub — wire to app.state.billing_cache when added)",
|
||||
count,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def start_trial(db: AsyncSession, account_id) -> Subscription:
|
||||
"""Idempotent. Creates a trialing Subscription on Pro for the account if
|
||||
|
||||
Reference in New Issue
Block a user