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:
2026-04-27 21:20:39 -04:00
parent 80b0e547cc
commit 90bc0d965f
20 changed files with 730 additions and 878 deletions
@@ -3,14 +3,13 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using PowderCoating.Application.DTOs.Common;
using PowderCoating.Application.DTOs.PurchaseOrder;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using PowderCoating.Infrastructure.Data;
using PowderCoating.Core.Interfaces.Repositories;
using PowderCoating.Shared.Constants;
using PowderCoating.Web.Helpers;
@@ -23,7 +22,6 @@ public class PurchaseOrdersController : Controller
private readonly IMapper _mapper;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<PurchaseOrdersController> _logger;
private readonly ApplicationDbContext _context;
private readonly IPdfService _pdfService;
public PurchaseOrdersController(
@@ -31,14 +29,12 @@ public class PurchaseOrdersController : Controller
IMapper mapper,
UserManager<ApplicationUser> userManager,
ILogger<PurchaseOrdersController> logger,
ApplicationDbContext context,
IPdfService pdfService)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_userManager = userManager;
_logger = logger;
_context = context;
_pdfService = pdfService;
}
@@ -69,55 +65,10 @@ public class PurchaseOrdersController : Controller
pageSize = Math.Clamp(pageSize, 10, 100);
pageNumber = Math.Max(1, pageNumber);
var query = _context.Set<PurchaseOrder>()
.Include(po => po.Vendor)
.Include(po => po.Items.Where(i => !i.IsDeleted))
.Where(po => !po.IsDeleted && po.CompanyId == currentUser.CompanyId)
.AsQueryable();
if (statusFilter.HasValue)
query = query.Where(po => po.Status == statusFilter.Value);
if (vendorId.HasValue)
query = query.Where(po => po.VendorId == vendorId.Value);
if (dateFrom.HasValue)
query = query.Where(po => po.OrderDate >= dateFrom.Value);
if (dateTo.HasValue)
query = query.Where(po => po.OrderDate <= dateTo.Value.AddDays(1));
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var term = searchTerm.Trim().ToLower();
query = query.Where(po =>
po.PoNumber.ToLower().Contains(term) ||
po.Vendor.CompanyName.ToLower().Contains(term) ||
(po.Notes != null && po.Notes.ToLower().Contains(term)));
}
query = (sortColumn?.ToLower(), sortDirection?.ToLower()) switch
{
("ponumber", "asc") => query.OrderBy(po => po.PoNumber),
("ponumber", _) => query.OrderByDescending(po => po.PoNumber),
("vendor", "asc") => query.OrderBy(po => po.Vendor.CompanyName),
("vendor", _) => query.OrderByDescending(po => po.Vendor.CompanyName),
("status", "asc") => query.OrderBy(po => po.Status),
("status", _) => query.OrderByDescending(po => po.Status),
("orderdate", "asc") => query.OrderBy(po => po.OrderDate),
("orderdate", _) => query.OrderByDescending(po => po.OrderDate),
("expected", "asc") => query.OrderBy(po => po.ExpectedDeliveryDate),
("expected", _) => query.OrderByDescending(po => po.ExpectedDeliveryDate),
("total", "asc") => query.OrderBy(po => po.TotalAmount),
("total", _) => query.OrderByDescending(po => po.TotalAmount),
_ => query.OrderByDescending(po => po.OrderDate)
};
var totalCount = await query.CountAsync();
var items = await query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var (items, totalCount) = await _unitOfWork.PurchaseOrders.GetPagedAsync(
currentUser.CompanyId, pageNumber, pageSize,
statusFilter, vendorId, dateFrom, dateTo,
searchTerm, sortColumn, sortDirection);
var dtos = _mapper.Map<List<PurchaseOrderListDto>>(items);
@@ -129,24 +80,12 @@ public class PurchaseOrdersController : Controller
PageSize = pageSize
};
// Stats
var allForStats = await _context.Set<PurchaseOrder>()
.Where(po => !po.IsDeleted && po.CompanyId == currentUser.CompanyId)
.Select(po => new { po.Status, po.TotalAmount, po.ExpectedDeliveryDate })
.ToListAsync();
ViewBag.TotalCount = allForStats.Count;
ViewBag.OpenCount = allForStats.Count(p =>
p.Status == PurchaseOrderStatus.Draft ||
p.Status == PurchaseOrderStatus.Submitted ||
p.Status == PurchaseOrderStatus.PartiallyReceived);
ViewBag.CommittedValue = allForStats
.Where(p => p.Status != PurchaseOrderStatus.Cancelled)
.Sum(p => p.TotalAmount);
ViewBag.OverdueCount = allForStats.Count(p =>
(p.Status == PurchaseOrderStatus.Draft || p.Status == PurchaseOrderStatus.Submitted || p.Status == PurchaseOrderStatus.PartiallyReceived)
&& p.ExpectedDeliveryDate.HasValue
&& p.ExpectedDeliveryDate.Value.Date < DateTime.UtcNow.Date);
// Stats (server-side projection — only three columns fetched)
var stats = await _unitOfWork.PurchaseOrders.GetStatsAsync(currentUser.CompanyId);
ViewBag.TotalCount = stats.TotalCount;
ViewBag.OpenCount = stats.OpenCount;
ViewBag.CommittedValue = stats.CommittedValue;
ViewBag.OverdueCount = stats.OverdueCount;
await PopulateVendorFilterDropdownAsync(currentUser.CompanyId);
ViewBag.SearchTerm = searchTerm;
@@ -172,13 +111,7 @@ public class PurchaseOrdersController : Controller
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Unauthorized();
var po = await _context.Set<PurchaseOrder>()
.Include(p => p.Vendor)
.Include(p => p.Bill)
.Include(p => p.Items.Where(i => !i.IsDeleted))
.ThenInclude(i => i.InventoryItem)
.FirstOrDefaultAsync(p => p.Id == id && !p.IsDeleted && p.CompanyId == currentUser.CompanyId);
var po = await _unitOfWork.PurchaseOrders.LoadForViewAsync(id, currentUser.CompanyId);
if (po == null) return NotFound();
var dto = _mapper.Map<PurchaseOrderDto>(po);
@@ -283,10 +216,7 @@ public class PurchaseOrdersController : Controller
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Unauthorized();
var po = await _context.Set<PurchaseOrder>()
.Include(p => p.Items.Where(i => !i.IsDeleted))
.FirstOrDefaultAsync(p => p.Id == id && !p.IsDeleted && p.CompanyId == currentUser.CompanyId);
var po = await _unitOfWork.PurchaseOrders.LoadForViewAsync(id, currentUser.CompanyId);
if (po == null) return NotFound();
if (po.Status != PurchaseOrderStatus.Draft)
{
@@ -335,10 +265,7 @@ public class PurchaseOrdersController : Controller
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Unauthorized();
var po = await _context.Set<PurchaseOrder>()
.Include(p => p.Items.Where(i => !i.IsDeleted))
.FirstOrDefaultAsync(p => p.Id == id && !p.IsDeleted && p.CompanyId == currentUser.CompanyId);
var po = await _unitOfWork.PurchaseOrders.LoadForViewAsync(id, currentUser.CompanyId);
if (po == null) return NotFound();
if (po.Status != PurchaseOrderStatus.Draft)
{
@@ -416,10 +343,7 @@ public class PurchaseOrdersController : Controller
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Unauthorized();
var po = await _context.Set<PurchaseOrder>()
.Include(p => p.Vendor)
.FirstOrDefaultAsync(p => p.Id == id && !p.IsDeleted && p.CompanyId == currentUser.CompanyId);
var po = await _unitOfWork.PurchaseOrders.LoadForViewAsync(id, currentUser.CompanyId);
if (po == null) return NotFound();
if (po.Status != PurchaseOrderStatus.Draft && po.Status != PurchaseOrderStatus.Cancelled)
@@ -487,10 +411,7 @@ public class PurchaseOrdersController : Controller
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Unauthorized();
var po = await _context.Set<PurchaseOrder>()
.Include(p => p.Items.Where(i => !i.IsDeleted))
.FirstOrDefaultAsync(p => p.Id == id && !p.IsDeleted && p.CompanyId == currentUser.CompanyId);
var po = await _unitOfWork.PurchaseOrders.LoadForViewAsync(id, currentUser.CompanyId);
if (po == null) return NotFound();
if (po.Status != PurchaseOrderStatus.Draft)
@@ -560,11 +481,7 @@ public class PurchaseOrdersController : Controller
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Unauthorized();
var po = await _context.Set<PurchaseOrder>()
.Include(p => p.Vendor)
.Include(p => p.Items.Where(i => !i.IsDeleted))
.ThenInclude(i => i.InventoryItem)
.FirstOrDefaultAsync(p => p.Id == id && !p.IsDeleted && p.CompanyId == currentUser.CompanyId);
var po = await _unitOfWork.PurchaseOrders.LoadForViewAsync(id, currentUser.CompanyId);
if (po == null) return NotFound();
@@ -618,11 +535,7 @@ public class PurchaseOrdersController : Controller
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Unauthorized();
var po = await _context.Set<PurchaseOrder>()
.Include(p => p.Vendor)
.Include(p => p.Items.Where(i => !i.IsDeleted))
.ThenInclude(i => i.InventoryItem)
.FirstOrDefaultAsync(p => p.Id == id && !p.IsDeleted && p.CompanyId == currentUser.CompanyId);
var po = await _unitOfWork.PurchaseOrders.LoadForViewAsync(id, currentUser.CompanyId);
if (po == null) return NotFound();
@@ -692,7 +605,7 @@ public class PurchaseOrdersController : Controller
CompanyId = po.CompanyId
};
await _context.Set<InventoryTransaction>().AddAsync(transaction);
await _unitOfWork.InventoryTransactions.AddAsync(transaction);
}
}
@@ -720,9 +633,8 @@ public class PurchaseOrdersController : Controller
}
po.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
}); // end ExecuteInTransactionAsync
}); // end ExecuteInTransactionAsync — SaveChangesAsync called automatically before commit
this.ToastSuccess(allReceived
? $"All items received for {po.PoNumber}."
@@ -754,11 +666,7 @@ public class PurchaseOrdersController : Controller
try
{
var po = await _context.Set<PurchaseOrder>()
.Include(p => p.Vendor)
.Include(p => p.Items.Where(i => !i.IsDeleted))
.ThenInclude(i => i.InventoryItem)
.FirstOrDefaultAsync(p => p.Id == id && !p.IsDeleted && p.CompanyId == currentUser.CompanyId);
var po = await _unitOfWork.PurchaseOrders.LoadForViewAsync(id, currentUser.CompanyId);
if (po == null) return NotFound();
@@ -806,14 +714,12 @@ public class PurchaseOrdersController : Controller
if (currentUser == null) return Unauthorized();
// Find low-stock items that have a primary vendor
var lowStockItems = await _context.Set<InventoryItem>()
.Include(i => i.PrimaryVendor)
.Where(i => !i.IsDeleted
&& i.IsActive
&& i.CompanyId == currentUser.CompanyId
&& i.PrimaryVendorId != null
&& i.QuantityOnHand <= i.ReorderPoint)
.ToListAsync();
var lowStockItems = (await _unitOfWork.InventoryItems.FindAsync(
i => i.IsActive && i.CompanyId == currentUser.CompanyId &&
i.PrimaryVendorId != null && i.QuantityOnHand <= i.ReorderPoint,
false,
i => i.PrimaryVendor!))
.ToList();
if (!lowStockItems.Any())
{
@@ -878,11 +784,10 @@ public class PurchaseOrdersController : Controller
{
var prefix = $"PO-{DateTime.UtcNow:yy}{DateTime.UtcNow.Month:D2}-";
var existing = await _context.Set<PurchaseOrder>()
.IgnoreQueryFilters()
.Where(po => po.CompanyId == companyId && po.PoNumber.StartsWith(prefix))
.Select(po => po.PoNumber)
.ToListAsync();
var existingPos = await _unitOfWork.PurchaseOrders.FindAsync(
po => po.CompanyId == companyId && po.PoNumber.StartsWith(prefix),
ignoreQueryFilters: true);
var existing = existingPos.Select(po => po.PoNumber).ToList();
var maxNum = 0;
foreach (var num in existing)
@@ -903,17 +808,18 @@ public class PurchaseOrdersController : Controller
/// </summary>
private async Task PopulateCreateViewBagAsync(int companyId)
{
var vendors = await _context.Set<Vendor>()
.Where(v => !v.IsDeleted && v.CompanyId == companyId && v.IsActive)
var vendorEntities = await _unitOfWork.Vendors.FindAsync(
v => v.CompanyId == companyId && v.IsActive);
var vendors = vendorEntities
.OrderBy(v => v.CompanyName)
.Select(v => new SelectListItem(v.CompanyName, v.Id.ToString()))
.ToListAsync();
.ToList();
vendors.Insert(0, new SelectListItem("— Select Vendor —", ""));
ViewBag.Vendors = vendors;
var inventoryItems = await _context.Set<InventoryItem>()
.Where(i => !i.IsDeleted && i.CompanyId == companyId && i.IsActive)
var inventoryEntities = await _unitOfWork.InventoryItems.FindAsync(
i => i.CompanyId == companyId && i.IsActive);
var inventoryItems = inventoryEntities
.OrderBy(i => i.Name)
.Select(i => new
{
@@ -922,8 +828,7 @@ public class PurchaseOrdersController : Controller
uom = i.UnitOfMeasure ?? "units",
cost = i.LastPurchasePrice > 0 ? i.LastPurchasePrice : i.UnitCost
})
.ToListAsync();
.ToList();
ViewBag.InventoryItemsJson = System.Text.Json.JsonSerializer.Serialize(inventoryItems);
}
@@ -934,12 +839,11 @@ public class PurchaseOrdersController : Controller
/// </summary>
private async Task PopulateVendorFilterDropdownAsync(int companyId)
{
var vendors = await _context.Set<Vendor>()
.Where(v => !v.IsDeleted && v.CompanyId == companyId)
var vendorEntities = await _unitOfWork.Vendors.FindAsync(v => v.CompanyId == companyId);
var vendors = vendorEntities
.OrderBy(v => v.CompanyName)
.Select(v => new SelectListItem(v.CompanyName, v.Id.ToString()))
.ToListAsync();
.ToList();
vendors.Insert(0, new SelectListItem("All Vendors", ""));
ViewBag.VendorList = vendors;
}