Multi-tenancy hardening: explicit companyId on all typed repository methods
All typed repository methods that previously relied solely on global query filters now require an explicit companyId parameter, providing defense-in- depth so IgnoreQueryFilters calls cannot leak cross-tenant data. - IBillRepository/BillRepository: GetForIndexAsync, LoadForViewAsync, LoadForEditAsync, GetLastBillNumberAsync, GetLastPaymentNumberAsync, GetForDateRangeAsync all scoped to companyId - IJobRepository/JobRepository: LoadForDetailsAsync, LoadForEditAsync, LoadForStatusChangeAsync, GetChangeHistoryAsync, LoadForTemplateSnapshotAsync, GetReworkJobCountAsync - IQuoteRepository/QuoteRepository: LoadForDetailsAsync, GetChangeHistoryAsync, GetItemsWithCoatsAsync - IInvoiceRepository/InvoiceRepository: LoadForViewAsync - ICustomerRepository/CustomerRepository: LoadForDetailsAsync - INotificationLogRepository/NotificationLogRepository: all 6 FK methods - BillsController: ITenantContext injected, all call sites updated - AccountingExportController, InvoicesController, JobsController, JobTemplatesController, QuotesController: call sites updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ public class BillsController : Controller
|
||||
private readonly IAzureBlobStorageService _blobStorage;
|
||||
private readonly StorageSettings _storageSettings;
|
||||
private readonly IAiUsageLogger _usageLogger;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
|
||||
public BillsController(
|
||||
IUnitOfWork unitOfWork,
|
||||
@@ -45,7 +46,8 @@ public class BillsController : Controller
|
||||
IAccountingAiService accountingAi,
|
||||
IAzureBlobStorageService blobStorage,
|
||||
IOptions<StorageSettings> storageSettings,
|
||||
IAiUsageLogger usageLogger)
|
||||
IAiUsageLogger usageLogger,
|
||||
ITenantContext tenantContext)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_mapper = mapper;
|
||||
@@ -56,6 +58,7 @@ public class BillsController : Controller
|
||||
_blobStorage = blobStorage;
|
||||
_storageSettings = storageSettings.Value;
|
||||
_usageLogger = usageLogger;
|
||||
_tenantContext = tenantContext;
|
||||
}
|
||||
|
||||
// -- Index ----------------------------------------------------------------
|
||||
@@ -64,7 +67,7 @@ public class BillsController : Controller
|
||||
/// Lists bills and direct expenses in a unified AP ledger view. The <paramref name="type"/>
|
||||
/// parameter lets the caller pin the list to Bills only, Expenses only, or both (null).
|
||||
/// Expenses are inherently fully paid so they are always excluded when the caller filters to
|
||||
/// "Unpaid" or "Overdue" — preventing them from inflating the "amount owed" summary.
|
||||
/// "Unpaid" or "Overdue" � preventing them from inflating the "amount owed" summary.
|
||||
/// Amount-based search strips leading $ and commas before comparing so "$1,234" works naturally.
|
||||
/// </summary>
|
||||
public async Task<IActionResult> Index(string? type, string? search, string? status, int page = 1, int pageSize = 25)
|
||||
@@ -81,10 +84,12 @@ public class BillsController : Controller
|
||||
searchAmount = parsed;
|
||||
}
|
||||
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
|
||||
// Bills
|
||||
if (type == null || type == "Bill")
|
||||
{
|
||||
var bills = await _unitOfWork.Bills.GetForIndexAsync(status, search, searchAmount);
|
||||
var bills = await _unitOfWork.Bills.GetForIndexAsync(companyId, status, search, searchAmount);
|
||||
|
||||
entries.AddRange(bills.Select(b => new BillExpenseListDto
|
||||
{
|
||||
@@ -112,7 +117,7 @@ public class BillsController : Controller
|
||||
}));
|
||||
}
|
||||
|
||||
// Expenses are always fully paid — exclude when filtering to unpaid/overdue bills only
|
||||
// Expenses are always fully paid � exclude when filtering to unpaid/overdue bills only
|
||||
if ((type == null || type == "Expense") && status != "Unpaid" && status != "Overdue")
|
||||
{
|
||||
var expSearch = search;
|
||||
@@ -166,7 +171,7 @@ public class BillsController : Controller
|
||||
|
||||
/// <summary>
|
||||
/// Scaffolds a new bill pre-filled from a received purchase order. Only POs in
|
||||
/// <c>Received</c> or <c>PartiallyReceived</c> status can be billed — earlier states mean
|
||||
/// <c>Received</c> or <c>PartiallyReceived</c> status can be billed � earlier states mean
|
||||
/// goods have not yet arrived and no liability has been incurred. If a bill already exists for
|
||||
/// the PO the user is redirected to the existing bill to prevent duplicate AP entries.
|
||||
/// Line items are copied from PO items (using inventory item names where available), and
|
||||
@@ -291,7 +296,7 @@ public class BillsController : Controller
|
||||
/// review before committing to AP. Empty line items (zero account or zero price) are stripped
|
||||
/// before validation to avoid spurious errors when the browser submits blank rows.
|
||||
/// If <paramref name="payNow"/> is true a <see cref="BillPayment"/> record is inserted
|
||||
/// immediately and the bill status is advanced to <c>Paid</c> or <c>PartiallyPaid</c> —
|
||||
/// immediately and the bill status is advanced to <c>Paid</c> or <c>PartiallyPaid</c> �
|
||||
/// useful for entering historical bills that were already settled. Account balance side
|
||||
/// effects are deliberately deferred to <see cref="MarkOpen"/> so that Draft bills do not
|
||||
/// affect the AP ledger until they are approved. If the bill was created from a PO the
|
||||
@@ -322,7 +327,7 @@ public class BillsController : Controller
|
||||
{
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
|
||||
// Period lock check — block if the bill date is in a locked period
|
||||
// Period lock check � block if the bill date is in a locked period
|
||||
if (currentUser != null)
|
||||
{
|
||||
var co = await _unitOfWork.Companies.GetByIdAsync(currentUser.CompanyId);
|
||||
@@ -399,7 +404,7 @@ public class BillsController : Controller
|
||||
await _unitOfWork.CompleteAsync();
|
||||
});
|
||||
|
||||
// Receipt upload after the transaction commits — bill.Id is set and core data
|
||||
// Receipt upload after the transaction commits � bill.Id is set and core data
|
||||
// is secure. A blob failure here leaves the bill intact without an attachment.
|
||||
if (receiptFile != null && receiptFile.Length > 0)
|
||||
{
|
||||
@@ -439,7 +444,8 @@ public class BillsController : Controller
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var bill = await _unitOfWork.Bills.LoadForViewAsync(id.Value);
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var bill = await _unitOfWork.Bills.LoadForViewAsync(id.Value, companyId);
|
||||
if (bill == null) return NotFound();
|
||||
|
||||
var dto = _mapper.Map<BillDto>(bill);
|
||||
@@ -454,7 +460,7 @@ public class BillsController : Controller
|
||||
.ToList();
|
||||
|
||||
ViewBag.BankAccounts = bankAccounts
|
||||
.Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString()))
|
||||
.Select(a => new SelectListItem($"{a.AccountNumber} � {a.Name}", a.Id.ToString()))
|
||||
.ToList();
|
||||
|
||||
ViewBag.PaymentMethods = Enum.GetValues<PaymentMethod>()
|
||||
@@ -477,7 +483,8 @@ public class BillsController : Controller
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var bill = await _unitOfWork.Bills.LoadForEditAsync(id.Value);
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var bill = await _unitOfWork.Bills.LoadForEditAsync(id.Value, companyId);
|
||||
if (bill == null) return NotFound();
|
||||
|
||||
if (bill.Status == BillStatus.Paid || bill.Status == BillStatus.Voided)
|
||||
@@ -540,7 +547,8 @@ public class BillsController : Controller
|
||||
|
||||
try
|
||||
{
|
||||
var bill = await _unitOfWork.Bills.LoadForEditAsync(id);
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var bill = await _unitOfWork.Bills.LoadForEditAsync(id, companyId);
|
||||
|
||||
if (bill == null) return NotFound();
|
||||
|
||||
@@ -867,7 +875,7 @@ public class BillsController : Controller
|
||||
|
||||
/// <summary>
|
||||
/// Voids an open or partially-paid bill, removing the remaining AP liability from the ledger.
|
||||
/// Only the unpaid portion (<c>BalanceDue</c>) is reversed on the AP account — any payments
|
||||
/// Only the unpaid portion (<c>BalanceDue</c>) is reversed on the AP account � any payments
|
||||
/// already recorded remain as historical cash transactions. The vendor balance is likewise
|
||||
/// reduced only by the outstanding balance, not the total. To signal "fully settled" without
|
||||
/// leaving a positive <c>BalanceDue</c>, <c>AmountPaid</c> is set equal to <c>Total</c>
|
||||
@@ -968,7 +976,8 @@ public class BillsController : Controller
|
||||
private async Task<string> GenerateBillNumberAsync()
|
||||
{
|
||||
var prefix = $"BILL-{DateTime.Now:yyMM}-";
|
||||
var last = await _unitOfWork.Bills.GetLastBillNumberAsync(prefix);
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var last = await _unitOfWork.Bills.GetLastBillNumberAsync(companyId, prefix);
|
||||
|
||||
int next = 1;
|
||||
if (last != null && int.TryParse(last[prefix.Length..], out int num))
|
||||
@@ -979,13 +988,14 @@ public class BillsController : Controller
|
||||
|
||||
/// <summary>
|
||||
/// Generates a sequential payment reference number in the format <c>BPMT-YYMM-####</c>.
|
||||
/// Same monotonic sequence logic as <see cref="GenerateBillNumberAsync"/> — soft-deleted
|
||||
/// Same monotonic sequence logic as <see cref="GenerateBillNumberAsync"/> � soft-deleted
|
||||
/// records are included in the scan so payment numbers are never reused.
|
||||
/// </summary>
|
||||
private async Task<string> GeneratePaymentNumberAsync()
|
||||
{
|
||||
var prefix = $"BPMT-{DateTime.Now:yyMM}-";
|
||||
var last = await _unitOfWork.Bills.GetLastPaymentNumberAsync(prefix);
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var last = await _unitOfWork.Bills.GetLastPaymentNumberAsync(companyId, prefix);
|
||||
|
||||
int next = 1;
|
||||
if (last != null && int.TryParse(last[prefix.Length..], out int num))
|
||||
@@ -1139,13 +1149,13 @@ public class BillsController : Controller
|
||||
// -- AI: Recurring Bill Detection ------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// GET page — displays the recurring bill detection tool. No data is pre-fetched here;
|
||||
/// GET page � displays the recurring bill detection tool. No data is pre-fetched here;
|
||||
/// the user triggers the scan by clicking a button which calls <see cref="RunRecurringDetection"/>.
|
||||
/// </summary>
|
||||
public IActionResult RecurringDetection() => View();
|
||||
|
||||
/// <summary>
|
||||
/// AJAX POST — loads up to 12 months of bill history for the company and passes it to
|
||||
/// AJAX POST � loads up to 12 months of bill history for the company and passes it to
|
||||
/// Claude for recurring pattern analysis. Only posted bills (Draft/Open/Partial/Paid) are
|
||||
/// included; Voided bills are excluded so cancelled payments do not distort the pattern.
|
||||
/// Results are returned as JSON for client-side rendering in the view.
|
||||
|
||||
Reference in New Issue
Block a user