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:
@@ -13,37 +13,39 @@ public interface IBillRepository : IRepository<Bill>
|
||||
/// APAccount, LineItems (filtered to non-deleted) with Account and Job navigations, and
|
||||
/// Payments (filtered to non-deleted) with BankAccount. Returns null if not found.
|
||||
/// </summary>
|
||||
Task<Bill?> LoadForViewAsync(int id);
|
||||
Task<Bill?> LoadForViewAsync(int id, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Task<Bill?> LoadForEditAsync(int id);
|
||||
Task<Bill?> LoadForEditAsync(int id, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Task<List<Bill>> GetForIndexAsync(string? statusFilter, string? searchTerm, decimal? searchAmount);
|
||||
Task<List<Bill>> GetForIndexAsync(int companyId, string? statusFilter, string? searchTerm, decimal? searchAmount);
|
||||
|
||||
/// <summary>
|
||||
/// 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 <paramref name="companyId"/> so sequences are per-tenant.
|
||||
/// </summary>
|
||||
Task<string?> GetLastBillNumberAsync(string prefix);
|
||||
Task<string?> GetLastBillNumberAsync(int companyId, string prefix);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last payment number with the given prefix (including soft-deleted records)
|
||||
/// for sequential payment reference generation.
|
||||
/// Scoped to <paramref name="companyId"/> so sequences are per-tenant.
|
||||
/// </summary>
|
||||
Task<string?> GetLastPaymentNumberAsync(string prefix);
|
||||
Task<string?> GetLastPaymentNumberAsync(int companyId, string prefix);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all non-deleted bills whose <c>BillDate</c> falls within [<paramref name="start"/>,
|
||||
/// <paramref name="end"/>], with Vendor, LineItems → Account, and Payments loaded.
|
||||
/// Used by the accounting data export to produce QuickBooks IIF / CSV files.
|
||||
/// Scoped to <paramref name="companyId"/>. Used by the accounting data export.
|
||||
/// </summary>
|
||||
Task<List<Bill>> GetForDateRangeAsync(DateTime start, DateTime end);
|
||||
Task<List<Bill>> GetForDateRangeAsync(int companyId, DateTime start, DateTime end);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public interface ICustomerRepository : IRepository<Customer>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Task<Customer?> LoadForDetailsAsync(int id);
|
||||
Task<Customer?> LoadForDetailsAsync(int id, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Finds a customer by email address within the current tenant. Used for duplicate-email
|
||||
|
||||
@@ -16,7 +16,7 @@ public interface IInvoiceRepository : IRepository<Invoice>
|
||||
/// Refunds with IssuedBy, CreditApplications with CreditMemo, and GiftCertificateRedemptions.
|
||||
/// Filtered includes exclude soft-deleted children. Returns null if not found or soft-deleted.
|
||||
/// </summary>
|
||||
Task<Invoice?> LoadForViewAsync(int id);
|
||||
Task<Invoice?> LoadForViewAsync(int id, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the invoice linked to a job, or null if none exists. Pass
|
||||
|
||||
@@ -22,26 +22,26 @@ public interface IJobRepository : IRepository<Job>
|
||||
/// and all JobItems with their Coats (InventoryItem + Vendor) and PrepServices.
|
||||
/// Also loads JobPrepServices (job-level prep) separately. Returns null if not found.
|
||||
/// </summary>
|
||||
Task<Job?> LoadForDetailsAsync(int id);
|
||||
Task<Job?> LoadForDetailsAsync(int id, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a single job with the include chain required by the Edit form: same as
|
||||
/// <see cref="LoadForDetailsAsync"/> but without the read-only audit navigations, and
|
||||
/// with tracking enabled so changes can be saved.
|
||||
/// </summary>
|
||||
Task<Job?> LoadForEditAsync(int id);
|
||||
Task<Job?> LoadForEditAsync(int id, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the lightweight job record needed for status-change operations (MoveCard, StatusBump).
|
||||
/// Includes only JobStatus. Returns null if not found or soft-deleted.
|
||||
/// </summary>
|
||||
Task<Job?> LoadForStatusChangeAsync(int id);
|
||||
Task<Job?> LoadForStatusChangeAsync(int id, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the change history for a job, ordered newest-first, with ChangedBy navigation
|
||||
/// loaded. Used by the Details view changelog tab.
|
||||
/// </summary>
|
||||
Task<List<JobChangeHistory>> GetChangeHistoryAsync(int jobId);
|
||||
Task<List<JobChangeHistory>> GetChangeHistoryAsync(int jobId, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last job number that starts with <paramref name="prefix"/> for the given
|
||||
@@ -84,7 +84,7 @@ public interface IJobRepository : IRepository<Job>
|
||||
/// into a new <see cref="JobTemplate"/> via <c>SaveJobAsTemplate</c>.
|
||||
/// Returns null if not found or soft-deleted.
|
||||
/// </summary>
|
||||
Task<Job?> LoadForTemplateSnapshotAsync(int jobId);
|
||||
Task<Job?> LoadForTemplateSnapshotAsync(int jobId, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all non-terminal jobs whose <c>ScheduledDate</c> is before today and not null,
|
||||
@@ -96,6 +96,7 @@ public interface IJobRepository : IRepository<Job>
|
||||
/// <summary>
|
||||
/// Returns the count of rework jobs linked to <paramref name="originalJobId"/>
|
||||
/// (including soft-deleted) so the next rework suffix (R1, R2, …) can be determined.
|
||||
/// Scoped to <paramref name="companyId"/> to prevent cross-tenant count collisions.
|
||||
/// </summary>
|
||||
Task<int> GetReworkJobCountAsync(int originalJobId);
|
||||
Task<int> GetReworkJobCountAsync(int originalJobId, int companyId);
|
||||
}
|
||||
|
||||
@@ -12,22 +12,22 @@ namespace PowderCoating.Core.Interfaces.Repositories;
|
||||
public interface INotificationLogRepository : IRepository<NotificationLog>
|
||||
{
|
||||
/// <summary>Returns the most recent notification log entry for the given invoice, or null.</summary>
|
||||
Task<NotificationLog?> GetLatestForInvoiceAsync(int invoiceId);
|
||||
Task<NotificationLog?> GetLatestForInvoiceAsync(int invoiceId, int companyId);
|
||||
|
||||
/// <summary>Returns all notification log entries for the given invoice, newest-first.</summary>
|
||||
Task<List<NotificationLog>> GetAllForInvoiceAsync(int invoiceId);
|
||||
Task<List<NotificationLog>> GetAllForInvoiceAsync(int invoiceId, int companyId);
|
||||
|
||||
/// <summary>Returns the most recent notification log entry for the given quote, or null.</summary>
|
||||
Task<NotificationLog?> GetLatestForQuoteAsync(int quoteId);
|
||||
Task<NotificationLog?> GetLatestForQuoteAsync(int quoteId, int companyId);
|
||||
|
||||
/// <summary>Returns all notification log entries for the given quote, newest-first.</summary>
|
||||
Task<List<NotificationLog>> GetAllForQuoteAsync(int quoteId);
|
||||
Task<List<NotificationLog>> GetAllForQuoteAsync(int quoteId, int companyId);
|
||||
|
||||
/// <summary>Returns the most recent notification log entry for the given job, or null.</summary>
|
||||
Task<NotificationLog?> GetLatestForJobAsync(int jobId);
|
||||
Task<NotificationLog?> GetLatestForJobAsync(int jobId, int companyId);
|
||||
|
||||
/// <summary>Returns all notification log entries for the given job, newest-first.</summary>
|
||||
Task<List<NotificationLog>> GetAllForJobAsync(int jobId);
|
||||
Task<List<NotificationLog>> GetAllForJobAsync(int jobId, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a paginated, filtered, and sorted page of notification log entries with Customer,
|
||||
|
||||
@@ -17,7 +17,7 @@ public interface IQuoteRepository : IRepository<Quote>
|
||||
/// CatalogItem, and PrepServices; plus QuotePrepServices (quote-level prep).
|
||||
/// Returns null if not found or soft-deleted.
|
||||
/// </summary>
|
||||
Task<Quote?> LoadForDetailsAsync(int id);
|
||||
Task<Quote?> LoadForDetailsAsync(int id, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a single quote by its customer-facing approval token. Ignores global query filters
|
||||
@@ -29,7 +29,7 @@ public interface IQuoteRepository : IRepository<Quote>
|
||||
/// <summary>
|
||||
/// Returns the change history for a quote, ordered newest-first, with ChangedBy loaded.
|
||||
/// </summary>
|
||||
Task<List<QuoteChangeHistory>> GetChangeHistoryAsync(int quoteId);
|
||||
Task<List<QuoteChangeHistory>> GetChangeHistoryAsync(int quoteId, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns aggregate stat counts and total value for the Index view stat cards, scoped to the
|
||||
@@ -43,7 +43,7 @@ public interface IQuoteRepository : IRepository<Quote>
|
||||
/// PDF generation and quote→job conversion. Cheaper than <see cref="LoadForDetailsAsync"/>
|
||||
/// because it skips the parent-quote navigations that callers already have.
|
||||
/// </summary>
|
||||
Task<List<QuoteItem>> GetItemsWithCoatsAsync(int quoteId);
|
||||
Task<List<QuoteItem>> GetItemsWithCoatsAsync(int quoteId, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last quote number that starts with <paramref name="prefix"/> for the given
|
||||
|
||||
Reference in New Issue
Block a user