Files
MyPOS/app/admin/tenants/routes.py
T

178 lines
7.3 KiB
Python

"""
app/admin/tenants/routes.py
Tenant management: create, view, edit, suspend, cancel, assign plan.
All status changes logged. Superadmin only.
"""
import logging
from datetime import datetime, timezone, timedelta
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required
from app.extensions import db, bcrypt
from app.models.platform import Tenant, Plan
from app.models.salon import User, Location
from app.admin.utils import superadmin_required, log_admin_action, model_to_dict
logger = logging.getLogger(__name__)
tenants_bp = Blueprint("tenants", __name__, url_prefix="/tenants")
_AUDIT_FIELDS = ["slug", "name", "owner_email", "plan_id", "status", "is_demo",
"trial_ends_at", "subscription_expires_at"]
@tenants_bp.route("/")
@login_required
@superadmin_required
def index():
status_filter = request.args.get("status", "")
q = Tenant.query.order_by(Tenant.created_at.desc())
if status_filter:
q = q.filter_by(status=status_filter)
tenants = q.all()
return render_template("admin/tenants/index.html", tenants=tenants,
status_filter=status_filter)
@tenants_bp.route("/<int:tenant_id>")
@login_required
@superadmin_required
def detail(tenant_id):
tenant = Tenant.query.get_or_404(tenant_id)
plans = Plan.query.filter_by(is_active=True).order_by(Plan.price_monthly).all()
locations = Location.query.filter_by(tenant_id=tenant_id).all()
users = User.query.filter_by(tenant_id=tenant_id).all()
from app.models.platform import TenantBillingHistory, TenantSettingOverride
billing = TenantBillingHistory.query.filter_by(
tenant_id=tenant_id).order_by(TenantBillingHistory.created_at.desc()).limit(20).all()
overrides = TenantSettingOverride.query.filter_by(
tenant_id=tenant_id).filter(TenantSettingOverride.lifted_at.is_(None)).all()
return render_template(
"admin/tenants/detail.html", tenant=tenant, plans=plans,
locations=locations, users=users, billing=billing, overrides=overrides,
)
@tenants_bp.route("/new", methods=["GET", "POST"])
@login_required
@superadmin_required
def create():
plans = Plan.query.filter_by(is_active=True).order_by(Plan.price_monthly).all()
if request.method == "POST":
slug = request.form.get("slug", "").strip().lower()
name = request.form.get("name", "").strip()
owner_email = request.form.get("owner_email", "").strip().lower()
plan_id = request.form.get("plan_id", type=int)
owner_password = request.form.get("owner_password", "")
confirm = request.form.get("confirm_password", "")
trial_days = request.form.get("trial_days", "14")
from app.security import validate_slug
if not validate_slug(slug):
return render_template("admin/tenants/form.html", mode="create",
plans=plans, error="Invalid slug (lowercase letters, digits, hyphens only).")
if Tenant.query.filter_by(slug=slug).first():
return render_template("admin/tenants/form.html", mode="create",
plans=plans, error="That slug is already taken.")
from app.forms import validate_password_strength
err = validate_password_strength(owner_password, confirm)
if err:
return render_template("admin/tenants/form.html", mode="create",
plans=plans, error=err)
if not plan_id or not Plan.query.get(plan_id):
return render_template("admin/tenants/form.html", mode="create",
plans=plans, error="Please select a valid plan.")
try:
days = int(trial_days)
except ValueError:
days = 14
tenant = Tenant(
slug=slug, name=name, owner_email=owner_email,
plan_id=plan_id, status="trial",
trial_ends_at=datetime.now(timezone.utc) + timedelta(days=days),
)
db.session.add(tenant)
db.session.flush()
# Primary location
location = Location(
tenant_id=tenant.id, name=name,
is_primary=True, is_active=True,
)
db.session.add(location)
# Owner user
owner = User(
tenant_id=tenant.id, email=owner_email, role="tenant_admin",
password_hash=bcrypt.generate_password_hash(owner_password).decode("utf-8"),
is_active=True,
)
db.session.add(owner)
log_admin_action("tenant.create", "tenant", tenant.id,
after=model_to_dict(tenant, _AUDIT_FIELDS))
db.session.commit()
logger.info("Tenant created: id=%s slug=%s", tenant.id, slug)
flash(f"Tenant \'{name}\' created (slug: {slug}).", "success")
return redirect(url_for("tenants.detail", tenant_id=tenant.id))
return render_template("admin/tenants/form.html", mode="create", plans=plans)
@tenants_bp.route("/<int:tenant_id>/edit", methods=["GET", "POST"])
@login_required
@superadmin_required
def edit(tenant_id):
tenant = Tenant.query.get_or_404(tenant_id)
plans = Plan.query.filter_by(is_active=True).order_by(Plan.price_monthly).all()
before = model_to_dict(tenant, _AUDIT_FIELDS)
if request.method == "POST":
tenant.name = request.form.get("name", tenant.name).strip()
tenant.owner_email = request.form.get("owner_email", tenant.owner_email).strip().lower()
new_plan_id = request.form.get("plan_id", type=int)
if new_plan_id and Plan.query.get(new_plan_id):
tenant.plan_id = new_plan_id
sub_expires_raw = request.form.get("subscription_expires_at", "").strip()
if sub_expires_raw:
try:
tenant.subscription_expires_at = datetime.fromisoformat(sub_expires_raw)
except ValueError:
pass
tenant.is_demo = request.form.get("is_demo") == "1"
log_admin_action("tenant.edit", "tenant", tenant.id,
before=before, after=model_to_dict(tenant, _AUDIT_FIELDS))
db.session.commit()
logger.info("Tenant edited: id=%s slug=%s", tenant.id, tenant.slug)
flash(f"Tenant \'{tenant.name}\' updated.", "success")
return redirect(url_for("tenants.detail", tenant_id=tenant.id))
return render_template("admin/tenants/form.html", mode="edit",
tenant=tenant, plans=plans)
@tenants_bp.route("/<int:tenant_id>/set-status", methods=["POST"])
@login_required
@superadmin_required
def set_status(tenant_id):
tenant = Tenant.query.get_or_404(tenant_id)
new_status = request.form.get("status", "").strip()
allowed = {"active", "trial", "suspended", "cancelled"}
if new_status not in allowed:
flash("Invalid status.", "danger")
return redirect(url_for("tenants.detail", tenant_id=tenant_id))
before = model_to_dict(tenant, _AUDIT_FIELDS)
tenant.status = new_status
log_admin_action(f"tenant.set_status.{new_status}", "tenant", tenant.id,
before=before, after=model_to_dict(tenant, _AUDIT_FIELDS))
db.session.commit()
logger.info("Tenant status changed: id=%s slug=%s status=%s", tenant.id, tenant.slug, new_status)
flash(f"Tenant \'{tenant.name}\' status set to \'{new_status}\'.", "success")
return redirect(url_for("tenants.detail", tenant_id=tenant_id))