Demo data realism + invoice resend via SMS on any status

Seed data fixes:
- Fix EF interceptor: no longer overwrites explicitly-set CreatedAt on Added
  entities — root cause of all "same month" chart issues
- Customer seeder: generates 15 customers/month from Jan → current month;
  keeps 10 commercial anchors in deterministic order for job seeder index map
- Invoice seeder: historical range bumped from 2→8 paid invoices/month so
  P&L shows consistent profit (~$5,200 collected vs ~$4,200 monthly expenses)
- Month -1 bumped to 7 paid invoices to stay above expenses
- Jobs: set UpdatedAt to historical event date so analytics don't need null fallback
- Analytics (ReportsController): use CompletedDate ?? UpdatedAt ?? CreatedAt for
  revenue chart grouping; fixes empty Revenue Trend charts on Overview/Revenue tabs
- SeedDataService: inject IAccountBalanceService; auto-recalculate account balances
  after seeding; patch checking/savings opening balances unconditionally on reset
- Customer list: sort by CompanyName ?? ContactLastName so individuals and
  commercial accounts interleave instead of appearing as two blocks

Invoice resend:
- ResendInvoice action now accepts sendEmail + sendSms parameters; SMS-only
  resend no longer requires an email address on file
- Ensures PublicViewToken exists before SMS so the view link is always valid
- canResend in Details view now allows Paid invoices (removed != Paid guard)
- Resend button shows channel-choice modal when customer has both email + SMS,
  direct SMS button when SMS only, or email button when email only
- New #resendChannelModal mirrors the Send channel modal but posts to ResendInvoice
- resendInvoice() JS updated to pass sendEmail/sendSms query params

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 13:20:04 -04:00
parent 249128e852
commit 7735fe3cce
16 changed files with 1142 additions and 487 deletions
@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
namespace PowderCoating.Infrastructure.Services;
@@ -79,28 +80,25 @@ public partial class SeedDataService
/// </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.
/// When <c>ForceRemoveAll = true</c> 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 <c>sys.foreign_keys</c>:
/// 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.
/// </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.
/// 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.
/// </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 };
@@ -120,11 +118,73 @@ public partial class SeedDataService
return result;
}
// --- Customers (+ their jobs, quotes, and related items) ---
// ── 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<int> Sweep<T>() where T : BaseEntity
{
var rows = await _context.Set<T>().IgnoreQueryFilters()
.Where(e => e.CompanyId == companyId).ToListAsync();
if (rows.Any()) _context.Set<T>().RemoveRange(rows);
return rows.Count;
}
// Tier 1 — pure leaf records (block nothing of their own)
await Sweep<JobTimeEntry>(); // FK → Jobs (Cascade by convention — sweep before jobs)
await Sweep<OvenBatchItem>(); // NO_ACTION → Jobs, JobItems, JobItemCoats
await Sweep<PowderUsageLog>(); // NO_ACTION → Jobs, JobItems, JobItemCoats
await Sweep<InAppNotification>(); // NO_ACTION → Customers, Invoices, Quotes
await Sweep<QuotePhoto>(); // NO_ACTION → Quotes
await Sweep<GiftCertificate>(); // NO_ACTION → Customers (GiftCertRedemptions CASCADE)
await Sweep<JobTemplate>(); // NO_ACTION → Customers (JobTemplateItems CASCADE)
await _context.SaveChangesAsync();
// Tier 2 — credit/rework chain (each row blocks the next tier)
await Sweep<CreditMemoApplication>(); // NO_ACTION → Bills, VendorCredits
await Sweep<Refund>(); // NO_ACTION → Invoices, Payments, CreditMemos
await Sweep<CreditMemo>(); // NO_ACTION → Customers, Invoices, ReworkRecords
await _context.SaveChangesAsync();
await Sweep<ReworkRecord>(); // NO_ACTION → Jobs, JobItems (after CreditMemos gone)
await _context.SaveChangesAsync();
// Tier 3 — financial records that block Invoices / Jobs / Quotes
await Sweep<Deposit>(); // NO_ACTION → Jobs, Quotes (CASCADE from Customer anyway)
await Sweep<Payment>(); // NO_ACTION → Invoices
await Sweep<InventoryTransaction>(); // NO_ACTION → Jobs, PurchaseOrders
await _context.SaveChangesAsync();
await Sweep<Invoice>(); // NO_ACTION → Jobs (InvoiceItems, GiftCertRedemptions CASCADE)
await _context.SaveChangesAsync();
// Tier 4 — appointments (NO_ACTION → Customers AND Jobs)
await Sweep<Appointment>();
await _context.SaveChangesAsync();
// Tier 5 — vendor/purchasing chain (BillLineItems.JobId NO_ACTION blocks Jobs)
await Sweep<VendorCreditApplication>(); // NO_ACTION → Bills
await _context.SaveChangesAsync();
await Sweep<Bill>(); // CASCADE → BillLineItems, BillPayments
await Sweep<VendorCredit>(); // CASCADE → VendorCreditLineItems
await Sweep<PurchaseOrder>(); // CASCADE → PurchaseOrderItems
await Sweep<Expense>(); // NO_ACTION → Jobs
await Sweep<OvenBatch>(); // 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)
{
// 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)
@@ -135,13 +195,99 @@ public partial class SeedDataService
if (seededCustomerIds.Any())
{
// Jobs and their child records
var seededJobIds = await _context.Jobs
.IgnoreQueryFilters()
var seededJobIds = await _context.Jobs.IgnoreQueryFilters()
.Where(j => j.CompanyId == companyId && seededCustomerIds.Contains(j.CustomerId))
.Select(j => j.Id)
.ToListAsync();
.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<Appointment>().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<Appointment>().RemoveRange(appts);
}
// InAppNotifications — NO_ACTION → Customers, Quotes (Invoices handled below)
if (seededCustomerIds.Any() || seededQuoteIds.Any())
{
var nots = await _context.Set<InAppNotification>().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<InAppNotification>().RemoveRange(nots);
}
// QuotePhotos — NO_ACTION → Quotes
if (seededQuoteIds.Any())
{
var qp = await _context.Set<QuotePhoto>().IgnoreQueryFilters()
.Where(p => p.QuoteId.HasValue && seededQuoteIds.Contains(p.QuoteId.Value)).ToListAsync();
if (qp.Any()) _context.Set<QuotePhoto>().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<Deposit>().IgnoreQueryFilters()
.Where(d => seededCustomerIds.Contains(d.CustomerId)).ToListAsync();
if (deps.Any()) _context.Set<Deposit>().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<int> seededInvoiceIds = [];
if (seededCustomerIds.Any())
{
seededInvoiceIds = await _context.Set<Invoice>().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<InAppNotification>().IgnoreQueryFilters()
.Where(n => n.InvoiceId.HasValue && seededInvoiceIds.Contains(n.InvoiceId.Value))
.ToListAsync();
if (invNots.Any()) _context.Set<InAppNotification>().RemoveRange(invNots);
// Payments — NO_ACTION → Invoices
var pmts = await _context.Set<Payment>().IgnoreQueryFilters()
.Where(p => seededInvoiceIds.Contains(p.InvoiceId)).ToListAsync();
if (pmts.Any()) _context.Set<Payment>().RemoveRange(pmts);
await _context.SaveChangesAsync();
var invoices = await _context.Set<Invoice>().IgnoreQueryFilters()
.Where(i => seededInvoiceIds.Contains(i.Id)).ToListAsync();
if (invoices.Any())
{
_context.Set<Invoice>().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()
@@ -164,28 +310,22 @@ public partial class SeedDataService
.Where(p => seededJobIds.Contains(p.JobId)).ToListAsync();
if (jobPrepServices.Any()) _context.JobPrepServices.RemoveRange(jobPrepServices);
var timeEntries = await _context.Set<Core.Entities.JobTimeEntry>().IgnoreQueryFilters()
var timeEntries = await _context.Set<JobTimeEntry>().IgnoreQueryFilters()
.Where(te => seededJobIds.Contains(te.JobId)).ToListAsync();
if (timeEntries.Any()) _context.Set<Core.Entities.JobTimeEntry>().RemoveRange(timeEntries);
if (timeEntries.Any()) _context.Set<JobTimeEntry>().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} seeded job(s)");
details.Add($"✓ Removed {jobs.Count} 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();
// ── Quotes and their cascade children ───────────────────────────────
if (seededQuoteIds.Any())
{
// Collect prediction IDs before removing items (FK is NoAction — predictions
// must be deleted after the items that reference them are gone).
// 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)
@@ -204,25 +344,24 @@ public partial class SeedDataService
.Where(q => seededQuoteIds.Contains(q.Id)).ToListAsync();
_context.Quotes.RemoveRange(quotes);
totalRemoved += quotes.Count;
details.Add($"✓ Removed {quotes.Count} seeded quote(s)");
details.Add($"✓ Removed {quotes.Count} quote(s)");
// Remove orphaned AI predictions now that QuoteItems no longer reference them
if (predictionIds.Any())
{
var predictions = await _context.Set<Core.Entities.AiItemPrediction>()
var predictions = await _context.Set<AiItemPrediction>()
.IgnoreQueryFilters()
.Where(p => predictionIds.Contains(p.Id))
.ToListAsync();
if (predictions.Any())
{
_context.Set<Core.Entities.AiItemPrediction>().RemoveRange(predictions);
_context.Set<AiItemPrediction>().RemoveRange(predictions);
totalRemoved += predictions.Count;
details.Add($"✓ Removed {predictions.Count} AI prediction(s)");
}
}
}
// Customer notes
// 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);
@@ -231,7 +370,7 @@ public partial class SeedDataService
.Where(c => seededCustomerIds.Contains(c.Id)).ToListAsync();
_context.Customers.RemoveRange(customers);
totalRemoved += customers.Count;
details.Add($"✓ Removed {customers.Count} seeded customer(s)");
details.Add($"✓ Removed {customers.Count} customer(s)");
await _context.SaveChangesAsync();
}
@@ -241,7 +380,7 @@ public partial class SeedDataService
}
}
// --- Inventory Items ---
// ── Inventory Items ───────────────────────────────────────────────────
if (options.InventoryItems)
{
var seededSkus = SeededInventorySkuSuffixes.Select(s => $"{company.CompanyCode}{s}").ToArray();
@@ -259,7 +398,7 @@ public partial class SeedDataService
_context.InventoryItems.RemoveRange(inventoryItems);
totalRemoved += inventoryItems.Count;
details.Add($"✓ Removed {inventoryItems.Count} seeded inventory item(s)");
details.Add($"✓ Removed {inventoryItems.Count} inventory item(s)");
await _context.SaveChangesAsync();
}
else
@@ -268,7 +407,7 @@ public partial class SeedDataService
}
}
// --- Equipment (+ maintenance records) ---
// ── Equipment (+ maintenance records) ────────────────────────────────
if (options.Equipment)
{
var seededEquipment = await _context.Equipment
@@ -285,7 +424,7 @@ public partial class SeedDataService
_context.Equipment.RemoveRange(seededEquipment);
totalRemoved += seededEquipment.Count;
details.Add($"✓ Removed {seededEquipment.Count} seeded equipment record(s)");
details.Add($"✓ Removed {seededEquipment.Count} equipment record(s)");
await _context.SaveChangesAsync();
}
else
@@ -294,7 +433,7 @@ public partial class SeedDataService
}
}
// --- Catalog Items & Categories ---
// ── Catalog Items & Categories ────────────────────────────────────────
if (options.Catalog)
{
var seededCategories = await _context.CatalogCategories
@@ -313,12 +452,12 @@ public partial class SeedDataService
{
_context.CatalogItems.RemoveRange(catalogItems);
totalRemoved += catalogItems.Count;
details.Add($"✓ Removed {catalogItems.Count} seeded catalog item(s)");
details.Add($"✓ Removed {catalogItems.Count} catalog item(s)");
}
_context.CatalogCategories.RemoveRange(seededCategories);
totalRemoved += seededCategories.Count;
details.Add($"✓ Removed {seededCategories.Count} seeded catalog categor(y/ies)");
details.Add($"✓ Removed {seededCategories.Count} catalog categor(y/ies)");
await _context.SaveChangesAsync();
}
else
@@ -327,7 +466,7 @@ public partial class SeedDataService
}
}
// --- Pricing Tiers ---
// ── Pricing Tiers ─────────────────────────────────────────────────────
if (options.PricingTiers)
{
var tiers = await _context.PricingTiers
@@ -339,7 +478,7 @@ public partial class SeedDataService
{
_context.PricingTiers.RemoveRange(tiers);
totalRemoved += tiers.Count;
details.Add($"✓ Removed {tiers.Count} seeded pricing tier(s)");
details.Add($"✓ Removed {tiers.Count} pricing tier(s)");
await _context.SaveChangesAsync();
}
else
@@ -348,7 +487,7 @@ public partial class SeedDataService
}
}
// --- Operating Costs ---
// ── Operating Costs ───────────────────────────────────────────────────
if (options.OperatingCosts)
{
var costs = await _context.CompanyOperatingCosts
@@ -360,7 +499,7 @@ public partial class SeedDataService
{
_context.CompanyOperatingCosts.RemoveRange(costs);
totalRemoved += costs.Count;
details.Add($"✓ Removed operating costs record");
details.Add("✓ Removed operating costs record");
await _context.SaveChangesAsync();
}
else
@@ -369,10 +508,11 @@ public partial class SeedDataService
}
}
// --- Purchase Orders (removed alongside bills — tightly coupled in demo workflows) ---
if (options.Bills)
// ── Purchase Orders ───────────────────────────────────────────────────
// Handled in the pre-sweep for ForceRemoveAll; this block serves selective removal.
if (options.Bills && !options.ForceRemoveAll)
{
var poIds = await _context.Set<Core.Entities.PurchaseOrder>()
var poIds = await _context.Set<PurchaseOrder>()
.IgnoreQueryFilters()
.Where(p => p.CompanyId == companyId)
.Select(p => p.Id)
@@ -380,27 +520,28 @@ public partial class SeedDataService
if (poIds.Any())
{
var poItems = await _context.Set<Core.Entities.PurchaseOrderItem>()
var poItems = await _context.Set<PurchaseOrderItem>()
.IgnoreQueryFilters()
.Where(i => poIds.Contains(i.PurchaseOrderId))
.ToListAsync();
if (poItems.Any()) _context.Set<Core.Entities.PurchaseOrderItem>().RemoveRange(poItems);
if (poItems.Any()) _context.Set<PurchaseOrderItem>().RemoveRange(poItems);
var pos = await _context.Set<Core.Entities.PurchaseOrder>()
var pos = await _context.Set<PurchaseOrder>()
.IgnoreQueryFilters()
.Where(p => poIds.Contains(p.Id))
.ToListAsync();
_context.Set<Core.Entities.PurchaseOrder>().RemoveRange(pos);
_context.Set<PurchaseOrder>().RemoveRange(pos);
totalRemoved += pos.Count;
details.Add($"✓ Removed {pos.Count} purchase order(s)");
await _context.SaveChangesAsync();
}
}
// --- Vendor Bills (all bills for the company) ---
if (options.Bills)
// ── Vendor Bills ──────────────────────────────────────────────────────
// Handled in the pre-sweep for ForceRemoveAll; this block serves selective removal.
if (options.Bills && !options.ForceRemoveAll)
{
var billIds = await _context.Set<Core.Entities.Bill>()
var billIds = await _context.Set<Bill>()
.IgnoreQueryFilters()
.Where(b => b.CompanyId == companyId)
.Select(b => b.Id)
@@ -408,23 +549,23 @@ public partial class SeedDataService
if (billIds.Any())
{
var payments = await _context.Set<Core.Entities.BillPayment>()
var payments = await _context.Set<BillPayment>()
.IgnoreQueryFilters()
.Where(p => billIds.Contains(p.BillId))
.ToListAsync();
if (payments.Any()) _context.Set<Core.Entities.BillPayment>().RemoveRange(payments);
if (payments.Any()) _context.Set<BillPayment>().RemoveRange(payments);
var lineItems = await _context.Set<Core.Entities.BillLineItem>()
var lineItems = await _context.Set<BillLineItem>()
.IgnoreQueryFilters()
.Where(li => billIds.Contains(li.BillId))
.ToListAsync();
if (lineItems.Any()) _context.Set<Core.Entities.BillLineItem>().RemoveRange(lineItems);
if (lineItems.Any()) _context.Set<BillLineItem>().RemoveRange(lineItems);
var bills = await _context.Set<Core.Entities.Bill>()
var bills = await _context.Set<Bill>()
.IgnoreQueryFilters()
.Where(b => billIds.Contains(b.Id))
.ToListAsync();
_context.Set<Core.Entities.Bill>().RemoveRange(bills);
_context.Set<Bill>().RemoveRange(bills);
totalRemoved += bills.Count;
details.Add($"✓ Removed {bills.Count} vendor bill(s)");
await _context.SaveChangesAsync();
@@ -435,17 +576,18 @@ public partial class SeedDataService
}
}
// --- Expenses ---
if (options.Expenses)
// ── Expenses ──────────────────────────────────────────────────────────
// Handled in the pre-sweep for ForceRemoveAll; this block serves selective removal.
if (options.Expenses && !options.ForceRemoveAll)
{
var expenses = await _context.Set<Core.Entities.Expense>()
var expenses = await _context.Set<Expense>()
.IgnoreQueryFilters()
.Where(e => e.CompanyId == companyId)
.ToListAsync();
if (expenses.Any())
{
_context.Set<Core.Entities.Expense>().RemoveRange(expenses);
_context.Set<Expense>().RemoveRange(expenses);
totalRemoved += expenses.Count;
details.Add($"✓ Removed {expenses.Count} expense(s)");
await _context.SaveChangesAsync();
@@ -456,17 +598,17 @@ public partial class SeedDataService
}
}
// --- Vendors ---
// ── Vendors ───────────────────────────────────────────────────────────
if (options.Vendors || options.ForceRemoveAll)
{
var vendors = await _context.Set<Core.Entities.Vendor>()
var vendors = await _context.Set<Vendor>()
.IgnoreQueryFilters()
.Where(v => v.CompanyId == companyId)
.ToListAsync();
if (vendors.Any())
{
_context.Set<Core.Entities.Vendor>().RemoveRange(vendors);
_context.Set<Vendor>().RemoveRange(vendors);
totalRemoved += vendors.Count;
details.Add($"✓ Removed {vendors.Count} vendor(s)");
await _context.SaveChangesAsync();
@@ -477,17 +619,17 @@ public partial class SeedDataService
}
}
// --- Named Ovens (OvenCost) ---
// ── Named Ovens (OvenCost) ────────────────────────────────────────────
if (options.NamedOvens || options.ForceRemoveAll)
{
var ovens = await _context.Set<Core.Entities.OvenCost>()
var ovens = await _context.Set<OvenCost>()
.IgnoreQueryFilters()
.Where(o => o.CompanyId == companyId)
.ToListAsync();
if (ovens.Any())
{
_context.Set<Core.Entities.OvenCost>().RemoveRange(ovens);
_context.Set<OvenCost>().RemoveRange(ovens);
totalRemoved += ovens.Count;
details.Add($"✓ Removed {ovens.Count} named oven(s)");
await _context.SaveChangesAsync();
@@ -498,32 +640,9 @@ public partial class SeedDataService
}
}
// --- Appointments ---
if (options.Appointments || options.ForceRemoveAll)
{
var appointments = await _context.Set<Core.Entities.Appointment>()
.IgnoreQueryFilters()
.Where(a => a.CompanyId == companyId)
.ToListAsync();
if (appointments.Any())
{
_context.Set<Core.Entities.Appointment>().RemoveRange(appointments);
totalRemoved += appointments.Count;
details.Add($"✓ Removed {appointments.Count} appointment(s)");
await _context.SaveChangesAsync();
}
else
{
details.Add("• No appointments found");
}
}
// --- Shop Workers ---
// ── Shop Workers ──────────────────────────────────────────────────────
if (options.Workers)
{
// 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")