Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -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;
}
}