6a918c2afc
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>
774 lines
40 KiB
C#
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();
|
|
}
|
|
}
|