127 lines
4.5 KiB
Python
127 lines
4.5 KiB
Python
"""
|
|
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("/<int:plan_id>/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("/<int:plan_id>/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
|