Phase 2: Eliminate ApplicationDbContext from domain controllers
Migrated InvoicesController, QuotesController, JobsController, BillsController, PurchaseOrdersController, and CustomersController to route all data access through IUnitOfWork typed/generic repositories instead of injecting ApplicationDbContext directly. New typed repositories added: IJobRepository (GetScheduledJobsForDateAsync, GetActiveJobsForMobileAsync, LoadForCostingAsync), INotificationLogRepository (GetLatestForJobAsync, GetAllForJobAsync), IQuoteRepository (GetItemsWithCoatsAsync with CatalogItem eager load + AsNoTracking), and IJobRepository.GetOrphanedConversionJobAsync. All EF complex include chains relocated into repository methods; controllers now call named query methods rather than composing raw IQueryable chains. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Enums;
|
||||
using PowderCoating.Core.Interfaces.Repositories;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
|
||||
@@ -37,4 +38,55 @@ public class BillRepository : Repository<Bill>, IBillRepository
|
||||
.Include(b => b.LineItems.Where(li => !li.IsDeleted))
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<Bill>> GetForIndexAsync(string? statusFilter, string? searchTerm, decimal? searchAmount)
|
||||
{
|
||||
var query = _context.Bills
|
||||
.Include(b => b.Vendor)
|
||||
.Include(b => b.LineItems.Where(li => !li.IsDeleted))
|
||||
.Where(b => !b.IsDeleted);
|
||||
|
||||
if (statusFilter == "Unpaid")
|
||||
query = query.Where(b => b.Status == BillStatus.Open || b.Status == BillStatus.PartiallyPaid);
|
||||
else if (statusFilter == "Overdue")
|
||||
query = query.Where(b => b.Status != BillStatus.Paid && b.Status != BillStatus.Voided &&
|
||||
b.DueDate.HasValue && b.DueDate.Value.Date < DateTime.Today);
|
||||
|
||||
if (!string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
var term = searchTerm;
|
||||
query = query.Where(b =>
|
||||
b.BillNumber.Contains(term) ||
|
||||
b.Vendor.CompanyName.Contains(term) ||
|
||||
(b.VendorInvoiceNumber != null && b.VendorInvoiceNumber.Contains(term)) ||
|
||||
(b.Memo != null && b.Memo.Contains(term)) ||
|
||||
b.LineItems.Any(li => li.Description.Contains(term)) ||
|
||||
(searchAmount.HasValue && (b.Total == searchAmount.Value || b.AmountPaid == searchAmount.Value)));
|
||||
}
|
||||
|
||||
return await query.OrderByDescending(b => b.BillDate).ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<string?> GetLastBillNumberAsync(string prefix)
|
||||
{
|
||||
return await _context.Bills
|
||||
.IgnoreQueryFilters()
|
||||
.Where(b => b.BillNumber.StartsWith(prefix))
|
||||
.OrderByDescending(b => b.BillNumber)
|
||||
.Select(b => b.BillNumber)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<string?> GetLastPaymentNumberAsync(string prefix)
|
||||
{
|
||||
return await _context.BillPayments
|
||||
.IgnoreQueryFilters()
|
||||
.Where(p => p.PaymentNumber.StartsWith(prefix))
|
||||
.OrderByDescending(p => p.PaymentNumber)
|
||||
.Select(p => p.PaymentNumber)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Enums;
|
||||
using PowderCoating.Core.Interfaces.Repositories;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
|
||||
@@ -59,4 +60,45 @@ public class InvoiceRepository : Repository<Invoice>, IInvoiceRepository
|
||||
.Include(i => i.InvoiceItems.Where(ii => !ii.IsDeleted))
|
||||
.FirstOrDefaultAsync(i => i.PaymentLinkToken == token && !i.IsDeleted);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<string?> GetLastInvoiceNumberByPrefixAsync(int companyId, string prefix)
|
||||
{
|
||||
return await _context.Set<Invoice>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(i => i.CompanyId == companyId && i.InvoiceNumber.StartsWith(prefix))
|
||||
.OrderByDescending(i => i.InvoiceNumber)
|
||||
.Select(i => i.InvoiceNumber)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<Invoice>> GetOnlineInvoicesForPeriodAsync(int companyId, DateTime from, DateTime to)
|
||||
{
|
||||
return await _context.Set<Invoice>()
|
||||
.AsNoTracking()
|
||||
.Include(i => i.Customer)
|
||||
.Where(i => !i.IsDeleted
|
||||
&& i.CompanyId == companyId
|
||||
&& i.OnlineAmountPaid > 0
|
||||
&& i.UpdatedAt >= from
|
||||
&& i.UpdatedAt < to)
|
||||
.OrderByDescending(i => i.UpdatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<Refund>> GetOnlineRefundsForPeriodAsync(int companyId, DateTime from, DateTime to)
|
||||
{
|
||||
return await _context.Set<Refund>()
|
||||
.AsNoTracking()
|
||||
.Include(r => r.Invoice).ThenInclude(inv => inv!.Customer)
|
||||
.Where(r => !r.IsDeleted
|
||||
&& r.CompanyId == companyId
|
||||
&& r.RefundMethod == PaymentMethod.CreditDebitCard
|
||||
&& r.RefundDate >= from
|
||||
&& r.RefundDate < to)
|
||||
.OrderByDescending(r => r.RefundDate)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,4 +103,76 @@ public class JobRepository : Repository<Job>, IJobRepository
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<string?> GetLastJobNumberByPrefixAsync(int companyId, string prefix)
|
||||
{
|
||||
return await _context.Jobs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(j => j.CompanyId == companyId && j.JobNumber.StartsWith(prefix))
|
||||
.OrderByDescending(j => j.JobNumber)
|
||||
.Select(j => j.JobNumber)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Job?> GetOrphanedConversionJobAsync(int quoteId, int companyId)
|
||||
{
|
||||
return await _context.Jobs
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(j => j.QuoteId == quoteId && j.CompanyId == companyId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<Job>> GetScheduledJobsForDateAsync(DateTime date, string? userId = null)
|
||||
{
|
||||
var query = _context.Jobs
|
||||
.Include(j => j.Customer)
|
||||
.Include(j => j.JobStatus)
|
||||
.Include(j => j.JobPriority)
|
||||
.Include(j => j.AssignedUser)
|
||||
.Include(j => j.JobItems.Where(i => !i.IsDeleted))
|
||||
.ThenInclude(i => i.Coats)
|
||||
.Where(j => j.ScheduledDate.HasValue && j.ScheduledDate.Value.Date == date.Date
|
||||
&& !j.IsDeleted && !j.JobStatus.IsTerminalStatus);
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
query = query.Where(j => j.AssignedUserId == userId);
|
||||
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<Job>> GetActiveJobsForMobileAsync(int companyId, string? workerId = null)
|
||||
{
|
||||
var query = _context.Jobs
|
||||
.Include(j => j.Customer)
|
||||
.Include(j => j.JobStatus)
|
||||
.Include(j => j.JobPriority)
|
||||
.Include(j => j.AssignedUser)
|
||||
.Include(j => j.JobItems.Where(i => !i.IsDeleted))
|
||||
.ThenInclude(i => i.Coats)
|
||||
.Where(j => j.CompanyId == companyId && !j.IsDeleted && !j.JobStatus.IsTerminalStatus
|
||||
&& j.JobStatus.StatusCode != "ON_HOLD" && j.JobStatus.StatusCode != "CANCELLED");
|
||||
|
||||
if (!string.IsNullOrEmpty(workerId))
|
||||
query = query.Where(j => j.AssignedUserId == workerId);
|
||||
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Job?> LoadForCostingAsync(int jobId, int companyId)
|
||||
{
|
||||
return await _context.Jobs
|
||||
.Where(j => j.Id == jobId && j.CompanyId == companyId && !j.IsDeleted)
|
||||
.Include(j => j.OvenCost)
|
||||
.Include(j => j.Invoice)
|
||||
.Include(j => j.JobItems.Where(i => !i.IsDeleted))
|
||||
.ThenInclude(i => i.Coats.Where(c => !c.IsDeleted))
|
||||
.Include(j => j.TimeEntries.Where(t => !t.IsDeleted))
|
||||
.ThenInclude(t => t.Worker)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Interfaces.Repositories;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
|
||||
namespace PowderCoating.Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Typed repository for <see cref="NotificationLog"/> that provides IgnoreQueryFilters-based
|
||||
/// lookups by entity FK (InvoiceId, QuoteId, JobId).
|
||||
/// </summary>
|
||||
public class NotificationLogRepository : Repository<NotificationLog>, INotificationLogRepository
|
||||
{
|
||||
public NotificationLogRepository(ApplicationDbContext context) : base(context) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<NotificationLog?> GetLatestForInvoiceAsync(int invoiceId) =>
|
||||
await _context.Set<NotificationLog>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.InvoiceId == invoiceId)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<NotificationLog>> GetAllForInvoiceAsync(int invoiceId) =>
|
||||
await _context.Set<NotificationLog>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.InvoiceId == invoiceId)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.ToListAsync();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<NotificationLog?> GetLatestForQuoteAsync(int quoteId) =>
|
||||
await _context.Set<NotificationLog>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.QuoteId == quoteId)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<NotificationLog>> GetAllForQuoteAsync(int quoteId) =>
|
||||
await _context.Set<NotificationLog>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.QuoteId == quoteId)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.ToListAsync();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<NotificationLog?> GetLatestForJobAsync(int jobId) =>
|
||||
await _context.Set<NotificationLog>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.JobId == jobId)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<NotificationLog>> GetAllForJobAsync(int jobId) =>
|
||||
await _context.Set<NotificationLog>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.JobId == jobId)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
@@ -26,6 +26,29 @@ public class PurchaseOrderRepository : Repository<PurchaseOrder>, IPurchaseOrder
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<PurchaseOrderStats> GetStatsAsync(int companyId)
|
||||
{
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var items = await _context.Set<PurchaseOrder>()
|
||||
.Where(po => !po.IsDeleted && po.CompanyId == companyId)
|
||||
.Select(po => new { po.Status, po.TotalAmount, po.ExpectedDeliveryDate })
|
||||
.ToListAsync();
|
||||
|
||||
return new PurchaseOrderStats(
|
||||
TotalCount: items.Count,
|
||||
OpenCount: items.Count(p =>
|
||||
p.Status == PurchaseOrderStatus.Draft ||
|
||||
p.Status == PurchaseOrderStatus.Submitted ||
|
||||
p.Status == PurchaseOrderStatus.PartiallyReceived),
|
||||
CommittedValue: items.Where(p => p.Status != PurchaseOrderStatus.Cancelled).Sum(p => p.TotalAmount),
|
||||
OverdueCount: items.Count(p =>
|
||||
(p.Status == PurchaseOrderStatus.Draft || p.Status == PurchaseOrderStatus.Submitted || p.Status == PurchaseOrderStatus.PartiallyReceived) &&
|
||||
p.ExpectedDeliveryDate.HasValue &&
|
||||
p.ExpectedDeliveryDate.Value.Date < today)
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<(List<PurchaseOrder> Items, int TotalCount)> GetPagedAsync(
|
||||
int companyId,
|
||||
|
||||
@@ -67,4 +67,45 @@ public class QuoteRepository : Repository<Quote>, IQuoteRepository
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<QuoteIndexStats> GetIndexStatsAsync(List<int> openStatusIds, List<int> approvedConvertedStatusIds)
|
||||
{
|
||||
var stats = await _context.Quotes
|
||||
.Where(q => !q.IsDeleted)
|
||||
.Select(q => new { q.QuoteStatusId, q.Total })
|
||||
.ToListAsync();
|
||||
|
||||
return new QuoteIndexStats(
|
||||
OpenCount: stats.Count(q => openStatusIds.Contains(q.QuoteStatusId)),
|
||||
ApprovedConvertedCount: stats.Count(q => approvedConvertedStatusIds.Contains(q.QuoteStatusId)),
|
||||
TotalValue: stats.Sum(q => q.Total));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<QuoteItem>> GetItemsWithCoatsAsync(int quoteId)
|
||||
{
|
||||
return await _context.QuoteItems
|
||||
.Where(qi => qi.QuoteId == quoteId && !qi.IsDeleted)
|
||||
.Include(qi => qi.Coats)
|
||||
.ThenInclude(c => c.InventoryItem)
|
||||
.Include(qi => qi.Coats)
|
||||
.ThenInclude(c => c.Vendor)
|
||||
.Include(qi => qi.CatalogItem)
|
||||
.Include(qi => qi.PrepServices)
|
||||
.ThenInclude(ps => ps.PrepService)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<string?> GetLastQuoteNumberByPrefixAsync(int companyId, string prefix)
|
||||
{
|
||||
return await _context.Quotes
|
||||
.IgnoreQueryFilters()
|
||||
.Where(q => q.CompanyId == companyId && q.QuoteNumber.StartsWith(prefix))
|
||||
.OrderByDescending(q => q.QuoteNumber)
|
||||
.Select(q => q.QuoteNumber)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,9 @@ public class UnitOfWork : IUnitOfWork
|
||||
private IRepository<JobDailyPriority>? _jobDailyPriorities;
|
||||
private IRepository<JobItem>? _jobItems;
|
||||
private IRepository<JobItemCoat>? _jobItemCoats;
|
||||
private IRepository<JobItemPrepService>? _jobItemPrepServices;
|
||||
private IRepository<JobChangeHistory>? _jobChangeHistories;
|
||||
private IRepository<JobPrepService>? _jobPrepServices;
|
||||
private IQuoteRepository? _quotes;
|
||||
private IRepository<QuotePhoto>? _quotePhotos;
|
||||
private IRepository<QuoteItem>? _quoteItems;
|
||||
@@ -88,7 +90,7 @@ public class UnitOfWork : IUnitOfWork
|
||||
private IRepository<CatalogPriceCheckReport>? _catalogPriceCheckReports;
|
||||
|
||||
// Notifications
|
||||
private IRepository<NotificationLog>? _notificationLogs;
|
||||
private INotificationLogRepository? _notificationLogs;
|
||||
private IRepository<NotificationTemplate>? _notificationTemplates;
|
||||
|
||||
// Subscription
|
||||
@@ -190,11 +192,17 @@ public class UnitOfWork : IUnitOfWork
|
||||
/// <summary>Repository for <see cref="JobItemCoat"/> powder coat passes; tenant-filtered with soft delete.</summary>
|
||||
public IRepository<JobItemCoat> JobItemCoats =>
|
||||
_jobItemCoats ??= new Repository<JobItemCoat>(_context);
|
||||
public IRepository<JobItemPrepService> JobItemPrepServices =>
|
||||
_jobItemPrepServices ??= new Repository<JobItemPrepService>(_context);
|
||||
|
||||
/// <summary>Repository for <see cref="JobChangeHistory"/> audit entries; tenant-filtered with soft delete.</summary>
|
||||
public IRepository<JobChangeHistory> JobChangeHistories =>
|
||||
_jobChangeHistories ??= new Repository<JobChangeHistory>(_context);
|
||||
|
||||
/// <summary>Repository for <see cref="JobPrepService"/> job-level prep service assignments; tenant-filtered with soft delete.</summary>
|
||||
public IRepository<JobPrepService> JobPrepServices =>
|
||||
_jobPrepServices ??= new Repository<JobPrepService>(_context);
|
||||
|
||||
/// <summary>Repository for <see cref="Quote"/> records with multi-item pricing; tenant-filtered with soft delete.</summary>
|
||||
public IQuoteRepository Quotes =>
|
||||
_quotes ??= new QuoteRepository(_context);
|
||||
@@ -351,9 +359,9 @@ public class UnitOfWork : IUnitOfWork
|
||||
_catalogPriceCheckReports ??= new Repository<CatalogPriceCheckReport>(_context);
|
||||
|
||||
// Notifications
|
||||
/// <summary>Repository for <see cref="NotificationLog"/> outbound notification audit records; tenant-filtered with soft delete.</summary>
|
||||
public IRepository<NotificationLog> NotificationLogs =>
|
||||
_notificationLogs ??= new Repository<NotificationLog>(_context);
|
||||
/// <summary>Repository for <see cref="NotificationLog"/> outbound notification audit records; provides IgnoreQueryFilters lookups by InvoiceId, QuoteId, and JobId for notification history panels.</summary>
|
||||
public INotificationLogRepository NotificationLogs =>
|
||||
_notificationLogs ??= new NotificationLogRepository(_context);
|
||||
|
||||
/// <summary>Repository for <see cref="NotificationTemplate"/> per-company channel template overrides; unique on (CompanyId, Type, Channel).</summary>
|
||||
public IRepository<NotificationTemplate> NotificationTemplates =>
|
||||
|
||||
Reference in New Issue
Block a user