Files
PowderCoatingLogix/src/PowderCoating.Infrastructure/Services/AiUsageReportService.cs
T
spouliot 1cb7a8ca4a Phases 3 & 4: Complete data access architecture migration
Phase 3 — eliminated ApplicationDbContext from all non-exempt controllers,
routing all data access through IUnitOfWork. Added IPlainRepository<T> for
the four platform entities (Announcement, BannedIp, DashboardTip, ReleaseNote)
that intentionally don't extend BaseEntity and therefore can't use the
constrained IRepository<T>. Added permanent-exception comments to the 18
controllers that legitimately retain direct DbContext access (Identity infra,
cross-tenant platform ops, bulk streaming exports).

Phase 4 — added EnforceDataAccessArchitecture() to Program.cs, a startup
gate that reflects over every Controller subclass and throws at boot if any
non-exempt controller injects ApplicationDbContext. The app cannot start with
a violation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:17:29 -04:00

56 lines
2.1 KiB
C#

using Microsoft.EntityFrameworkCore;
using PowderCoating.Application.Interfaces;
using PowderCoating.Infrastructure.Data;
namespace PowderCoating.Infrastructure.Services;
/// <summary>
/// Implements <see cref="IAiUsageReportService"/> by querying <c>AiUsageLogs</c> and
/// <c>QuotePhotos</c> directly via <c>ApplicationDbContext</c>. Both tables either are
/// not BaseEntity (AiUsageLog) or require cross-tenant GROUP BY aggregations that must
/// execute in SQL; this service encapsulates those queries so the controller stays clean.
/// </summary>
public class AiUsageReportService : IAiUsageReportService
{
private readonly ApplicationDbContext _context;
public AiUsageReportService(ApplicationDbContext context)
{
_context = context;
}
/// <inheritdoc/>
public async Task<AiUsageReportData> GetReportDataAsync()
{
var now = DateTime.UtcNow;
var todayStart = now.Date;
var last7Start = todayStart.AddDays(-7);
var last30Start = todayStart.AddDays(-30);
var usageByCompany = await _context.AiUsageLogs
.GroupBy(l => l.CompanyId)
.Select(g => new AiCompanyUsage(
g.Key,
g.Count(l => l.CalledAt >= todayStart),
g.Count(l => l.CalledAt >= last7Start),
g.Count(l => l.CalledAt >= last30Start),
g.Count()))
.ToListAsync();
var featureStats = await _context.AiUsageLogs
.Where(l => l.CalledAt >= last30Start)
.GroupBy(l => new { l.CompanyId, l.Feature })
.Select(g => new AiFeatureStat(g.Key.CompanyId, g.Key.Feature, g.Count()))
.ToListAsync();
var photoCounts = await _context.QuotePhotos
.IgnoreQueryFilters()
.Where(p => p.IsAiAnalysisPhoto && !p.IsDeleted)
.GroupBy(p => p.CompanyId)
.Select(g => new { CompanyId = g.Key, Count = g.Count() })
.ToDictionaryAsync(x => x.CompanyId, x => x.Count);
return new AiUsageReportData(usageByCompany, featureStats, photoCounts);
}
}