From 249128e8527677c64964efd3e71ea21ebb66963f Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 10 Jun 2026 22:49:30 -0400 Subject: [PATCH] Fix Reset Demo Company: full wipe mode + missing removal categories Root cause: fingerprint-based removal failed on databases seeded with older code (different emails/SKUs); plus Vendors, Named Ovens, and Appointments had no removal path at all. - Add ForceRemoveAll flag to RemoveSeedDataOptions: when true, all removal blocks delete by CompanyId instead of fingerprint matching - Customers block: ForceRemoveAll deletes all company customers - Workers block: ForceRemoveAll deletes all users with CompanyRole=Worker - New Vendors block (triggered by options.Vendors || ForceRemoveAll) - New NamedOvens (OvenCost) block (triggered by options.NamedOvens || ForceRemoveAll) - New Appointments block (triggered by options.Appointments || ForceRemoveAll) - ResetDemoCompany: set ForceRemoveAll=true and enable all new flags so every re-seedable table is wiped clean before re-seeding Co-Authored-By: Claude Sonnet 4.6 --- .../Interfaces/ISeedDataService.cs | 10 +++ .../Services/SeedDataService.Remove.cs | 89 +++++++++++++++++-- .../Controllers/SeedDataController.cs | 7 +- 3 files changed, 97 insertions(+), 9 deletions(-) diff --git a/src/PowderCoating.Application/Interfaces/ISeedDataService.cs b/src/PowderCoating.Application/Interfaces/ISeedDataService.cs index d60169a..38486ce 100644 --- a/src/PowderCoating.Application/Interfaces/ISeedDataService.cs +++ b/src/PowderCoating.Application/Interfaces/ISeedDataService.cs @@ -48,6 +48,16 @@ public class RemoveSeedDataOptions public bool Bills { get; set; } public bool Expenses { get; set; } public bool Workers { get; set; } + public bool Vendors { get; set; } + public bool NamedOvens { get; set; } + public bool Appointments { get; set; } + + /// + /// When true, all removal blocks skip fingerprint matching and delete by CompanyId only. + /// Use for demo resets where the goal is a full wipe regardless of which code version seeded + /// the data. Never set this on a real tenant company. + /// + public bool ForceRemoveAll { get; set; } } public class SeedDataResult diff --git a/src/PowderCoating.Infrastructure/Services/SeedDataService.Remove.cs b/src/PowderCoating.Infrastructure/Services/SeedDataService.Remove.cs index cf2c356..ca4f69a 100644 --- a/src/PowderCoating.Infrastructure/Services/SeedDataService.Remove.cs +++ b/src/PowderCoating.Infrastructure/Services/SeedDataService.Remove.cs @@ -123,11 +123,15 @@ public partial class SeedDataService // --- 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(); + // ForceRemoveAll: wipe every customer for the company (demo reset path). + // Normal mode: fingerprint-match by seeded email addresses (selective removal). + 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()) { @@ -452,12 +456,81 @@ public partial class SeedDataService } } + // --- 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"); + } + } + + // --- Appointments --- + if (options.Appointments || options.ForceRemoveAll) + { + var appointments = await _context.Set() + .IgnoreQueryFilters() + .Where(a => a.CompanyId == companyId) + .ToListAsync(); + + if (appointments.Any()) + { + _context.Set().RemoveRange(appointments); + totalRemoved += appointments.Count; + details.Add($"✓ Removed {appointments.Count} appointment(s)"); + await _context.SaveChangesAsync(); + } + else + { + details.Add("• No appointments found"); + } + } + // --- Shop Workers --- if (options.Workers) { - var workerUsers = await _userManager.Users - .Where(u => SeededWorkerEmails.Contains(u.Email) && u.CompanyId == companyId) - .ToListAsync(); + // ForceRemoveAll: remove all non-admin company users (role = Worker/Employee). + // Normal mode: fingerprint-match by @pcldemo.com email domain. + 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()) { diff --git a/src/PowderCoating.Web/Controllers/SeedDataController.cs b/src/PowderCoating.Web/Controllers/SeedDataController.cs index 0e88a5e..e178e21 100644 --- a/src/PowderCoating.Web/Controllers/SeedDataController.cs +++ b/src/PowderCoating.Web/Controllers/SeedDataController.cs @@ -125,7 +125,8 @@ public class SeedDataController : Controller return RedirectToAction(nameof(Index)); } - // Remove all seed data categories + // Full wipe — ForceRemoveAll bypasses fingerprint matching so stale seed data from + // previous code versions (different emails, renamed SKUs, etc.) is always cleared. var removeOptions = new RemoveSeedDataOptions { Customers = true, @@ -137,6 +138,10 @@ public class SeedDataController : Controller Bills = true, Expenses = true, Workers = true, + Vendors = true, + NamedOvens = true, + Appointments = true, + ForceRemoveAll = true, }; var removeResult = await _seedDataService.RemoveSeedDataAsync(demo.Id, removeOptions);