- 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:
@@ -29,6 +29,22 @@ def create_branch():
|
||||
if not data.get('name'):
|
||||
return jsonify({'error': 'name required'}), 400
|
||||
|
||||
# Plan limit check
|
||||
from services.billing import check_limit, next_plan, PLANS, get_plan
|
||||
conn_chk = get_tenant_conn(g.tenant_id)
|
||||
cur_chk = conn_chk.cursor()
|
||||
cur_chk.execute("SELECT count(*) FROM branches WHERE is_active = true")
|
||||
current_branches = cur_chk.fetchone()[0]
|
||||
cur_chk.close()
|
||||
conn_chk.close()
|
||||
|
||||
allowed, limit, current = check_limit(g.tenant_id, 'max_branches', current_branches)
|
||||
if not allowed:
|
||||
plan_key = get_plan(g.tenant_id)
|
||||
nxt = next_plan(plan_key)
|
||||
nxt_name = PLANS[nxt]['name'] if nxt else 'Enterprise'
|
||||
return jsonify({'error': f'Plan limit reached ({limit} branches). Upgrade to {nxt_name}.'}), 403
|
||||
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
@@ -76,6 +92,22 @@ def create_employee():
|
||||
if not data.get(f):
|
||||
return jsonify({'error': f'{f} required'}), 400
|
||||
|
||||
# Plan limit check
|
||||
from services.billing import check_limit, next_plan, PLANS, get_plan
|
||||
conn_chk = get_tenant_conn(g.tenant_id)
|
||||
cur_chk = conn_chk.cursor()
|
||||
cur_chk.execute("SELECT count(*) FROM employees WHERE is_active = true")
|
||||
current_employees = cur_chk.fetchone()[0]
|
||||
cur_chk.close()
|
||||
conn_chk.close()
|
||||
|
||||
allowed, limit, current = check_limit(g.tenant_id, 'max_employees', current_employees)
|
||||
if not allowed:
|
||||
plan_key = get_plan(g.tenant_id)
|
||||
nxt = next_plan(plan_key)
|
||||
nxt_name = PLANS[nxt]['name'] if nxt else 'Enterprise'
|
||||
return jsonify({'error': f'Plan limit reached ({limit} employees). Upgrade to {nxt_name}.'}), 403
|
||||
|
||||
valid_roles = ['admin', 'cashier', 'warehouse', 'accountant']
|
||||
if data['role'] not in valid_roles:
|
||||
return jsonify({'error': f'role must be one of: {", ".join(valid_roles)}'}), 400
|
||||
@@ -126,6 +158,69 @@ def create_employee():
|
||||
return jsonify({'id': emp_id, 'message': 'Employee created'}), 201
|
||||
|
||||
|
||||
@config_bp.route('/currency', methods=['GET'])
|
||||
@require_auth()
|
||||
def get_currency():
|
||||
"""Get currency config for this tenant."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT key, value FROM tenant_config WHERE key IN ('currency', 'exchange_rate_usd_mxn')")
|
||||
cfg = {}
|
||||
for row in cur.fetchall():
|
||||
cfg[row[0]] = row[1]
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
from config import DEFAULT_CURRENCY, EXCHANGE_RATE_USD_MXN
|
||||
return jsonify({
|
||||
'currency': cfg.get('currency', DEFAULT_CURRENCY),
|
||||
'exchange_rate': float(cfg.get('exchange_rate_usd_mxn', str(EXCHANGE_RATE_USD_MXN))),
|
||||
'currencies': {
|
||||
'MXN': {'symbol': '$', 'name': 'Peso Mexicano'},
|
||||
'USD': {'symbol': 'US$', 'name': 'US Dollar'},
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@config_bp.route('/currency', methods=['PUT'])
|
||||
@require_auth('config.edit')
|
||||
def update_currency():
|
||||
"""Update currency config for this tenant."""
|
||||
data = request.get_json() or {}
|
||||
currency = data.get('currency', 'MXN')
|
||||
rate = data.get('exchange_rate')
|
||||
|
||||
if currency not in ('MXN', 'USD'):
|
||||
return jsonify({'error': 'currency must be MXN or USD'}), 400
|
||||
if rate is not None:
|
||||
try:
|
||||
rate = float(rate)
|
||||
if rate <= 0:
|
||||
raise ValueError
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({'error': 'exchange_rate must be a positive number'}), 400
|
||||
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO tenant_config (key, value) VALUES ('currency', %s)
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
||||
""", (currency,))
|
||||
|
||||
if rate is not None:
|
||||
cur.execute("""
|
||||
INSERT INTO tenant_config (key, value) VALUES ('exchange_rate_usd_mxn', %s)
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
||||
""", (str(rate),))
|
||||
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return jsonify({'message': 'Currency config updated', 'currency': currency})
|
||||
|
||||
|
||||
@config_bp.route('/business', methods=['GET'])
|
||||
@require_auth()
|
||||
def get_business():
|
||||
@@ -170,3 +265,52 @@ def get_theme():
|
||||
'--radius': '8px',
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
# ─── Billing / Subscription ──────────────────────────
|
||||
|
||||
@config_bp.route('/billing', methods=['GET'])
|
||||
@require_auth()
|
||||
def get_billing():
|
||||
"""Get current plan, usage stats, and available plans."""
|
||||
from services.billing import get_plan_details, PLANS, PLAN_ORDER
|
||||
|
||||
plan = get_plan_details(g.tenant_id)
|
||||
|
||||
# Get current usage counts
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT count(*) FROM inventory WHERE is_active = true")
|
||||
products = cur.fetchone()[0]
|
||||
cur.execute("SELECT count(*) FROM employees WHERE is_active = true")
|
||||
employees = cur.fetchone()[0]
|
||||
cur.execute("SELECT count(*) FROM branches WHERE is_active = true")
|
||||
branches = cur.fetchone()[0]
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
'current_plan': plan,
|
||||
'usage': {
|
||||
'products': products,
|
||||
'employees': employees,
|
||||
'branches': branches,
|
||||
},
|
||||
'plans': {k: {**v, 'key': k} for k, v in PLANS.items()},
|
||||
'plan_order': PLAN_ORDER,
|
||||
})
|
||||
|
||||
|
||||
@config_bp.route('/billing/upgrade', methods=['POST'])
|
||||
@require_auth('config.edit')
|
||||
def upgrade_billing():
|
||||
"""Upgrade tenant plan."""
|
||||
from services.billing import upgrade_plan
|
||||
data = request.get_json() or {}
|
||||
new_plan = data.get('plan')
|
||||
if not new_plan:
|
||||
return jsonify({'error': 'plan required'}), 400
|
||||
result = upgrade_plan(g.tenant_id, new_plan)
|
||||
if 'error' in result:
|
||||
return jsonify(result), 400
|
||||
return jsonify(result)
|
||||
|
||||
Reference in New Issue
Block a user