120 lines
4.1 KiB
Python
120 lines
4.1 KiB
Python
"""
|
|
app/tenant/gift_cards/routes.py
|
|
Gift card issuance, management, and balance enquiry.
|
|
"""
|
|
import logging
|
|
import secrets
|
|
import string
|
|
from datetime import datetime, timezone
|
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, g
|
|
from flask_login import login_required
|
|
from app.extensions import db
|
|
from app.models.salon import GiftCard, Customer
|
|
from app.decorators import require_role, demo_readonly
|
|
from app.tenant.utils import log_tenant_action
|
|
|
|
logger = logging.getLogger(__name__)
|
|
gift_cards_bp = Blueprint("gift_cards", __name__, url_prefix="/gift-cards")
|
|
|
|
|
|
def _generate_code(tenant_id: int) -> str:
|
|
"""Generate a unique gift card code for this tenant."""
|
|
alphabet = string.ascii_uppercase + string.digits
|
|
for _ in range(20):
|
|
code = "".join(secrets.choice(alphabet) for _ in range(12))
|
|
code = f"{code[:4]}-{code[4:8]}-{code[8:12]}"
|
|
if not GiftCard.query.filter_by(tenant_id=tenant_id, code=code).first():
|
|
return code
|
|
raise RuntimeError("Failed to generate unique gift card code")
|
|
|
|
|
|
@gift_cards_bp.route("/")
|
|
@login_required
|
|
@require_role("tenant_admin", "tenant_manager")
|
|
def index():
|
|
cards = GiftCard.query.filter_by(
|
|
tenant_id=g.tenant.id
|
|
).order_by(GiftCard.created_at.desc()).limit(200).all()
|
|
return render_template("tenant/gift_cards/index.html", cards=cards)
|
|
|
|
|
|
@gift_cards_bp.route("/issue", methods=["GET", "POST"])
|
|
@login_required
|
|
@require_role("tenant_admin", "tenant_manager")
|
|
@demo_readonly
|
|
def issue():
|
|
customers = Customer.query.filter_by(
|
|
tenant_id=g.tenant.id, is_active=True).filter(
|
|
Customer.deleted_at.is_(None)).order_by(Customer.name).limit(500).all()
|
|
|
|
if request.method == "POST":
|
|
try:
|
|
value = float(request.form.get("value", 0))
|
|
assert value > 0
|
|
except (ValueError, AssertionError):
|
|
return render_template("tenant/gift_cards/form.html",
|
|
customers=customers, error="Value must be greater than 0.")
|
|
|
|
customer_id = request.form.get("customer_id", type=int) or None
|
|
expires_raw = request.form.get("expires_at", "").strip()
|
|
expires_at = None
|
|
if expires_raw:
|
|
try:
|
|
expires_at = datetime.fromisoformat(expires_raw)
|
|
except ValueError:
|
|
pass
|
|
|
|
code = _generate_code(g.tenant.id)
|
|
card = GiftCard(
|
|
tenant_id=g.tenant.id, code=code,
|
|
original_value=value, remaining_balance=value,
|
|
issued_by=_user_db_id(),
|
|
issued_to_customer_id=customer_id,
|
|
expires_at=expires_at, is_active=True,
|
|
)
|
|
db.session.add(card)
|
|
db.session.flush()
|
|
log_tenant_action("gift_card.issue", "gift_card", card.id,
|
|
{"code": code, "value": value})
|
|
db.session.commit()
|
|
flash(f"Gift card issued: {code} (${value:.2f})", "success")
|
|
return redirect(url_for("gift_cards.index"))
|
|
return render_template("tenant/gift_cards/form.html", customers=customers)
|
|
|
|
|
|
@gift_cards_bp.route("/lookup")
|
|
@login_required
|
|
@require_role("tenant_admin", "tenant_manager")
|
|
def lookup():
|
|
code = request.args.get("code", "").strip().upper()
|
|
card = None
|
|
if code:
|
|
card = GiftCard.query.filter_by(
|
|
tenant_id=g.tenant.id, code=code).first()
|
|
return render_template("tenant/gift_cards/lookup.html",
|
|
card=card, code=code)
|
|
|
|
|
|
@gift_cards_bp.route("/<int:card_id>/deactivate", methods=["POST"])
|
|
@login_required
|
|
@require_role("tenant_admin")
|
|
@demo_readonly
|
|
def deactivate(card_id):
|
|
card = GiftCard.query.filter_by(
|
|
id=card_id, tenant_id=g.tenant.id).first_or_404()
|
|
card.is_active = False
|
|
log_tenant_action("gift_card.deactivate", "gift_card", card.id,
|
|
{"code": card.code})
|
|
db.session.commit()
|
|
flash(f"Gift card {card.code} deactivated.", "success")
|
|
return redirect(url_for("gift_cards.index"))
|
|
|
|
|
|
def _user_db_id():
|
|
from flask_login import current_user
|
|
try:
|
|
uid_str = current_user.get_id()
|
|
return int(uid_str.split(":")[1]) if uid_str and ":" in uid_str else None
|
|
except Exception:
|
|
return None
|