using Microsoft.EntityFrameworkCore; using PowderCoating.Application.Interfaces; using PowderCoating.Core.Entities; namespace PowderCoating.Infrastructure.Services; public partial class SeedDataService { /// /// Canonical email addresses of all customers created by the seed operation. /// Used during removal to identify seeded customers without relying on a flag column — /// matching on email is stable even if the records were soft-deleted between seed and remove. /// private static readonly string[] SeededCustomerEmails = [ // Commercial — NC Triangle area "matt@carolinafab.com", "ctanner@apexmotorsports.com", "jpruitt@triangleoffroad.com", "bsmith@smithwelding.com", "kmorales@raleigharchitectural.com", "tgreco@eastcoastpw.com", "dshaw@piedmontmetalworks.com", "lpatel@caryindustrial.com", "rblake@durhamtech.com", "mcoleman@wakecountyfleet.gov", // Individual residential "jdavis@email.com", "sjenkins@email.com", "mthompson@email.com", "rmiller@email.com", "jclark@email.com", "dwilson@email.com", "landerson@email.com", "tharris@email.com", "kwhite@email.com", "jtaylor@email.com", "mbrown@email.com", "clee@email.com", "agarcia@email.com", "kmartinez@email.com", "nrodriguez@email.com", "bhall@email.com", "pyoung@email.com" ]; /// /// Serial numbers assigned to all equipment records created by the seed operation. /// Serial numbers are manufacturer-assigned strings that are stable identifiers even across /// soft-delete cycles, making them safe fingerprints for seeded equipment detection. /// private static readonly string[] SeededEquipmentSerials = [ "RFS240023456", "RFS180012789", "NOR120045678", "CC800034512", "EMP483623890", "CLM101223456", "ATC7523467", "PAC50034521", "BE489612345", "GEM0623456" ]; /// /// Display names of the catalog categories created by the seed operation. /// Category name is the only reliable fingerprint because seeded categories carry no /// special flag; the name list is kept in sync with . /// private static readonly string[] SeededCatalogCategoryNames = [ "Automotive Wheels", "Engine Components", "Outdoor Furniture", "Railings & Handrails", "Gates & Fencing", "Fitness Equipment", "Office & Commercial" ]; /// /// SKU suffixes appended to the company code when seeding inventory items /// (e.g. DEMO-PWD-BLK-001). The full SKU is reconstructed at removal time /// as {CompanyCode}{suffix}, matching the pattern used in the inventory seeder. /// private static readonly string[] SeededInventorySkuSuffixes = [ // 6 powders "-PWD-GBK-001", "-PWD-MBK-001", "-PWD-CHR-001", "-PWD-CRD-001", "-PWD-SWH-001", "-PWD-IPU-001", // 5 consumables "-MSK-001", "-PLG-001", "-HKS-001", "-ACT-001", "-BLM-001" ]; /// /// Display names of the pricing tiers created by the seed operation. /// Tiers are matched by name at removal time; the list must stay in sync with the /// pricing tier seeder to avoid leaving orphaned tiers behind. /// private static readonly string[] SeededPricingTierNames = [ "Standard", "Silver", "Gold", "Platinum" ]; /// /// Physically removes previously seeded demo data for the specified company, respecting /// the caller-supplied flags so operators can selectively /// remove only the data categories they want to clean up. /// /// /// /// When ForceRemoveAll = true a topologically ordered pre-sweep deletes every /// child record for the company that has a NO_ACTION FK pointing at a parent we need /// to delete later. This avoids FK constraint errors regardless of how much user-created /// data has accumulated since the last seed. The sweep order mirrors the FK dependency /// graph derived from sys.foreign_keys: /// OvenBatchItems → PowderUsageLogs → InAppNotifications → QuotePhotos → GiftCertificates /// → JobTemplates → CreditMemoApplications → Refunds → CreditMemos → ReworkRecords /// → Deposits → Payments → InventoryTransactions → Invoices → Appointments /// → VendorCreditApplications → Bills → VendorCredits → PurchaseOrders → Expenses /// → OvenBatches → (self-referential Jobs.OriginalJobId nulled via raw SQL) /// then the main entity blocks run in safe order. /// /// /// For the selective (non-force) path the Customers block also pre-deletes Invoices, /// Payments, Deposits, QuotePhotos, and InAppNotifications that reference the seeded /// customer/job/quote IDs, so those deletes succeed even when the broader pre-sweep /// has not run. /// /// public async Task RemoveSeedDataAsync(int companyId, RemoveSeedDataOptions options) { var result = new SeedDataResult { Success = true }; var details = new List(); int totalRemoved = 0; try { var company = await _context.Companies .IgnoreQueryFilters() .FirstOrDefaultAsync(c => c.Id == companyId && !c.IsDeleted); if (company == null) { result.Success = false; result.Message = "Company not found"; return result; } // ── ForceRemoveAll pre-sweep ────────────────────────────────────────── // Deletes every record that has a NO_ACTION FK pointing at a table we delete // later. Order follows the FK dependency graph (leaves first, roots last). // Each tier is committed before the next so EF's change tracker stays clean. if (options.ForceRemoveAll) { // Local helper: delete all rows of T for this company, return count. // All entities inherit BaseEntity which exposes CompanyId. async Task Sweep() where T : BaseEntity { var rows = await _context.Set().IgnoreQueryFilters() .Where(e => e.CompanyId == companyId).ToListAsync(); if (rows.Any()) _context.Set().RemoveRange(rows); return rows.Count; } // Tier 1 — pure leaf records (block nothing of their own) await Sweep(); // FK → Jobs (Cascade by convention — sweep before jobs) await Sweep(); // NO_ACTION → Jobs, JobItems, JobItemCoats await Sweep(); // NO_ACTION → Jobs, JobItems, JobItemCoats await Sweep(); // NO_ACTION → Customers, Invoices, Quotes await Sweep(); // NO_ACTION → Quotes await Sweep(); // NO_ACTION → Customers (GiftCertRedemptions CASCADE) await Sweep(); // NO_ACTION → Customers (JobTemplateItems CASCADE) await _context.SaveChangesAsync(); // Tier 2 — credit/rework chain (each row blocks the next tier) await Sweep(); // NO_ACTION → Bills, VendorCredits await Sweep(); // NO_ACTION → Invoices, Payments, CreditMemos await Sweep(); // NO_ACTION → Customers, Invoices, ReworkRecords await _context.SaveChangesAsync(); await Sweep(); // NO_ACTION → Jobs, JobItems (after CreditMemos gone) await _context.SaveChangesAsync(); // Tier 3 — financial records that block Invoices / Jobs / Quotes await Sweep(); // NO_ACTION → Jobs, Quotes (CASCADE from Customer anyway) await Sweep(); // NO_ACTION → Invoices await Sweep(); // NO_ACTION → Jobs, PurchaseOrders await _context.SaveChangesAsync(); await Sweep(); // NO_ACTION → Jobs (InvoiceItems, GiftCertRedemptions CASCADE) await _context.SaveChangesAsync(); // Tier 4 — appointments (NO_ACTION → Customers AND Jobs) await Sweep(); await _context.SaveChangesAsync(); // Tier 5 — vendor/purchasing chain (BillLineItems.JobId NO_ACTION blocks Jobs) await Sweep(); // NO_ACTION → Bills await _context.SaveChangesAsync(); await Sweep(); // CASCADE → BillLineItems, BillPayments await Sweep(); // CASCADE → VendorCreditLineItems await Sweep(); // CASCADE → PurchaseOrderItems await Sweep(); // NO_ACTION → Jobs await Sweep(); // NO_ACTION → Equipment, OvenCosts await _context.SaveChangesAsync(); // Jobs have a self-referential NO_ACTION FK (OriginalJobId). NULL it before // deleting so EF doesn't fail on ordering within the same-table batch delete. await _context.Database.ExecuteSqlRawAsync( "UPDATE Jobs SET OriginalJobId = NULL WHERE CompanyId = {0}", companyId); details.Add("✓ Pre-sweep complete: child records cleared in FK-safe order"); } // ── Customers (+ jobs, quotes, invoices, and all related children) ──── if (options.Customers) { var seededCustomerIds = options.ForceRemoveAll ? await _context.Customers.IgnoreQueryFilters() .Where(c => c.CompanyId == companyId) .Select(c => c.Id).ToListAsync() : await _context.Customers.IgnoreQueryFilters() .Where(c => c.CompanyId == companyId && SeededCustomerEmails.Contains(c.Email)) .Select(c => c.Id).ToListAsync(); if (seededCustomerIds.Any()) { var seededJobIds = await _context.Jobs.IgnoreQueryFilters() .Where(j => j.CompanyId == companyId && seededCustomerIds.Contains(j.CustomerId)) .Select(j => j.Id).ToListAsync(); var seededQuoteIds = await _context.Quotes.IgnoreQueryFilters() .Where(q => q.CompanyId == companyId && q.CustomerId.HasValue && seededCustomerIds.Contains(q.CustomerId.Value)) .Select(q => q.Id).ToListAsync(); // ── Pre-delete records with NO_ACTION FKs pointing at Jobs/Quotes/Customers ── // For ForceRemoveAll these are already gone (pre-sweep above). For selective // removal this is the only pass, so we scope to the seeded entity IDs. // Appointments — NO_ACTION → Customers AND Jobs if (seededCustomerIds.Any() || seededJobIds.Any()) { var appts = await _context.Set().IgnoreQueryFilters() .Where(a => a.CompanyId == companyId && (a.CustomerId.HasValue && seededCustomerIds.Contains(a.CustomerId.Value) || a.JobId.HasValue && seededJobIds.Contains(a.JobId.Value))) .ToListAsync(); if (appts.Any()) _context.Set().RemoveRange(appts); } // InAppNotifications — NO_ACTION → Customers, Quotes (Invoices handled below) if (seededCustomerIds.Any() || seededQuoteIds.Any()) { var nots = await _context.Set().IgnoreQueryFilters() .Where(n => n.CompanyId == companyId && (n.CustomerId.HasValue && seededCustomerIds.Contains(n.CustomerId.Value) || n.QuoteId.HasValue && seededQuoteIds.Contains(n.QuoteId.Value))) .ToListAsync(); if (nots.Any()) _context.Set().RemoveRange(nots); } // QuotePhotos — NO_ACTION → Quotes if (seededQuoteIds.Any()) { var qp = await _context.Set().IgnoreQueryFilters() .Where(p => p.QuoteId.HasValue && seededQuoteIds.Contains(p.QuoteId.Value)).ToListAsync(); if (qp.Any()) _context.Set().RemoveRange(qp); } // Deposits — NO_ACTION → Jobs, Quotes (CustomerId is CASCADE from Customer but // we delete deposits explicitly so jobs/quotes can be deleted first) if (seededCustomerIds.Any()) { var deps = await _context.Set().IgnoreQueryFilters() .Where(d => seededCustomerIds.Contains(d.CustomerId)).ToListAsync(); if (deps.Any()) _context.Set().RemoveRange(deps); } await _context.SaveChangesAsync(); // Invoices — NO_ACTION → Jobs AND Customers; must be gone before Jobs are deleted. // Collect invoice IDs first so Payments (NO_ACTION → Invoices) can be cleared. List seededInvoiceIds = []; if (seededCustomerIds.Any()) { seededInvoiceIds = await _context.Set().IgnoreQueryFilters() .Where(i => i.CompanyId == companyId && seededCustomerIds.Contains(i.CustomerId)) .Select(i => i.Id).ToListAsync(); if (seededInvoiceIds.Any()) { // InAppNotifications referencing these invoices var invNots = await _context.Set().IgnoreQueryFilters() .Where(n => n.InvoiceId.HasValue && seededInvoiceIds.Contains(n.InvoiceId.Value)) .ToListAsync(); if (invNots.Any()) _context.Set().RemoveRange(invNots); // Payments — NO_ACTION → Invoices var pmts = await _context.Set().IgnoreQueryFilters() .Where(p => seededInvoiceIds.Contains(p.InvoiceId)).ToListAsync(); if (pmts.Any()) _context.Set().RemoveRange(pmts); await _context.SaveChangesAsync(); var invoices = await _context.Set().IgnoreQueryFilters() .Where(i => seededInvoiceIds.Contains(i.Id)).ToListAsync(); if (invoices.Any()) { _context.Set().RemoveRange(invoices); // InvoiceItems CASCADE totalRemoved += invoices.Count; details.Add($"✓ Removed {invoices.Count} invoice(s)"); } await _context.SaveChangesAsync(); } } // ── Jobs and their cascade children ───────────────────────────────── if (seededJobIds.Any()) { var jobPhotos = await _context.JobPhotos.IgnoreQueryFilters() .Where(p => seededJobIds.Contains(p.JobId)).ToListAsync(); if (jobPhotos.Any()) _context.JobPhotos.RemoveRange(jobPhotos); var jobNotes = await _context.JobNotes.IgnoreQueryFilters() .Where(n => seededJobIds.Contains(n.JobId)).ToListAsync(); if (jobNotes.Any()) _context.JobNotes.RemoveRange(jobNotes); var jobItems = await _context.JobItems.IgnoreQueryFilters() .Where(i => seededJobIds.Contains(i.JobId)).ToListAsync(); if (jobItems.Any()) _context.JobItems.RemoveRange(jobItems); var jobStatusHistory = await _context.JobStatusHistory.IgnoreQueryFilters() .Where(h => seededJobIds.Contains(h.JobId)).ToListAsync(); if (jobStatusHistory.Any()) _context.JobStatusHistory.RemoveRange(jobStatusHistory); var jobPrepServices = await _context.JobPrepServices.IgnoreQueryFilters() .Where(p => seededJobIds.Contains(p.JobId)).ToListAsync(); if (jobPrepServices.Any()) _context.JobPrepServices.RemoveRange(jobPrepServices); var timeEntries = await _context.Set().IgnoreQueryFilters() .Where(te => seededJobIds.Contains(te.JobId)).ToListAsync(); if (timeEntries.Any()) _context.Set().RemoveRange(timeEntries); var jobs = await _context.Jobs.IgnoreQueryFilters() .Where(j => seededJobIds.Contains(j.Id)).ToListAsync(); _context.Jobs.RemoveRange(jobs); totalRemoved += jobs.Count; details.Add($"✓ Removed {jobs.Count} job(s)"); } // ── Quotes and their cascade children ─────────────────────────────── if (seededQuoteIds.Any()) { // Collect AiItemPrediction IDs before removing QuoteItems (FK is NoAction — // predictions must be orphaned after items are gone, then deleted separately). var predictionIds = await _context.QuoteItems.IgnoreQueryFilters() .Where(qi => seededQuoteIds.Contains(qi.QuoteId) && qi.AiPredictionId != null) .Select(qi => qi.AiPredictionId!.Value) .Distinct() .ToListAsync(); var quoteItems = await _context.QuoteItems.IgnoreQueryFilters() .Where(qi => seededQuoteIds.Contains(qi.QuoteId)).ToListAsync(); if (quoteItems.Any()) _context.QuoteItems.RemoveRange(quoteItems); var quotePrepServices = await _context.QuotePrepServices.IgnoreQueryFilters() .Where(p => seededQuoteIds.Contains(p.QuoteId)).ToListAsync(); if (quotePrepServices.Any()) _context.QuotePrepServices.RemoveRange(quotePrepServices); var quotes = await _context.Quotes.IgnoreQueryFilters() .Where(q => seededQuoteIds.Contains(q.Id)).ToListAsync(); _context.Quotes.RemoveRange(quotes); totalRemoved += quotes.Count; details.Add($"✓ Removed {quotes.Count} quote(s)"); if (predictionIds.Any()) { var predictions = await _context.Set() .IgnoreQueryFilters() .Where(p => predictionIds.Contains(p.Id)) .ToListAsync(); if (predictions.Any()) { _context.Set().RemoveRange(predictions); totalRemoved += predictions.Count; details.Add($"✓ Removed {predictions.Count} AI prediction(s)"); } } } // Customer notes (CASCADE from Customer, but explicit for clarity) var customerNotes = await _context.CustomerNotes.IgnoreQueryFilters() .Where(n => seededCustomerIds.Contains(n.CustomerId)).ToListAsync(); if (customerNotes.Any()) _context.CustomerNotes.RemoveRange(customerNotes); var customers = await _context.Customers.IgnoreQueryFilters() .Where(c => seededCustomerIds.Contains(c.Id)).ToListAsync(); _context.Customers.RemoveRange(customers); totalRemoved += customers.Count; details.Add($"✓ Removed {customers.Count} customer(s)"); await _context.SaveChangesAsync(); } else { details.Add("• No seeded customers found"); } } // ── Inventory Items ─────────────────────────────────────────────────── if (options.InventoryItems) { var seededSkus = SeededInventorySkuSuffixes.Select(s => $"{company.CompanyCode}{s}").ToArray(); var inventoryItems = await _context.InventoryItems .IgnoreQueryFilters() .Where(i => i.CompanyId == companyId && seededSkus.Contains(i.SKU)) .ToListAsync(); if (inventoryItems.Any()) { var inventoryIds = inventoryItems.Select(i => i.Id).ToList(); var transactions = await _context.InventoryTransactions.IgnoreQueryFilters() .Where(t => inventoryIds.Contains(t.InventoryItemId)).ToListAsync(); if (transactions.Any()) _context.InventoryTransactions.RemoveRange(transactions); _context.InventoryItems.RemoveRange(inventoryItems); totalRemoved += inventoryItems.Count; details.Add($"✓ Removed {inventoryItems.Count} inventory item(s)"); await _context.SaveChangesAsync(); } else { details.Add("• No seeded inventory items found"); } } // ── Equipment (+ maintenance records) ──────────────────────────────── if (options.Equipment) { var seededEquipment = await _context.Equipment .IgnoreQueryFilters() .Where(e => e.CompanyId == companyId && SeededEquipmentSerials.Contains(e.SerialNumber)) .ToListAsync(); if (seededEquipment.Any()) { var equipmentIds = seededEquipment.Select(e => e.Id).ToList(); var maintenance = await _context.MaintenanceRecords.IgnoreQueryFilters() .Where(m => equipmentIds.Contains(m.EquipmentId)).ToListAsync(); if (maintenance.Any()) _context.MaintenanceRecords.RemoveRange(maintenance); _context.Equipment.RemoveRange(seededEquipment); totalRemoved += seededEquipment.Count; details.Add($"✓ Removed {seededEquipment.Count} equipment record(s)"); await _context.SaveChangesAsync(); } else { details.Add("• No seeded equipment found"); } } // ── Catalog Items & Categories ──────────────────────────────────────── if (options.Catalog) { var seededCategories = await _context.CatalogCategories .IgnoreQueryFilters() .Where(c => c.CompanyId == companyId && SeededCatalogCategoryNames.Contains(c.Name)) .ToListAsync(); if (seededCategories.Any()) { var categoryIds = seededCategories.Select(c => c.Id).ToList(); var catalogItems = await _context.CatalogItems.IgnoreQueryFilters() .Where(i => i.CompanyId == companyId && categoryIds.Contains(i.CategoryId)) .ToListAsync(); if (catalogItems.Any()) { _context.CatalogItems.RemoveRange(catalogItems); totalRemoved += catalogItems.Count; details.Add($"✓ Removed {catalogItems.Count} catalog item(s)"); } _context.CatalogCategories.RemoveRange(seededCategories); totalRemoved += seededCategories.Count; details.Add($"✓ Removed {seededCategories.Count} catalog categor(y/ies)"); await _context.SaveChangesAsync(); } else { details.Add("• No seeded catalog data found"); } } // ── Pricing Tiers ───────────────────────────────────────────────────── if (options.PricingTiers) { var tiers = await _context.PricingTiers .IgnoreQueryFilters() .Where(t => t.CompanyId == companyId && SeededPricingTierNames.Contains(t.TierName)) .ToListAsync(); if (tiers.Any()) { _context.PricingTiers.RemoveRange(tiers); totalRemoved += tiers.Count; details.Add($"✓ Removed {tiers.Count} pricing tier(s)"); await _context.SaveChangesAsync(); } else { details.Add("• No seeded pricing tiers found"); } } // ── Operating Costs ─────────────────────────────────────────────────── if (options.OperatingCosts) { var costs = await _context.CompanyOperatingCosts .IgnoreQueryFilters() .Where(c => c.CompanyId == companyId) .ToListAsync(); if (costs.Any()) { _context.CompanyOperatingCosts.RemoveRange(costs); totalRemoved += costs.Count; details.Add("✓ Removed operating costs record"); await _context.SaveChangesAsync(); } else { details.Add("• No operating costs record found"); } } // ── Purchase Orders ─────────────────────────────────────────────────── // Handled in the pre-sweep for ForceRemoveAll; this block serves selective removal. if (options.Bills && !options.ForceRemoveAll) { var poIds = await _context.Set() .IgnoreQueryFilters() .Where(p => p.CompanyId == companyId) .Select(p => p.Id) .ToListAsync(); if (poIds.Any()) { var poItems = await _context.Set() .IgnoreQueryFilters() .Where(i => poIds.Contains(i.PurchaseOrderId)) .ToListAsync(); if (poItems.Any()) _context.Set().RemoveRange(poItems); var pos = await _context.Set() .IgnoreQueryFilters() .Where(p => poIds.Contains(p.Id)) .ToListAsync(); _context.Set().RemoveRange(pos); totalRemoved += pos.Count; details.Add($"✓ Removed {pos.Count} purchase order(s)"); await _context.SaveChangesAsync(); } } // ── Vendor Bills ────────────────────────────────────────────────────── // Handled in the pre-sweep for ForceRemoveAll; this block serves selective removal. if (options.Bills && !options.ForceRemoveAll) { var billIds = await _context.Set() .IgnoreQueryFilters() .Where(b => b.CompanyId == companyId) .Select(b => b.Id) .ToListAsync(); if (billIds.Any()) { var payments = await _context.Set() .IgnoreQueryFilters() .Where(p => billIds.Contains(p.BillId)) .ToListAsync(); if (payments.Any()) _context.Set().RemoveRange(payments); var lineItems = await _context.Set() .IgnoreQueryFilters() .Where(li => billIds.Contains(li.BillId)) .ToListAsync(); if (lineItems.Any()) _context.Set().RemoveRange(lineItems); var bills = await _context.Set() .IgnoreQueryFilters() .Where(b => billIds.Contains(b.Id)) .ToListAsync(); _context.Set().RemoveRange(bills); totalRemoved += bills.Count; details.Add($"✓ Removed {bills.Count} vendor bill(s)"); await _context.SaveChangesAsync(); } else { details.Add("• No vendor bills found"); } } // ── Expenses ────────────────────────────────────────────────────────── // Handled in the pre-sweep for ForceRemoveAll; this block serves selective removal. if (options.Expenses && !options.ForceRemoveAll) { var expenses = await _context.Set() .IgnoreQueryFilters() .Where(e => e.CompanyId == companyId) .ToListAsync(); if (expenses.Any()) { _context.Set().RemoveRange(expenses); totalRemoved += expenses.Count; details.Add($"✓ Removed {expenses.Count} expense(s)"); await _context.SaveChangesAsync(); } else { details.Add("• No expenses found"); } } // ── Vendors ─────────────────────────────────────────────────────────── if (options.Vendors || options.ForceRemoveAll) { var vendors = await _context.Set() .IgnoreQueryFilters() .Where(v => v.CompanyId == companyId) .ToListAsync(); if (vendors.Any()) { _context.Set().RemoveRange(vendors); totalRemoved += vendors.Count; details.Add($"✓ Removed {vendors.Count} vendor(s)"); await _context.SaveChangesAsync(); } else { details.Add("• No vendors found"); } } // ── Named Ovens (OvenCost) ──────────────────────────────────────────── if (options.NamedOvens || options.ForceRemoveAll) { var ovens = await _context.Set() .IgnoreQueryFilters() .Where(o => o.CompanyId == companyId) .ToListAsync(); if (ovens.Any()) { _context.Set().RemoveRange(ovens); totalRemoved += ovens.Count; details.Add($"✓ Removed {ovens.Count} named oven(s)"); await _context.SaveChangesAsync(); } else { details.Add("• No named ovens found"); } } // ── Shop Workers ────────────────────────────────────────────────────── if (options.Workers) { var workerUsers = options.ForceRemoveAll ? await _userManager.Users .Where(u => u.CompanyId == companyId && u.CompanyRole == "Worker") .ToListAsync() : await _userManager.Users .Where(u => SeededWorkerEmails.Contains(u.Email) && u.CompanyId == companyId) .ToListAsync(); if (workerUsers.Any()) { foreach (var wu in workerUsers) await _userManager.DeleteAsync(wu); totalRemoved += workerUsers.Count; details.Add($"✓ Removed {workerUsers.Count} demo shop worker(s)"); } else { details.Add("• No demo shop workers found"); } } result.ItemsSeeded = totalRemoved; result.Details = details; result.Message = totalRemoved > 0 ? $"Removed {totalRemoved} seeded record(s) from {company.CompanyName}" : $"No matching seeded records found for {company.CompanyName}"; } catch (Exception ex) { result.Success = false; result.Message = $"Error removing seed data: {ex.Message}"; } return result; } }