Refactor: centralize accounting helpers, status constants, and query deduplication
- AccountingDropdownHelper: wired into BillsController and ExpensesController, replacing 35-40 lines of duplicated DB queries per controller - AppConstants.StatusCodes: added Job.* and Quote.* constants to replace all magic status strings across Jobs, Quotes, Appointments, OvenScheduler, AiQuickQuote, QuoteApproval, and AccountingDropdownHelper - AccountingRules: extracted IsNormalDebitBalance into shared Infrastructure helper; removed duplicate private method from AccountBalanceService and LedgerService (~50 lines deleted) - AccountDataExportController: extracted 9 Fetch*Async methods (superset of includes) so Add*Sheet and Build*Csv no longer duplicate DB queries; each entity is queried once regardless of whether XLSX or CSV format is requested - BillsController.Create and ExpensesController.Create wrapped in ExecuteInTransactionAsync; blob uploads moved after commit to keep financial data atomic and prevent orphaned blobs from rolling back - Number generators (Appointments, CreditMemo, OvenBatch) fixed from full-table GetAllAsync to prefix-filtered FindAsync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using AutoMapper;
|
||||
using AutoMapper;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using PowderCoating.Shared.Constants;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -98,7 +98,7 @@ public class QuotesController : Controller
|
||||
/// Supports filtering by free-text search and/or status. Tag filtering is applied post-query
|
||||
/// because Tags is a comma-separated string column that can't be efficiently queried server-side.
|
||||
/// The <paramref name="statusCode"/> parameter lets dashboard links deep-link into a specific
|
||||
/// status bucket by code name (e.g. "DRAFT") without knowing the database ID.
|
||||
/// status bucket by code name (e.g. AppConstants.StatusCodes.Quote.Draft) without knowing the database ID.
|
||||
/// </summary>
|
||||
public async Task<IActionResult> Index(
|
||||
string? searchTerm,
|
||||
@@ -231,10 +231,10 @@ public class QuotesController : Controller
|
||||
// Aggregate stats — computed over ALL quotes (not just current page) so stat
|
||||
// cards always reflect the full dataset regardless of current page or page size.
|
||||
var draftSentIds = quoteStatuses
|
||||
.Where(s => s.StatusCode == "DRAFT" || s.StatusCode == "SENT")
|
||||
.Where(s => s.StatusCode == AppConstants.StatusCodes.Quote.Draft || s.StatusCode == AppConstants.StatusCodes.Quote.Sent)
|
||||
.Select(s => s.Id).ToList();
|
||||
var approvedConvertedIds = quoteStatuses
|
||||
.Where(s => s.StatusCode == "APPROVED" || s.StatusCode == "CONVERTED")
|
||||
.Where(s => s.StatusCode == AppConstants.StatusCodes.Quote.Approved || s.StatusCode == AppConstants.StatusCodes.Quote.Converted)
|
||||
.Select(s => s.Id).ToList();
|
||||
var indexStats = await _unitOfWork.Quotes.GetIndexStatsAsync(draftSentIds, approvedConvertedIds);
|
||||
ViewBag.StatOpenCount = indexStats.OpenCount;
|
||||
@@ -892,8 +892,8 @@ public class QuotesController : Controller
|
||||
// Get status lookups (cached)
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var statuses = await _lookupCache.GetQuoteStatusLookupsAsync(companyId);
|
||||
var sentStatus = statuses.FirstOrDefault(s => s.StatusCode == "SENT");
|
||||
var draftStatus = statuses.FirstOrDefault(s => s.StatusCode == "DRAFT");
|
||||
var sentStatus = statuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Quote.Sent);
|
||||
var draftStatus = statuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Quote.Draft);
|
||||
|
||||
// Create quote entity
|
||||
var quote = _mapper.Map<Quote>(dto);
|
||||
@@ -1835,7 +1835,7 @@ public class QuotesController : Controller
|
||||
}
|
||||
|
||||
// Check if quote is approved
|
||||
if (quote.QuoteStatus.StatusCode != "APPROVED")
|
||||
if (quote.QuoteStatus.StatusCode != AppConstants.StatusCodes.Quote.Approved)
|
||||
{
|
||||
TempData["Error"] = "Only approved quotes can be converted to customers.";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
@@ -1925,7 +1925,7 @@ public class QuotesController : Controller
|
||||
// Get "Converted" status (cached)
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var statuses = await _lookupCache.GetQuoteStatusLookupsAsync(companyId);
|
||||
var convertedStatus = statuses.FirstOrDefault(s => s.StatusCode == "CONVERTED");
|
||||
var convertedStatus = statuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Quote.Converted);
|
||||
|
||||
// Update quote to link to new customer
|
||||
quote.CustomerId = customer.Id;
|
||||
@@ -2249,7 +2249,7 @@ public class QuotesController : Controller
|
||||
}
|
||||
|
||||
// Check if already approved
|
||||
if (quote.QuoteStatus.StatusCode == "APPROVED")
|
||||
if (quote.QuoteStatus.StatusCode == AppConstants.StatusCodes.Quote.Approved)
|
||||
{
|
||||
TempData["Info"] = $"Quote {quote.QuoteNumber} is already approved.";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
@@ -2263,7 +2263,7 @@ public class QuotesController : Controller
|
||||
|
||||
// Find the Approved status for this company
|
||||
var approvedStatus = await _unitOfWork.QuoteStatusLookups.FirstOrDefaultAsync(
|
||||
s => s.StatusCode == "APPROVED" && s.CompanyId == currentUser!.CompanyId);
|
||||
s => s.StatusCode == AppConstants.StatusCodes.Quote.Approved && s.CompanyId == currentUser!.CompanyId);
|
||||
|
||||
if (approvedStatus == null)
|
||||
{
|
||||
@@ -2750,7 +2750,7 @@ public class QuotesController : Controller
|
||||
quote.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Set approved date when status changes to Approved
|
||||
if (newStatus.StatusCode == "APPROVED" && oldStatusCode != "APPROVED")
|
||||
if (newStatus.StatusCode == AppConstants.StatusCodes.Quote.Approved && oldStatusCode != AppConstants.StatusCodes.Quote.Approved)
|
||||
{
|
||||
quote.ApprovedDate = DateTime.UtcNow;
|
||||
}
|
||||
@@ -2760,7 +2760,7 @@ public class QuotesController : Controller
|
||||
|
||||
// Auto-create job when quote is approved — guard against double-conversion
|
||||
// (race condition: two simultaneous approval calls could both pass the oldStatusCode check)
|
||||
if (newStatus.StatusCode == "APPROVED" && oldStatusCode != "APPROVED"
|
||||
if (newStatus.StatusCode == AppConstants.StatusCodes.Quote.Approved && oldStatusCode != AppConstants.StatusCodes.Quote.Approved
|
||||
&& !quote.ConvertedToJobId.HasValue)
|
||||
{
|
||||
try
|
||||
@@ -2816,7 +2816,7 @@ public class QuotesController : Controller
|
||||
// Get default job statuses and priorities
|
||||
var jobStatuses = await _unitOfWork.JobStatusLookups.GetAllAsync();
|
||||
var jobPriorities = await _unitOfWork.JobPriorityLookups.GetAllAsync();
|
||||
var approvedStatus = jobStatuses.FirstOrDefault(s => s.StatusCode == "APPROVED");
|
||||
var approvedStatus = jobStatuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Job.Approved);
|
||||
var normalPriority = jobPriorities.FirstOrDefault(p => p.PriorityCode == "NORMAL");
|
||||
var rushPriority = jobPriorities.FirstOrDefault(p => p.PriorityCode == "RUSH");
|
||||
|
||||
@@ -2906,7 +2906,7 @@ public class QuotesController : Controller
|
||||
quote.ConvertedDate = DateTime.UtcNow;
|
||||
var companyIdForStatus = quote.CompanyId;
|
||||
var quoteStatuses = await _lookupCache.GetQuoteStatusLookupsAsync(companyIdForStatus);
|
||||
var convertedQuoteStatus = quoteStatuses.FirstOrDefault(s => s.StatusCode == "CONVERTED");
|
||||
var convertedQuoteStatus = quoteStatuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Quote.Converted);
|
||||
if (convertedQuoteStatus != null)
|
||||
quote.QuoteStatusId = convertedQuoteStatus.Id;
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
@@ -3126,8 +3126,8 @@ public class QuotesController : Controller
|
||||
// Advance quote to Sent status when it is still in Draft — mirrors what the email send path does.
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var statuses = await _lookupCache.GetQuoteStatusLookupsAsync(companyId);
|
||||
var sentStatus = statuses.FirstOrDefault(s => s.StatusCode == "SENT");
|
||||
var draftStatus = statuses.FirstOrDefault(s => s.StatusCode == "DRAFT");
|
||||
var sentStatus = statuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Quote.Sent);
|
||||
var draftStatus = statuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Quote.Draft);
|
||||
|
||||
if (sentStatus != null && quote.QuoteStatusId == (draftStatus?.Id ?? 0))
|
||||
{
|
||||
@@ -3509,7 +3509,7 @@ public class QuotesController : Controller
|
||||
|
||||
var jobIds = matches.Select(ji => ji.JobId).Distinct().ToList();
|
||||
var completedStatusIds = (await _unitOfWork.JobStatusLookups.FindAsync(
|
||||
s => s.StatusCode == "COMPLETED" || s.StatusCode == "DELIVERED"))
|
||||
s => s.StatusCode == AppConstants.StatusCodes.Job.Completed || s.StatusCode == AppConstants.StatusCodes.Job.Delivered))
|
||||
.Select(s => s.Id).ToHashSet();
|
||||
var completedJobs = await _unitOfWork.Jobs.FindAsync(
|
||||
j => jobIds.Contains(j.Id) && completedStatusIds.Contains(j.JobStatusId));
|
||||
|
||||
Reference in New Issue
Block a user