Files
PowderCoatingLogix/src/PowderCoating.Infrastructure/Repositories/UnitOfWork.cs
T
spouliot 6a918c2afc Add invoice SMS notifications and customer intake kiosk
Invoice SMS:
- Send Invoice modal now prompts Email/SMS/Both based on customer contact data
- New /invoice/{token} customer-facing view page with full line items and pay button
- PublicViewToken (permanent) added to Invoice; separate from expiring PaymentLinkToken
- InvoiceSent SMS default template added; customizable via Notification Templates settings
- {{viewUrl}} placeholder documented in template editor

Customer Intake Kiosk:
- Tablet kiosk flow: Contact → Job → Terms/Signature → Confirmation
- Remote link mode for off-site customers (lighter form, no signature)
- KioskHub (AllowAnonymous SignalR) for staff-to-tablet push without login
- Staff activates tablet via cookie; sends remote link manually
- Submitted sessions create Customer + Job automatically; fires in-app notification

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:25:27 -04:00

774 lines
40 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Interfaces;
using PowderCoating.Core.Interfaces.Repositories;
using PowderCoating.Infrastructure.Data;
namespace PowderCoating.Infrastructure.Repositories;
/// <summary>
/// Concrete implementation of <see cref="IUnitOfWork"/> that coordinates all entity repositories
/// and exposes transaction management for the application layer.
/// <para>
/// All entity repositories are lazily instantiated — a <see cref="Repository{T}"/> is only
/// allocated the first time the corresponding property is accessed. This keeps the per-request
/// memory footprint small when only a handful of entities are needed.
/// </para>
/// <para>
/// <b>Persistence</b>: call <see cref="SaveChangesAsync"/> or <see cref="CompleteAsync"/>
/// (identical behaviour) to flush pending changes to the database.
/// </para>
/// <para>
/// <b>Explicit transactions</b>: use <see cref="BeginTransactionAsync"/> /
/// <see cref="CommitTransactionAsync"/> / <see cref="RollbackTransactionAsync"/> when multiple
/// SaveChanges calls must succeed or fail atomically. For a simpler fire-and-forget pattern,
/// prefer <see cref="ExecuteInTransactionAsync(Func{Task})"/> which wraps the operation in an
/// EF Core execution-strategy-aware transaction automatically.
/// </para>
/// </summary>
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
private IDbContextTransaction? _transaction;
// Multi-tenancy
private IRepository<Company>? _companies;
private IRepository<CompanyOperatingCosts>? _companyOperatingCosts;
private IRepository<CompanyPreferences>? _companyPreferences;
private IRepository<CompanySmsAgreement>? _companySmsAgreements;
// AI Predictions
private IRepository<AiItemPrediction>? _aiItemPredictions;
// Powder Insights
private IPowderUsageLogRepository? _powderUsageLogs;
// Core repositories
private ICustomerRepository? _customers;
private IJobRepository? _jobs;
private IRepository<JobDailyPriority>? _jobDailyPriorities;
private IRepository<JobItem>? _jobItems;
private IJobItemCoatRepository? _jobItemCoats;
private IRepository<JobItemPrepService>? _jobItemPrepServices;
private IRepository<JobChangeHistory>? _jobChangeHistories;
private IRepository<JobPrepService>? _jobPrepServices;
private IQuoteRepository? _quotes;
private IRepository<QuotePhoto>? _quotePhotos;
private IRepository<QuoteItem>? _quoteItems;
private IRepository<QuoteItemCoat>? _quoteItemCoats;
private IRepository<QuoteItemPrepService>? _quoteItemPrepServices;
private IRepository<QuoteChangeHistory>? _quoteChangeHistories;
private IRepository<InventoryItem>? _inventoryItems;
private IPlainRepository<PowderCatalogItem>? _powderCatalog;
private IInventoryTransactionRepository? _inventoryTransactions;
private IRepository<Equipment>? _equipment;
private IRepository<OvenCost>? _ovenCosts;
private IRepository<CompanyBlastSetup>? _blastSetups;
private IRepository<MaintenanceRecord>? _maintenanceRecords;
private IRepository<Vendor>? _vendors;
private IJobPhotoRepository? _jobPhotos;
private IRepository<JobNote>? _jobNotes;
private IRepository<CustomerNote>? _customerNotes;
private IRepository<JobStatusHistory>? _jobStatusHistory;
private IRepository<PricingTier>? _pricingTiers;
// Lookup tables (replacing enums)
private IRepository<JobStatusLookup>? _jobStatusLookups;
private IRepository<JobPriorityLookup>? _jobPriorityLookups;
private IRepository<QuoteStatusLookup>? _quoteStatusLookups;
private IRepository<InventoryCategoryLookup>? _inventoryCategoryLookups;
private IRepository<AppointmentStatusLookup>? _appointmentStatusLookups;
private IRepository<AppointmentTypeLookup>? _appointmentTypeLookups;
private IRepository<PrepService>? _prepServices;
private IRepository<ShopWorker>? _shopWorkers;
// Appointments
private IRepository<Appointment>? _appointments;
// Product Catalog
private IRepository<CatalogCategory>? _catalogCategories;
private IRepository<CatalogItem>? _catalogItems;
private IRepository<CatalogPriceCheckReport>? _catalogPriceCheckReports;
// Notifications
private INotificationLogRepository? _notificationLogs;
private IRepository<NotificationTemplate>? _notificationTemplates;
// Subscription
private IRepository<SubscriptionPlanConfig>? _subscriptionPlanConfigs;
// Job Templates
private IJobTemplateRepository? _jobTemplates;
private IRepository<JobTemplateItem>? _jobTemplateItems;
private IRepository<JobTemplateItemCoat>? _jobTemplateItemCoats;
private IRepository<JobTemplateItemPrepService>? _jobTemplateItemPrepServices;
// Platform content
private IPlainRepository<Announcement>? _announcements;
private IPlainRepository<BannedIp>? _bannedIps;
private IPlainRepository<DashboardTip>? _dashboardTips;
private IRepository<InAppNotification>? _inAppNotifications;
private IPlainRepository<ReleaseNote>? _releaseNotes;
// Bug Reports
private IRepository<BugReport>? _bugReports;
private IRepository<BugReportAttachment>? _bugReportAttachments;
private IRepository<ContactSubmission>? _contactSubmissions;
private IRepository<ManufacturerLookupPattern>? _manufacturerLookupPatterns;
// Gift Certificates
private IRepository<GiftCertificate>? _giftCertificates;
private IRepository<GiftCertificateRedemption>? _giftCertificateRedemptions;
// Customer Intake Kiosk
private IRepository<KioskSession>? _kioskSessions;
// Purchase Orders
private IPurchaseOrderRepository? _purchaseOrders;
private IRepository<PurchaseOrderItem>? _purchaseOrderItems;
// Oven Scheduling
private IRepository<OvenBatch>? _ovenBatches;
private IRepository<OvenBatchItem>? _ovenBatchItems;
// Invoices, Payments & Deposits
private IInvoiceRepository? _invoices;
private IRepository<InvoiceItem>? _invoiceItems;
private IRepository<Payment>? _payments;
private IRepository<Deposit>? _deposits;
// Expense Tracking / Accounts Payable
private IRepository<Account>? _accounts;
private IBillRepository? _bills;
private IRepository<BillLineItem>? _billLineItems;
private IRepository<BillPayment>? _billPayments;
private IRepository<Expense>? _expenses;
// Manual Journal Entries
private IRepository<JournalEntry>? _journalEntries;
private IRepository<JournalEntryLine>? _journalEntryLines;
// Vendor Credits
private IRepository<VendorCredit>? _vendorCredits;
private IRepository<VendorCreditLineItem>? _vendorCreditLineItems;
private IRepository<VendorCreditApplication>? _vendorCreditApplications;
// Bank Reconciliation
private IRepository<BankReconciliation>? _bankReconciliations;
// Tax Rates
private IRepository<TaxRate>? _taxRates;
// Recurring Transactions
private IRepository<RecurringTemplate>? _recurringTemplates;
private IRepository<FixedAsset>? _fixedAssets;
private IRepository<FixedAssetDepreciationEntry>? _fixedAssetDepreciationEntries;
private IRepository<Budget>? _budgets;
private IRepository<BudgetLine>? _budgetLines;
private IRepository<YearEndClose>? _yearEndCloses;
/// <summary>
/// Initialises the unit of work with the scoped <paramref name="context"/>.
/// The context is shared across all repositories created by this instance so that
/// all reads and writes within a single request participate in the same EF Core identity map
/// and change-tracker, enabling zero-copy entity sharing between repositories.
/// </summary>
public UnitOfWork(ApplicationDbContext context)
{
_context = context;
}
// -------------------------------------------------------------------------
// Repository properties — lazy-initialised via the null-coalescing assignment
// operator (??=). Each property creates one Repository<T> instance at most
// per UnitOfWork lifetime (= per HTTP request when registered as Scoped).
// Repositories share the underlying DbContext so they share the change tracker.
// -------------------------------------------------------------------------
/// <summary>Repository for <see cref="Company"/> tenant records. Soft-delete filter only (no tenant filter — SuperAdmin manages all companies).</summary>
public IRepository<Company> Companies =>
_companies ??= new Repository<Company>(_context);
/// <summary>Repository for <see cref="CompanyOperatingCosts"/> (one-to-one with Company).</summary>
public IRepository<CompanyOperatingCosts> CompanyOperatingCosts =>
_companyOperatingCosts ??= new Repository<CompanyOperatingCosts>(_context);
/// <summary>Repository for <see cref="CompanyPreferences"/> (one-to-one with Company).</summary>
public IRepository<CompanyPreferences> CompanyPreferences =>
_companyPreferences ??= new Repository<CompanyPreferences>(_context);
/// <summary>Repository for <see cref="CompanySmsAgreement"/> audit records. Tenant-filtered; never soft-deleted — legal audit trail.</summary>
public IRepository<CompanySmsAgreement> CompanySmsAgreements =>
_companySmsAgreements ??= new Repository<CompanySmsAgreement>(_context);
// AI Predictions
/// <summary>Repository for <see cref="AiItemPrediction"/> records; tenant-filtered. Shared between QuoteItem and JobItem via a single nullable FK — no duplication on quote→job conversion.</summary>
public IRepository<AiItemPrediction> AiItemPredictions =>
_aiItemPredictions ??= new Repository<AiItemPrediction>(_context);
// Powder Insights
/// <summary>Repository for <see cref="PowderUsageLog"/> records capturing per-coat powder consumption; used by powder-usage analytics.</summary>
public IPowderUsageLogRepository PowderUsageLogs =>
_powderUsageLogs ??= new PowderUsageLogRepository(_context);
// Core repositories
/// <summary>Repository for <see cref="Customer"/> records (commercial and non-commercial); tenant-filtered with soft delete.</summary>
public ICustomerRepository Customers =>
_customers ??= new CustomerRepository(_context);
/// <summary>Repository for <see cref="Job"/> records progressing through the 16-status lifecycle; tenant-filtered with soft delete.</summary>
public IJobRepository Jobs =>
_jobs ??= new JobRepository(_context);
/// <summary>Repository for <see cref="JobDailyPriority"/> overrides that let supervisors re-order the shop floor queue.</summary>
public IRepository<JobDailyPriority> JobDailyPriorities =>
_jobDailyPriorities ??= new Repository<JobDailyPriority>(_context);
/// <summary>Repository for <see cref="JobItem"/> line-items; tenant-filtered with soft delete.</summary>
public IRepository<JobItem> JobItems =>
_jobItems ??= new Repository<JobItem>(_context);
/// <summary>Repository for <see cref="JobItemCoat"/> powder coat passes; tenant-filtered with soft delete.</summary>
public IJobItemCoatRepository JobItemCoats =>
_jobItemCoats ??= new JobItemCoatRepository(_context);
public IRepository<JobItemPrepService> JobItemPrepServices =>
_jobItemPrepServices ??= new Repository<JobItemPrepService>(_context);
/// <summary>Repository for <see cref="JobChangeHistory"/> audit entries; tenant-filtered with soft delete.</summary>
public IRepository<JobChangeHistory> JobChangeHistories =>
_jobChangeHistories ??= new Repository<JobChangeHistory>(_context);
/// <summary>Repository for <see cref="JobPrepService"/> job-level prep service assignments; tenant-filtered with soft delete.</summary>
public IRepository<JobPrepService> JobPrepServices =>
_jobPrepServices ??= new Repository<JobPrepService>(_context);
/// <summary>Repository for <see cref="Quote"/> records with multi-item pricing; tenant-filtered with soft delete.</summary>
public IQuoteRepository Quotes =>
_quotes ??= new QuoteRepository(_context);
/// <summary>Repository for <see cref="QuotePhoto"/> AI photo uploads; tenant-filtered with soft delete.</summary>
public IRepository<QuotePhoto> QuotePhotos =>
_quotePhotos ??= new Repository<QuotePhoto>(_context);
/// <summary>Repository for <see cref="QuoteItem"/> line-items; tenant-filtered with soft delete.</summary>
public IRepository<QuoteItem> QuoteItems =>
_quoteItems ??= new Repository<QuoteItem>(_context);
/// <summary>Repository for <see cref="QuoteItemCoat"/> powder coat passes on a quote item; tenant-filtered with soft delete.</summary>
public IRepository<QuoteItemCoat> QuoteItemCoats =>
_quoteItemCoats ??= new Repository<QuoteItemCoat>(_context);
/// <summary>Repository for <see cref="QuoteItemPrepService"/> prep-service assignments on a quote item; tenant-filtered with soft delete.</summary>
public IRepository<QuoteItemPrepService> QuoteItemPrepServices =>
_quoteItemPrepServices ??= new Repository<QuoteItemPrepService>(_context);
/// <summary>Repository for <see cref="QuoteChangeHistory"/> audit entries; tenant-filtered with soft delete.</summary>
public IRepository<QuoteChangeHistory> QuoteChangeHistories =>
_quoteChangeHistories ??= new Repository<QuoteChangeHistory>(_context);
/// <summary>Repository for <see cref="InventoryItem"/> powder and material stock; tenant-filtered with soft delete.</summary>
public IRepository<InventoryItem> InventoryItems =>
_inventoryItems ??= new Repository<InventoryItem>(_context);
/// <summary>Platform-level powder catalog — no tenant filter, no soft delete.</summary>
public IPlainRepository<PowderCatalogItem> PowderCatalog =>
_powderCatalog ??= new PlainRepository<PowderCatalogItem>(_context);
/// <summary>Repository for <see cref="InventoryTransaction"/> stock movements; tenant-filtered with soft delete.</summary>
public IInventoryTransactionRepository InventoryTransactions =>
_inventoryTransactions ??= new InventoryTransactionRepository(_context);
/// <summary>Repository for <see cref="Equipment"/> records (ovens, sandblasters, booths); tenant-filtered with soft delete.</summary>
public IRepository<Equipment> Equipment =>
_equipment ??= new Repository<Equipment>(_context);
/// <summary>Repository for <see cref="OvenCost"/> named oven configurations used by the Oven Scheduler; tenant-filtered with soft delete.</summary>
public IRepository<OvenCost> OvenCosts =>
_ovenCosts ??= new Repository<OvenCost>(_context);
/// <summary>Repository for <see cref="CompanyBlastSetup"/> named blast setups; tenant-filtered with soft delete.</summary>
public IRepository<CompanyBlastSetup> BlastSetups =>
_blastSetups ??= new Repository<CompanyBlastSetup>(_context);
/// <summary>Repository for <see cref="MaintenanceRecord"/> equipment maintenance records; tenant-filtered with soft delete.</summary>
public IRepository<MaintenanceRecord> MaintenanceRecords =>
_maintenanceRecords ??= new Repository<MaintenanceRecord>(_context);
/// <summary>Repository for <see cref="Vendor"/> supplier records; tenant-filtered with soft delete.</summary>
public IRepository<Vendor> Vendors =>
_vendors ??= new Repository<Vendor>(_context);
/// <summary>Repository for <see cref="JobPhoto"/> attachments; tenant-filtered with soft delete.</summary>
public IJobPhotoRepository JobPhotos =>
_jobPhotos ??= new JobPhotoRepository(_context);
/// <summary>Repository for <see cref="JobNote"/> free-text staff notes on jobs; tenant-filtered with soft delete.</summary>
public IRepository<JobNote> JobNotes =>
_jobNotes ??= new Repository<JobNote>(_context);
/// <summary>Repository for <see cref="CustomerNote"/> free-text staff notes on customer records; tenant-filtered with soft delete.</summary>
public IRepository<CustomerNote> CustomerNotes =>
_customerNotes ??= new Repository<CustomerNote>(_context);
/// <summary>Repository for <see cref="JobStatusHistory"/> status-transition audit records; tenant-filtered with soft delete.</summary>
public IRepository<JobStatusHistory> JobStatusHistory =>
_jobStatusHistory ??= new Repository<JobStatusHistory>(_context);
/// <summary>Repository for <see cref="PricingTier"/> customer discount tiers; tenant-filtered with soft delete.</summary>
public IRepository<PricingTier> PricingTiers =>
_pricingTiers ??= new Repository<PricingTier>(_context);
// Lookup tables (replacing enums)
// These are stored in the database instead of C# enums so that tenants can
// customise display names, colours, and display order without a code deployment.
/// <summary>Repository for <see cref="JobStatusLookup"/> DB-backed job status definitions.</summary>
public IRepository<JobStatusLookup> JobStatusLookups =>
_jobStatusLookups ??= new Repository<JobStatusLookup>(_context);
/// <summary>Repository for <see cref="JobPriorityLookup"/> DB-backed job priority definitions.</summary>
public IRepository<JobPriorityLookup> JobPriorityLookups =>
_jobPriorityLookups ??= new Repository<JobPriorityLookup>(_context);
/// <summary>Repository for <see cref="QuoteStatusLookup"/> DB-backed quote status definitions.</summary>
public IRepository<QuoteStatusLookup> QuoteStatusLookups =>
_quoteStatusLookups ??= new Repository<QuoteStatusLookup>(_context);
/// <summary>Repository for <see cref="InventoryCategoryLookup"/> user-defined inventory categories.</summary>
public IRepository<InventoryCategoryLookup> InventoryCategoryLookups =>
_inventoryCategoryLookups ??= new Repository<InventoryCategoryLookup>(_context);
/// <summary>Repository for <see cref="AppointmentStatusLookup"/> appointment status definitions.</summary>
public IRepository<AppointmentStatusLookup> AppointmentStatusLookups =>
_appointmentStatusLookups ??= new Repository<AppointmentStatusLookup>(_context);
/// <summary>Repository for <see cref="AppointmentTypeLookup"/> appointment type definitions.</summary>
public IRepository<AppointmentTypeLookup> AppointmentTypeLookups =>
_appointmentTypeLookups ??= new Repository<AppointmentTypeLookup>(_context);
/// <summary>Repository for <see cref="PrepService"/> prep-service catalog entries shared across quotes and jobs.</summary>
public IRepository<PrepService> PrepServices =>
_prepServices ??= new Repository<PrepService>(_context);
/// <summary>Repository for <see cref="ShopWorker"/> profiles with role assignments; tenant-filtered with soft delete.</summary>
public IRepository<ShopWorker> ShopWorkers =>
_shopWorkers ??= new Repository<ShopWorker>(_context);
/// <summary>Repository for <see cref="ShopWorkerRoleCost"/> per-role labour cost rates; unique on (CompanyId, Role).</summary>
private IRepository<ShopWorkerRoleCost>? _shopWorkerRoleCosts;
public IRepository<ShopWorkerRoleCost> ShopWorkerRoleCosts =>
_shopWorkerRoleCosts ??= new Repository<ShopWorkerRoleCost>(_context);
/// <summary>Repository for <see cref="ReworkRecord"/> quality-failure and remediation records; tenant-filtered with soft delete.</summary>
private IRepository<ReworkRecord>? _reworkRecords;
public IRepository<ReworkRecord> ReworkRecords =>
_reworkRecords ??= new Repository<ReworkRecord>(_context);
/// <summary>Repository for <see cref="Refund"/> customer refund records; tenant-filtered with soft delete.</summary>
private IRepository<Refund>? _refunds;
public IRepository<Refund> Refunds =>
_refunds ??= new Repository<Refund>(_context);
/// <summary>Repository for <see cref="CreditMemo"/> records issued to customers; unique memo number per company.</summary>
private IRepository<CreditMemo>? _creditMemos;
public IRepository<CreditMemo> CreditMemos =>
_creditMemos ??= new Repository<CreditMemo>(_context);
/// <summary>Repository for <see cref="CreditMemoApplication"/> records linking credit memos to specific invoices.</summary>
private IRepository<CreditMemoApplication>? _creditMemoApplications;
public IRepository<CreditMemoApplication> CreditMemoApplications =>
_creditMemoApplications ??= new Repository<CreditMemoApplication>(_context);
/// <summary>Repository for <see cref="JobTimeEntry"/> clock-in/clock-out worker time records; used for labour-cost calculations.</summary>
private IRepository<JobTimeEntry>? _jobTimeEntries;
public IRepository<JobTimeEntry> JobTimeEntries =>
_jobTimeEntries ??= new Repository<JobTimeEntry>(_context);
// Appointments
/// <summary>Repository for <see cref="Appointment"/> customer appointment records; tenant-filtered with soft delete.</summary>
public IRepository<Appointment> Appointments =>
_appointments ??= new Repository<Appointment>(_context);
// Product Catalog
/// <summary>Repository for <see cref="CatalogCategory"/> hierarchical service catalog categories; tenant-filtered with soft delete.</summary>
public IRepository<CatalogCategory> CatalogCategories =>
_catalogCategories ??= new Repository<CatalogCategory>(_context);
/// <summary>Repository for <see cref="CatalogItem"/> pre-priced service catalog items; tenant-filtered with soft delete.</summary>
public IRepository<CatalogItem> CatalogItems =>
_catalogItems ??= new Repository<CatalogItem>(_context);
/// <summary>Repository for <see cref="CatalogPriceCheckReport"/> AI price-check results archived per company.</summary>
public IRepository<CatalogPriceCheckReport> CatalogPriceCheckReports =>
_catalogPriceCheckReports ??= new Repository<CatalogPriceCheckReport>(_context);
// Notifications
/// <summary>Repository for <see cref="NotificationLog"/> outbound notification audit records; provides IgnoreQueryFilters lookups by InvoiceId, QuoteId, and JobId for notification history panels.</summary>
public INotificationLogRepository NotificationLogs =>
_notificationLogs ??= new NotificationLogRepository(_context);
/// <summary>Repository for <see cref="NotificationTemplate"/> per-company channel template overrides; unique on (CompanyId, Type, Channel).</summary>
public IRepository<NotificationTemplate> NotificationTemplates =>
_notificationTemplates ??= new Repository<NotificationTemplate>(_context);
// Subscription
/// <summary>Repository for <see cref="SubscriptionPlanConfig"/> Stripe plan definitions; global (no tenant filter).</summary>
public IRepository<SubscriptionPlanConfig> SubscriptionPlanConfigs =>
_subscriptionPlanConfigs ??= new Repository<SubscriptionPlanConfig>(_context);
// Platform content
/// <summary>Repository for <see cref="Announcement"/> platform-wide announcements; no tenant filter, no soft delete.</summary>
public IPlainRepository<Announcement> Announcements =>
_announcements ??= new PlainRepository<Announcement>(_context);
/// <summary>Repository for <see cref="BannedIp"/> IP ban records; no tenant filter, no soft delete.</summary>
public IPlainRepository<BannedIp> BannedIps =>
_bannedIps ??= new PlainRepository<BannedIp>(_context);
/// <summary>Repository for <see cref="DashboardTip"/> rotating tip-of-the-day entries; no tenant filter, no soft delete.</summary>
public IPlainRepository<DashboardTip> DashboardTips =>
_dashboardTips ??= new PlainRepository<DashboardTip>(_context);
/// <summary>Repository for <see cref="InAppNotification"/> bell-notification records; tenant-filtered with soft delete.</summary>
public IRepository<InAppNotification> InAppNotifications =>
_inAppNotifications ??= new Repository<InAppNotification>(_context);
/// <summary>Repository for <see cref="ReleaseNote"/> platform changelog entries; no tenant filter, no soft delete.</summary>
public IPlainRepository<ReleaseNote> ReleaseNotes =>
_releaseNotes ??= new PlainRepository<ReleaseNote>(_context);
// Bug Reports
/// <summary>Repository for <see cref="BugReport"/> user-submitted bug reports; tenant-filtered with soft delete.</summary>
public IRepository<BugReport> BugReports =>
_bugReports ??= new Repository<BugReport>(_context);
public IRepository<BugReportAttachment> BugReportAttachments =>
_bugReportAttachments ??= new Repository<BugReportAttachment>(_context);
// Contact Us
/// <summary>Repository for <see cref="ContactSubmission"/> contact form submissions; platform admins see all, company users see their own.</summary>
public IRepository<ContactSubmission> ContactSubmissions =>
_contactSubmissions ??= new Repository<ContactSubmission>(_context);
/// <summary>Repository for <see cref="ManufacturerLookupPattern"/> global AI URL patterns; soft-delete only (CompanyId = 0 by convention).</summary>
public IRepository<ManufacturerLookupPattern> ManufacturerLookupPatterns =>
_manufacturerLookupPatterns ??= new Repository<ManufacturerLookupPattern>(_context);
// Gift Certificates
/// <summary>Repository for <see cref="GiftCertificate"/> records; certificate code unique per company.</summary>
public IRepository<GiftCertificate> GiftCertificates =>
_giftCertificates ??= new Repository<GiftCertificate>(_context);
/// <summary>Repository for <see cref="GiftCertificateRedemption"/> usage events against a gift certificate.</summary>
public IRepository<GiftCertificateRedemption> GiftCertificateRedemptions =>
_giftCertificateRedemptions ??= new Repository<GiftCertificateRedemption>(_context);
/// <summary>Repository for <see cref="KioskSession"/> customer self-service intake sessions; tenant-filtered with soft delete.</summary>
public IRepository<KioskSession> KioskSessions =>
_kioskSessions ??= new Repository<KioskSession>(_context);
// Job Templates
/// <summary>Repository for <see cref="JobTemplate"/> reusable job blueprints; tenant-filtered with soft delete.</summary>
public IJobTemplateRepository JobTemplates =>
_jobTemplates ??= new JobTemplateRepository(_context);
/// <summary>Repository for <see cref="JobTemplateItem"/> item definitions within a job template.</summary>
public IRepository<JobTemplateItem> JobTemplateItems =>
_jobTemplateItems ??= new Repository<JobTemplateItem>(_context);
/// <summary>Repository for <see cref="JobTemplateItemCoat"/> coat definitions within a job template item.</summary>
public IRepository<JobTemplateItemCoat> JobTemplateItemCoats =>
_jobTemplateItemCoats ??= new Repository<JobTemplateItemCoat>(_context);
/// <summary>Repository for <see cref="JobTemplateItemPrepService"/> prep-service definitions within a job template item.</summary>
public IRepository<JobTemplateItemPrepService> JobTemplateItemPrepServices =>
_jobTemplateItemPrepServices ??= new Repository<JobTemplateItemPrepService>(_context);
// Purchase Orders
/// <summary>Repository for <see cref="PurchaseOrder"/> vendor purchase orders; tenant-filtered with soft delete.</summary>
public IPurchaseOrderRepository PurchaseOrders =>
_purchaseOrders ??= new PurchaseOrderRepository(_context);
/// <summary>Repository for <see cref="PurchaseOrderItem"/> line-items on a purchase order; cascade-deleted with the PO.</summary>
public IRepository<PurchaseOrderItem> PurchaseOrderItems =>
_purchaseOrderItems ??= new Repository<PurchaseOrderItem>(_context);
// Oven Scheduling
/// <summary>Repository for <see cref="OvenBatch"/> scheduled oven cure batches; tenant-filtered with soft delete.</summary>
public IRepository<OvenBatch> OvenBatches =>
_ovenBatches ??= new Repository<OvenBatch>(_context);
/// <summary>Repository for <see cref="OvenBatchItem"/> job/item assignments within an oven batch.</summary>
public IRepository<OvenBatchItem> OvenBatchItems =>
_ovenBatchItems ??= new Repository<OvenBatchItem>(_context);
// Invoices, Payments & Deposits
/// <summary>Repository for <see cref="Invoice"/> customer invoices (1:1 with Job); tenant-filtered with soft delete.</summary>
public IInvoiceRepository Invoices =>
_invoices ??= new InvoiceRepository(_context);
/// <summary>Repository for <see cref="InvoiceItem"/> line-items on an invoice; tenant-filtered with soft delete.</summary>
public IRepository<InvoiceItem> InvoiceItems =>
_invoiceItems ??= new Repository<InvoiceItem>(_context);
/// <summary>Repository for <see cref="Payment"/> customer payment records against invoices; tenant-filtered with soft delete.</summary>
public IRepository<Payment> Payments =>
_payments ??= new Repository<Payment>(_context);
/// <summary>
/// Repository for <see cref="Deposit"/> pre-invoice deposit records.
/// Unapplied deposits are auto-swept into <see cref="Payment"/> records when an invoice is created.
/// </summary>
public IRepository<Deposit> Deposits =>
_deposits ??= new Repository<Deposit>(_context);
// Expense Tracking / Accounts Payable
/// <summary>Repository for <see cref="Account"/> chart-of-accounts entries; supports self-referencing parent/child hierarchy.</summary>
public IRepository<Account> Accounts =>
_accounts ??= new Repository<Account>(_context);
/// <summary>Repository for <see cref="Bill"/> vendor bills (accounts payable); tenant-filtered with soft delete.</summary>
public IBillRepository Bills =>
_bills ??= new BillRepository(_context);
/// <summary>Repository for <see cref="BillLineItem"/> expense line-items on a vendor bill; each assigned to a chart-of-accounts entry.</summary>
public IRepository<BillLineItem> BillLineItems =>
_billLineItems ??= new Repository<BillLineItem>(_context);
/// <summary>Repository for <see cref="BillPayment"/> cash disbursement records against a vendor bill.</summary>
public IRepository<BillPayment> BillPayments =>
_billPayments ??= new Repository<BillPayment>(_context);
/// <summary>Repository for <see cref="Expense"/> ad-hoc non-bill expense records; tenant-filtered with soft delete.</summary>
public IRepository<Expense> Expenses =>
_expenses ??= new Repository<Expense>(_context);
// Manual Journal Entries
/// <summary>Repository for <see cref="JournalEntry"/> double-entry manual journal entries; tenant-filtered with soft delete.</summary>
public IRepository<JournalEntry> JournalEntries =>
_journalEntries ??= new Repository<JournalEntry>(_context);
/// <summary>Repository for <see cref="JournalEntryLine"/> individual debit/credit lines within a journal entry.</summary>
public IRepository<JournalEntryLine> JournalEntryLines =>
_journalEntryLines ??= new Repository<JournalEntryLine>(_context);
// Vendor Credits
/// <summary>Repository for <see cref="VendorCredit"/> credit notes received from vendors; tenant-filtered with soft delete.</summary>
public IRepository<VendorCredit> VendorCredits =>
_vendorCredits ??= new Repository<VendorCredit>(_context);
/// <summary>Repository for <see cref="VendorCreditLineItem"/> expense-reversal lines on a vendor credit.</summary>
public IRepository<VendorCreditLineItem> VendorCreditLineItems =>
_vendorCreditLineItems ??= new Repository<VendorCreditLineItem>(_context);
/// <summary>Repository for <see cref="VendorCreditApplication"/> records linking a vendor credit to a specific bill.</summary>
public IRepository<VendorCreditApplication> VendorCreditApplications =>
_vendorCreditApplications ??= new Repository<VendorCreditApplication>(_context);
// Bank Reconciliation
/// <summary>Repository for <see cref="BankReconciliation"/> sessions reconciling a bank account against a statement.</summary>
public IRepository<BankReconciliation> BankReconciliations =>
_bankReconciliations ??= new Repository<BankReconciliation>(_context);
// Tax Rates
/// <summary>Repository for <see cref="TaxRate"/> named tax rates used to pre-fill invoice tax percent by jurisdiction.</summary>
public IRepository<TaxRate> TaxRates =>
_taxRates ??= new Repository<TaxRate>(_context);
// Recurring Transactions
/// <summary>Repository for <see cref="RecurringTemplate"/> — saved recipes that auto-generate bills or expenses on a schedule.</summary>
public IRepository<RecurringTemplate> RecurringTemplates =>
_recurringTemplates ??= new Repository<RecurringTemplate>(_context);
public IRepository<FixedAsset> FixedAssets =>
_fixedAssets ??= new Repository<FixedAsset>(_context);
public IRepository<FixedAssetDepreciationEntry> FixedAssetDepreciationEntries =>
_fixedAssetDepreciationEntries ??= new Repository<FixedAssetDepreciationEntry>(_context);
public IRepository<Budget> Budgets =>
_budgets ??= new Repository<Budget>(_context);
public IRepository<BudgetLine> BudgetLines =>
_budgetLines ??= new Repository<BudgetLine>(_context);
public IRepository<YearEndClose> YearEndCloses =>
_yearEndCloses ??= new Repository<YearEndClose>(_context);
/// <summary>
/// Flushes all pending changes in the EF Core change tracker to the database.
/// Returns the number of state entries written.
/// <para>
/// Internally delegates to <see cref="ApplicationDbContext.SaveChangesAsync"/> which
/// automatically stamps <c>CreatedAt</c>, <c>UpdatedAt</c>, <c>CompanyId</c>, and audit
/// fields on every modified entity before the SQL is sent.
/// </para>
/// </summary>
public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}
/// <summary>
/// Alias for <see cref="SaveChangesAsync"/>; provided so controllers can use the more
/// expressive <c>await _unitOfWork.CompleteAsync()</c> idiom that signals intent
/// ("I am done with this unit of work") rather than the more technical "save changes".
/// Both methods are functionally identical.
/// </summary>
public async Task<int> CompleteAsync()
{
return await SaveChangesAsync();
}
/// <summary>
/// Executes <paramref name="operation"/> inside a database transaction that is automatically
/// committed if the operation succeeds or rolled back if it throws.
/// <para>
/// Uses the EF Core <c>IExecutionStrategy</c> so that the transaction is automatically
/// retried on transient SQL errors (e.g., deadlocks) when the database provider supports it.
/// This is the preferred approach over the manual <see cref="BeginTransactionAsync"/> /
/// <see cref="CommitTransactionAsync"/> pair for most use cases.
/// </para>
/// </summary>
/// <param name="operation">An async delegate whose work must be atomic. Do NOT call SaveChangesAsync inside — it is called automatically before commit.</param>
public async Task ExecuteInTransactionAsync(Func<Task> operation)
{
var strategy = _context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync<object?, bool>(
null,
async (dbCtx, state, ct) =>
{
await using var tx = await _context.Database.BeginTransactionAsync(ct);
try
{
await operation();
await _context.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return true;
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
},
null);
}
/// <summary>
/// Executes <paramref name="operation"/> inside an execution-strategy-aware database transaction
/// and returns the result produced by the operation.
/// <para>
/// Identical to <see cref="ExecuteInTransactionAsync(Func{Task})"/> but for operations that
/// produce a value (e.g., returning a newly created entity or a generated document number).
/// SaveChangesAsync is called automatically before the transaction is committed.
/// </para>
/// </summary>
/// <typeparam name="T">The type of value returned by the operation.</typeparam>
/// <param name="operation">An async delegate that returns a value and whose work must be atomic.</param>
/// <returns>The value returned by <paramref name="operation"/>.</returns>
public async Task<T> ExecuteInTransactionAsync<T>(Func<Task<T>> operation)
{
var strategy = _context.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync<object?, T>(
null,
async (dbCtx, state, ct) =>
{
await using var tx = await _context.Database.BeginTransactionAsync(ct);
try
{
var result = await operation();
await _context.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return result;
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
},
null);
}
/// <summary>
/// Detaches all tracked entities from the EF Core change tracker.
/// <para>
/// Use this in long-running processes (e.g., bulk import, seed data) to prevent the
/// change tracker from accumulating thousands of entity snapshots, which would cause
/// quadratic memory and CPU growth as EF Core compares snapshots on each SaveChanges.
/// Not typically needed in per-request controllers because the DbContext is scoped.
/// </para>
/// </summary>
public void ClearChangeTracker()
{
_context.ChangeTracker.Clear();
}
/// <summary>
/// Starts an explicit database transaction and stores it in <c>_transaction</c>.
/// Use this when you need to interleave multiple <see cref="SaveChangesAsync"/> calls
/// within one atomic operation, or when you need finer control than
/// <see cref="ExecuteInTransactionAsync(Func{Task})"/> provides.
/// Always pair with a matching <see cref="CommitTransactionAsync"/> or
/// <see cref="RollbackTransactionAsync"/> in a try/finally block.
/// </summary>
public async Task BeginTransactionAsync()
{
_transaction = await _context.Database.BeginTransactionAsync();
}
/// <summary>
/// Saves all pending changes and commits the active transaction.
/// If the save or commit fails the transaction is automatically rolled back and
/// disposed, and the exception is re-thrown so callers can handle it.
/// The <c>_transaction</c> field is nulled after disposal so the instance is safe to reuse.
/// </summary>
public async Task CommitTransactionAsync()
{
try
{
await SaveChangesAsync();
if (_transaction != null)
{
await _transaction.CommitAsync();
}
}
catch
{
await RollbackTransactionAsync();
throw;
}
finally
{
if (_transaction != null)
{
await _transaction.DisposeAsync();
_transaction = null;
}
}
}
/// <summary>
/// Rolls back the active transaction (if one exists), discarding all changes made since
/// <see cref="BeginTransactionAsync"/> was called, then disposes and nulls the transaction.
/// Safe to call even if no transaction is active — the null check prevents an exception.
/// </summary>
public async Task RollbackTransactionAsync()
{
if (_transaction != null)
{
await _transaction.RollbackAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
}
/// <summary>
/// Disposes the active transaction (if any) and the underlying <see cref="ApplicationDbContext"/>.
/// Called automatically by the ASP.NET Core DI container at the end of each HTTP request
/// because <c>UnitOfWork</c> is registered as a scoped service.
/// Explicit disposal is only needed in non-DI scenarios (e.g., unit tests).
/// </summary>
public void Dispose()
{
_transaction?.Dispose();
_context.Dispose();
}
}