""" app/admin/plans/routes.py Subscription plan management: create, edit, toggle active. Feature flags stored as JSON. All changes logged. """ import logging from flask import Blueprint, render_template, redirect, url_for, flash, request from flask_login import login_required from app.extensions import db from app.models.platform import Plan from app.admin.utils import superadmin_required, log_admin_action, model_to_dict logger = logging.getLogger(__name__) plans_bp = Blueprint("plans", __name__, url_prefix="/plans") _AUDIT_FIELDS = ["name", "price_monthly", "max_staff", "max_locations", "features_json", "is_active"] # All known feature flags — used to build the form checkboxes ALL_FEATURES = [ "pos", "appointments", "customers", "services", "promotions", "appointment_reminders", "customer_reviews", "reconciliation", "basic_reports", "inventory", "commission", "full_reports", "multi_location", "online_booking", "waitlist", "marketing", ] @plans_bp.route("/") @login_required @superadmin_required def index(): all_plans = Plan.query.order_by(Plan.price_monthly.asc()).all() return render_template("admin/plans/index.html", plans=all_plans) @plans_bp.route("/new", methods=["GET", "POST"]) @login_required @superadmin_required def create(): if request.method == "POST": plan, error = _plan_from_form(None) if error: return render_template("admin/plans/form.html", mode="create", all_features=ALL_FEATURES, error=error) db.session.add(plan) db.session.flush() log_admin_action("plan.create", "plan", plan.id, after=model_to_dict(plan, _AUDIT_FIELDS)) db.session.commit() logger.info("Plan created: id=%s name=%s", plan.id, plan.name) flash(f"Plan \'{plan.name}\' created.", "success") return redirect(url_for("plans.index")) return render_template("admin/plans/form.html", mode="create", all_features=ALL_FEATURES) @plans_bp.route("//edit", methods=["GET", "POST"]) @login_required @superadmin_required def edit(plan_id): plan = Plan.query.get_or_404(plan_id) before = model_to_dict(plan, _AUDIT_FIELDS) if request.method == "POST": _, error = _plan_from_form(plan) if error: return render_template("admin/plans/form.html", mode="edit", plan=plan, all_features=ALL_FEATURES, error=error) log_admin_action("plan.edit", "plan", plan.id, before=before, after=model_to_dict(plan, _AUDIT_FIELDS)) db.session.commit() logger.info("Plan edited: id=%s name=%s", plan.id, plan.name) flash(f"Plan \'{plan.name}\' updated.", "success") return redirect(url_for("plans.index")) return render_template("admin/plans/form.html", mode="edit", plan=plan, all_features=ALL_FEATURES) @plans_bp.route("//toggle-active", methods=["POST"]) @login_required @superadmin_required def toggle_active(plan_id): plan = Plan.query.get_or_404(plan_id) before = model_to_dict(plan, _AUDIT_FIELDS) plan.is_active = not plan.is_active action = "plan.activate" if plan.is_active else "plan.deactivate" log_admin_action(action, "plan", plan.id, before=before, after=model_to_dict(plan, _AUDIT_FIELDS)) db.session.commit() status = "activated" if plan.is_active else "deactivated" flash(f"Plan \'{plan.name}\' {status}.", "success") return redirect(url_for("plans.index")) def _plan_from_form(plan): """Parse form data into a Plan object. Returns (plan, error_string|None).""" name = request.form.get("name", "").strip() if not name: return plan, "Plan name is required." try: price = float(request.form.get("price_monthly", 0)) except ValueError: return plan, "Invalid price." max_staff_raw = request.form.get("max_staff", "").strip() max_loc_raw = request.form.get("max_locations", "").strip() max_staff = int(max_staff_raw) if max_staff_raw.isdigit() else None max_locations = int(max_loc_raw) if max_loc_raw.isdigit() else None features = {flag: (request.form.get(f"feature_{flag}") == "1") for flag in ALL_FEATURES} if plan is None: plan = Plan(name=name) else: plan.name = name plan.price_monthly = price plan.max_staff = max_staff plan.max_locations = max_locations plan.features_json = features plan.is_active = request.form.get("is_active") == "1" return plan, None