Saltar a contenido

11 — Plan Policy Engine

Qué hace

backend_django/api/services/plan_policy_engine.py es el módulo único que decide:

Dado un bot y la acción que quiere hacer, ¿el plan al que está suscrito lo permite o no?

Es el corazón del SaaS. Cualquier acción del cliente que toque un recurso "limitado por plan" pasa por aquí.

Limits actuales del Help Center Builder

Los limits viven en plans.features.limits (jsonb, editable desde /api/sa/plans/):

Limit Free Starter Pro
faqs_per_language 20 100 500
languages (idiomas activos) 1 2 5
branding (mostrar "Powered by") true true false

Estos números son los defaults del seed. Edítalos en plans.features cuando quieras vender otra estructura comercial — no requiere redeploy.

Forma canónica del jsonb:

{
  "limits": {
    "faqs_per_language": 100,
    "languages": 2,
    "branding": true
  }
}

API del policy engine

evaluate_quota(bot, resource, current=None)

Devuelve un QuotaSummary:

QuotaSummary(
    limit=100,           # tope del plan
    used=87,             # consumo actual
    remaining=13,        # cuánto queda
    over=False,          # ¿está sobre el limit?
    blocked=False        # ¿debe bloquear nuevas creaciones?
)

Recursos soportados:

  • 'faqs' — usa bot_id + idioma actual del request
  • 'languages' — cuenta idiomas con al menos 1 FAQ activa
  • 'branding' — devuelve si debe ocultar el "Powered by"

enforce_quota(bot, resource, request_increment=1) (alias enforce_limit)

Levanta PlanLimitExceeded (HTTP 403) si la acción viola el limit.

from api.services.plan_policy_engine import enforce_quota, PlanLimitExceeded

try:
    enforce_quota(bot, 'faqs', request_increment=1)
except PlanLimitExceeded as e:
    return Response(
        {"error": "PLAN_LIMIT_EXCEEDED", "detail": str(e), "quota": e.summary},
        status=403
    )

Hard vs soft limits

Decisión de producto: todos los limits son hard (rechazan con 403). No hay warnings tipo "estás cerca del limit". Si el usuario quiere más, tiene que upgrade.

Razones:

  • Coherencia comercial — todo o nada.
  • Simplicidad — no hay que limpiar marcadores cuando el limit se reduce por downgrade.
  • UX clara para el comprador del SaaS — "100 FAQs en Starter" es no-negociable.

Si quieres soft limits, edita enforce_quota() para devolver el QuotaSummary sin levantar excepción y deja que cada view decida.

Integración con las views

Los puntos donde el policy engine se invoca:

Acción del usuario View Recurso
Crear FAQ views_faqs_mixin.bot_faqs(POST) faqs
Importar batch de FAQs views.DashboardViewSet.bot_faqs_import faqs (n incremento)
Activar nuevo idioma views_config_mixin.bot_config(PATCH) languages
Crear bot views_bots_mixin.bots(POST) (no aplica — el limit de bots vive en bot_user_permissions, no aquí)

Cómo afecta el frontend

El frontend anticipa los limits para no mostrar botones que el backend va a rechazar:

  1. Carga el plan + summary llamando a GET /api/dashboard/bots/<bot>/stats/.
  2. Si summary.faqs.remaining === 0, deshabilita el botón "Add FAQ" y muestra "Upgrade to Pro".
  3. Cuando el backend rechaza con 403 + error: "PLAN_LIMIT_EXCEEDED", el frontend abre un modal con CTA al upgrade.

Toda la lógica del frontend vive en apps/dashboard/src/lib/planLimits.ts y los hooks usePlanLimits().

Cómo afecta el widget público

El widget también respeta los limits:

  • Si el bot está en plan Free (languages=1), el widget solo muestra el idioma default del bot, ignorando los demás aunque estén en tenant_frontend_content.content.supported_languages.
  • El badge "Powered by BLIMX" en el footer del modal se oculta si plan.features.limits.branding === false.

Esto lo hace el endpoint /v3/frontend/config que filtra los datos según el plan antes de devolverlos.

Casos de borde

Downgrade de Pro → Free

Bot en Pro tiene 350 FAQs (en 5 idiomas). El owner cancela y vuelve a Free.

  • El sistema NO borra las FAQs ni los idiomas extras.
  • El QuotaSummary.over queda en True.
  • Al usuario se le muestra un banner: "Tienes 350 FAQs pero tu plan solo permite 20. Borra las que sobran o haz upgrade."
  • No puede crear nuevas FAQs hasta volver bajo el limit.
  • El widget público sigue mostrando todas las FAQs viejas (no se las castiga al visitante).

Cambio de límite en el plan

El superadmin sube el limit de Starter de 100 a 200 desde /superadmin/plans/:

  • Toma efecto inmediato en la próxima llamada (no hay caché).
  • No requiere migración ni reinicio.

Plan inactivo (is_active=False)

  • Subscripciones existentes siguen funcionando hasta current_period_end.
  • No se pueden crear nuevos checkout sessions de ese plan.
  • Aparece tachado en /api/dashboard/plans/.

Tests

cd backend_django
source venv/bin/activate
pytest api/tests/test_plan_policy_engine.py -v

Cubre:

  • Hard limit en faqs (rechaza creación 21ª en Free).
  • Conteo correcto de idiomas activos.
  • Branding flag toggleable.
  • Plan downgrade — over=True pero queries de lectura siguen funcionando.

Cómo añadir un limit nuevo

  1. Define el campo en plans.features.limits.<nombre> (con un valor default por plan).
  2. Añade un branch en evaluate_quota() que cuente el used para ese recurso.
  3. Llama enforce_quota(bot, 'tu_recurso') en la view que crea/activa.
  4. Documenta en 08-endpoints-api.md qué endpoint puede devolver PLAN_LIMIT_EXCEEDED.
  5. Añade test en api/tests/test_plan_policy_engine.py.

Total: ~30 minutos.