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 AutoMapper;
using AutoMapper;
using PowderCoating.Shared.Constants;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
@@ -543,7 +543,7 @@ public class AppointmentsController : Controller
j => j.Customer,
j => j.JobStatus);
var terminalCodes = new[] { "COMPLETED", "DELIVERED", "CANCELLED" };
var terminalCodes = new[] { AppConstants.StatusCodes.Job.Completed, AppConstants.StatusCodes.Job.Delivered, AppConstants.StatusCodes.Job.Cancelled };
var jobsInRange = allJobs.Where(j =>
!terminalCodes.Contains(j.JobStatus.StatusCode) &&
((j.ScheduledDate.HasValue && j.ScheduledDate.Value.Date >= start.Date && j.ScheduledDate.Value.Date <= end.Date) ||
@@ -618,16 +618,16 @@ public class AppointmentsController : Controller
return statusCode switch
{
"PENDING" or "QUOTED" => "#6c757d", // Gray
AppConstants.StatusCodes.Job.Pending or "QUOTED" => "#6c757d", // Gray
"APPROVED" => "#0dcaf0", // Cyan
"IN_PREPARATION" or "SANDBLASTING" or
"MASKING_TAPING" or "CLEANING" => "#0d6efd", // Blue
"IN_OVEN" or "CURING" => "#fd7e14", // Orange
"COATING" => "#6610f2", // Indigo
"QUALITY_CHECK" => "#20c997", // Teal
"COMPLETED" or "DELIVERED" or "READY_FOR_PICKUP" => "#198754", // Green
"ON_HOLD" => "#ffc107", // Yellow
"CANCELLED" => "#adb5bd", // Light gray
AppConstants.StatusCodes.Job.InPreparation or AppConstants.StatusCodes.Job.Sandblasting or
AppConstants.StatusCodes.Job.MaskingTaping or AppConstants.StatusCodes.Job.Cleaning => "#0d6efd", // Blue
AppConstants.StatusCodes.Job.InOven or AppConstants.StatusCodes.Job.Curing => "#fd7e14", // Orange
AppConstants.StatusCodes.Job.Coating => "#6610f2", // Indigo
AppConstants.StatusCodes.Job.QualityCheck => "#20c997", // Teal
AppConstants.StatusCodes.Job.Completed or AppConstants.StatusCodes.Job.Delivered or AppConstants.StatusCodes.Job.ReadyForPickup => "#198754", // Green
AppConstants.StatusCodes.Job.OnHold => "#ffc107", // Yellow
AppConstants.StatusCodes.Job.Cancelled => "#adb5bd", // Light gray
_ => "#0d6efd"
};
}
@@ -745,7 +745,7 @@ public class AppointmentsController : Controller
{
try
{
var terminalCodes = new[] { "COMPLETED", "DELIVERED", "CANCELLED" };
var terminalCodes = new[] { AppConstants.StatusCodes.Job.Completed, AppConstants.StatusCodes.Job.Delivered, AppConstants.StatusCodes.Job.Cancelled };
var allJobs = await _unitOfWork.Jobs.GetAllAsync(false,
j => j.Customer, j => j.JobStatus, j => j.JobItems);
@@ -869,27 +869,18 @@ public class AppointmentsController : Controller
/// </summary>
private async Task<string> GenerateAppointmentNumberAsync()
{
var now = DateTime.UtcNow;
var prefix = $"APT-{now:yyMM}-";
// Get all appointments for current month (including soft-deleted)
var allAppointments = await _unitOfWork.Appointments.GetAllAsync(ignoreQueryFilters: true);
var monthAppointments = allAppointments
.Where(a => a.AppointmentNumber.StartsWith(prefix))
var prefix = $"APT-{DateTime.UtcNow:yyMM}-";
var last = (await _unitOfWork.Appointments.FindAsync(
a => a.AppointmentNumber.StartsWith(prefix), ignoreQueryFilters: true))
.OrderByDescending(a => a.AppointmentNumber)
.ToList();
.Select(a => a.AppointmentNumber)
.FirstOrDefault();
var lastNumber = 0;
if (monthAppointments.Any())
{
var lastAppointmentNumber = monthAppointments.First().AppointmentNumber;
var numberPart = lastAppointmentNumber.Split('-').Last();
int.TryParse(numberPart, out lastNumber);
}
int next = 1;
if (last != null && int.TryParse(last[prefix.Length..], out int num))
next = num + 1;
var newNumber = lastNumber + 1;
return $"{prefix}{newNumber:D4}";
return $"{prefix}{next:D4}";
}
/// <summary>