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:
2026-05-09 22:42:39 -04:00
parent edd7389d7d
commit 379b0de885
15 changed files with 394 additions and 359 deletions
@@ -1,4 +1,4 @@
using System.Text.Json;
using System.Text.Json;
using AutoMapper;
using PowderCoating.Shared.Constants;
using Microsoft.AspNetCore.Authorization;
@@ -125,18 +125,18 @@ public class JobsController : Controller
var todayDate = DateTime.Today;
if (statusGroup == "active")
{
filter = j => j.JobStatus.StatusCode != "COMPLETED"
&& j.JobStatus.StatusCode != "READY_FOR_PICKUP"
&& j.JobStatus.StatusCode != "DELIVERED"
&& j.JobStatus.StatusCode != "CANCELLED";
filter = j => j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Completed
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.ReadyForPickup
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Delivered
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Cancelled;
}
else if (statusGroup == "overdue")
{
filter = j => j.DueDate < todayDate
&& j.JobStatus.StatusCode != "COMPLETED"
&& j.JobStatus.StatusCode != "READY_FOR_PICKUP"
&& j.JobStatus.StatusCode != "DELIVERED"
&& j.JobStatus.StatusCode != "CANCELLED";
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Completed
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.ReadyForPickup
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Delivered
&& j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Cancelled;
}
}
else if (!string.IsNullOrWhiteSpace(searchTerm))
@@ -577,7 +577,7 @@ public class JobsController : Controller
ViewBag.SourceQuoteId = job.QuoteId;
ViewBag.SourceQuoteNumber = job.Quote.QuoteNumber;
var preProductionCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ "PENDING", "QUOTED", "APPROVED" };
{ AppConstants.StatusCodes.Job.Pending, AppConstants.StatusCodes.Job.Quoted, AppConstants.StatusCodes.Job.Approved };
ViewBag.CanResyncFromQuote = preProductionCodes.Contains(job.JobStatus?.StatusCode ?? "");
}
@@ -691,7 +691,7 @@ public class JobsController : Controller
var oldStatusId = job.JobStatusId;
job.JobStatusId = newStatusId;
job.UpdatedAt = DateTime.UtcNow;
if (newStatus.StatusCode == "COMPLETED") job.CompletedDate = DateTime.UtcNow;
if (newStatus.StatusCode == AppConstants.StatusCodes.Job.Completed) job.CompletedDate = DateTime.UtcNow;
var userName = User.Identity?.Name ?? "Shop Floor";
await _unitOfWork.JobStatusHistory.AddAsync(new JobStatusHistory
@@ -870,10 +870,10 @@ public class JobsController : Controller
jobToUpdate.UpdatedAt = now;
// Optionally advance status to In Preparation
if (advanceToInPreparation && jobToUpdate.JobStatus.StatusCode != "IN_PREPARATION")
if (advanceToInPreparation && jobToUpdate.JobStatus.StatusCode != AppConstants.StatusCodes.Job.InPreparation)
{
var allStatuses = await _unitOfWork.JobStatusLookups.GetAllAsync();
var inPrepStatus = allStatuses.FirstOrDefault(s => s.StatusCode == "IN_PREPARATION");
var inPrepStatus = allStatuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Job.InPreparation);
if (inPrepStatus != null)
{
var oldStatusId = jobToUpdate.JobStatusId;
@@ -927,10 +927,10 @@ public class JobsController : Controller
job.IntakeCheckedByUserId = userId;
job.UpdatedAt = now;
if (advanceToInPreparation && job.JobStatus.StatusCode != "IN_PREPARATION" && !job.JobStatus.IsTerminalStatus)
if (advanceToInPreparation && job.JobStatus.StatusCode != AppConstants.StatusCodes.Job.InPreparation && !job.JobStatus.IsTerminalStatus)
{
var allStatuses = await _unitOfWork.JobStatusLookups.GetAllAsync();
var inPrepStatus = allStatuses.FirstOrDefault(s => s.StatusCode == "IN_PREPARATION");
var inPrepStatus = allStatuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Job.InPreparation);
if (inPrepStatus != null)
{
var oldStatusId = job.JobStatusId;
@@ -1095,7 +1095,7 @@ public class JobsController : Controller
{
// Get default "Pending" status (cached)
var statuses = await _lookupCache.GetJobStatusLookupsAsync(companyId);
var pendingStatus = statuses.FirstOrDefault(s => s.StatusCode == "PENDING");
var pendingStatus = statuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Job.Pending);
var job = new Job
{
@@ -1427,11 +1427,11 @@ public class JobsController : Controller
// Update status-related dates
if (oldStatusId != dto.JobStatusId && newStatus != null)
{
if (newStatus.StatusCode == "IN_PREPARATION" && job.StartedDate == null)
if (newStatus.StatusCode == AppConstants.StatusCodes.Job.InPreparation && job.StartedDate == null)
{
job.StartedDate = DateTime.UtcNow;
}
else if (newStatus.StatusCode == "COMPLETED" && job.CompletedDate == null)
else if (newStatus.StatusCode == AppConstants.StatusCodes.Job.Completed && job.CompletedDate == null)
{
job.CompletedDate = DateTime.UtcNow;
}
@@ -1916,9 +1916,9 @@ public class JobsController : Controller
// Load all non-terminal statuses for the progress strip (excluding nav/hold/cancel)
var allStatusesEnum = await _unitOfWork.JobStatusLookups.FindAsync(s =>
s.StatusCode != "ON_HOLD" && s.StatusCode != "CANCELLED"
&& s.StatusCode != "DELIVERED" && s.StatusCode != "QUOTED"
&& s.StatusCode != "PENDING" && s.StatusCode != "APPROVED");
s.StatusCode != AppConstants.StatusCodes.Job.OnHold && s.StatusCode != AppConstants.StatusCodes.Job.Cancelled
&& s.StatusCode != AppConstants.StatusCodes.Job.Delivered && s.StatusCode != AppConstants.StatusCodes.Job.Quoted
&& s.StatusCode != AppConstants.StatusCodes.Job.Pending && s.StatusCode != AppConstants.StatusCodes.Job.Approved);
var allStatuses = allStatusesEnum.OrderBy(s => s.DisplayOrder).ToList();
// Get all jobs scheduled for today with related data including items and coats
@@ -1935,8 +1935,8 @@ public class JobsController : Controller
{
var nextStatus = allStatuses
.Where(s => s.DisplayOrder > j.JobStatus.DisplayOrder
&& s.StatusCode != "ON_HOLD" && s.StatusCode != "CANCELLED"
&& s.StatusCode != "DELIVERED")
&& s.StatusCode != AppConstants.StatusCodes.Job.OnHold && s.StatusCode != AppConstants.StatusCodes.Job.Cancelled
&& s.StatusCode != AppConstants.StatusCodes.Job.Delivered)
.OrderBy(s => s.DisplayOrder)
.FirstOrDefault();
@@ -2024,8 +2024,8 @@ public class JobsController : Controller
var allStatusesEnum = await _unitOfWork.JobStatusLookups.FindAsync(s =>
!s.IsTerminalStatus
&& s.StatusCode != "ON_HOLD" && s.StatusCode != "CANCELLED"
&& s.StatusCode != "DELIVERED");
&& s.StatusCode != AppConstants.StatusCodes.Job.OnHold && s.StatusCode != AppConstants.StatusCodes.Job.Cancelled
&& s.StatusCode != AppConstants.StatusCodes.Job.Delivered);
var allStatuses = allStatusesEnum.OrderBy(s => s.DisplayOrder).ToList();
var jobs = await _unitOfWork.Jobs.GetActiveJobsForMobileAsync(companyId.Value, workerId);
@@ -2164,7 +2164,7 @@ public class JobsController : Controller
var oldStatusId = job.JobStatusId;
job.JobStatusId = request.NewStatusId;
job.UpdatedAt = DateTime.UtcNow;
if (newStatus.StatusCode == "COMPLETED") job.CompletedDate = DateTime.UtcNow;
if (newStatus.StatusCode == AppConstants.StatusCodes.Job.Completed) job.CompletedDate = DateTime.UtcNow;
// Log status history
await _unitOfWork.JobStatusHistory.AddAsync(new JobStatusHistory
@@ -2655,7 +2655,7 @@ public class JobsController : Controller
// Find the "Completed" status
var completedStatus = await _unitOfWork.JobStatusLookups
.FirstOrDefaultAsync(s => s.StatusCode == "COMPLETED" && s.CompanyId == job.CompanyId);
.FirstOrDefaultAsync(s => s.StatusCode == AppConstants.StatusCodes.Job.Completed && s.CompanyId == job.CompanyId);
if (completedStatus != null)
{
@@ -3410,7 +3410,7 @@ public class JobsController : Controller
// Generate rework job number
var statuses = await _lookupCache.GetJobStatusLookupsAsync(companyId);
var pendingStatus = statuses.FirstOrDefault(s => s.StatusCode == "PENDING");
var pendingStatus = statuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Job.Pending);
var priorities = await _lookupCache.GetJobPriorityLookupsAsync(companyId);
var normalPriority = priorities.FirstOrDefault(p => p.PriorityCode == "NORMAL") ?? priorities.First();
@@ -3564,7 +3564,7 @@ public class JobsController : Controller
// Load status lookups to find Pending status
var statuses = await _lookupCache.GetJobStatusLookupsAsync(companyId);
var pendingStatus = statuses.FirstOrDefault(s => s.StatusCode == "PENDING");
var pendingStatus = statuses.FirstOrDefault(s => s.StatusCode == AppConstants.StatusCodes.Job.Pending);
if (pendingStatus == null) return Json(new { success = false, message = "Could not find Pending status." });
var priorities = await _lookupCache.GetJobPriorityLookupsAsync(companyId);
@@ -3642,7 +3642,7 @@ public class JobsController : Controller
// Guard: only allow re-sync while job is pre-production
var preProductionCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ "PENDING", "QUOTED", "APPROVED" };
{ AppConstants.StatusCodes.Job.Pending, AppConstants.StatusCodes.Job.Quoted, AppConstants.StatusCodes.Job.Approved };
if (!preProductionCodes.Contains(job.JobStatus?.StatusCode ?? ""))
{
TempData["Error"] = "Re-sync is only available before shop work has started.";