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:
@@ -172,6 +172,21 @@ class PlanLimitResponse(BaseModel):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PlanLimitWithBillingResponse(PlanLimitResponse):
|
||||
"""PlanLimits + plan_billing fields merged. Billing fields are None when no
|
||||
plan_billing row exists for the plan yet."""
|
||||
display_name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
monthly_price_cents: Optional[int] = None
|
||||
annual_price_cents: Optional[int] = None
|
||||
stripe_product_id: Optional[str] = None
|
||||
stripe_monthly_price_id: Optional[str] = None
|
||||
stripe_annual_price_id: Optional[str] = None
|
||||
is_public: Optional[bool] = None
|
||||
is_archived: Optional[bool] = None
|
||||
sort_order: Optional[int] = None
|
||||
|
||||
|
||||
class PlanLimitUpdate(BaseModel):
|
||||
plan: str
|
||||
max_trees: Optional[int] = None
|
||||
@@ -180,6 +195,19 @@ class PlanLimitUpdate(BaseModel):
|
||||
custom_branding: bool = False
|
||||
priority_support: bool = False
|
||||
export_formats: list = Field(default_factory=lambda: ["markdown", "text"])
|
||||
# plan_billing fields — all optional, partial-update semantics. If any are
|
||||
# set in the body, the admin endpoint upserts the plan_billing row in the
|
||||
# same transaction.
|
||||
display_name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
monthly_price_cents: Optional[int] = None
|
||||
annual_price_cents: Optional[int] = None
|
||||
stripe_product_id: Optional[str] = None
|
||||
stripe_monthly_price_id: Optional[str] = None
|
||||
stripe_annual_price_id: Optional[str] = None
|
||||
is_public: Optional[bool] = None
|
||||
is_archived: Optional[bool] = None
|
||||
sort_order: Optional[int] = None
|
||||
|
||||
|
||||
class AccountOverrideCreate(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user