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:
@@ -14,7 +14,6 @@ using PowderCoating.Application.Interfaces;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Enums;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
using PowderCoating.Application.DTOs.PurchaseOrder;
|
||||
|
||||
namespace PowderCoating.Web.Controllers;
|
||||
@@ -29,7 +28,6 @@ public class BillsController : Controller
|
||||
private readonly IMapper _mapper;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ILogger<BillsController> _logger;
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IAccountBalanceService _accountBalanceService;
|
||||
private readonly IAccountingAiService _accountingAi;
|
||||
private readonly IAzureBlobStorageService _blobStorage;
|
||||
@@ -41,7 +39,6 @@ public class BillsController : Controller
|
||||
IMapper mapper,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ILogger<BillsController> logger,
|
||||
ApplicationDbContext context,
|
||||
IAccountBalanceService accountBalanceService,
|
||||
IAccountingAiService accountingAi,
|
||||
IAzureBlobStorageService blobStorage,
|
||||
@@ -52,7 +49,6 @@ public class BillsController : Controller
|
||||
_mapper = mapper;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_context = context;
|
||||
_accountBalanceService = accountBalanceService;
|
||||
_accountingAi = accountingAi;
|
||||
_blobStorage = blobStorage;
|
||||
@@ -86,23 +82,7 @@ public class BillsController : Controller
|
||||
// Bills
|
||||
if (type == null || type == "Bill")
|
||||
{
|
||||
var bills = await _context.Bills
|
||||
.Include(b => b.Vendor)
|
||||
.Where(b => !b.IsDeleted)
|
||||
.Where(b => status != "Unpaid" ||
|
||||
(b.Status == BillStatus.Open || b.Status == BillStatus.PartiallyPaid))
|
||||
.Where(b => status != "Overdue" ||
|
||||
(b.Status != BillStatus.Paid && b.Status != BillStatus.Voided &&
|
||||
b.DueDate.HasValue && b.DueDate.Value.Date < DateTime.Today))
|
||||
.Where(b => string.IsNullOrEmpty(search) ||
|
||||
b.BillNumber.Contains(search) ||
|
||||
b.Vendor.CompanyName.Contains(search) ||
|
||||
(b.VendorInvoiceNumber != null && b.VendorInvoiceNumber.Contains(search)) ||
|
||||
(b.Memo != null && b.Memo.Contains(search)) ||
|
||||
b.LineItems.Any(li => li.Description.Contains(search)) ||
|
||||
(searchAmount.HasValue && (b.Total == searchAmount.Value || b.AmountPaid == searchAmount.Value)))
|
||||
.OrderByDescending(b => b.BillDate)
|
||||
.ToListAsync();
|
||||
var bills = await _unitOfWork.Bills.GetForIndexAsync(status, search, searchAmount);
|
||||
|
||||
entries.AddRange(bills.Select(b => new BillExpenseListDto
|
||||
{
|
||||
@@ -133,17 +113,17 @@ public class BillsController : Controller
|
||||
// Expenses are always fully paid — exclude when filtering to unpaid/overdue bills only
|
||||
if ((type == null || type == "Expense") && status != "Unpaid" && status != "Overdue")
|
||||
{
|
||||
var expenses = await _context.Set<Core.Entities.Expense>()
|
||||
.Include(e => e.Vendor)
|
||||
.Include(e => e.ExpenseAccount)
|
||||
.Where(e => !e.IsDeleted)
|
||||
.Where(e => string.IsNullOrEmpty(search) ||
|
||||
e.ExpenseNumber.Contains(search) ||
|
||||
(e.Vendor != null && e.Vendor.CompanyName.Contains(search)) ||
|
||||
(e.Memo != null && e.Memo.Contains(search)) ||
|
||||
(searchAmount.HasValue && e.Amount == searchAmount.Value))
|
||||
.OrderByDescending(e => e.Date)
|
||||
.ToListAsync();
|
||||
var expSearch = search;
|
||||
var expAmount = searchAmount;
|
||||
var expenseList = await _unitOfWork.Expenses.FindAsync(
|
||||
e => string.IsNullOrEmpty(expSearch) ||
|
||||
e.ExpenseNumber.Contains(expSearch) ||
|
||||
(e.Vendor != null && e.Vendor.CompanyName.Contains(expSearch)) ||
|
||||
(e.Memo != null && e.Memo.Contains(expSearch)) ||
|
||||
(expAmount.HasValue && e.Amount == expAmount.Value),
|
||||
false,
|
||||
e => e.Vendor!, e => e.ExpenseAccount!);
|
||||
var expenses = expenseList.OrderByDescending(e => e.Date).ToList();
|
||||
|
||||
entries.AddRange(expenses.Select(e => new BillExpenseListDto
|
||||
{
|
||||
@@ -198,11 +178,7 @@ public class BillsController : 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 == purchaseOrderId && !p.IsDeleted && p.CompanyId == currentUser.CompanyId);
|
||||
var po = await _unitOfWork.PurchaseOrders.LoadForViewAsync(purchaseOrderId, currentUser.CompanyId);
|
||||
|
||||
if (po == null) return NotFound();
|
||||
|
||||
@@ -218,20 +194,16 @@ public class BillsController : Controller
|
||||
return RedirectToAction(nameof(Details), new { id = po.BillId });
|
||||
}
|
||||
|
||||
var apAccount = await _context.Accounts
|
||||
.Where(a => !a.IsDeleted && a.AccountSubType == AccountSubType.AccountsPayable)
|
||||
.OrderBy(a => a.AccountNumber)
|
||||
.FirstOrDefaultAsync();
|
||||
var apAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.AccountSubType == AccountSubType.AccountsPayable);
|
||||
|
||||
// Vendor default expense account, fall back to first expense/COGS account
|
||||
int? defaultExpenseAccountId = po.Vendor?.DefaultExpenseAccountId;
|
||||
if (!defaultExpenseAccountId.HasValue)
|
||||
{
|
||||
defaultExpenseAccountId = (await _context.Accounts
|
||||
.Where(a => !a.IsDeleted && a.IsActive &&
|
||||
(a.AccountType == AccountType.Expense || a.AccountType == AccountType.CostOfGoods))
|
||||
.OrderBy(a => a.AccountNumber)
|
||||
.FirstOrDefaultAsync())?.Id;
|
||||
var fallbackAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.IsActive && (a.AccountType == AccountType.Expense || a.AccountType == AccountType.CostOfGoods));
|
||||
defaultExpenseAccountId = fallbackAccount?.Id;
|
||||
}
|
||||
|
||||
var lineItems = po.Items
|
||||
@@ -293,10 +265,8 @@ public class BillsController : Controller
|
||||
};
|
||||
|
||||
// Pre-fill AP account
|
||||
var apAccount = await _context.Accounts
|
||||
.Where(a => !a.IsDeleted && a.AccountSubType == AccountSubType.AccountsPayable)
|
||||
.OrderBy(a => a.AccountNumber)
|
||||
.FirstOrDefaultAsync();
|
||||
var apAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.AccountSubType == AccountSubType.AccountsPayable);
|
||||
dto.APAccountId = apAccount?.Id ?? 0;
|
||||
|
||||
// Pre-fill default expense account for vendor
|
||||
@@ -385,13 +355,12 @@ public class BillsController : Controller
|
||||
// Link bill back to source PO if created from one
|
||||
if (dto.PurchaseOrderId > 0)
|
||||
{
|
||||
var po = await _context.Set<PurchaseOrder>()
|
||||
.FirstOrDefaultAsync(p => p.Id == dto.PurchaseOrderId && !p.IsDeleted);
|
||||
var po = await _unitOfWork.PurchaseOrders.GetByIdAsync(dto.PurchaseOrderId!.Value);
|
||||
if (po != null)
|
||||
{
|
||||
po.BillId = bill.Id;
|
||||
po.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
await _unitOfWork.CompleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,8 +385,8 @@ public class BillsController : Controller
|
||||
bill.AmountPaid = payment.Amount;
|
||||
bill.Status = bill.AmountPaid >= bill.Total ? BillStatus.Paid : BillStatus.PartiallyPaid;
|
||||
|
||||
await _context.BillPayments.AddAsync(payment);
|
||||
await _context.SaveChangesAsync();
|
||||
await _unitOfWork.BillPayments.AddAsync(payment);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
TempData["Success"] = $"Bill {bill.BillNumber} saved and marked as paid.";
|
||||
}
|
||||
@@ -448,28 +417,18 @@ public class BillsController : Controller
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var bill = await _context.Bills
|
||||
.Include(b => b.Vendor)
|
||||
.Include(b => b.APAccount)
|
||||
.Include(b => b.LineItems.Where(li => !li.IsDeleted))
|
||||
.ThenInclude(li => li.Account)
|
||||
.Include(b => b.LineItems.Where(li => !li.IsDeleted))
|
||||
.ThenInclude(li => li.Job)
|
||||
.Include(b => b.Payments.Where(p => !p.IsDeleted))
|
||||
.ThenInclude(p => p.BankAccount)
|
||||
.FirstOrDefaultAsync(b => b.Id == id && !b.IsDeleted);
|
||||
|
||||
var bill = await _unitOfWork.Bills.LoadForViewAsync(id.Value);
|
||||
if (bill == null) return NotFound();
|
||||
|
||||
var dto = _mapper.Map<BillDto>(bill);
|
||||
|
||||
// Payment form defaults
|
||||
var bankAccounts = await _context.Accounts
|
||||
.Where(a => !a.IsDeleted && (a.AccountSubType == AccountSubType.Checking ||
|
||||
a.AccountSubType == AccountSubType.Savings ||
|
||||
a.AccountSubType == AccountSubType.CreditCard))
|
||||
var bankAccounts = (await _unitOfWork.Accounts.FindAsync(
|
||||
a => a.AccountSubType == AccountSubType.Checking ||
|
||||
a.AccountSubType == AccountSubType.Savings ||
|
||||
a.AccountSubType == AccountSubType.CreditCard))
|
||||
.OrderBy(a => a.AccountNumber)
|
||||
.ToListAsync();
|
||||
.ToList();
|
||||
|
||||
ViewBag.BankAccounts = bankAccounts
|
||||
.Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString()))
|
||||
@@ -495,10 +454,7 @@ public class BillsController : Controller
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var bill = await _context.Bills
|
||||
.Include(b => b.LineItems.Where(li => !li.IsDeleted))
|
||||
.FirstOrDefaultAsync(b => b.Id == id && !b.IsDeleted);
|
||||
|
||||
var bill = await _unitOfWork.Bills.LoadForEditAsync(id.Value);
|
||||
if (bill == null) return NotFound();
|
||||
|
||||
if (bill.Status == BillStatus.Paid || bill.Status == BillStatus.Voided)
|
||||
@@ -561,9 +517,7 @@ public class BillsController : Controller
|
||||
|
||||
try
|
||||
{
|
||||
var bill = await _context.Bills
|
||||
.Include(b => b.LineItems)
|
||||
.FirstOrDefaultAsync(b => b.Id == id && !b.IsDeleted);
|
||||
var bill = await _unitOfWork.Bills.LoadForEditAsync(id);
|
||||
|
||||
if (bill == null) return NotFound();
|
||||
|
||||
@@ -611,7 +565,7 @@ public class BillsController : Controller
|
||||
bill.TaxAmount = Math.Round(bill.SubTotal * (dto.TaxPercent / 100m), 2);
|
||||
bill.Total = bill.SubTotal + bill.TaxAmount;
|
||||
|
||||
await _context.BillLineItems.AddRangeAsync(newLineItems);
|
||||
await _unitOfWork.BillLineItems.AddRangeAsync(newLineItems);
|
||||
|
||||
// Handle receipt file replacement
|
||||
if (receiptFile != null && receiptFile.Length > 0)
|
||||
@@ -628,7 +582,7 @@ public class BillsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
TempData["Success"] = $"Bill {bill.BillNumber} updated.";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
@@ -1024,12 +978,7 @@ public class BillsController : Controller
|
||||
private async Task<string> GenerateBillNumberAsync()
|
||||
{
|
||||
var prefix = $"BILL-{DateTime.Now:yyMM}-";
|
||||
var last = await _context.Bills
|
||||
.IgnoreQueryFilters()
|
||||
.Where(b => b.BillNumber.StartsWith(prefix))
|
||||
.OrderByDescending(b => b.BillNumber)
|
||||
.Select(b => b.BillNumber)
|
||||
.FirstOrDefaultAsync();
|
||||
var last = await _unitOfWork.Bills.GetLastBillNumberAsync(prefix);
|
||||
|
||||
int next = 1;
|
||||
if (last != null && int.TryParse(last[prefix.Length..], out int num))
|
||||
@@ -1046,12 +995,7 @@ public class BillsController : Controller
|
||||
private async Task<string> GeneratePaymentNumberAsync()
|
||||
{
|
||||
var prefix = $"BPMT-{DateTime.Now:yyMM}-";
|
||||
var last = await _context.BillPayments
|
||||
.IgnoreQueryFilters()
|
||||
.Where(p => p.PaymentNumber.StartsWith(prefix))
|
||||
.OrderByDescending(p => p.PaymentNumber)
|
||||
.Select(p => p.PaymentNumber)
|
||||
.FirstOrDefaultAsync();
|
||||
var last = await _unitOfWork.Bills.GetLastPaymentNumberAsync(prefix);
|
||||
|
||||
int next = 1;
|
||||
if (last != null && int.TryParse(last[prefix.Length..], out int num))
|
||||
|
||||
Reference in New Issue
Block a user