05/06 Phase 2: updated and added new files
This commit is contained in:
@@ -1,7 +1,92 @@
|
||||
"""
|
||||
app/admin/billing/routes.py
|
||||
Phase 2 implementation.
|
||||
Billing history: manual invoice entry per tenant, list view.
|
||||
All entries logged. Superadmin only.
|
||||
"""
|
||||
from flask import Blueprint
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from app.extensions import db
|
||||
from app.models.platform import Tenant, TenantBillingHistory
|
||||
from app.admin.utils import superadmin_required, log_admin_action
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
billing_bp = Blueprint("billing", __name__, url_prefix="/billing")
|
||||
|
||||
|
||||
@billing_bp.route("/")
|
||||
@login_required
|
||||
@superadmin_required
|
||||
def index():
|
||||
entries = (
|
||||
TenantBillingHistory.query
|
||||
.order_by(TenantBillingHistory.created_at.desc())
|
||||
.limit(100)
|
||||
.all()
|
||||
)
|
||||
return render_template("admin/billing/index.html", entries=entries)
|
||||
|
||||
|
||||
@billing_bp.route("/tenant/<int:tenant_id>")
|
||||
@login_required
|
||||
@superadmin_required
|
||||
def tenant_billing(tenant_id):
|
||||
tenant = Tenant.query.get_or_404(tenant_id)
|
||||
entries = (
|
||||
TenantBillingHistory.query
|
||||
.filter_by(tenant_id=tenant_id)
|
||||
.order_by(TenantBillingHistory.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
return render_template("admin/billing/tenant.html", tenant=tenant, entries=entries)
|
||||
|
||||
|
||||
@billing_bp.route("/tenant/<int:tenant_id>/add", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@superadmin_required
|
||||
def add_entry(tenant_id):
|
||||
tenant = Tenant.query.get_or_404(tenant_id)
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
amount = float(request.form.get("amount", 0))
|
||||
except ValueError:
|
||||
return render_template("admin/billing/form.html", tenant=tenant,
|
||||
error="Invalid amount.")
|
||||
description = request.form.get("description", "").strip()
|
||||
invoice_ref = request.form.get("invoice_ref", "").strip() or None
|
||||
paid_at_raw = request.form.get("paid_at", "").strip()
|
||||
paid_at = None
|
||||
if paid_at_raw:
|
||||
try:
|
||||
paid_at = datetime.fromisoformat(paid_at_raw)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not description:
|
||||
return render_template("admin/billing/form.html", tenant=tenant,
|
||||
error="Description is required.")
|
||||
|
||||
entry = TenantBillingHistory(
|
||||
tenant_id=tenant_id, amount=amount, description=description,
|
||||
invoice_ref=invoice_ref, paid_at=paid_at,
|
||||
recorded_by=current_user.id,
|
||||
)
|
||||
db.session.add(entry)
|
||||
db.session.flush()
|
||||
log_admin_action(
|
||||
"billing.add_entry", "tenant", tenant_id,
|
||||
after={"amount": amount, "description": description, "invoice_ref": invoice_ref},
|
||||
)
|
||||
db.session.commit()
|
||||
logger.info("Billing entry added: tenant_id=%s amount=%s by=%s",
|
||||
tenant_id, amount, current_user.id)
|
||||
flash(f"Billing entry added for \'{tenant.name}\'.", "success")
|
||||
return redirect(url_for("billing.tenant_billing", tenant_id=tenant_id))
|
||||
|
||||
return render_template("admin/billing/form.html", tenant=tenant)
|
||||
|
||||
Reference in New Issue
Block a user