Files
PowderCoatingLogix/src/PowderCoating.Web/Helpers/AccountingDropdownHelper.cs
T
spouliot 379b0de885 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>
2026-05-09 22:42:39 -04:00

101 lines
4.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.AspNetCore.Mvc.Rendering;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using PowderCoating.Shared.Constants;
namespace PowderCoating.Web.Helpers;
/// <summary>
/// Centralizes the repeated DB queries and SelectListItem projections used by the accounting
/// controllers (Bills, Expenses). Each controller assigns only the properties it needs to ViewBag,
/// so the naming mismatch between controllers (BankAccounts vs PaymentAccounts) is harmless.
/// </summary>
internal static class AccountingDropdownHelper
{
/// <summary>
/// Loads vendors, accounts, payment methods, and active jobs in a single call.
/// Returns pre-projected SelectListItem collections so controllers avoid duplicating the
/// LINQ-to-SelectListItem transform.
/// </summary>
internal static async Task<AccountingDropdowns> LoadAsync(IUnitOfWork unitOfWork)
{
var vendors = await unitOfWork.Vendors.FindAsync(v => v.IsActive);
var allAccounts = await unitOfWork.Accounts.FindAsync(a => a.IsActive);
var jobs = await unitOfWork.Jobs.FindAsync(j =>
j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Completed &&
j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Cancelled &&
j.JobStatus.StatusCode != AppConstants.StatusCodes.Job.Delivered);
var accountLabel = (Core.Entities.Account a) => $"{a.AccountNumber} {a.Name}";
return new AccountingDropdowns
{
Vendors = vendors
.OrderBy(v => v.CompanyName)
.Select(v => new SelectListItem(v.CompanyName, v.Id.ToString()))
.ToList(),
ExpenseAccounts = allAccounts
.Where(a => a.AccountType == AccountType.Expense ||
a.AccountType == AccountType.CostOfGoods)
.OrderBy(a => a.AccountNumber)
.Select(a => new SelectListItem(accountLabel(a), a.Id.ToString()))
.ToList(),
ExpenseAndAssetAccounts = allAccounts
.Where(a => a.AccountType == AccountType.Expense ||
a.AccountType == AccountType.CostOfGoods ||
a.AccountType == AccountType.Asset)
.OrderBy(a => a.AccountNumber)
.Select(a => new SelectListItem(accountLabel(a), a.Id.ToString()))
.ToList(),
ApAccounts = allAccounts
.Where(a => a.AccountSubType == AccountSubType.AccountsPayable)
.OrderBy(a => a.AccountNumber)
.Select(a => new SelectListItem(accountLabel(a), a.Id.ToString()))
.ToList(),
BankAccounts = allAccounts
.Where(a => a.AccountSubType == AccountSubType.Cash ||
a.AccountSubType == AccountSubType.Checking ||
a.AccountSubType == AccountSubType.Savings ||
a.AccountSubType == AccountSubType.CreditCard)
.OrderBy(a => a.AccountNumber)
.Select(a => new SelectListItem(accountLabel(a), a.Id.ToString()))
.ToList(),
PaymentMethods = Enum.GetValues<PaymentMethod>()
.Select(m => new SelectListItem(m.ToString(), ((int)m).ToString()))
.ToList(),
ActiveJobs = jobs
.OrderBy(j => j.JobNumber)
.Select(j => new SelectListItem(
$"{j.JobNumber} {j.Description ?? "No description"}",
j.Id.ToString()))
.ToList()
};
}
}
internal sealed class AccountingDropdowns
{
public IReadOnlyList<SelectListItem> Vendors { get; init; } = [];
/// <summary>Expense + Cost of Goods accounts (used by Expenses controller).</summary>
public IReadOnlyList<SelectListItem> ExpenseAccounts { get; init; } = [];
/// <summary>Expense + Cost of Goods + Asset accounts (used by Bills controller).</summary>
public IReadOnlyList<SelectListItem> ExpenseAndAssetAccounts { get; init; } = [];
/// <summary>Accounts Payable accounts (used by Bills controller).</summary>
public IReadOnlyList<SelectListItem> ApAccounts { get; init; } = [];
/// <summary>Cash, Checking, Savings, and Credit Card accounts.</summary>
public IReadOnlyList<SelectListItem> BankAccounts { get; init; } = [];
public IReadOnlyList<SelectListItem> PaymentMethods { get; init; } = [];
public IReadOnlyList<SelectListItem> ActiveJobs { get; init; } = [];
}