Initial commit
This commit is contained in:
@@ -0,0 +1,640 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
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;
|
||||
|
||||
// AI Predictions
|
||||
private IRepository<AiItemPrediction>? _aiItemPredictions;
|
||||
|
||||
// Powder Insights
|
||||
private IRepository<PowderUsageLog>? _powderUsageLogs;
|
||||
|
||||
// Core repositories
|
||||
private IRepository<Customer>? _customers;
|
||||
private IRepository<Job>? _jobs;
|
||||
private IRepository<JobDailyPriority>? _jobDailyPriorities;
|
||||
private IRepository<JobItem>? _jobItems;
|
||||
private IRepository<JobItemCoat>? _jobItemCoats;
|
||||
private IRepository<JobChangeHistory>? _jobChangeHistories;
|
||||
private IRepository<Quote>? _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 IRepository<InventoryTransaction>? _inventoryTransactions;
|
||||
private IRepository<Equipment>? _equipment;
|
||||
private IRepository<OvenCost>? _ovenCosts;
|
||||
private IRepository<CompanyBlastSetup>? _blastSetups;
|
||||
private IRepository<MaintenanceRecord>? _maintenanceRecords;
|
||||
private IRepository<Vendor>? _vendors;
|
||||
private IRepository<JobPhoto>? _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;
|
||||
|
||||
// Notifications
|
||||
private IRepository<NotificationLog>? _notificationLogs;
|
||||
private IRepository<NotificationTemplate>? _notificationTemplates;
|
||||
|
||||
// Subscription
|
||||
private IRepository<SubscriptionPlanConfig>? _subscriptionPlanConfigs;
|
||||
|
||||
// Job Templates
|
||||
private IRepository<JobTemplate>? _jobTemplates;
|
||||
private IRepository<JobTemplateItem>? _jobTemplateItems;
|
||||
private IRepository<JobTemplateItemCoat>? _jobTemplateItemCoats;
|
||||
private IRepository<JobTemplateItemPrepService>? _jobTemplateItemPrepServices;
|
||||
|
||||
// Bug Reports
|
||||
private IRepository<BugReport>? _bugReports;
|
||||
private IRepository<ContactSubmission>? _contactSubmissions;
|
||||
private IRepository<ManufacturerLookupPattern>? _manufacturerLookupPatterns;
|
||||
|
||||
// Gift Certificates
|
||||
private IRepository<GiftCertificate>? _giftCertificates;
|
||||
private IRepository<GiftCertificateRedemption>? _giftCertificateRedemptions;
|
||||
|
||||
// Purchase Orders
|
||||
private IRepository<PurchaseOrder>? _purchaseOrders;
|
||||
private IRepository<PurchaseOrderItem>? _purchaseOrderItems;
|
||||
|
||||
// Oven Scheduling
|
||||
private IRepository<OvenBatch>? _ovenBatches;
|
||||
private IRepository<OvenBatchItem>? _ovenBatchItems;
|
||||
|
||||
// Invoices, Payments & Deposits
|
||||
private IRepository<Invoice>? _invoices;
|
||||
private IRepository<InvoiceItem>? _invoiceItems;
|
||||
private IRepository<Payment>? _payments;
|
||||
private IRepository<Deposit>? _deposits;
|
||||
|
||||
// Expense Tracking / Accounts Payable
|
||||
private IRepository<Account>? _accounts;
|
||||
private IRepository<Bill>? _bills;
|
||||
private IRepository<BillLineItem>? _billLineItems;
|
||||
private IRepository<BillPayment>? _billPayments;
|
||||
private IRepository<Expense>? _expenses;
|
||||
|
||||
/// <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);
|
||||
|
||||
// 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 IRepository<PowderUsageLog> PowderUsageLogs =>
|
||||
_powderUsageLogs ??= new Repository<PowderUsageLog>(_context);
|
||||
|
||||
// Core repositories
|
||||
/// <summary>Repository for <see cref="Customer"/> records (commercial and non-commercial); tenant-filtered with soft delete.</summary>
|
||||
public IRepository<Customer> Customers =>
|
||||
_customers ??= new Repository<Customer>(_context);
|
||||
|
||||
/// <summary>Repository for <see cref="Job"/> records progressing through the 16-status lifecycle; tenant-filtered with soft delete.</summary>
|
||||
public IRepository<Job> Jobs =>
|
||||
_jobs ??= new Repository<Job>(_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 IRepository<JobItemCoat> JobItemCoats =>
|
||||
_jobItemCoats ??= new Repository<JobItemCoat>(_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="Quote"/> records with multi-item pricing; tenant-filtered with soft delete.</summary>
|
||||
public IRepository<Quote> Quotes =>
|
||||
_quotes ??= new Repository<Quote>(_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>Repository for <see cref="InventoryTransaction"/> stock movements; tenant-filtered with soft delete.</summary>
|
||||
public IRepository<InventoryTransaction> InventoryTransactions =>
|
||||
_inventoryTransactions ??= new Repository<InventoryTransaction>(_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 IRepository<JobPhoto> JobPhotos =>
|
||||
_jobPhotos ??= new Repository<JobPhoto>(_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);
|
||||
|
||||
// Notifications
|
||||
/// <summary>Repository for <see cref="NotificationLog"/> outbound notification audit records; tenant-filtered with soft delete.</summary>
|
||||
public IRepository<NotificationLog> NotificationLogs =>
|
||||
_notificationLogs ??= new Repository<NotificationLog>(_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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// Job Templates
|
||||
/// <summary>Repository for <see cref="JobTemplate"/> reusable job blueprints; tenant-filtered with soft delete.</summary>
|
||||
public IRepository<JobTemplate> JobTemplates =>
|
||||
_jobTemplates ??= new Repository<JobTemplate>(_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 IRepository<PurchaseOrder> PurchaseOrders =>
|
||||
_purchaseOrders ??= new Repository<PurchaseOrder>(_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 IRepository<Invoice> Invoices =>
|
||||
_invoices ??= new Repository<Invoice>(_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 IRepository<Bill> Bills =>
|
||||
_bills ??= new Repository<Bill>(_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);
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user