- Gunicorn production server with auto-scaled workers, run.sh, updated systemd service - Marketplace B2B: cross-tenant inventory search, ordering, seller management with full UI - Subscription billing: plan limits enforced on products/employees/branches, billing API + upgrade flow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
74
pos/services/billing.py
Normal file
74
pos/services/billing.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Subscription billing for Nexus POS SaaS."""
|
||||
|
||||
from tenant_db import get_master_conn
|
||||
|
||||
PLANS = {
|
||||
'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},
|
||||
'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', 'basic', '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]}
|
||||
Reference in New Issue
Block a user