diff --git a/src/PowderCoating.Core/Interfaces/Repositories/IBillRepository.cs b/src/PowderCoating.Core/Interfaces/Repositories/IBillRepository.cs index 3ebc7df..2919f0f 100644 --- a/src/PowderCoating.Core/Interfaces/Repositories/IBillRepository.cs +++ b/src/PowderCoating.Core/Interfaces/Repositories/IBillRepository.cs @@ -13,37 +13,39 @@ public interface IBillRepository : IRepository /// APAccount, LineItems (filtered to non-deleted) with Account and Job navigations, and /// Payments (filtered to non-deleted) with BankAccount. Returns null if not found. /// - Task LoadForViewAsync(int id); + Task LoadForViewAsync(int id, int companyId); /// /// Loads a single bill with only its line items for the Edit form. Excludes payment /// navigations since those are read-only after the bill is opened. /// - Task LoadForEditAsync(int id); + Task LoadForEditAsync(int id, int companyId); /// /// Returns all bills for the Index/AP ledger view filtered by status and/or search term. /// Includes Vendor so the list row can display vendor name without a second round trip. /// LineItems are included for the search-in-description condition only. /// - Task> GetForIndexAsync(string? statusFilter, string? searchTerm, decimal? searchAmount); + Task> GetForIndexAsync(int companyId, string? statusFilter, string? searchTerm, decimal? searchAmount); /// /// Returns the last bill number with the given prefix (including soft-deleted records) for /// sequential number generation. Uses IgnoreQueryFilters so deleted bills are counted. + /// Scoped to so sequences are per-tenant. /// - Task GetLastBillNumberAsync(string prefix); + Task GetLastBillNumberAsync(int companyId, string prefix); /// /// Returns the last payment number with the given prefix (including soft-deleted records) /// for sequential payment reference generation. + /// Scoped to so sequences are per-tenant. /// - Task GetLastPaymentNumberAsync(string prefix); + Task GetLastPaymentNumberAsync(int companyId, string prefix); /// /// Returns all non-deleted bills whose BillDate falls within [, /// ], with Vendor, LineItems → Account, and Payments loaded. - /// Used by the accounting data export to produce QuickBooks IIF / CSV files. + /// Scoped to . Used by the accounting data export. /// - Task> GetForDateRangeAsync(DateTime start, DateTime end); + Task> GetForDateRangeAsync(int companyId, DateTime start, DateTime end); } diff --git a/src/PowderCoating.Core/Interfaces/Repositories/ICustomerRepository.cs b/src/PowderCoating.Core/Interfaces/Repositories/ICustomerRepository.cs index dc5c1b2..9c330b2 100644 --- a/src/PowderCoating.Core/Interfaces/Repositories/ICustomerRepository.cs +++ b/src/PowderCoating.Core/Interfaces/Repositories/ICustomerRepository.cs @@ -12,7 +12,7 @@ public interface ICustomerRepository : IRepository /// Loads a single customer with the navigations needed by the Details view: PricingTier, /// and recent CustomerNotes ordered newest-first. Returns null if not found or soft-deleted. /// - Task LoadForDetailsAsync(int id); + Task LoadForDetailsAsync(int id, int companyId); /// /// Finds a customer by email address within the current tenant. Used for duplicate-email diff --git a/src/PowderCoating.Core/Interfaces/Repositories/IInvoiceRepository.cs b/src/PowderCoating.Core/Interfaces/Repositories/IInvoiceRepository.cs index f82407a..092d440 100644 --- a/src/PowderCoating.Core/Interfaces/Repositories/IInvoiceRepository.cs +++ b/src/PowderCoating.Core/Interfaces/Repositories/IInvoiceRepository.cs @@ -16,7 +16,7 @@ public interface IInvoiceRepository : IRepository /// Refunds with IssuedBy, CreditApplications with CreditMemo, and GiftCertificateRedemptions. /// Filtered includes exclude soft-deleted children. Returns null if not found or soft-deleted. /// - Task LoadForViewAsync(int id); + Task LoadForViewAsync(int id, int companyId); /// /// Returns the invoice linked to a job, or null if none exists. Pass diff --git a/src/PowderCoating.Core/Interfaces/Repositories/IJobRepository.cs b/src/PowderCoating.Core/Interfaces/Repositories/IJobRepository.cs index ca316cd..af2484a 100644 --- a/src/PowderCoating.Core/Interfaces/Repositories/IJobRepository.cs +++ b/src/PowderCoating.Core/Interfaces/Repositories/IJobRepository.cs @@ -22,26 +22,26 @@ public interface IJobRepository : IRepository /// and all JobItems with their Coats (InventoryItem + Vendor) and PrepServices. /// Also loads JobPrepServices (job-level prep) separately. Returns null if not found. /// - Task LoadForDetailsAsync(int id); + Task LoadForDetailsAsync(int id, int companyId); /// /// Loads a single job with the include chain required by the Edit form: same as /// but without the read-only audit navigations, and /// with tracking enabled so changes can be saved. /// - Task LoadForEditAsync(int id); + Task LoadForEditAsync(int id, int companyId); /// /// Loads the lightweight job record needed for status-change operations (MoveCard, StatusBump). /// Includes only JobStatus. Returns null if not found or soft-deleted. /// - Task LoadForStatusChangeAsync(int id); + Task LoadForStatusChangeAsync(int id, int companyId); /// /// Returns the change history for a job, ordered newest-first, with ChangedBy navigation /// loaded. Used by the Details view changelog tab. /// - Task> GetChangeHistoryAsync(int jobId); + Task> GetChangeHistoryAsync(int jobId, int companyId); /// /// Returns the last job number that starts with for the given @@ -84,7 +84,7 @@ public interface IJobRepository : IRepository /// into a new via SaveJobAsTemplate. /// Returns null if not found or soft-deleted. /// - Task LoadForTemplateSnapshotAsync(int jobId); + Task LoadForTemplateSnapshotAsync(int jobId, int companyId); /// /// Returns all non-terminal jobs whose ScheduledDate is before today and not null, @@ -96,6 +96,7 @@ public interface IJobRepository : IRepository /// /// Returns the count of rework jobs linked to /// (including soft-deleted) so the next rework suffix (R1, R2, …) can be determined. + /// Scoped to to prevent cross-tenant count collisions. /// - Task GetReworkJobCountAsync(int originalJobId); + Task GetReworkJobCountAsync(int originalJobId, int companyId); } diff --git a/src/PowderCoating.Core/Interfaces/Repositories/INotificationLogRepository.cs b/src/PowderCoating.Core/Interfaces/Repositories/INotificationLogRepository.cs index ad4421b..01ee201 100644 --- a/src/PowderCoating.Core/Interfaces/Repositories/INotificationLogRepository.cs +++ b/src/PowderCoating.Core/Interfaces/Repositories/INotificationLogRepository.cs @@ -12,22 +12,22 @@ namespace PowderCoating.Core.Interfaces.Repositories; public interface INotificationLogRepository : IRepository { /// Returns the most recent notification log entry for the given invoice, or null. - Task GetLatestForInvoiceAsync(int invoiceId); + Task GetLatestForInvoiceAsync(int invoiceId, int companyId); /// Returns all notification log entries for the given invoice, newest-first. - Task> GetAllForInvoiceAsync(int invoiceId); + Task> GetAllForInvoiceAsync(int invoiceId, int companyId); /// Returns the most recent notification log entry for the given quote, or null. - Task GetLatestForQuoteAsync(int quoteId); + Task GetLatestForQuoteAsync(int quoteId, int companyId); /// Returns all notification log entries for the given quote, newest-first. - Task> GetAllForQuoteAsync(int quoteId); + Task> GetAllForQuoteAsync(int quoteId, int companyId); /// Returns the most recent notification log entry for the given job, or null. - Task GetLatestForJobAsync(int jobId); + Task GetLatestForJobAsync(int jobId, int companyId); /// Returns all notification log entries for the given job, newest-first. - Task> GetAllForJobAsync(int jobId); + Task> GetAllForJobAsync(int jobId, int companyId); /// /// Returns a paginated, filtered, and sorted page of notification log entries with Customer, diff --git a/src/PowderCoating.Core/Interfaces/Repositories/IQuoteRepository.cs b/src/PowderCoating.Core/Interfaces/Repositories/IQuoteRepository.cs index c42cb19..f4a0655 100644 --- a/src/PowderCoating.Core/Interfaces/Repositories/IQuoteRepository.cs +++ b/src/PowderCoating.Core/Interfaces/Repositories/IQuoteRepository.cs @@ -17,7 +17,7 @@ public interface IQuoteRepository : IRepository /// CatalogItem, and PrepServices; plus QuotePrepServices (quote-level prep). /// Returns null if not found or soft-deleted. /// - Task LoadForDetailsAsync(int id); + Task LoadForDetailsAsync(int id, int companyId); /// /// Loads a single quote by its customer-facing approval token. Ignores global query filters @@ -29,7 +29,7 @@ public interface IQuoteRepository : IRepository /// /// Returns the change history for a quote, ordered newest-first, with ChangedBy loaded. /// - Task> GetChangeHistoryAsync(int quoteId); + Task> GetChangeHistoryAsync(int quoteId, int companyId); /// /// Returns aggregate stat counts and total value for the Index view stat cards, scoped to the @@ -43,7 +43,7 @@ public interface IQuoteRepository : IRepository /// PDF generation and quote→job conversion. Cheaper than /// because it skips the parent-quote navigations that callers already have. /// - Task> GetItemsWithCoatsAsync(int quoteId); + Task> GetItemsWithCoatsAsync(int quoteId, int companyId); /// /// Returns the last quote number that starts with for the given diff --git a/src/PowderCoating.Infrastructure/Repositories/BillRepository.cs b/src/PowderCoating.Infrastructure/Repositories/BillRepository.cs index ef7eed9..7c23c7e 100644 --- a/src/PowderCoating.Infrastructure/Repositories/BillRepository.cs +++ b/src/PowderCoating.Infrastructure/Repositories/BillRepository.cs @@ -15,10 +15,10 @@ public class BillRepository : Repository, IBillRepository public BillRepository(ApplicationDbContext context) : base(context) { } /// - public async Task LoadForViewAsync(int id) + public async Task LoadForViewAsync(int id, int companyId) { return await _context.Bills - .Where(b => b.Id == id && !b.IsDeleted) + .Where(b => b.Id == id && b.CompanyId == companyId && !b.IsDeleted) .Include(b => b.Vendor) .Include(b => b.APAccount) .Include(b => b.LineItems.Where(li => !li.IsDeleted)) @@ -31,21 +31,21 @@ public class BillRepository : Repository, IBillRepository } /// - public async Task LoadForEditAsync(int id) + public async Task LoadForEditAsync(int id, int companyId) { return await _context.Bills - .Where(b => b.Id == id && !b.IsDeleted) + .Where(b => b.Id == id && b.CompanyId == companyId && !b.IsDeleted) .Include(b => b.LineItems.Where(li => !li.IsDeleted)) .FirstOrDefaultAsync(); } /// - public async Task> GetForIndexAsync(string? statusFilter, string? searchTerm, decimal? searchAmount) + public async Task> GetForIndexAsync(int companyId, string? statusFilter, string? searchTerm, decimal? searchAmount) { var query = _context.Bills .Include(b => b.Vendor) .Include(b => b.LineItems.Where(li => !li.IsDeleted)) - .Where(b => !b.IsDeleted); + .Where(b => b.CompanyId == companyId && !b.IsDeleted); if (statusFilter == "Unpaid") query = query.Where(b => b.Status == BillStatus.Open || b.Status == BillStatus.PartiallyPaid); @@ -69,32 +69,32 @@ public class BillRepository : Repository, IBillRepository } /// - public async Task GetLastBillNumberAsync(string prefix) + public async Task GetLastBillNumberAsync(int companyId, string prefix) { return await _context.Bills .IgnoreQueryFilters() - .Where(b => b.BillNumber.StartsWith(prefix)) + .Where(b => b.CompanyId == companyId && b.BillNumber.StartsWith(prefix)) .OrderByDescending(b => b.BillNumber) .Select(b => b.BillNumber) .FirstOrDefaultAsync(); } /// - public async Task GetLastPaymentNumberAsync(string prefix) + public async Task GetLastPaymentNumberAsync(int companyId, string prefix) { return await _context.BillPayments .IgnoreQueryFilters() - .Where(p => p.PaymentNumber.StartsWith(prefix)) + .Where(p => p.CompanyId == companyId && p.PaymentNumber.StartsWith(prefix)) .OrderByDescending(p => p.PaymentNumber) .Select(p => p.PaymentNumber) .FirstOrDefaultAsync(); } /// - public async Task> GetForDateRangeAsync(DateTime start, DateTime end) + public async Task> GetForDateRangeAsync(int companyId, DateTime start, DateTime end) { return await _context.Bills - .Where(b => !b.IsDeleted && b.BillDate >= start && b.BillDate <= end) + .Where(b => b.CompanyId == companyId && !b.IsDeleted && b.BillDate >= start && b.BillDate <= end) .Include(b => b.Vendor) .Include(b => b.LineItems.Where(li => !li.IsDeleted)) .ThenInclude(li => li.Account) diff --git a/src/PowderCoating.Infrastructure/Repositories/CustomerRepository.cs b/src/PowderCoating.Infrastructure/Repositories/CustomerRepository.cs index 175be52..aa4bfce 100644 --- a/src/PowderCoating.Infrastructure/Repositories/CustomerRepository.cs +++ b/src/PowderCoating.Infrastructure/Repositories/CustomerRepository.cs @@ -14,10 +14,10 @@ public class CustomerRepository : Repository, ICustomerRepository public CustomerRepository(ApplicationDbContext context) : base(context) { } /// - public async Task LoadForDetailsAsync(int id) + public async Task LoadForDetailsAsync(int id, int companyId) { return await _context.Customers - .Where(c => c.Id == id && !c.IsDeleted) + .Where(c => c.Id == id && c.CompanyId == companyId && !c.IsDeleted) .Include(c => c.PricingTier) .Include(c => c.CustomerNotes.Where(n => !n.IsDeleted)) .FirstOrDefaultAsync(); diff --git a/src/PowderCoating.Infrastructure/Repositories/InvoiceRepository.cs b/src/PowderCoating.Infrastructure/Repositories/InvoiceRepository.cs index a98f81f..af7bda1 100644 --- a/src/PowderCoating.Infrastructure/Repositories/InvoiceRepository.cs +++ b/src/PowderCoating.Infrastructure/Repositories/InvoiceRepository.cs @@ -15,10 +15,10 @@ public class InvoiceRepository : Repository, IInvoiceRepository public InvoiceRepository(ApplicationDbContext context) : base(context) { } /// - public async Task LoadForViewAsync(int id) + public async Task LoadForViewAsync(int id, int companyId) { return await _context.Set() - .Where(i => i.Id == id && !i.IsDeleted) + .Where(i => i.Id == id && i.CompanyId == companyId && !i.IsDeleted) .Include(i => i.Customer) .Include(i => i.Job) .Include(i => i.PreparedBy) diff --git a/src/PowderCoating.Infrastructure/Repositories/JobRepository.cs b/src/PowderCoating.Infrastructure/Repositories/JobRepository.cs index ca6ecaf..2697d68 100644 --- a/src/PowderCoating.Infrastructure/Repositories/JobRepository.cs +++ b/src/PowderCoating.Infrastructure/Repositories/JobRepository.cs @@ -32,13 +32,13 @@ public class JobRepository : Repository, IJobRepository } /// - public async Task LoadForDetailsAsync(int id) + public async Task LoadForDetailsAsync(int id, int companyId) { // Single query replaces the per-item N+1 loop that was in JobsController.Details. // EF Core splits the multi-level ThenIncludes across two SQL queries automatically // (split query behavior), keeping result set size manageable. return await _context.Jobs - .Where(j => j.Id == id && !j.IsDeleted) + .Where(j => j.Id == id && j.CompanyId == companyId && !j.IsDeleted) .Include(j => j.Customer) .Include(j => j.JobStatus) .Include(j => j.JobPriority) @@ -62,10 +62,10 @@ public class JobRepository : Repository, IJobRepository } /// - public async Task LoadForEditAsync(int id) + public async Task LoadForEditAsync(int id, int companyId) { return await _context.Jobs - .Where(j => j.Id == id && !j.IsDeleted) + .Where(j => j.Id == id && j.CompanyId == companyId && !j.IsDeleted) .Include(j => j.Customer) .Include(j => j.JobStatus) .Include(j => j.JobPriority) @@ -86,18 +86,18 @@ public class JobRepository : Repository, IJobRepository } /// - public async Task LoadForStatusChangeAsync(int id) + public async Task LoadForStatusChangeAsync(int id, int companyId) { return await _context.Jobs .Include(j => j.JobStatus) - .FirstOrDefaultAsync(j => j.Id == id && !j.IsDeleted); + .FirstOrDefaultAsync(j => j.Id == id && j.CompanyId == companyId && !j.IsDeleted); } /// - public async Task> GetChangeHistoryAsync(int jobId) + public async Task> GetChangeHistoryAsync(int jobId, int companyId) { return await _context.JobChangeHistories - .Where(h => h.JobId == jobId && !h.IsDeleted) + .Where(h => h.JobId == jobId && h.CompanyId == companyId && !h.IsDeleted) .Include(h => h.ChangedBy) .OrderByDescending(h => h.ChangedAt) .AsNoTracking() @@ -176,10 +176,10 @@ public class JobRepository : Repository, IJobRepository } /// - public async Task LoadForTemplateSnapshotAsync(int jobId) + public async Task LoadForTemplateSnapshotAsync(int jobId, int companyId) { return await _context.Jobs - .Where(j => j.Id == jobId && !j.IsDeleted) + .Where(j => j.Id == jobId && j.CompanyId == companyId && !j.IsDeleted) .Include(j => j.JobItems.Where(i => !i.IsDeleted)) .ThenInclude(i => i.Coats.Where(c => !c.IsDeleted)) .Include(j => j.JobItems.Where(i => !i.IsDeleted)) @@ -188,11 +188,11 @@ public class JobRepository : Repository, IJobRepository } /// - public async Task GetReworkJobCountAsync(int originalJobId) + public async Task GetReworkJobCountAsync(int originalJobId, int companyId) { return await _context.Jobs .IgnoreQueryFilters() - .CountAsync(j => j.OriginalJobId == originalJobId); + .CountAsync(j => j.OriginalJobId == originalJobId && j.CompanyId == companyId); } /// diff --git a/src/PowderCoating.Infrastructure/Repositories/NotificationLogRepository.cs b/src/PowderCoating.Infrastructure/Repositories/NotificationLogRepository.cs index d6fd49e..dcc0c02 100644 --- a/src/PowderCoating.Infrastructure/Repositories/NotificationLogRepository.cs +++ b/src/PowderCoating.Infrastructure/Repositories/NotificationLogRepository.cs @@ -15,50 +15,50 @@ public class NotificationLogRepository : Repository, INotificat public NotificationLogRepository(ApplicationDbContext context) : base(context) { } /// - public async Task GetLatestForInvoiceAsync(int invoiceId) => + public async Task GetLatestForInvoiceAsync(int invoiceId, int companyId) => await _context.Set() .IgnoreQueryFilters() - .Where(n => n.InvoiceId == invoiceId) + .Where(n => n.InvoiceId == invoiceId && n.CompanyId == companyId) .OrderByDescending(n => n.SentAt) .FirstOrDefaultAsync(); /// - public async Task> GetAllForInvoiceAsync(int invoiceId) => + public async Task> GetAllForInvoiceAsync(int invoiceId, int companyId) => await _context.Set() .IgnoreQueryFilters() - .Where(n => n.InvoiceId == invoiceId) + .Where(n => n.InvoiceId == invoiceId && n.CompanyId == companyId) .OrderByDescending(n => n.SentAt) .ToListAsync(); /// - public async Task GetLatestForQuoteAsync(int quoteId) => + public async Task GetLatestForQuoteAsync(int quoteId, int companyId) => await _context.Set() .IgnoreQueryFilters() - .Where(n => n.QuoteId == quoteId) + .Where(n => n.QuoteId == quoteId && n.CompanyId == companyId) .OrderByDescending(n => n.SentAt) .FirstOrDefaultAsync(); /// - public async Task> GetAllForQuoteAsync(int quoteId) => + public async Task> GetAllForQuoteAsync(int quoteId, int companyId) => await _context.Set() .IgnoreQueryFilters() - .Where(n => n.QuoteId == quoteId) + .Where(n => n.QuoteId == quoteId && n.CompanyId == companyId) .OrderByDescending(n => n.SentAt) .ToListAsync(); /// - public async Task GetLatestForJobAsync(int jobId) => + public async Task GetLatestForJobAsync(int jobId, int companyId) => await _context.Set() .IgnoreQueryFilters() - .Where(n => n.JobId == jobId) + .Where(n => n.JobId == jobId && n.CompanyId == companyId) .OrderByDescending(n => n.SentAt) .FirstOrDefaultAsync(); /// - public async Task> GetAllForJobAsync(int jobId) => + public async Task> GetAllForJobAsync(int jobId, int companyId) => await _context.Set() .IgnoreQueryFilters() - .Where(n => n.JobId == jobId) + .Where(n => n.JobId == jobId && n.CompanyId == companyId) .OrderByDescending(n => n.SentAt) .ToListAsync(); diff --git a/src/PowderCoating.Infrastructure/Repositories/QuoteRepository.cs b/src/PowderCoating.Infrastructure/Repositories/QuoteRepository.cs index 445a463..25e0d86 100644 --- a/src/PowderCoating.Infrastructure/Repositories/QuoteRepository.cs +++ b/src/PowderCoating.Infrastructure/Repositories/QuoteRepository.cs @@ -15,10 +15,10 @@ public class QuoteRepository : Repository, IQuoteRepository public QuoteRepository(ApplicationDbContext context) : base(context) { } /// - public async Task LoadForDetailsAsync(int id) + public async Task LoadForDetailsAsync(int id, int companyId) { var quote = await _context.Quotes - .Where(q => q.Id == id && !q.IsDeleted) + .Where(q => q.Id == id && q.CompanyId == companyId && !q.IsDeleted) .Include(q => q.Customer) .Include(q => q.PreparedBy) .Include(q => q.QuoteStatus) @@ -32,7 +32,7 @@ public class QuoteRepository : Repository, IQuoteRepository // QuoteItems with nested coats and prep services loaded separately to avoid // cartesian explosion from multiple collection includes in a single query. quote.QuoteItems = await _context.QuoteItems - .Where(qi => qi.QuoteId == id && !qi.IsDeleted) + .Where(qi => qi.QuoteId == id && qi.CompanyId == companyId && !qi.IsDeleted) .Include(qi => qi.Coats) .ThenInclude(c => c.InventoryItem) .Include(qi => qi.Coats) @@ -58,10 +58,10 @@ public class QuoteRepository : Repository, IQuoteRepository } /// - public async Task> GetChangeHistoryAsync(int quoteId) + public async Task> GetChangeHistoryAsync(int quoteId, int companyId) { return await _context.QuoteChangeHistories - .Where(h => h.QuoteId == quoteId && !h.IsDeleted) + .Where(h => h.QuoteId == quoteId && h.CompanyId == companyId && !h.IsDeleted) .Include(h => h.ChangedBy) .OrderByDescending(h => h.ChangedAt) .AsNoTracking() @@ -83,10 +83,10 @@ public class QuoteRepository : Repository, IQuoteRepository } /// - public async Task> GetItemsWithCoatsAsync(int quoteId) + public async Task> GetItemsWithCoatsAsync(int quoteId, int companyId) { return await _context.QuoteItems - .Where(qi => qi.QuoteId == quoteId && !qi.IsDeleted) + .Where(qi => qi.QuoteId == quoteId && qi.CompanyId == companyId && !qi.IsDeleted) .Include(qi => qi.Coats) .ThenInclude(c => c.InventoryItem) .Include(qi => qi.Coats) diff --git a/src/PowderCoating.Web/Controllers/AccountingExportController.cs b/src/PowderCoating.Web/Controllers/AccountingExportController.cs index cdecab5..df575ba 100644 --- a/src/PowderCoating.Web/Controllers/AccountingExportController.cs +++ b/src/PowderCoating.Web/Controllers/AccountingExportController.cs @@ -81,7 +81,7 @@ public class AccountingExportController : Controller .OrderBy(e => e.Date) .ToList(); - var bills = await _unitOfWork.Bills.GetForDateRangeAsync(start, end); + var bills = await _unitOfWork.Bills.GetForDateRangeAsync(companyId, start, end); var customers = (await _unitOfWork.Customers.FindAsync(c => c.CompanyId == companyId)) .OrderBy(c => c.CompanyName ?? c.ContactFirstName) diff --git a/src/PowderCoating.Web/Controllers/BillsController.cs b/src/PowderCoating.Web/Controllers/BillsController.cs index a4c88ae..91aa41f 100644 --- a/src/PowderCoating.Web/Controllers/BillsController.cs +++ b/src/PowderCoating.Web/Controllers/BillsController.cs @@ -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, - 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 /// 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. /// public async Task 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 /// /// Scaffolds a new bill pre-filled from a received purchase order. Only POs in - /// Received or PartiallyReceived status can be billed — earlier states mean + /// Received or PartiallyReceived 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 is true a record is inserted - /// immediately and the bill status is advanced to Paid or PartiallyPaid — + /// immediately and the bill status is advanced to Paid or PartiallyPaid � /// useful for entering historical bills that were already settled. Account balance side /// effects are deliberately deferred to 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(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() @@ -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 /// /// Voids an open or partially-paid bill, removing the remaining AP liability from the ledger. - /// Only the unpaid portion (BalanceDue) is reversed on the AP account — any payments + /// Only the unpaid portion (BalanceDue) 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 BalanceDue, AmountPaid is set equal to Total @@ -968,7 +976,8 @@ public class BillsController : Controller private async Task 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 /// /// Generates a sequential payment reference number in the format BPMT-YYMM-####. - /// Same monotonic sequence logic as — soft-deleted + /// Same monotonic sequence logic as � soft-deleted /// records are included in the scan so payment numbers are never reused. /// private async Task 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 ------------------------------------------ /// - /// 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 . /// public IActionResult RecurringDetection() => View(); /// - /// 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. diff --git a/src/PowderCoating.Web/Controllers/InvoicesController.cs b/src/PowderCoating.Web/Controllers/InvoicesController.cs index 13c69fd..2c8a2a0 100644 --- a/src/PowderCoating.Web/Controllers/InvoicesController.cs +++ b/src/PowderCoating.Web/Controllers/InvoicesController.cs @@ -1103,7 +1103,7 @@ public class InvoicesController : Controller paymentUrl = $"{Request.Scheme}://{Request.Host}/pay/{invoice.PaymentLinkToken}"; var viewUrl = $"{Request.Scheme}://{Request.Host}/invoice/{invoice.PublicViewToken}"; await _notificationService.NotifyInvoiceSentAsync(invoice, pdfBytes, $"Invoice-{invoice.InvoiceNumber}.pdf", paymentUrl, viewUrl: viewUrl); - var notifLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id); + var notifLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id, invoice.CompanyId); this.SetNotificationResultToast(notifLog); } catch (Exception notifyEx) @@ -1190,7 +1190,7 @@ public class InvoicesController : Controller id, invoice.InvoiceNumber, notifyEx.InnerException?.Message ?? "none"); } - var notifLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id); + var notifLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id, invoice.CompanyId); this.SetNotificationResultToast(notifLog); TempData["Success"] = $"Invoice {invoice.InvoiceNumber} marked as sent."; @@ -1321,7 +1321,7 @@ public class InvoicesController : Controller } - var paymentNotifLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id); + var paymentNotifLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id, _tenantContext.GetCurrentCompanyId() ?? 0); this.SetNotificationResultToast(paymentNotifLog); TempData["Success"] = overpayment > 0 @@ -1954,7 +1954,7 @@ public class InvoicesController : Controller sendSms: sendSms, viewUrl: viewUrl); - var latestLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id); + var latestLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id, invoice.CompanyId); if (latestLog?.Status == NotificationStatus.Failed) return Json(new { success = false, message = $"Delivery failed: {latestLog.ErrorMessage}" }); @@ -1987,7 +1987,7 @@ public class InvoicesController : Controller public async Task NotificationsSent(int id) { var tz = ViewBag.CompanyTimeZone as string; - var entries = await _unitOfWork.NotificationLogs.GetAllForInvoiceAsync(id); + var entries = await _unitOfWork.NotificationLogs.GetAllForInvoiceAsync(id, _tenantContext.GetCurrentCompanyId() ?? 0); var logs = entries.Select(n => new { n.Id, Channel = n.Channel.ToString(), Type = n.NotificationType.ToString(), Status = n.Status.ToString(), n.RecipientName, n.Recipient, n.Subject, n.ErrorMessage, n.Message, SentAt = n.SentAt.Tz(tz).ToString("MMM d, yyyy h:mm tt") }); @@ -2107,7 +2107,7 @@ public class InvoicesController : Controller /// eight-table include chain. Returns null if not found or soft-deleted. /// private async Task LoadInvoiceForViewAsync(int id) => - await _unitOfWork.Invoices.LoadForViewAsync(id); + await _unitOfWork.Invoices.LoadForViewAsync(id, _tenantContext.GetCurrentCompanyId() ?? 0); /// /// Converts an Invoice entity to a fully populated InvoiceDto for the view layer. AutoMapper diff --git a/src/PowderCoating.Web/Controllers/JobTemplatesController.cs b/src/PowderCoating.Web/Controllers/JobTemplatesController.cs index c0876a5..6bac436 100644 --- a/src/PowderCoating.Web/Controllers/JobTemplatesController.cs +++ b/src/PowderCoating.Web/Controllers/JobTemplatesController.cs @@ -130,7 +130,7 @@ public class JobTemplatesController : Controller { var companyId = _tenantContext.GetCurrentCompanyId() ?? 0; - var job = await _unitOfWork.Jobs.LoadForTemplateSnapshotAsync(jobId); + var job = await _unitOfWork.Jobs.LoadForTemplateSnapshotAsync(jobId, companyId); if (job == null) return NotFound(); diff --git a/src/PowderCoating.Web/Controllers/JobsController.cs b/src/PowderCoating.Web/Controllers/JobsController.cs index cf06f22..3cd51ca 100644 --- a/src/PowderCoating.Web/Controllers/JobsController.cs +++ b/src/PowderCoating.Web/Controllers/JobsController.cs @@ -333,7 +333,7 @@ public class JobsController : Controller [HttpPost, ValidateAntiForgeryToken] public async Task MoveCard([FromBody] MoveCardRequest req) { - var job = await _unitOfWork.Jobs.LoadForStatusChangeAsync(req.JobId); + var job = await _unitOfWork.Jobs.LoadForStatusChangeAsync(req.JobId, _tenantContext.GetCurrentCompanyId() ?? 0); if (job == null) return Json(new { success = false, message = "Job not found." }); @@ -396,7 +396,8 @@ public class JobsController : Controller try { - var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id.Value); + var companyId = _tenantContext.GetCurrentCompanyId() ?? 0; + var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id.Value, companyId); if (job == null) { return NotFound(); @@ -416,7 +417,7 @@ public class JobsController : Controller var jobDto = _mapper.Map(job); // Load change history - var changeHistories = await _unitOfWork.Jobs.GetChangeHistoryAsync(id.Value); + var changeHistories = await _unitOfWork.Jobs.GetChangeHistoryAsync(id.Value, companyId); _logger.LogInformation("Loaded {Count} change history records for Job {JobId}", changeHistories.Count, id.Value); var changeHistoryDtos = _mapper.Map>(changeHistories); @@ -741,7 +742,8 @@ public class JobsController : Controller try { - var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id.Value); + var companyId = _tenantContext.GetCurrentCompanyId(); + var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id.Value, companyId ?? 0); if (job == null) { return NotFound(); @@ -751,7 +753,6 @@ public class JobsController : Controller var jobDto = _mapper.Map(job); // Get company info for the header - var companyId = _tenantContext.GetCurrentCompanyId(); if (companyId.HasValue) { var company = await _unitOfWork.Companies.GetByIdAsync(companyId.Value); @@ -1268,7 +1269,7 @@ public class JobsController : Controller try { - var job = await _unitOfWork.Jobs.LoadForEditAsync(id.Value); + var job = await _unitOfWork.Jobs.LoadForEditAsync(id.Value, _tenantContext.GetCurrentCompanyId() ?? 0); if (job == null) { return NotFound(); @@ -1782,7 +1783,7 @@ public class JobsController : Controller _logger.LogWarning(ex, "Notification failed for job {Id}", job.Id); } - var editNotifLog = await _unitOfWork.NotificationLogs.GetLatestForJobAsync(job.Id); + var editNotifLog = await _unitOfWork.NotificationLogs.GetLatestForJobAsync(job.Id, job.CompanyId); this.SetNotificationResultToast(editNotifLog); } @@ -1996,10 +1997,9 @@ public class JobsController : Controller { try { - var source = await _unitOfWork.Jobs.LoadForDetailsAsync(id); - if (source == null) return NotFound(); - var companyId = _tenantContext.GetCurrentCompanyId() ?? 0; + var source = await _unitOfWork.Jobs.LoadForDetailsAsync(id, companyId); + if (source == null) return NotFound(); var pendingStatus = await _unitOfWork.JobStatusLookups.FirstOrDefaultAsync( s => s.StatusCode == AppConstants.StatusCodes.Job.Pending && s.CompanyId == companyId); if (pendingStatus == null) @@ -2410,7 +2410,7 @@ public class JobsController : Controller { try { - var job = await _unitOfWork.Jobs.LoadForStatusChangeAsync(request.JobId); + var job = await _unitOfWork.Jobs.LoadForStatusChangeAsync(request.JobId, _tenantContext.GetCurrentCompanyId() ?? 0); if (job == null) return Json(new { success = false, message = "Job not found" }); var newStatus = await _unitOfWork.JobStatusLookups.GetByIdAsync(request.NewStatusId); @@ -2661,7 +2661,7 @@ public class JobsController : Controller _logger.LogWarning(ex, "Notification failed for job {Id}", request.JobId); } - var statusNotifLog = await _unitOfWork.NotificationLogs.GetLatestForJobAsync(request.JobId); + var statusNotifLog = await _unitOfWork.NotificationLogs.GetLatestForJobAsync(request.JobId, job.CompanyId); this.SetNotificationResultToast(statusNotifLog); } @@ -2888,7 +2888,7 @@ public class JobsController : Controller [HttpGet] public async Task CompleteJobModal(int id) { - var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id); + var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id, _tenantContext.GetCurrentCompanyId() ?? 0); if (job == null) return NotFound(); var currentUser = await _userManager.GetUserAsync(User); if (currentUser != null) @@ -3072,7 +3072,7 @@ public class JobsController : Controller _logger.LogWarning(ex, "Notification failed for job {Id}", dto.JobId); } - var completeNotifLog = await _unitOfWork.NotificationLogs.GetLatestForJobAsync(dto.JobId); + var completeNotifLog = await _unitOfWork.NotificationLogs.GetLatestForJobAsync(dto.JobId, _tenantContext.GetCurrentCompanyId() ?? 0); this.SetNotificationResultToast(completeNotifLog); } @@ -3879,7 +3879,7 @@ public class JobsController : Controller [HttpPost] public async Task AddReworkRecord([FromBody] CreateReworkRecordDto dto) { - var job = await _unitOfWork.Jobs.LoadForDetailsAsync(dto.JobId); + var job = await _unitOfWork.Jobs.LoadForDetailsAsync(dto.JobId, _tenantContext.GetCurrentCompanyId() ?? 0); if (job == null) return NotFound(); var companyId = job.CompanyId; @@ -3957,7 +3957,7 @@ public class JobsController : Controller var reworkRecord = await _unitOfWork.ReworkRecords.GetByIdAsync(req.ReworkRecordId, false, r => r.Job); if (reworkRecord == null) return NotFound(); - var originalJob = await _unitOfWork.Jobs.LoadForDetailsAsync(reworkRecord.JobId); + var originalJob = await _unitOfWork.Jobs.LoadForDetailsAsync(reworkRecord.JobId, _tenantContext.GetCurrentCompanyId() ?? 0); if (originalJob == null) return NotFound(); var companyId = originalJob.CompanyId; @@ -4013,7 +4013,7 @@ public class JobsController : Controller var normalPriority = priorities.FirstOrDefault(p => p.PriorityCode == "NORMAL") ?? priorities.First(); // Sub-number: {parentJobNumber}-R{n+1} - var reworkCount = await _unitOfWork.Jobs.GetReworkJobCountAsync(originalJob.Id); + var reworkCount = await _unitOfWork.Jobs.GetReworkJobCountAsync(originalJob.Id, companyId); var reworkNumber = $"{originalJob.JobNumber}-R{reworkCount + 1}"; var reworkJob = new Job @@ -4165,7 +4165,7 @@ public class JobsController : Controller [ValidateAntiForgeryToken] public async Task ResyncFromQuote(int id) { - var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id); + var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id, _tenantContext.GetCurrentCompanyId() ?? 0); if (job == null) return NotFound(); // Guard: only allow re-sync while job is pre-production @@ -4205,7 +4205,7 @@ public class JobsController : Controller // Load quote items with full coat + prep-service data var quote = job.Quote!; - var fullItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(job.QuoteId.Value); + var fullItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(job.QuoteId.Value, job.CompanyId); foreach (var quoteItem in fullItems.Where(qi => !qi.IsDeleted)) { diff --git a/src/PowderCoating.Web/Controllers/QuotesController.cs b/src/PowderCoating.Web/Controllers/QuotesController.cs index fb48b53..a58604f 100644 --- a/src/PowderCoating.Web/Controllers/QuotesController.cs +++ b/src/PowderCoating.Web/Controllers/QuotesController.cs @@ -288,8 +288,9 @@ public class QuotesController : Controller try { + var companyId = _tenantContext.GetCurrentCompanyId() ?? 0; // Load quote with all navigations needed for the Details view - var quote = await _unitOfWork.Quotes.LoadForDetailsAsync(id.Value); + var quote = await _unitOfWork.Quotes.LoadForDetailsAsync(id.Value, companyId); if (quote == null) { @@ -412,7 +413,7 @@ public class QuotesController : Controller }; // Load change history - var changeHistories = await _unitOfWork.Quotes.GetChangeHistoryAsync(id.Value); + var changeHistories = await _unitOfWork.Quotes.GetChangeHistoryAsync(id.Value, companyId); var changeHistoryDtos = _mapper.Map>(changeHistories); ViewBag.ChangeHistory = changeHistoryDtos; @@ -560,7 +561,7 @@ public class QuotesController : Controller } } - var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(id.Value); + var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(id.Value, _tenantContext.GetCurrentCompanyId() ?? 0); quoteDto.QuoteItems = _mapper.Map>(quoteItems); // Get company info and logo @@ -1082,7 +1083,7 @@ public class QuotesController : Controller _logger.LogWarning(ex, "Notification failed for quote {Id}", quote.Id); } - var quoteCreateNotifLog = await _unitOfWork.NotificationLogs.GetLatestForQuoteAsync(quote.Id); + var quoteCreateNotifLog = await _unitOfWork.NotificationLogs.GetLatestForQuoteAsync(quote.Id, currentUser!.CompanyId); this.SetNotificationResultToast(quoteCreateNotifLog); } @@ -1135,7 +1136,7 @@ public class QuotesController : Controller } // Get quote items with their coats, prep services and catalog item - var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(id.Value); + var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(id.Value, _tenantContext.GetCurrentCompanyId() ?? 0); _logger.LogInformation("=== LOADING QUOTE {QuoteId} FOR EDIT ===", id.Value); foreach (var item in quoteItems) @@ -2197,7 +2198,7 @@ public class QuotesController : Controller } } - var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(id.Value); + var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(id.Value, _tenantContext.GetCurrentCompanyId() ?? 0); quoteDto.QuoteItems = _mapper.Map>(quoteItems); // Warn on confirmation page if a job is linked @@ -2356,7 +2357,7 @@ public class QuotesController : Controller _logger.LogWarning(ex, "Notification failed for quote {Id}", id); } - var approveNotifLog = await _unitOfWork.NotificationLogs.GetLatestForQuoteAsync(id); + var approveNotifLog = await _unitOfWork.NotificationLogs.GetLatestForQuoteAsync(id, quote.CompanyId); this.SetNotificationResultToast(approveNotifLog); } @@ -2718,7 +2719,7 @@ public class QuotesController : Controller } } - var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(quoteId); + var quoteItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(quoteId, currentUser.CompanyId); quoteDto.QuoteItems = _mapper.Map>(quoteItems); var company = await _unitOfWork.Companies.GetByIdAsync(currentUser.CompanyId); @@ -2892,7 +2893,7 @@ public class QuotesController : Controller // Always reload quote items with full coat/prep-service data so this works // regardless of which caller loaded the quote (some callers don't include coats). - var fullItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(quote.Id); + var fullItems = await _unitOfWork.Quotes.GetItemsWithCoatsAsync(quote.Id, quote.CompanyId); // Do NOT assign fullItems to quote.QuoteItems — quote is a tracked entity and assigning // no-tracking children (which may share InventoryItem instances) causes EF identity conflicts. @@ -3210,7 +3211,7 @@ public class QuotesController : Controller await _notificationService.NotifyQuoteSentAsync(quote, pdfBytes, pdfFilename, trimmedOverride); // Check the most recent log entry to get actual send status - var latestLog = await _unitOfWork.NotificationLogs.GetLatestForQuoteAsync(id); + var latestLog = await _unitOfWork.NotificationLogs.GetLatestForQuoteAsync(id, quote.CompanyId); if (latestLog?.Status == NotificationStatus.Failed) return Json(new { success = false, message = $"Email delivery failed: {latestLog.ErrorMessage}" }); @@ -3312,7 +3313,7 @@ public class QuotesController : Controller public async Task NotificationsSent(int id) { var tz = ViewBag.CompanyTimeZone as string; - var rawLogs = await _unitOfWork.NotificationLogs.GetAllForQuoteAsync(id); + var rawLogs = await _unitOfWork.NotificationLogs.GetAllForQuoteAsync(id, _tenantContext.GetCurrentCompanyId() ?? 0); var logs = rawLogs.Select(n => new { n.Id, Channel = n.Channel.ToString(), Type = n.NotificationType.ToString(), Status = n.Status.ToString(), n.RecipientName, n.Recipient, n.Subject, n.ErrorMessage, SentAt = n.SentAt.Tz(tz).ToString("MMM d, yyyy h:mm tt") });