Phase 2: Eliminate ApplicationDbContext from domain controllers
Migrated InvoicesController, QuotesController, JobsController, BillsController, PurchaseOrdersController, and CustomersController to route all data access through IUnitOfWork typed/generic repositories instead of injecting ApplicationDbContext directly. New typed repositories added: IJobRepository (GetScheduledJobsForDateAsync, GetActiveJobsForMobileAsync, LoadForCostingAsync), INotificationLogRepository (GetLatestForJobAsync, GetAllForJobAsync), IQuoteRepository (GetItemsWithCoatsAsync with CatalogItem eager load + AsNoTracking), and IJobRepository.GetOrphanedConversionJobAsync. All EF complex include chains relocated into repository methods; controllers now call named query methods rather than composing raw IQueryable chains. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,6 @@ using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Application.DTOs.Common;
|
||||
using PowderCoating.Application.DTOs.Invoice;
|
||||
using PowderCoating.Application.Interfaces;
|
||||
@@ -10,7 +9,6 @@ using PowderCoating.Core.Entities;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using PowderCoating.Core.Enums;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
using PowderCoating.Shared.Constants;
|
||||
using PowderCoating.Web.Extensions;
|
||||
using PowderCoating.Web.Helpers;
|
||||
@@ -26,7 +24,6 @@ public class InvoicesController : Controller
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ILogger<InvoicesController> _logger;
|
||||
private readonly IPdfService _pdfService;
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly IAccountBalanceService _accountBalanceService;
|
||||
@@ -37,7 +34,6 @@ public class InvoicesController : Controller
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ILogger<InvoicesController> logger,
|
||||
IPdfService pdfService,
|
||||
ApplicationDbContext context,
|
||||
ITenantContext tenantContext,
|
||||
INotificationService notificationService,
|
||||
IAccountBalanceService accountBalanceService)
|
||||
@@ -47,7 +43,6 @@ public class InvoicesController : Controller
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_pdfService = pdfService;
|
||||
_context = context;
|
||||
_tenantContext = tenantContext;
|
||||
_notificationService = notificationService;
|
||||
_accountBalanceService = accountBalanceService;
|
||||
@@ -256,8 +251,7 @@ public class InvoicesController : Controller
|
||||
|
||||
// Check whether this company's plan allows online payments
|
||||
var company = await _unitOfWork.Companies.GetByIdAsync(_tenantContext.GetCurrentCompanyId() ?? 0);
|
||||
var planConfig = company == null ? null : await _context.Set<SubscriptionPlanConfig>()
|
||||
.AsNoTracking()
|
||||
var planConfig = company == null ? null : await _unitOfWork.SubscriptionPlanConfigs
|
||||
.FirstOrDefaultAsync(p => p.Plan == company.SubscriptionPlan);
|
||||
var onlinePaymentsAllowed = company?.OnlinePaymentsOverride ?? (planConfig?.AllowOnlinePayments ?? false);
|
||||
ViewBag.OnlinePaymentsEnabled = onlinePaymentsAllowed
|
||||
@@ -298,12 +292,10 @@ public class InvoicesController : Controller
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
if (currentUser == null) return Unauthorized();
|
||||
|
||||
var prefs = await _context.CompanyPreferences
|
||||
.AsNoTracking()
|
||||
var prefs = await _unitOfWork.CompanyPreferences
|
||||
.FirstOrDefaultAsync(p => p.CompanyId == currentUser.CompanyId && !p.IsDeleted);
|
||||
|
||||
var costs = await _context.CompanyOperatingCosts
|
||||
.AsNoTracking()
|
||||
var costs = await _unitOfWork.CompanyOperatingCosts
|
||||
.FirstOrDefaultAsync(c => c.CompanyId == currentUser.CompanyId && !c.IsDeleted);
|
||||
|
||||
var dto = new CreateInvoiceDto
|
||||
@@ -321,9 +313,7 @@ public class InvoicesController : Controller
|
||||
if (job == null) return NotFound();
|
||||
|
||||
// Validate no existing invoice for this job
|
||||
var existing = await _context.Set<Invoice>()
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(i => i.JobId == jobId.Value && i.CompanyId == currentUser.CompanyId && !i.IsDeleted);
|
||||
var existing = await _unitOfWork.Invoices.GetForJobAsync(jobId.Value, includeDeleted: true);
|
||||
if (existing != null)
|
||||
return RedirectToAction(nameof(Details), new { id = existing.Id });
|
||||
|
||||
@@ -343,8 +333,8 @@ public class InvoicesController : Controller
|
||||
: new Dictionary<int, CatalogItem>();
|
||||
|
||||
// Fall back to the default revenue account (4000) if a catalog item has no specific account
|
||||
var defaultRevenueAccount = await _context.Set<Account>()
|
||||
.FirstOrDefaultAsync(a => a.AccountNumber == "4000" && a.CompanyId == currentUser.CompanyId && !a.IsDeleted);
|
||||
var defaultRevenueAccount = await _unitOfWork.Accounts
|
||||
.FirstOrDefaultAsync(a => a.AccountNumber == "4000" && a.IsActive);
|
||||
|
||||
// If the job came from a quote, load it so we can use the agreed pricing.
|
||||
// The quote stores the approved total including oven batch cost and shop supplies —
|
||||
@@ -352,9 +342,7 @@ public class InvoicesController : Controller
|
||||
PowderCoating.Core.Entities.Quote? sourceQuote = null;
|
||||
if (job.QuoteId.HasValue)
|
||||
{
|
||||
sourceQuote = await _context.Set<PowderCoating.Core.Entities.Quote>()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(q => q.Id == job.QuoteId.Value && !q.IsDeleted);
|
||||
sourceQuote = await _unitOfWork.Quotes.GetByIdAsync(job.QuoteId.Value);
|
||||
}
|
||||
|
||||
// Pre-populate from job items
|
||||
@@ -494,9 +482,7 @@ public class InvoicesController : Controller
|
||||
// Validate no existing invoice for this job before starting the transaction
|
||||
if (dto.JobId.HasValue)
|
||||
{
|
||||
var existing = await _context.Set<Invoice>()
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(i => i.JobId == dto.JobId && i.CompanyId == currentUser.CompanyId && !i.IsDeleted);
|
||||
var existing = await _unitOfWork.Invoices.GetForJobAsync(dto.JobId.Value, includeDeleted: true);
|
||||
if (existing != null)
|
||||
{
|
||||
ModelState.AddModelError("", "An invoice already exists for this job.");
|
||||
@@ -591,11 +577,10 @@ public class InvoicesController : Controller
|
||||
|
||||
// Auto-apply any unapplied deposits for this job (and its linked quote)
|
||||
var job = dto.JobId.HasValue ? await _unitOfWork.Jobs.GetByIdAsync(dto.JobId.Value) : null;
|
||||
var depositQuery = _context.Set<Deposit>()
|
||||
.Where(d => !d.IsDeleted && d.CompanyId == currentUser.CompanyId
|
||||
&& d.AppliedToInvoiceId == null
|
||||
&& (d.JobId == dto.JobId || (job != null && job.QuoteId.HasValue && d.QuoteId == job.QuoteId.Value)));
|
||||
pendingDeposits = await depositQuery.ToListAsync();
|
||||
pendingDeposits = (await _unitOfWork.Deposits.FindAsync(
|
||||
d => d.AppliedToInvoiceId == null
|
||||
&& (d.JobId == dto.JobId || (job != null && job.QuoteId.HasValue && d.QuoteId == job.QuoteId.Value))))
|
||||
.ToList();
|
||||
|
||||
foreach (var deposit in pendingDeposits)
|
||||
{
|
||||
@@ -908,11 +893,7 @@ public class InvoicesController : Controller
|
||||
id, invoice.InvoiceNumber, notifyEx.InnerException?.Message ?? "none");
|
||||
}
|
||||
|
||||
var notifLog = await _context.NotificationLogs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.InvoiceId == id)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.FirstOrDefaultAsync();
|
||||
var notifLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id);
|
||||
this.SetNotificationResultToast(notifLog);
|
||||
|
||||
TempData["Success"] = $"Invoice {invoice.InvoiceNumber} marked as sent.";
|
||||
@@ -1043,11 +1024,7 @@ public class InvoicesController : Controller
|
||||
}
|
||||
|
||||
|
||||
var paymentNotifLog = await _context.NotificationLogs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.InvoiceId == id)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.FirstOrDefaultAsync();
|
||||
var paymentNotifLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id);
|
||||
this.SetNotificationResultToast(paymentNotifLog);
|
||||
|
||||
TempData["Success"] = overpayment > 0
|
||||
@@ -1325,9 +1302,7 @@ public class InvoicesController : Controller
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
if (currentUser == null) return Unauthorized();
|
||||
|
||||
var existing = await _context.Set<Invoice>()
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(i => i.JobId == jobId && i.CompanyId == currentUser.CompanyId && !i.IsDeleted);
|
||||
var existing = await _unitOfWork.Invoices.GetForJobAsync(jobId, includeDeleted: true);
|
||||
|
||||
if (existing != null)
|
||||
return RedirectToAction(nameof(Details), new { id = existing.Id });
|
||||
@@ -1386,11 +1361,7 @@ public class InvoicesController : Controller
|
||||
|
||||
await _notificationService.NotifyInvoiceSentAsync(invoice, pdfBytes, pdfFilename);
|
||||
|
||||
var latestLog = await _context.NotificationLogs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.InvoiceId == id)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.FirstOrDefaultAsync();
|
||||
var latestLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id);
|
||||
|
||||
if (latestLog?.Status == NotificationStatus.Failed)
|
||||
return Json(new { success = false, message = $"Email delivery failed: {latestLog.ErrorMessage}" });
|
||||
@@ -1420,16 +1391,10 @@ public class InvoicesController : Controller
|
||||
public async Task<IActionResult> NotificationsSent(int id)
|
||||
{
|
||||
var tz = ViewBag.CompanyTimeZone as string;
|
||||
var raw = await _context.NotificationLogs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => n.InvoiceId == id)
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.Select(n => new { n.Id, Channel = n.Channel.ToString(), Type = n.NotificationType.ToString(),
|
||||
Status = n.Status.ToString(), n.RecipientName, n.Recipient, n.Subject, n.ErrorMessage, n.Message, n.SentAt })
|
||||
.ToListAsync();
|
||||
|
||||
var logs = raw.Select(n => new { n.Id, n.Channel, n.Type, n.Status, n.RecipientName,
|
||||
n.Recipient, n.Subject, n.ErrorMessage, n.Message, SentAt = n.SentAt.Tz(tz).ToString("MMM d, yyyy h:mm tt") });
|
||||
var entries = await _unitOfWork.NotificationLogs.GetAllForInvoiceAsync(id);
|
||||
var logs = entries.Select(n => new { n.Id, Channel = n.Channel.ToString(), Type = n.NotificationType.ToString(),
|
||||
Status = n.Status.ToString(), n.RecipientName, n.Recipient, n.Subject, n.ErrorMessage, n.Message,
|
||||
SentAt = n.SentAt.Tz(tz).ToString("MMM d, yyyy h:mm tt") });
|
||||
return Json(logs);
|
||||
}
|
||||
|
||||
@@ -1474,32 +1439,24 @@ public class InvoicesController : Controller
|
||||
}
|
||||
|
||||
// Soft-delete line items
|
||||
var invoiceItems = await _context.InvoiceItems
|
||||
.Where(ii => ii.InvoiceId == id && !ii.IsDeleted)
|
||||
.ToListAsync();
|
||||
var invoiceItems = await _unitOfWork.InvoiceItems.FindAsync(ii => ii.InvoiceId == id);
|
||||
foreach (var item in invoiceItems)
|
||||
await _unitOfWork.InvoiceItems.SoftDeleteAsync(item.Id);
|
||||
|
||||
// Soft-delete any payments (draft invoices shouldn't have them, but be safe)
|
||||
var payments = await _context.Payments
|
||||
.Where(p => p.InvoiceId == id && !p.IsDeleted)
|
||||
.ToListAsync();
|
||||
var payments = await _unitOfWork.Payments.FindAsync(p => p.InvoiceId == id);
|
||||
foreach (var payment in payments)
|
||||
await _unitOfWork.Payments.SoftDeleteAsync(payment.Id);
|
||||
|
||||
// Un-apply any deposits that were applied to this invoice so they can be
|
||||
// re-applied if the invoice is recreated from the same job
|
||||
var appliedDeposits = await _context.Deposits
|
||||
.Where(d => d.AppliedToInvoiceId == id && !d.IsDeleted)
|
||||
.ToListAsync();
|
||||
var appliedDeposits = await _unitOfWork.Deposits.FindAsync(d => d.AppliedToInvoiceId == id);
|
||||
foreach (var deposit in appliedDeposits)
|
||||
{
|
||||
deposit.AppliedToInvoiceId = null;
|
||||
deposit.AppliedDate = null;
|
||||
deposit.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
if (appliedDeposits.Any())
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Reverse account balances (mirror of Create): credit AR, debit revenue + sales tax
|
||||
var arAccountId = await GetArAccountIdAsync(invoice.CompanyId);
|
||||
@@ -1529,36 +1486,11 @@ public class InvoicesController : Controller
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Loads a complete invoice aggregate from the DB context (bypasses the generic repository
|
||||
/// to allow the deep multi-level Include chain). Includes customer, job, preparer, all
|
||||
/// non-deleted line items with revenue accounts and generated GCs, all non-deleted payments
|
||||
/// with recorders and deposit accounts, refunds, credit applications, and GC redemptions.
|
||||
/// Returns null if the invoice doesn't exist or is soft-deleted — callers check for null
|
||||
/// and return NotFound rather than loading a partial object.
|
||||
/// Delegates to <see cref="IInvoiceRepository.LoadForViewAsync"/> which expresses the full
|
||||
/// eight-table include chain. Returns null if not found or soft-deleted.
|
||||
/// </summary>
|
||||
private async Task<Invoice?> LoadInvoiceForViewAsync(int id)
|
||||
{
|
||||
return await _context.Set<Invoice>()
|
||||
.Where(i => i.Id == id && !i.IsDeleted)
|
||||
.Include(i => i.Customer)
|
||||
.Include(i => i.Job)
|
||||
.Include(i => i.PreparedBy)
|
||||
.Include(i => i.SalesTaxAccount)
|
||||
.Include(i => i.InvoiceItems.Where(ii => !ii.IsDeleted))
|
||||
.ThenInclude(ii => ii.RevenueAccount)
|
||||
.Include(i => i.InvoiceItems.Where(ii => !ii.IsDeleted))
|
||||
.ThenInclude(ii => ii.GeneratedGiftCertificate)
|
||||
.Include(i => i.Payments.Where(p => !p.IsDeleted))
|
||||
.ThenInclude(p => p.RecordedBy)
|
||||
.Include(i => i.Payments.Where(p => !p.IsDeleted))
|
||||
.ThenInclude(p => p.DepositAccount)
|
||||
.Include(i => i.Refunds.Where(r => !r.IsDeleted))
|
||||
.ThenInclude(r => r.IssuedBy)
|
||||
.Include(i => i.CreditApplications.Where(ca => !ca.IsDeleted))
|
||||
.ThenInclude(ca => ca.CreditMemo)
|
||||
.Include(i => i.GiftCertificateRedemptions)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
private async Task<Invoice?> LoadInvoiceForViewAsync(int id) =>
|
||||
await _unitOfWork.Invoices.LoadForViewAsync(id);
|
||||
|
||||
/// <summary>
|
||||
/// Converts an Invoice entity to a fully populated InvoiceDto for the view layer. AutoMapper
|
||||
@@ -1737,11 +1669,10 @@ public class InvoicesController : Controller
|
||||
{
|
||||
var prefix = $"CM-{DateTime.UtcNow:yy}{DateTime.UtcNow.Month:D2}-";
|
||||
|
||||
var existing = await _context.Set<CreditMemo>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(m => m.CompanyId == companyId && m.MemoNumber.StartsWith(prefix))
|
||||
var existing = (await _unitOfWork.CreditMemos.FindAsync(
|
||||
m => m.CompanyId == companyId && m.MemoNumber.StartsWith(prefix), true))
|
||||
.Select(m => m.MemoNumber)
|
||||
.ToListAsync();
|
||||
.ToList();
|
||||
|
||||
var maxNum = 0;
|
||||
foreach (var num in existing)
|
||||
@@ -1764,26 +1695,18 @@ public class InvoicesController : Controller
|
||||
/// </summary>
|
||||
private async Task<string> GenerateInvoiceNumberAsync(int companyId)
|
||||
{
|
||||
var prefs = await _context.CompanyPreferences
|
||||
.IgnoreQueryFilters()
|
||||
.Where(p => p.CompanyId == companyId && !p.IsDeleted)
|
||||
.Select(p => new { p.InvoiceNumberPrefix })
|
||||
.FirstOrDefaultAsync();
|
||||
var prefs = await _unitOfWork.CompanyPreferences
|
||||
.FirstOrDefaultAsync(p => p.CompanyId == companyId && !p.IsDeleted);
|
||||
|
||||
var invoicePrefix = !string.IsNullOrWhiteSpace(prefs?.InvoiceNumberPrefix) ? prefs.InvoiceNumberPrefix : "INV";
|
||||
var prefix = $"{invoicePrefix}-{DateTime.UtcNow:yy}{DateTime.UtcNow.Month:D2}-";
|
||||
|
||||
var existing = await _context.Set<Invoice>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(i => i.CompanyId == companyId && i.InvoiceNumber.StartsWith(prefix))
|
||||
.Select(i => i.InvoiceNumber)
|
||||
.ToListAsync();
|
||||
|
||||
var last = await _unitOfWork.Invoices.GetLastInvoiceNumberByPrefixAsync(companyId, prefix);
|
||||
var maxNum = 0;
|
||||
foreach (var num in existing)
|
||||
if (last != null && last.Length >= prefix.Length + 4)
|
||||
{
|
||||
var suffix = num.Length >= prefix.Length + 4 ? num.Substring(prefix.Length) : "";
|
||||
if (int.TryParse(suffix, out int n) && n > maxNum)
|
||||
var suffix = last.Substring(prefix.Length);
|
||||
if (int.TryParse(suffix, out int n))
|
||||
maxNum = n;
|
||||
}
|
||||
|
||||
@@ -1802,8 +1725,7 @@ public class InvoicesController : Controller
|
||||
ViewBag.Customers = customers.Where(c => c.IsActive).OrderBy(c => c.CompanyName ?? c.ContactLastName).ToList();
|
||||
|
||||
// Expose company default tax rate and exempt customer IDs for client-side tax handling
|
||||
var costs = await _context.CompanyOperatingCosts
|
||||
.AsNoTracking()
|
||||
var costs = await _unitOfWork.CompanyOperatingCosts
|
||||
.FirstOrDefaultAsync(c => c.CompanyId == companyId && !c.IsDeleted);
|
||||
ViewBag.CompanyTaxPercent = costs?.TaxPercent ?? 0;
|
||||
ViewBag.TaxExemptCustomerIds = customers
|
||||
@@ -1812,12 +1734,12 @@ public class InvoicesController : Controller
|
||||
.ToHashSet();
|
||||
|
||||
// Merchandise items for the invoice merch picker (all active IsMerchandise items)
|
||||
var merchItems = await _context.Set<CatalogItem>()
|
||||
.Include(i => i.Category)
|
||||
.Where(i => i.IsMerchandise && i.IsActive && !i.IsDeleted && i.CompanyId == companyId)
|
||||
var allMerchItems = await _unitOfWork.CatalogItems.FindAsync(
|
||||
i => i.IsMerchandise && i.IsActive, false, i => i.Category);
|
||||
var merchItems = allMerchItems
|
||||
.OrderBy(i => i.Category.Name).ThenBy(i => i.DisplayOrder).ThenBy(i => i.Name)
|
||||
.Select(i => new { i.Id, i.Name, i.SKU, CategoryName = i.Category.Name, i.DefaultPrice, i.RevenueAccountId })
|
||||
.ToListAsync();
|
||||
.ToList();
|
||||
ViewBag.MerchandiseItems = System.Text.Json.JsonSerializer.Serialize(merchItems,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase });
|
||||
}
|
||||
@@ -1845,15 +1767,13 @@ public class InvoicesController : Controller
|
||||
return accounts.FirstOrDefault()?.Id;
|
||||
}
|
||||
|
||||
/// <summary>Looks up the "2200 Sales Tax Payable" account for this company, or any active OtherCurrentLiability with "tax" in the name.</summary>
|
||||
/// <summary>Looks up the "2200 Sales Tax Payable" account for this company, or any active Liability account with "tax" in the name.</summary>
|
||||
private async Task<int?> ResolveSalesTaxAccountIdAsync(int companyId)
|
||||
{
|
||||
var taxAccount = await _context.Set<Account>()
|
||||
.Where(a => a.CompanyId == companyId && !a.IsDeleted && a.IsActive)
|
||||
.OrderBy(a => a.AccountNumber == "2200" ? 0 : 1)
|
||||
.ThenBy(a => a.AccountNumber)
|
||||
.FirstOrDefaultAsync(a => a.AccountNumber == "2200"
|
||||
|| (a.AccountType == AccountType.Liability && a.Name.ToLower().Contains("tax")));
|
||||
var taxAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.AccountNumber == "2200" && a.IsActive);
|
||||
taxAccount ??= await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.AccountType == AccountType.Liability && a.IsActive && a.Name.ToLower().Contains("tax"));
|
||||
return taxAccount?.Id;
|
||||
}
|
||||
|
||||
@@ -2371,16 +2291,13 @@ public class InvoicesController : Controller
|
||||
/// </summary>
|
||||
private async Task<string?> TryGeneratePaymentTokenAsync(Invoice invoice)
|
||||
{
|
||||
var company = await _context.Companies
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.Id == invoice.CompanyId && !c.IsDeleted);
|
||||
var company = await _unitOfWork.Companies.GetByIdAsync(invoice.CompanyId);
|
||||
|
||||
if (company == null) return null;
|
||||
if (company.StripeConnectStatus != StripeConnectStatus.Active) return null;
|
||||
if (invoice.BalanceDue <= 0) return null;
|
||||
|
||||
var planConfig = await _context.Set<SubscriptionPlanConfig>()
|
||||
.AsNoTracking()
|
||||
var planConfig = await _unitOfWork.SubscriptionPlanConfigs
|
||||
.FirstOrDefaultAsync(p => p.Plan == company.SubscriptionPlan);
|
||||
|
||||
var onlinePaymentsAllowed = company.OnlinePaymentsOverride ?? (planConfig?.AllowOnlinePayments ?? false);
|
||||
@@ -2405,8 +2322,7 @@ public class InvoicesController : Controller
|
||||
{
|
||||
try
|
||||
{
|
||||
var invoice = await _context.Invoices
|
||||
.FirstOrDefaultAsync(i => i.Id == id && !i.IsDeleted);
|
||||
var invoice = await _unitOfWork.Invoices.GetByIdAsync(id);
|
||||
if (invoice == null) return NotFound();
|
||||
|
||||
if (invoice.BalanceDue <= 0)
|
||||
@@ -2417,8 +2333,8 @@ public class InvoicesController : Controller
|
||||
return Json(new { success = false, message = "Online payments are not available for this company." });
|
||||
|
||||
invoice.UpdatedAt = DateTime.UtcNow;
|
||||
_context.Update(invoice);
|
||||
await _context.SaveChangesAsync();
|
||||
await _unitOfWork.Invoices.UpdateAsync(invoice);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
var paymentUrl = Url.Action("Index", "Payment", new { token }, Request.Scheme)!
|
||||
.Replace("/Payment/Index/", "/pay/");
|
||||
@@ -2452,27 +2368,11 @@ public class InvoicesController : Controller
|
||||
var endDate = to ?? startDate.AddMonths(1).AddDays(-1);
|
||||
var endDateInclusive = endDate.AddDays(1);
|
||||
|
||||
var invoices = await _context.Invoices
|
||||
.AsNoTracking()
|
||||
.Include(i => i.Customer)
|
||||
.Where(i => !i.IsDeleted
|
||||
&& i.CompanyId == companyId
|
||||
&& i.OnlineAmountPaid > 0
|
||||
&& i.UpdatedAt >= startDate
|
||||
&& i.UpdatedAt < endDateInclusive)
|
||||
.OrderByDescending(i => i.UpdatedAt)
|
||||
.ToListAsync();
|
||||
var invoices = await _unitOfWork.Invoices
|
||||
.GetOnlineInvoicesForPeriodAsync(companyId, startDate, endDateInclusive);
|
||||
|
||||
var refunds = await _context.Refunds
|
||||
.AsNoTracking()
|
||||
.Include(r => r.Invoice).ThenInclude(inv => inv!.Customer)
|
||||
.Where(r => !r.IsDeleted
|
||||
&& r.CompanyId == companyId
|
||||
&& r.RefundMethod == PaymentMethod.CreditDebitCard
|
||||
&& r.RefundDate >= startDate
|
||||
&& r.RefundDate < endDateInclusive)
|
||||
.OrderByDescending(r => r.RefundDate)
|
||||
.ToListAsync();
|
||||
var refunds = await _unitOfWork.Invoices
|
||||
.GetOnlineRefundsForPeriodAsync(companyId, startDate, endDateInclusive);
|
||||
|
||||
var vm = new OnlinePaymentsViewModel
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user