Jun 28 - Update and polish UI/UX
This commit is contained in:
@@ -376,6 +376,72 @@ def provision_tenant():
|
||||
return redirect(url_for('tenants.tenant_detail', tenant_id=info['tenant_id']))
|
||||
|
||||
|
||||
# ── billing controls (superadmin override) ────────────────────────────────────
|
||||
|
||||
@bp.route('/<int:tenant_id>/billing', methods=['POST'])
|
||||
@superadmin_required
|
||||
def update_billing(tenant_id):
|
||||
"""Superadmin billing controls: extend trial, override status, apply coupon."""
|
||||
action = (request.form.get('action') or '').strip()
|
||||
|
||||
with control_session() as s:
|
||||
t = s.get(Tenant, tenant_id)
|
||||
if t is None:
|
||||
flash('Tenant not found.', 'danger')
|
||||
return redirect(url_for('tenants.list_tenants'))
|
||||
|
||||
if action == 'set_trial':
|
||||
from datetime import timedelta
|
||||
try:
|
||||
days = int(request.form.get('trial_days', 14))
|
||||
except (ValueError, TypeError):
|
||||
flash('Invalid trial days.', 'danger')
|
||||
return redirect(url_for('tenants.tenant_detail', tenant_id=tenant_id))
|
||||
new_ends = now_eastern() + timedelta(days=days)
|
||||
t.subscription_status = 'trial'
|
||||
t.trial_ends_at = new_ends
|
||||
_audit('BILLING_TRIAL', tenant_id=tenant_id,
|
||||
details=f'days={days} ends_at={new_ends.date()}')
|
||||
flash(f'Trial set to {days} days (expires {new_ends.strftime("%b %d, %Y")}).', 'success')
|
||||
|
||||
elif action == 'set_status':
|
||||
status = (request.form.get('subscription_status') or '').strip()
|
||||
allowed = ('trial', 'active', 'past_due', 'cancelled')
|
||||
if status not in allowed:
|
||||
flash(f'Invalid status. Choose from: {", ".join(allowed)}.', 'danger')
|
||||
return redirect(url_for('tenants.tenant_detail', tenant_id=tenant_id))
|
||||
old = t.subscription_status
|
||||
t.subscription_status = status
|
||||
_audit('BILLING_STATUS', tenant_id=tenant_id,
|
||||
details=f'status {old} → {status}')
|
||||
flash(f'Subscription status updated to "{status}".', 'success')
|
||||
|
||||
elif action == 'apply_coupon':
|
||||
coupon_id = (request.form.get('coupon_id') or '').strip()
|
||||
if not coupon_id:
|
||||
flash('Coupon ID is required.', 'danger')
|
||||
return redirect(url_for('tenants.tenant_detail', tenant_id=tenant_id))
|
||||
if not t.stripe_customer_id:
|
||||
flash('Tenant has no Stripe customer — cannot apply coupon.', 'danger')
|
||||
return redirect(url_for('tenants.tenant_detail', tenant_id=tenant_id))
|
||||
try:
|
||||
import os
|
||||
import stripe as _stripe
|
||||
_stripe.api_key = os.environ.get('STRIPE_SECRET_KEY', '')
|
||||
_stripe.Customer.modify(t.stripe_customer_id, coupon=coupon_id)
|
||||
_audit('BILLING_COUPON', tenant_id=tenant_id,
|
||||
details=f'coupon={coupon_id} customer={t.stripe_customer_id}')
|
||||
flash(f'Coupon "{coupon_id}" applied to Stripe customer.', 'success')
|
||||
except Exception as exc:
|
||||
logger.error('PANEL | apply_coupon | tenant=%s err=%s', tenant_id, exc)
|
||||
flash(f'Stripe error: {exc}', 'danger')
|
||||
|
||||
else:
|
||||
flash('Unknown billing action.', 'danger')
|
||||
|
||||
return redirect(url_for('tenants.tenant_detail', tenant_id=tenant_id))
|
||||
|
||||
|
||||
# ── run migration ─────────────────────────────────────────────────────────────
|
||||
|
||||
@bp.route('/<int:tenant_id>/migrate', methods=['POST'])
|
||||
|
||||
Reference in New Issue
Block a user