Phases 3 & 4: Complete data access architecture migration
Phase 3 — eliminated ApplicationDbContext from all non-exempt controllers, routing all data access through IUnitOfWork. Added IPlainRepository<T> for the four platform entities (Announcement, BannedIp, DashboardTip, ReleaseNote) that intentionally don't extend BaseEntity and therefore can't use the constrained IRepository<T>. Added permanent-exception comments to the 18 controllers that legitimately retain direct DbContext access (Identity infra, cross-tenant platform ops, bulk streaming exports). Phase 4 — added EnforceDataAccessArchitecture() to Program.cs, a startup gate that reflects over every Controller subclass and throws at boot if any non-exempt controller injects ApplicationDbContext. The app cannot start with a violation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ using PowderCoating.Application.DTOs.Company;
|
||||
using PowderCoating.Application.Interfaces;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
using PowderCoating.Core.Interfaces.Services;
|
||||
using PowderCoating.Shared.Constants;
|
||||
using PowderCoating.Web.Extensions;
|
||||
using System.Security.Claims;
|
||||
@@ -24,7 +24,9 @@ public class CompaniesController : Controller
|
||||
private readonly IMapper _mapper;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ISeedDataService _seedDataService;
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly ICompanyListService _companyList;
|
||||
private readonly ICompanyDataPurgeService _companyPurge;
|
||||
private readonly IAuditLogService _auditLog;
|
||||
private readonly IInAppNotificationService _inApp;
|
||||
private readonly ILogger<CompaniesController> _logger;
|
||||
|
||||
@@ -33,7 +35,9 @@ public class CompaniesController : Controller
|
||||
IMapper mapper,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ISeedDataService seedDataService,
|
||||
ApplicationDbContext context,
|
||||
ICompanyListService companyList,
|
||||
ICompanyDataPurgeService companyPurge,
|
||||
IAuditLogService auditLog,
|
||||
IInAppNotificationService inApp,
|
||||
ILogger<CompaniesController> logger)
|
||||
{
|
||||
@@ -41,7 +45,9 @@ public class CompaniesController : Controller
|
||||
_mapper = mapper;
|
||||
_userManager = userManager;
|
||||
_seedDataService = seedDataService;
|
||||
_context = context;
|
||||
_companyList = companyList;
|
||||
_companyPurge = companyPurge;
|
||||
_auditLog = auditLog;
|
||||
_inApp = inApp;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -67,88 +73,27 @@ public class CompaniesController : Controller
|
||||
pageNumber = Math.Max(1, pageNumber);
|
||||
pageSize = pageSize is 10 or 25 or 50 or 100 ? pageSize : 25;
|
||||
|
||||
var query = _context.Companies
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(c => !c.IsDeleted)
|
||||
.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
var s = searchTerm.ToLower();
|
||||
query = query.Where(c =>
|
||||
c.CompanyName.ToLower().Contains(s) ||
|
||||
(c.CompanyCode != null && c.CompanyCode.ToLower().Contains(s)) ||
|
||||
(c.PrimaryContactEmail != null && c.PrimaryContactEmail.ToLower().Contains(s)) ||
|
||||
(c.Phone != null && c.Phone.ToLower().Contains(s)));
|
||||
}
|
||||
|
||||
query = (sortColumn, sortDirection == "asc") switch
|
||||
{
|
||||
("CompanyName", true) => query.OrderBy(c => c.CompanyName),
|
||||
("CompanyName", false) => query.OrderByDescending(c => c.CompanyName),
|
||||
("Plan", true) => query.OrderBy(c => c.SubscriptionPlan),
|
||||
("Plan", false) => query.OrderByDescending(c => c.SubscriptionPlan),
|
||||
("Status", true) => query.OrderBy(c => c.IsActive),
|
||||
("Status", false) => query.OrderByDescending(c => c.IsActive),
|
||||
("Created", true) => query.OrderBy(c => c.CreatedAt),
|
||||
("Created", false) => query.OrderByDescending(c => c.CreatedAt),
|
||||
_ => query.OrderBy(c => c.CompanyName)
|
||||
};
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
var companies = await query
|
||||
.Include(c => c.Users)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
var (companies, totalCount) = await _companyList.GetPagedAsync(
|
||||
searchTerm, sortColumn, sortDirection, pageNumber, pageSize);
|
||||
|
||||
var companyDtos = _mapper.Map<List<CompanyListDto>>(companies);
|
||||
|
||||
// Populate job/quote/customer counts efficiently via group queries
|
||||
if (companyDtos.Any())
|
||||
if (companyDtos.Count > 0)
|
||||
{
|
||||
var ids = companyDtos.Select(c => c.Id).ToList();
|
||||
|
||||
var jobCounts = await _context.Jobs.IgnoreQueryFilters()
|
||||
.Where(j => ids.Contains(j.CompanyId) && !j.IsDeleted)
|
||||
.GroupBy(j => j.CompanyId)
|
||||
.Select(g => new { g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.Key, x => x.Count);
|
||||
|
||||
var quoteCounts = await _context.Quotes.IgnoreQueryFilters()
|
||||
.Where(q => ids.Contains(q.CompanyId) && !q.IsDeleted)
|
||||
.GroupBy(q => q.CompanyId)
|
||||
.Select(g => new { g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.Key, x => x.Count);
|
||||
|
||||
var customerCounts = await _context.Customers.IgnoreQueryFilters()
|
||||
.Where(c => ids.Contains(c.CompanyId) && !c.IsDeleted)
|
||||
.GroupBy(c => c.CompanyId)
|
||||
.Select(g => new { g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.Key, x => x.Count);
|
||||
|
||||
var wizardData = await _context.CompanyPreferences.IgnoreQueryFilters()
|
||||
.Where(p => ids.Contains(p.CompanyId) && p.SetupWizardCompleted)
|
||||
.Select(p => new
|
||||
{
|
||||
p.CompanyId,
|
||||
p.SetupWizardCompletedAt,
|
||||
p.SetupWizardCompletedByName
|
||||
})
|
||||
.ToDictionaryAsync(x => x.CompanyId);
|
||||
var summary = await _companyList.GetCountSummaryAsync(ids);
|
||||
|
||||
foreach (var dto in companyDtos)
|
||||
{
|
||||
dto.JobCount = jobCounts.GetValueOrDefault(dto.Id, 0);
|
||||
dto.QuoteCount = quoteCounts.GetValueOrDefault(dto.Id, 0);
|
||||
dto.CustomerCount = customerCounts.GetValueOrDefault(dto.Id, 0);
|
||||
dto.JobCount = summary.JobCounts.GetValueOrDefault(dto.Id, 0);
|
||||
dto.QuoteCount = summary.QuoteCounts.GetValueOrDefault(dto.Id, 0);
|
||||
dto.CustomerCount = summary.CustomerCounts.GetValueOrDefault(dto.Id, 0);
|
||||
|
||||
if (wizardData.TryGetValue(dto.Id, out var w))
|
||||
if (summary.WizardInfo.TryGetValue(dto.Id, out var w))
|
||||
{
|
||||
dto.WizardCompleted = true;
|
||||
dto.WizardCompletedAt = w.SetupWizardCompletedAt;
|
||||
dto.WizardCompletedByName = w.SetupWizardCompletedByName;
|
||||
dto.WizardCompletedAt = w.CompletedAt;
|
||||
dto.WizardCompletedByName = w.CompletedByName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,8 +325,6 @@ public class CompaniesController : Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
// If user creation failed, we should consider rolling back company creation
|
||||
// For now, log the error and inform the user
|
||||
_logger.LogError("Failed to create admin user for company {CompanyName}: {Errors}",
|
||||
company.CompanyName, string.Join(", ", result.Errors.Select(e => e.Description)));
|
||||
|
||||
@@ -441,14 +384,10 @@ public class CompaniesController : Controller
|
||||
public async Task<IActionResult> Edit(int id, UpdateCompanyDto model)
|
||||
{
|
||||
if (id != model.Id)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -473,9 +412,7 @@ public class CompaniesController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Update company properties
|
||||
_mapper.Map(model, company);
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
_logger.LogInformation("Company {CompanyName} updated successfully by {User}",
|
||||
@@ -560,7 +497,6 @@ public class CompaniesController : Controller
|
||||
var companyName = company.CompanyName;
|
||||
var userCount = company.Users.Count;
|
||||
|
||||
// Soft-delete the company and deactivate all users
|
||||
company.IsDeleted = true;
|
||||
company.IsActive = false;
|
||||
company.UpdatedAt = DateTime.UtcNow;
|
||||
@@ -573,8 +509,7 @@ public class CompaniesController : Controller
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// Write audit log
|
||||
_context.AuditLogs.Add(new AuditLog
|
||||
await _auditLog.LogAsync(new AuditLog
|
||||
{
|
||||
UserId = adminUserId,
|
||||
UserName = adminName,
|
||||
@@ -588,7 +523,6 @@ public class CompaniesController : Controller
|
||||
Timestamp = DateTime.UtcNow,
|
||||
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogWarning("Company {CompanyName} (ID:{CompanyId}) soft-deleted by {User}",
|
||||
companyName, id, adminName);
|
||||
@@ -625,8 +559,7 @@ public class CompaniesController : Controller
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
var company = await _context.Companies.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(c => c.Id == id);
|
||||
var company = await _unitOfWork.Companies.GetByIdAsync(id, ignoreQueryFilters: true);
|
||||
|
||||
if (company == null)
|
||||
{
|
||||
@@ -640,91 +573,25 @@ public class CompaniesController : Controller
|
||||
|
||||
try
|
||||
{
|
||||
// ── Tier 1: Leaf children (must go before their parents) ─────────────
|
||||
// JobItem children
|
||||
await _context.JobItemPrepServices.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobItemCoats.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
|
||||
// QuoteItem children
|
||||
await _context.QuoteItemPrepServices.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.QuoteItemCoats.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
|
||||
// AnnouncementDismissals (no CompanyId — delete by user or company-targeted announcement)
|
||||
// Load user IDs first — needed for announcement-dismissal cleanup in the purge service
|
||||
var userIds = await _userManager.Users.IgnoreQueryFilters()
|
||||
.Where(u => u.CompanyId == id).Select(u => u.Id).ToListAsync();
|
||||
var announcementIds = await _context.Announcements
|
||||
.Where(a => a.TargetCompanyId == id).Select(a => a.Id).ToListAsync();
|
||||
await _context.AnnouncementDismissals.IgnoreQueryFilters()
|
||||
.Where(x => userIds.Contains(x.UserId) || announcementIds.Contains(x.AnnouncementId))
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
// ── Tier 2: Mid-level children ────────────────────────────────────────
|
||||
await _context.JobItems.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.QuoteItems.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobStatusHistory.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobChangeHistories.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobPhotos.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobNotes.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobDailyPriorities.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.QuoteChangeHistories.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.CustomerNotes.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.InventoryTransactions.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.MaintenanceRecords.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.BillLineItems.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.BillPayments.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Payments.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.InvoiceItems.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobPrepServices.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.QuotePrepServices.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
// Tiers 1-4: bulk delete all business data (service mirrors the original tier ordering)
|
||||
await _companyPurge.DeleteAllBusinessDataAsync(id, userIds);
|
||||
|
||||
// ── Tier 3: Top-level company entities ───────────────────────────────
|
||||
// Order matters: child-side of FK must be deleted before parent-side.
|
||||
// Invoices/Appointments → Customers; Bills/Expenses → Vendors
|
||||
await _context.Invoices.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Appointments.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Jobs.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Quotes.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Customers.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Bills.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Expenses.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Vendors.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.InventoryItems.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Equipment.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.OvenCosts.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.CatalogItems.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Accounts.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.NotificationLogs.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.NotificationTemplates.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
// Announcements are platform-wide; only delete company-targeted ones (TargetCompanyId)
|
||||
await _context.Announcements.Where(x => x.TargetCompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.BugReports.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.ShopWorkers.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.PrepServices.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
|
||||
// ── Tier 4: Company configs and lookup tables ─────────────────────────
|
||||
await _context.CatalogCategories.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.InventoryCategoryLookups.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobStatusLookups.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobPriorityLookups.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.QuoteStatusLookups.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.AppointmentStatusLookups.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.AppointmentTypeLookups.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.PricingTiers.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.CompanyOperatingCosts.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.CompanyPreferences.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
|
||||
// ── Tier 5: Users (via Identity to cascade AspNetUser* tables) ────────
|
||||
// Tier 5: delete Identity users so AspNetUser* tables cascade correctly
|
||||
var users = await _userManager.Users.IgnoreQueryFilters()
|
||||
.Where(u => u.CompanyId == id).ToListAsync();
|
||||
var userCount = users.Count;
|
||||
foreach (var user in users)
|
||||
await _userManager.DeleteAsync(user);
|
||||
|
||||
// ── Tier 6: Company record ────────────────────────────────────────────
|
||||
await _context.Companies.IgnoreQueryFilters().Where(c => c.Id == id).ExecuteDeleteAsync();
|
||||
// Tier 6: delete company record
|
||||
await _unitOfWork.Companies.DeleteAsync(company);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// Write audit log (use platform default company context — no companyId since it's gone)
|
||||
_context.AuditLogs.Add(new AuditLog
|
||||
await _auditLog.LogAsync(new AuditLog
|
||||
{
|
||||
UserId = adminUserId,
|
||||
UserName = adminName,
|
||||
@@ -738,7 +605,6 @@ public class CompaniesController : Controller
|
||||
Timestamp = DateTime.UtcNow,
|
||||
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogWarning("Company {CompanyName} (ID:{CompanyId}) HARD DELETED by {User}. {UserCount} users removed.",
|
||||
companyName, id, adminName, userCount);
|
||||
@@ -764,8 +630,6 @@ public class CompaniesController : Controller
|
||||
/// to record the action. This operation is irreversible.
|
||||
/// </summary>
|
||||
// POST: Companies/ResetData/5
|
||||
// Permanently hard-deletes all business data for a company while keeping the company record,
|
||||
// its users, operating costs, preferences, and lookup tables intact.
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ResetData(int id, string confirmation)
|
||||
@@ -776,8 +640,7 @@ public class CompaniesController : Controller
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
var company = await _context.Companies.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(c => c.Id == id);
|
||||
var company = await _unitOfWork.Companies.GetByIdAsync(id, ignoreQueryFilters: true);
|
||||
|
||||
if (company == null)
|
||||
{
|
||||
@@ -791,94 +654,9 @@ public class CompaniesController : Controller
|
||||
|
||||
try
|
||||
{
|
||||
// ── Tier 0: Grandchildren ─────────────────────────────────────────────
|
||||
await _context.JobTemplateItemPrepServices .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobTemplateItemCoats .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobItemPrepServices .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobItemCoats .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.QuoteItemPrepServices .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.QuoteItemCoats .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.GiftCertificateRedemptions .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.CreditMemoApplications .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.OvenBatchItems .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _companyPurge.ResetBusinessDataAsync(id);
|
||||
|
||||
// AnnouncementDismissals for company-targeted announcements
|
||||
var announcementIds = await _context.Announcements
|
||||
.Where(a => a.TargetCompanyId == id).Select(a => a.Id).ToListAsync();
|
||||
if (announcementIds.Any())
|
||||
await _context.AnnouncementDismissals.IgnoreQueryFilters()
|
||||
.Where(x => announcementIds.Contains(x.AnnouncementId))
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
// ── Tier 1: Children ──────────────────────────────────────────────────
|
||||
await _context.JobTemplateItems .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobItems .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.QuoteItems .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobStatusHistory .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobChangeHistories .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobPhotos .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobNotes .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobDailyPriorities .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobTimeEntries .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobPrepServices .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.ReworkRecords .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.QuoteChangeHistories.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.QuotePrepServices .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.QuotePhotos .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.CustomerNotes .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.InventoryTransactions.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.MaintenanceRecords .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.BillLineItems .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.BillPayments .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Payments .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Deposits .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.InvoiceItems .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.PurchaseOrderItems .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.AiItemPredictions .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.PowderUsageLogs .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.ShopWorkerRoleCosts .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.OvenBatches .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Refunds .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.CreditMemos .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.GiftCertificates .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
|
||||
// ── Tier 2: Top-level business entities ──────────────────────────────
|
||||
// Order matters: child-side of FK must be deleted before parent-side.
|
||||
// Invoices/Appointments → Customers; Bills/PurchaseOrders/Expenses → Vendors
|
||||
await _context.Invoices .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Appointments .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Jobs .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.JobTemplates .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Quotes .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Customers .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Bills .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.PurchaseOrders .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Expenses .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Vendors .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.CatalogItems .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.InventoryItems .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Equipment .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.OvenCosts .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.Accounts .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.NotificationLogs .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.ShopWorkers .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.PrepServices .IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.CatalogCategories.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
await _context.InventoryCategoryLookups.IgnoreQueryFilters().Where(x => x.CompanyId == id).ExecuteDeleteAsync();
|
||||
// Company-targeted announcements only (platform-wide announcements are left alone)
|
||||
await _context.Announcements.Where(x => x.TargetCompanyId == id).ExecuteDeleteAsync();
|
||||
|
||||
// Reset QB migration wizard progress
|
||||
var prefs = await _context.CompanyPreferences.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(p => p.CompanyId == id);
|
||||
if (prefs?.QbMigrationStateJson != null)
|
||||
{
|
||||
prefs.QbMigrationStateJson = null;
|
||||
prefs.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// Audit log
|
||||
_context.AuditLogs.Add(new AuditLog
|
||||
await _auditLog.LogAsync(new AuditLog
|
||||
{
|
||||
UserId = adminUserId,
|
||||
UserName = adminName,
|
||||
@@ -892,7 +670,6 @@ public class CompaniesController : Controller
|
||||
Timestamp = DateTime.UtcNow,
|
||||
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogWarning(
|
||||
"Company {CompanyName} (ID:{CompanyId}) ALL BUSINESS DATA RESET by {User}.",
|
||||
@@ -960,9 +737,7 @@ public class CompaniesController : Controller
|
||||
{
|
||||
var company = await _unitOfWork.Companies.GetByIdAsync(model.CompanyId, ignoreQueryFilters: true);
|
||||
if (company != null)
|
||||
{
|
||||
model.CompanyName = company.CompanyName;
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
@@ -976,7 +751,6 @@ public class CompaniesController : Controller
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
var existingUser = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (existingUser != null)
|
||||
{
|
||||
@@ -985,7 +759,6 @@ public class CompaniesController : Controller
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// Create admin user for the company
|
||||
var adminUser = new ApplicationUser
|
||||
{
|
||||
UserName = model.Email,
|
||||
@@ -1018,9 +791,7 @@ public class CompaniesController : Controller
|
||||
else
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError("", error.Description);
|
||||
}
|
||||
model.CompanyName = company.CompanyName;
|
||||
return View(model);
|
||||
}
|
||||
@@ -1054,21 +825,13 @@ public class CompaniesController : Controller
|
||||
return NotFound(new { error = "User not found." });
|
||||
|
||||
// Use the viewed company's timezone so timestamps match the tenant's local time
|
||||
var tz = await _context.Companies
|
||||
.Where(c => c.Id == companyId)
|
||||
.Select(c => c.TimeZone)
|
||||
.FirstOrDefaultAsync();
|
||||
var company = await _unitOfWork.Companies.GetByIdAsync(companyId, ignoreQueryFilters: true);
|
||||
var tz = company?.TimeZone;
|
||||
|
||||
var logs = new List<dynamic>();
|
||||
try
|
||||
{
|
||||
var rawLogs = await _context.AuditLogs
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == userId && l.EntityType == "ApplicationUser")
|
||||
.OrderByDescending(l => l.Timestamp)
|
||||
.Take(50)
|
||||
.Select(l => new { l.Action, l.IpAddress, l.Timestamp, l.NewValues })
|
||||
.ToListAsync();
|
||||
var rawLogs = await _auditLog.GetUserActivityAsync(userId);
|
||||
|
||||
logs = rawLogs.Select(l => (dynamic)new
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user