Initial commit
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Application.Interfaces;
|
||||
|
||||
namespace PowderCoating.Infrastructure.Services;
|
||||
|
||||
public partial class SeedDataService
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static readonly string[] SeededCustomerEmails =
|
||||
[
|
||||
"john.smith@acmemfg.com", "sjohnson@precisionauto.com", "mchen@urbanrailings.com",
|
||||
"lmartinez@fitequip.com", "dwilliams@metrota.gov", "rtaylor@classicwheels.com",
|
||||
"janderson@indfurniture.com", "cbrown@motorsportscustom.com", "adavis@greenenergy.com",
|
||||
"tmiller@heritagemetal.com", "pwilson@marineequip.com", "kgarcia@commercialhvac.com",
|
||||
"nmartinez@playgroundusa.com", "blee@officesystems.com", "swhite@agequipment.com",
|
||||
"jthompson@email.com", "mharris@email.com", "wclark@email.com", "elewis@email.com",
|
||||
"rwalker@email.com", "bhall@email.com", "jallen@email.com", "syoung@email.com",
|
||||
"cking@email.com", "lwright@email.com"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static readonly string[] SeededEquipmentSerials =
|
||||
[
|
||||
"RFS240023456", "RFS180012789", "NOR120045678", "CC800034512",
|
||||
"EMP483623890", "CLM101223456", "ATC7523467", "PAC50034521",
|
||||
"BE489612345", "GEM0623456"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="SeedCatalogAsync"/>.
|
||||
/// </summary>
|
||||
private static readonly string[] SeededCatalogCategoryNames =
|
||||
[
|
||||
"Automotive Wheels", "Engine Components", "Outdoor Furniture",
|
||||
"Railings & Handrails", "Gates & Fencing", "Fitness Equipment", "Office & Commercial"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// SKU suffixes appended to the company code when seeding inventory items
|
||||
/// (e.g. <c>DEMO-PWD-BLK-001</c>). The full SKU is reconstructed at removal time
|
||||
/// as <c>{CompanyCode}{suffix}</c>, matching the pattern used in the inventory seeder.
|
||||
/// </summary>
|
||||
private static readonly string[] SeededInventorySkuSuffixes =
|
||||
[
|
||||
"-PWD-BLK-001", "-PWD-WHT-001", "-PWD-RED-001", "-PWD-BLU-001",
|
||||
"-PWD-GRY-001", "-PWD-YEL-001", "-PWD-ORG-001", "-PWD-GRN-001",
|
||||
"-CLN-001", "-MSK-001"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static readonly string[] SeededPricingTierNames =
|
||||
[
|
||||
"Standard", "Silver", "Gold", "Platinum"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Physically removes previously seeded demo data for the specified company, respecting
|
||||
/// the caller-supplied <paramref name="options"/> flags so operators can selectively
|
||||
/// remove only the data categories they want to clean up.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// All queries use <c>IgnoreQueryFilters()</c> so that records already soft-deleted by users
|
||||
/// are still found and physically removed — this prevents orphaned data from accumulating
|
||||
/// in the database after partial cleanup.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Child records (job items, quote items, transactions, maintenance records, etc.) are deleted
|
||||
/// first before their parent to avoid FK constraint violations. Each category is committed
|
||||
/// with its own <c>SaveChangesAsync()</c> call so a failure in one category does not roll
|
||||
/// back deletions already completed in an earlier category.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Lookup tables (job status, job priority, quote status) are intentionally NOT removed —
|
||||
/// they are system-level data shared across the company's real records.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="companyId">ID of the tenant company whose seed data should be removed.</param>
|
||||
/// <param name="options">Flags controlling which data categories to delete.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="SeedDataResult"/> with <c>Success = true</c> and a count of records removed,
|
||||
/// or <c>Success = false</c> with an error message if the company was not found or an
|
||||
/// exception was thrown.
|
||||
/// </returns>
|
||||
public async Task<SeedDataResult> RemoveSeedDataAsync(int companyId, RemoveSeedDataOptions options)
|
||||
{
|
||||
var result = new SeedDataResult { Success = true };
|
||||
var details = new List<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
// --- Customers (+ their jobs, quotes, and related items) ---
|
||||
if (options.Customers)
|
||||
{
|
||||
var seededCustomerIds = await _context.Customers
|
||||
.IgnoreQueryFilters()
|
||||
.Where(c => c.CompanyId == companyId && SeededCustomerEmails.Contains(c.Email))
|
||||
.Select(c => c.Id)
|
||||
.ToListAsync();
|
||||
|
||||
if (seededCustomerIds.Any())
|
||||
{
|
||||
// Jobs and their child records
|
||||
var seededJobIds = await _context.Jobs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(j => j.CompanyId == companyId && seededCustomerIds.Contains(j.CustomerId))
|
||||
.Select(j => j.Id)
|
||||
.ToListAsync();
|
||||
|
||||
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 jobs = await _context.Jobs.IgnoreQueryFilters()
|
||||
.Where(j => seededJobIds.Contains(j.Id)).ToListAsync();
|
||||
_context.Jobs.RemoveRange(jobs);
|
||||
totalRemoved += jobs.Count;
|
||||
details.Add($"✓ Removed {jobs.Count} seeded job(s)");
|
||||
}
|
||||
|
||||
// Quotes and their child records
|
||||
var seededQuoteIds = await _context.Quotes
|
||||
.IgnoreQueryFilters()
|
||||
.Where(q => q.CompanyId == companyId && q.CustomerId.HasValue && seededCustomerIds.Contains(q.CustomerId.Value))
|
||||
.Select(q => q.Id)
|
||||
.ToListAsync();
|
||||
|
||||
if (seededQuoteIds.Any())
|
||||
{
|
||||
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} seeded quote(s)");
|
||||
}
|
||||
|
||||
// Customer notes
|
||||
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} seeded 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} seeded 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} seeded 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} seeded catalog item(s)");
|
||||
}
|
||||
|
||||
_context.CatalogCategories.RemoveRange(seededCategories);
|
||||
totalRemoved += seededCategories.Count;
|
||||
details.Add($"✓ Removed {seededCategories.Count} seeded 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} seeded 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");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user