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 Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using PowderCoating.Application.DTOs.Scheduling;
@@ -6,6 +6,7 @@ using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using PowderCoating.Shared.Constants;
using PowderCoating.Web.Hubs;
namespace PowderCoating.Web.Controllers;
@@ -27,7 +28,7 @@ public class OvenSchedulerController : Controller
/// </summary>
private static readonly string[] QueueableStatuses =
{
"IN_PREPARATION", "SANDBLASTING", "MASKING_TAPING", "CLEANING", "COATING"
AppConstants.StatusCodes.Job.InPreparation, AppConstants.StatusCodes.Job.Sandblasting, AppConstants.StatusCodes.Job.MaskingTaping, AppConstants.StatusCodes.Job.Cleaning, AppConstants.StatusCodes.Job.Coating
};
public OvenSchedulerController(
@@ -646,7 +647,7 @@ public class OvenSchedulerController : Controller
foreach (var batchItem in batch.Items.Where(i => i.Status == OvenBatchItemStatus.Pending))
batchItem.Status = OvenBatchItemStatus.InOven;
var inOvenStatus = await _unitOfWork.JobStatusLookups.FirstOrDefaultAsync(s => s.StatusCode == "IN_OVEN");
var inOvenStatus = await _unitOfWork.JobStatusLookups.FirstOrDefaultAsync(s => s.StatusCode == AppConstants.StatusCodes.Job.InOven);
if (inOvenStatus != null)
{
var jobIds = batch.Items.Select(i => i.JobId).Distinct().ToHashSet();
@@ -693,8 +694,8 @@ public class OvenSchedulerController : Controller
foreach (var batchItem in batch.Items.Where(i => i.Status == OvenBatchItemStatus.InOven))
batchItem.Status = OvenBatchItemStatus.Completed;
var curingStatus = await _unitOfWork.JobStatusLookups.FirstOrDefaultAsync(s => s.StatusCode == "CURING");
var coatingStatus = await _unitOfWork.JobStatusLookups.FirstOrDefaultAsync(s => s.StatusCode == "COATING");
var curingStatus = await _unitOfWork.JobStatusLookups.FirstOrDefaultAsync(s => s.StatusCode == AppConstants.StatusCodes.Job.Curing);
var coatingStatus = await _unitOfWork.JobStatusLookups.FirstOrDefaultAsync(s => s.StatusCode == AppConstants.StatusCodes.Job.Coating);
var jobIds = batch.Items.Select(i => i.JobId).Distinct().ToList();
var allBatchedItems = await _unitOfWork.OvenBatchItems.GetAllAsync(false, i => i.Batch);
@@ -771,15 +772,18 @@ public class OvenSchedulerController : Controller
/// </summary>
private async Task<string> GenerateBatchNumberAsync()
{
var yearMonth = DateTime.Now.ToString("yyMM");
var all = await _unitOfWork.OvenBatches.GetAllAsync(ignoreQueryFilters: true);
var prefix = $"OVN-{yearMonth}-";
var maxSeq = all
.Where(b => b.BatchNumber.StartsWith(prefix))
.Select(b => int.TryParse(b.BatchNumber[prefix.Length..], out var n) ? n : 0)
.DefaultIfEmpty(0)
.Max();
return $"{prefix}{(maxSeq + 1):D4}";
var prefix = $"OVN-{DateTime.Now:yyMM}-";
var last = (await _unitOfWork.OvenBatches.FindAsync(
b => b.BatchNumber.StartsWith(prefix), ignoreQueryFilters: true))
.OrderByDescending(b => b.BatchNumber)
.Select(b => b.BatchNumber)
.FirstOrDefault();
int next = 1;
if (last != null && int.TryParse(last[prefix.Length..], out int num))
next = num + 1;
return $"{prefix}{next:D4}";
}
/// <summary>