Files
Autoparts-DB/pos/services/billing.py
consultoria-as a386ca5b4a
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled
Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos
- F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado
- F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment
- F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings)
- F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant
- Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas
- Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant)
- Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant
- Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
2026-10-12 00:57:31 +00:00

83 lines
3.2 KiB
Python

"""Subscription billing for Nexus POS SaaS."""
from tenant_db import get_master_conn
PLANS = {
# `demo` is a time-limited full trial (see scripts/expire_demos.py). It has
# no resource limits on purpose: a prospect must be able to import their
# real inventory without hitting a wall. Access is gated by the
# subscriptions.expires_at date, not by data volume.
'demo': {'name': 'Demo', 'price_mxn': 0, 'max_products': None, 'max_employees': None, 'max_branches': None},
# Yonke: plan comercial para negocios de SOLO partes usadas (sin catálogo TecDoc)
'yonke': {'name': 'Yonke', 'price_mxn': 299, 'max_products': None, 'max_employees': 10, 'max_branches': 1},
'free': {'name': 'Gratis', 'price_mxn': 0, 'max_products': 100, 'max_employees': 2, 'max_branches': 1},
'basic': {'name': 'Basico', 'price_mxn': 499, 'max_products': 5000, 'max_employees': 5, 'max_branches': 2},
'standard': {'name': 'Estandar', 'price_mxn': 999, 'max_products': 20000, 'max_employees': 10, 'max_branches': 3},
'pro': {'name': 'Pro', 'price_mxn': 1499, 'max_products': 50000, 'max_employees': 15, 'max_branches': 5},
'enterprise': {'name': 'Enterprise', 'price_mxn': 3999, 'max_products': None, 'max_employees': None, 'max_branches': None},
}
PLAN_ORDER = ['free', 'demo', 'basic', 'standard', 'pro', 'enterprise']
def get_plan(tenant_id):
"""Get current plan for a tenant."""
conn = get_master_conn()
cur = conn.cursor()
cur.execute("SELECT plan FROM tenants WHERE id = %s", (tenant_id,))
row = cur.fetchone()
cur.close()
conn.close()
if not row:
return 'free'
plan_key = row[0] or 'free'
if plan_key not in PLANS:
plan_key = 'free'
return plan_key
def get_plan_details(tenant_id):
"""Get plan key + full details for a tenant."""
plan_key = get_plan(tenant_id)
return {**PLANS[plan_key], 'key': plan_key}
def next_plan(current_plan):
"""Return the next upgrade plan key, or None if already enterprise."""
idx = PLAN_ORDER.index(current_plan) if current_plan in PLAN_ORDER else 0
if idx < len(PLAN_ORDER) - 1:
return PLAN_ORDER[idx + 1]
return None
def check_limit(tenant_id, resource, current_count):
"""Check if tenant is within plan limits.
Args:
tenant_id: Tenant ID
resource: One of 'max_products', 'max_employees', 'max_branches'
current_count: Current count of that resource
Returns:
(allowed: bool, limit: int|None, current: int)
"""
plan_key = get_plan(tenant_id)
plan = PLANS[plan_key]
limit = plan.get(resource)
if limit is None:
return (True, None, current_count)
return (current_count < limit, limit, current_count)
def upgrade_plan(tenant_id, new_plan):
"""Change tenant's plan."""
if new_plan not in PLANS:
return {'error': f'Invalid plan: {new_plan}'}
conn = get_master_conn()
cur = conn.cursor()
cur.execute("UPDATE tenants SET plan = %s WHERE id = %s", (new_plan, tenant_id))
conn.commit()
cur.close()
conn.close()
return {'success': True, 'plan': new_plan, 'details': PLANS[new_plan]}