using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Enums;
using PowderCoating.Infrastructure.Data;
using PowderCoating.Shared.Constants;
namespace PowderCoating.Web.Controllers;
///
/// SuperAdmin-only revenue intelligence dashboard that aggregates subscription
/// metrics across all tenant companies. Uses
/// directly (bypassing the UoW) because it queries across company boundaries and
/// joins plan configs — a pattern that would require multiple unrelated repositories.
///
// Intentional exception: cross-tenant MRR/ARR metrics joining Company + SubscriptionPlanConfig; same pattern as CompanyHealthController. See docs/DATA_ACCESS_ARCHITECTURE.md — Permanent Exceptions.
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
public class RevenueController : Controller
{
private readonly ApplicationDbContext _db;
public RevenueController(ApplicationDbContext db)
{
_db = db;
}
///
/// Renders the platform revenue dashboard with MRR, ARR, plan distribution,
/// a 12-month MRR trend, and a list of companies needing subscription attention.
///
/// Design decisions:
///
/// - Demo company (Id = 1) and comped companies are excluded from all
/// revenue figures because they do not represent real subscription income.
/// - The 12-month MRR trend is an approximation — it estimates active
/// subscribers per historical month using CreatedAt and current status,
/// not a ledger of actual billing events, because no billing-transactions table
/// exists yet.
/// - IgnoreQueryFilters() is required throughout so that the global
/// multi-tenancy filter does not restrict visibility to a single company.
///
///
///
public async Task Index()
{
var now = DateTime.UtcNow;
var thisMonthStart = new DateTime(now.Year, now.Month, 1);
var lastMonthStart = thisMonthStart.AddMonths(-1);
// Plan configs (price lookup)
var planConfigs = await _db.SubscriptionPlanConfigs
.AsNoTracking().IgnoreQueryFilters()
.Where(p => p.IsActive)
.OrderBy(p => p.SortOrder)
.ToListAsync();
var priceByPlan = planConfigs.ToDictionary(p => p.Plan, p => p.MonthlyPrice);
string PlanName(int plan) => planConfigs.FirstOrDefault(p => p.Plan == plan)?.DisplayName ?? plan.ToString();
// Exclude demo company (Id 1) and comped companies from revenue calculations;
// they don't represent real subscription income.
const int DemoCompanyId = 1;
var companies = await _db.Companies
.AsNoTracking().IgnoreQueryFilters()
.Where(c => !c.IsDeleted && c.Id != DemoCompanyId && !c.IsComped)
.ToListAsync();
// Comped companies are excluded from revenue but must be counted separately
// so the status table in the view sums to a correct total.
var compedCount = await _db.Companies
.AsNoTracking().IgnoreQueryFilters()
.CountAsync(c => !c.IsDeleted && c.Id != DemoCompanyId && c.IsComped);
// ── Revenue metrics ──────────────────────────────────────────────
// Active paying companies — exclude trials (no StripeSubscriptionId = on trial)
var payingActive = companies
.Where(c =>
!string.IsNullOrEmpty(c.StripeSubscriptionId) &&
(c.SubscriptionStatus == SubscriptionStatus.Active ||
c.SubscriptionStatus == SubscriptionStatus.GracePeriod))
.ToList();
var mrr = payingActive.Sum(c =>
priceByPlan.TryGetValue(c.SubscriptionPlan, out var price) ? price : 0m);
var arr = mrr * 12;
// Average revenue per paying company
var avgRevenue = payingActive.Count > 0 ? mrr / payingActive.Count : 0m;
// ── Company counts ───────────────────────────────────────────────
// totalCompanies includes comped so that Active + GracePeriod + Expired + Canceled + Comped = Total.
var totalCompanies = companies.Count + compedCount;
var activeCount = companies.Count(c => c.SubscriptionStatus == SubscriptionStatus.Active);
var gracePeriodCount = companies.Count(c => c.SubscriptionStatus == SubscriptionStatus.GracePeriod);
var expiredCount = companies.Count(c => c.SubscriptionStatus == SubscriptionStatus.Expired);
var canceledCount = companies.Count(c =>
c.SubscriptionStatus == SubscriptionStatus.Canceled ||
c.SubscriptionStatus == SubscriptionStatus.Inactive);
var stripeCount = companies.Count(c => !string.IsNullOrEmpty(c.StripeSubscriptionId));
// ── Month-over-month ─────────────────────────────────────────────
var newThisMonth = companies.Count(c => c.CreatedAt >= thisMonthStart);
var newLastMonth = companies.Count(c => c.CreatedAt >= lastMonthStart && c.CreatedAt < thisMonthStart);
var churnThisMonth = companies.Count(c =>
c.UpdatedAt >= thisMonthStart &&
(c.SubscriptionStatus == SubscriptionStatus.Canceled ||
c.SubscriptionStatus == SubscriptionStatus.Expired));
// ── Plan distribution ────────────────────────────────────────────
var planDistribution = companies
.Where(c => !c.IsComped &&
!string.IsNullOrEmpty(c.StripeSubscriptionId) &&
(c.SubscriptionStatus == SubscriptionStatus.Active ||
c.SubscriptionStatus == SubscriptionStatus.GracePeriod))
.GroupBy(c => c.SubscriptionPlan)
.Select(g => new PlanMetricRow
{
PlanId = g.Key,
PlanName = PlanName(g.Key),
CompanyCount = g.Count(),
MonthlyPrice = priceByPlan.TryGetValue(g.Key, out var p) ? p : 0m,
Revenue = g.Sum(c => priceByPlan.TryGetValue(c.SubscriptionPlan, out var pr) ? pr : 0m)
})
.OrderByDescending(r => r.Revenue)
.ToList();
// ── Approximated 12-month trend ──────────────────────────────────
// We estimate MRR per month by looking at company creation dates.
// (No billing transactions table yet — this is an approximation.)
var trendMonths = new List();
for (int i = 11; i >= 0; i--)
{
var monthStart = new DateTime(now.Year, now.Month, 1).AddMonths(-i);
var monthEnd = monthStart.AddMonths(1);
var activeInMonth = companies
.Where(c =>
!c.IsComped &&
!string.IsNullOrEmpty(c.StripeSubscriptionId) &&
c.CreatedAt < monthEnd &&
(c.SubscriptionStatus == SubscriptionStatus.Active ||
c.SubscriptionStatus == SubscriptionStatus.GracePeriod ||
c.CreatedAt >= monthStart)) // simplification
.ToList();
var mrrPoint = activeInMonth.Sum(c =>
priceByPlan.TryGetValue(c.SubscriptionPlan, out var pr) ? pr : 0m);
trendMonths.Add(new MrrTrendPoint
{
Month = monthStart.ToString("MMM yy"),
Mrr = mrrPoint,
CompanyCount = activeInMonth.Count
});
}
// ── Companies needing attention ──────────────────────────────────
var alerts = companies
.Where(c =>
c.SubscriptionStatus == SubscriptionStatus.GracePeriod ||
c.SubscriptionStatus == SubscriptionStatus.Expired ||
(c.SubscriptionStatus == SubscriptionStatus.Active &&
c.SubscriptionEndDate.HasValue &&
c.SubscriptionEndDate.Value <= now.AddDays(14) &&
!c.IsComped))
.OrderBy(c => c.SubscriptionEndDate)
.Take(20)
.Select(c => new SubscriptionAlertRow
{
Id = c.Id,
CompanyName = c.CompanyName,
PlanName = PlanName(c.SubscriptionPlan),
Status = c.SubscriptionStatus,
EndDate = c.SubscriptionEndDate,
DaysUntilExpiry = c.SubscriptionEndDate.HasValue
? (int)(c.SubscriptionEndDate.Value.Date - now.Date).TotalDays
: 0,
IsComped = c.IsComped
})
.ToList();
var vm = new RevenueDashboardViewModel
{
Mrr = mrr,
Arr = arr,
AvgRevenuePerCompany = avgRevenue,
TotalCompanies = totalCompanies,
ActiveCount = activeCount,
GracePeriodCount = gracePeriodCount,
ExpiredCount = expiredCount,
CanceledCount = canceledCount,
CompedCount = compedCount,
StripeManaged = stripeCount,
NewThisMonth = newThisMonth,
NewLastMonth = newLastMonth,
ChurnThisMonth = churnThisMonth,
PlanDistribution = planDistribution,
TrendMonths = trendMonths,
SubscriptionAlerts = alerts
};
return View(vm);
}
}
// ── Local view models (scoped to this controller file) ───────────────────────
public class RevenueDashboardViewModel
{
public decimal Mrr { get; set; }
public decimal Arr { get; set; }
public decimal AvgRevenuePerCompany { get; set; }
public int TotalCompanies { get; set; }
public int ActiveCount { get; set; }
public int GracePeriodCount { get; set; }
public int ExpiredCount { get; set; }
public int CanceledCount { get; set; }
public int CompedCount { get; set; }
public int StripeManaged { get; set; }
public int NewThisMonth { get; set; }
public int NewLastMonth { get; set; }
public int ChurnThisMonth { get; set; }
public List PlanDistribution { get; set; } = new();
public List TrendMonths { get; set; } = new();
public List SubscriptionAlerts { get; set; } = new();
}
public class PlanMetricRow
{
public int PlanId { get; set; }
public string PlanName { get; set; } = string.Empty;
public int CompanyCount { get; set; }
public decimal MonthlyPrice { get; set; }
public decimal Revenue { get; set; }
}
public class MrrTrendPoint
{
public string Month { get; set; } = string.Empty;
public decimal Mrr { get; set; }
public int CompanyCount { get; set; }
}
public class SubscriptionAlertRow
{
public int Id { get; set; }
public string CompanyName { get; set; } = string.Empty;
public string PlanName { get; set; } = string.Empty;
public SubscriptionStatus Status { get; set; }
public DateTime? EndDate { get; set; }
public int DaysUntilExpiry { get; set; }
public bool IsComped { get; set; }
}