using Microsoft.EntityFrameworkCore; using PowderCoating.Core.Entities; namespace PowderCoating.Infrastructure.Services; public partial class SeedDataService { /// /// Seeds the 16 standard job status lookup rows for a company, covering the full /// powder-coating workflow from Pending through Delivered/Cancelled. /// /// /// Job status is a lookup table () rather than a C# enum so /// that operators can customise display names, colours, and ordering without a code deploy. /// The StatusCode string constants (e.g., "PENDING", "COATING") are referenced /// throughout the application code and must not be changed after initial seeding. /// /// Idempotency check: if 16 or more rows already exist for the company, the method /// returns 0 immediately. This threshold equals the full set size, so a partially-seeded /// company (e.g., from a previous failed run) will be re-seeded automatically. /// /// Key flags per status row: /// - IsTerminalStatus = true for Completed, Delivered, Cancelled — the AI /// accounting service uses this to distinguish active vs closed jobs. /// - IsWorkInProgressStatus = true for production stages (In Preparation through /// Quality Check) — used by dashboard KPIs and scheduling views. /// - IsSystemDefined = true for statuses that drive automated workflows /// (Pending, Completed, Cancelled) — operators cannot delete these. /// - WorkflowCategory groups statuses for reporting (Pre-Production, Production, /// Post-Production, Other). /// /// The tenant company to seed statuses for. /// The number of status rows created (16), or 0 if already seeded. private async Task SeedJobStatusLookupsAsync(Company company) { // Check if job statuses already exist for this company var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(s => s.CompanyId == company.Id && !s.IsDeleted); if (existingCount >= 16) { return 0; // Already seeded } var statuses = new List { new JobStatusLookup { StatusCode = "PENDING", DisplayName = "Pending", DisplayOrder = 1, ColorClass = "secondary", IconClass = "bi-clock", IsActive = true, IsSystemDefined = true, IsTerminalStatus = false, IsWorkInProgressStatus = false, WorkflowCategory = "Pre-Production", Description = "Job has been created and is awaiting approval", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "QUOTED", DisplayName = "Quoted", DisplayOrder = 2, ColorClass = "info", IconClass = "bi-file-text", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, IsWorkInProgressStatus = false, WorkflowCategory = "Pre-Production", Description = "Quote has been generated for this job", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "APPROVED", DisplayName = "Approved", DisplayOrder = 3, ColorClass = "primary", IconClass = "bi-check-circle", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, IsWorkInProgressStatus = false, WorkflowCategory = "Pre-Production", Description = "Job has been approved and is ready to start", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "IN_PREPARATION", DisplayName = "In Preparation", DisplayOrder = 4, ColorClass = "warning", IconClass = "bi-tools", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, IsWorkInProgressStatus = true, WorkflowCategory = "Production", Description = "Job is being prepared for processing", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "SANDBLASTING", DisplayName = "Sandblasting", DisplayOrder = 5, ColorClass = "warning", IconClass = "bi-wind", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, IsWorkInProgressStatus = true, WorkflowCategory = "Production", Description = "Surface preparation in progress", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "MASKING_TAPING", DisplayName = "Masking/Taping", DisplayOrder = 6, ColorClass = "warning", IconClass = "bi-scissors", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, IsWorkInProgressStatus = true, WorkflowCategory = "Production", Description = "Masking areas that should not be coated", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "CLEANING", DisplayName = "Cleaning", DisplayOrder = 7, ColorClass = "warning", IconClass = "bi-droplet", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, IsWorkInProgressStatus = true, WorkflowCategory = "Production", Description = "Final cleaning before coating", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "IN_OVEN", DisplayName = "In Oven", DisplayOrder = 8, ColorClass = "warning", IconClass = "bi-thermometer-half", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, IsWorkInProgressStatus = true, WorkflowCategory = "Production", Description = "Parts are being pre-heated", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "COATING", DisplayName = "Coating", DisplayOrder = 9, ColorClass = "warning", IconClass = "bi-paint-bucket", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, IsWorkInProgressStatus = true, WorkflowCategory = "Production", Description = "Applying powder coating", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "CURING", DisplayName = "Curing", DisplayOrder = 10, ColorClass = "warning", IconClass = "bi-fire", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, IsWorkInProgressStatus = true, WorkflowCategory = "Production", Description = "Curing the powder coating in the oven", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "QUALITY_CHECK", DisplayName = "Quality Check", DisplayOrder = 11, ColorClass = "info", IconClass = "bi-search", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, IsWorkInProgressStatus = true, WorkflowCategory = "Post-Production", Description = "Quality inspection in progress", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "COMPLETED", DisplayName = "Completed", DisplayOrder = 12, ColorClass = "success", IconClass = "bi-check2-all", IsActive = true, IsSystemDefined = true, IsTerminalStatus = true, IsWorkInProgressStatus = false, WorkflowCategory = "Post-Production", Description = "Job work is completed", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "READY_FOR_PICKUP", DisplayName = "Ready for Pickup", DisplayOrder = 13, ColorClass = "success", IconClass = "bi-box-seam", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, IsWorkInProgressStatus = false, WorkflowCategory = "Post-Production", Description = "Job is ready for customer pickup", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "DELIVERED", DisplayName = "Delivered", DisplayOrder = 14, ColorClass = "success", IconClass = "bi-truck", IsActive = true, IsSystemDefined = false, IsTerminalStatus = true, IsWorkInProgressStatus = false, WorkflowCategory = "Post-Production", Description = "Job has been delivered to customer", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "ON_HOLD", DisplayName = "On Hold", DisplayOrder = 15, ColorClass = "dark", IconClass = "bi-pause-circle", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, IsWorkInProgressStatus = false, WorkflowCategory = "Other", Description = "Job has been paused", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobStatusLookup { StatusCode = "CANCELLED", DisplayName = "Cancelled", DisplayOrder = 16, ColorClass = "danger", IconClass = "bi-x-circle", IsActive = true, IsSystemDefined = true, IsTerminalStatus = true, IsWorkInProgressStatus = false, WorkflowCategory = "Other", Description = "Job has been cancelled", CompanyId = company.Id, CreatedAt = DateTime.UtcNow } }; await _context.Set().AddRangeAsync(statuses); await _context.SaveChangesAsync(); return statuses.Count; } /// /// Seeds the five standard job priority levels (Low, Normal, High, Urgent, Rush) as /// lookup rows for a company, each with a Bootstrap colour class and Bootstrap Icon. /// /// /// Like job statuses, priorities are a lookup table so display names and colours can be /// adjusted per tenant without a code change. The PriorityCode strings /// (e.g., "NORMAL", "RUSH") are referenced by the job creation UI and report filters /// and must not be renamed after seeding. /// /// Idempotency check: bails early if 5 or more rows already exist for the company. /// /// Both URGENT and RUSH use the "danger" colour class intentionally — they signal /// different urgency levels but both demand immediate visual attention on the shop floor. /// The distinction matters for rush-charge calculation: a Rush priority triggers the /// rush-charge percentage defined in . /// /// The tenant company to seed priorities for. /// The number of priority rows created (5), or 0 if already seeded. private async Task SeedJobPriorityLookupsAsync(Company company) { // Check if job priorities already exist for this company var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(p => p.CompanyId == company.Id && !p.IsDeleted); if (existingCount >= 5) { return 0; // Already seeded } var priorities = new List { new JobPriorityLookup { PriorityCode = "LOW", DisplayName = "Low", DisplayOrder = 1, ColorClass = "secondary", IconClass = "bi-arrow-down", IsActive = true, IsSystemDefined = false, Description = "Low priority - no rush", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobPriorityLookup { PriorityCode = "NORMAL", DisplayName = "Normal", DisplayOrder = 2, ColorClass = "info", IconClass = "bi-dash", IsActive = true, IsSystemDefined = false, Description = "Normal priority - standard turnaround", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobPriorityLookup { PriorityCode = "HIGH", DisplayName = "High", DisplayOrder = 3, ColorClass = "warning", IconClass = "bi-arrow-up", IsActive = true, IsSystemDefined = false, Description = "High priority - expedited processing", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobPriorityLookup { PriorityCode = "URGENT", DisplayName = "Urgent", DisplayOrder = 4, ColorClass = "danger", IconClass = "bi-exclamation-triangle", IsActive = true, IsSystemDefined = false, Description = "Urgent - requires immediate attention", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new JobPriorityLookup { PriorityCode = "RUSH", DisplayName = "Rush", DisplayOrder = 5, ColorClass = "danger", IconClass = "bi-lightning", IsActive = true, IsSystemDefined = false, Description = "Rush order - top priority", CompanyId = company.Id, CreatedAt = DateTime.UtcNow } }; await _context.Set().AddRangeAsync(priorities); await _context.SaveChangesAsync(); return priorities.Count; } /// /// Seeds the seven standard quote status lookup rows for a company /// (Draft, Sent, Approved, Rejected, Expired, Converted, Revised). /// /// /// Quote status uses a lookup table (like job status) so operators can adjust display /// names and colours. The StatusCode strings and the boolean semantic flags /// (IsApprovedStatus, IsConvertedStatus, IsDraftStatus, /// IsRejectedStatus) are what the application code queries — UI text is cosmetic. /// /// Idempotency check: if 7+ rows exist the method skips insertion, but it performs /// a retroactive fix for the REJECTED status: older seeded databases may have /// IsRejectedStatus = false because the field was added after initial release. /// The fix is applied on every call when the row already exists and the flag is wrong, /// ensuring the quote portal correctly blocks re-approval of rejected quotes. /// /// Key semantic flags: /// - IsApprovedStatus: only APPROVED — triggers quote-to-job conversion eligibility. /// - IsConvertedStatus: only CONVERTED — marks quotes that have already become jobs. /// - IsDraftStatus: only DRAFT — controls edit permissions (draft quotes are fully editable). /// - IsRejectedStatus: only REJECTED — prevents the customer approval portal from /// re-activating a quote the customer already declined. /// /// The tenant company to seed quote statuses for. /// The number of status rows created (7), or 0 if already seeded (retroactive fix may still apply). private async Task SeedQuoteStatusLookupsAsync(Company company) { // Check if quote statuses already exist for this company var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(s => s.CompanyId == company.Id && !s.IsDeleted); if (existingCount >= 7) { // Retroactive fix: ensure REJECTED status has IsRejectedStatus = true var rejectedStatus = await _context.Set() .IgnoreQueryFilters() .FirstOrDefaultAsync(s => s.CompanyId == company.Id && s.StatusCode == "REJECTED" && !s.IsDeleted); if (rejectedStatus != null && !rejectedStatus.IsRejectedStatus) { rejectedStatus.IsRejectedStatus = true; await _context.SaveChangesAsync(); } return 0; // Already seeded } var statuses = new List { new QuoteStatusLookup { StatusCode = "DRAFT", DisplayName = "Draft", DisplayOrder = 1, ColorClass = "secondary", IconClass = "bi-pencil", IsActive = true, IsSystemDefined = true, IsDraftStatus = true, IsApprovedStatus = false, IsConvertedStatus = false, Description = "Quote is being prepared", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new QuoteStatusLookup { StatusCode = "SENT", DisplayName = "Sent", DisplayOrder = 2, ColorClass = "info", IconClass = "bi-send", IsActive = true, IsSystemDefined = false, IsDraftStatus = false, IsApprovedStatus = false, IsConvertedStatus = false, Description = "Quote has been sent to customer", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new QuoteStatusLookup { StatusCode = "APPROVED", DisplayName = "Approved", DisplayOrder = 3, ColorClass = "success", IconClass = "bi-check-circle", IsActive = true, IsSystemDefined = true, IsDraftStatus = false, IsApprovedStatus = true, IsConvertedStatus = false, Description = "Quote has been approved by customer", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new QuoteStatusLookup { StatusCode = "REJECTED", DisplayName = "Rejected", DisplayOrder = 4, ColorClass = "danger", IconClass = "bi-x-circle", IsActive = true, IsSystemDefined = false, IsDraftStatus = false, IsApprovedStatus = false, IsRejectedStatus = true, IsConvertedStatus = false, Description = "Quote has been rejected by customer", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new QuoteStatusLookup { StatusCode = "EXPIRED", DisplayName = "Expired", DisplayOrder = 5, ColorClass = "warning", IconClass = "bi-clock-history", IsActive = true, IsSystemDefined = false, IsDraftStatus = false, IsApprovedStatus = false, IsConvertedStatus = false, Description = "Quote has passed its expiration date", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new QuoteStatusLookup { StatusCode = "CONVERTED", DisplayName = "Converted", DisplayOrder = 6, ColorClass = "primary", IconClass = "bi-arrow-right-circle", IsActive = true, IsSystemDefined = true, IsDraftStatus = false, IsApprovedStatus = false, IsConvertedStatus = true, Description = "Quote has been converted to a job", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new QuoteStatusLookup { StatusCode = "REVISED", DisplayName = "Revised", DisplayOrder = 7, ColorClass = "info", IconClass = "bi-arrow-repeat", IsActive = true, IsSystemDefined = false, IsDraftStatus = false, IsApprovedStatus = false, IsConvertedStatus = false, Description = "Quote has been revised", CompanyId = company.Id, CreatedAt = DateTime.UtcNow } }; await _context.Set().AddRangeAsync(statuses); await _context.SaveChangesAsync(); return statuses.Count; } /// /// Seeds nine inventory category lookup rows for a company: Powder, Primer, Cleaner, /// Masking Supplies, Abrasive Media, Chemicals, Consumables, Tools, and Other. /// /// /// Inventory categories are a lookup table so operators can rename them or add custom /// categories. The CategoryCode strings (e.g., "POWDER", "CLEANER") are used by /// to resolve the correct category FK when creating /// demo inventory items — they must not be changed without also updating that seeder. /// /// The IsCoating = true flag on POWDER and PRIMER distinguishes coating materials /// from consumables; the pricing engine uses this flag to identify which inventory items /// can be selected as powder coats on a quote/job item. /// /// Idempotency check: bails early if 9 or more rows exist for the company. /// /// The tenant company to seed inventory categories for. /// The number of category rows created (9), or 0 if already seeded. private async Task SeedInventoryCategoryLookupsAsync(Company company) { // Check if categories already exist for this company var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(c => c.CompanyId == company.Id && !c.IsDeleted); if (existingCount >= 9) { return 0; // Already seeded } var categories = new List { new InventoryCategoryLookup { CategoryCode = "POWDER", DisplayName = "Powder", DisplayOrder = 1, Description = "Powder coating materials", IsActive = true, IsSystemDefined = false, IsCoating = true, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new InventoryCategoryLookup { CategoryCode = "PRIMER", DisplayName = "Primer", DisplayOrder = 2, Description = "Primer coatings", IsActive = true, IsSystemDefined = false, IsCoating = true, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new InventoryCategoryLookup { CategoryCode = "CLEANER", DisplayName = "Cleaner", DisplayOrder = 3, Description = "Cleaning solutions and materials", IsActive = true, IsSystemDefined = false, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new InventoryCategoryLookup { CategoryCode = "MASKING", DisplayName = "Masking Supplies", DisplayOrder = 4, Description = "Masking tape, plugs, and supplies", IsActive = true, IsSystemDefined = false, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new InventoryCategoryLookup { CategoryCode = "ABRASIVE", DisplayName = "Abrasive Media", DisplayOrder = 5, Description = "Sandblasting and abrasive materials", IsActive = true, IsSystemDefined = false, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new InventoryCategoryLookup { CategoryCode = "CHEMICAL", DisplayName = "Chemicals", DisplayOrder = 6, Description = "Chemical supplies and solutions", IsActive = true, IsSystemDefined = false, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new InventoryCategoryLookup { CategoryCode = "CONSUMABLE", DisplayName = "Consumables", DisplayOrder = 7, Description = "General consumable supplies", IsActive = true, IsSystemDefined = false, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new InventoryCategoryLookup { CategoryCode = "TOOL", DisplayName = "Tools", DisplayOrder = 8, Description = "Tools and equipment supplies", IsActive = true, IsSystemDefined = false, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new InventoryCategoryLookup { CategoryCode = "OTHER", DisplayName = "Other", DisplayOrder = 9, Description = "Other miscellaneous inventory items", IsActive = true, IsSystemDefined = false, CompanyId = company.Id, CreatedAt = DateTime.UtcNow } }; await _context.Set().AddRangeAsync(categories); await _context.SaveChangesAsync(); return categories.Count; } /// /// Seeds eight standard surface-preparation services for a company: Sandblasting, Chemical /// Stripping, Hand Sanding, Media Blasting, Iron Phosphate Wash, Degreasing, Masking, and /// Outgassing. /// /// /// Prep services are selectable line-item additions on quote and job items, each adding /// labour cost to the pricing calculation. They are stored per-company so operators can /// rename, deactivate, or add custom services (e.g., "Zinc Phosphate Wash") without /// affecting other tenants. /// /// Idempotency check: bails if any rows exist for the company (count > 0), unlike /// status lookups which use a minimum-count threshold. This is intentional: if a /// partial seed left only some services, the operator can remove them and re-run to get /// the full set. /// /// Outgassing (pre-bake to release trapped gases from castings) is included because it is /// a mandatory step for cast aluminium and cast iron parts and is a common source of /// rework if omitted — including it in the default list helps new operators remember to /// quote it. /// /// The tenant company to seed prep services for. /// The number of prep service rows created (8), or 0 if any already existed. private async Task SeedPrepServicesAsync(Company company) { var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(s => s.CompanyId == company.Id && !s.IsDeleted); if (existingCount > 0) return 0; // Already seeded var services = new List { new PrepService { ServiceName = "Sandblasting", Description = "Abrasive blasting to remove rust, old coatings, and surface contaminants", DisplayOrder = 1, IsActive = true, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new PrepService { ServiceName = "Chemical Stripping", Description = "Chemical bath or application to strip existing paint or coatings", DisplayOrder = 2, IsActive = true, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new PrepService { ServiceName = "Hand Sanding", Description = "Manual sanding to smooth surfaces and improve adhesion", DisplayOrder = 3, IsActive = true, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new PrepService { ServiceName = "Media Blasting", Description = "Blasting with glass beads, walnut shells, or other media for delicate surfaces", DisplayOrder = 4, IsActive = true, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new PrepService { ServiceName = "Iron Phosphate Wash", Description = "Chemical pre-treatment wash to improve powder adhesion and corrosion resistance", DisplayOrder = 5, IsActive = true, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new PrepService { ServiceName = "Degreasing", Description = "Solvent or aqueous cleaning to remove oils, grease, and shop soils", DisplayOrder = 6, IsActive = true, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new PrepService { ServiceName = "Masking", Description = "Masking of threads, holes, or areas that must remain uncoated", DisplayOrder = 7, IsActive = true, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new PrepService { ServiceName = "Outgassing", Description = "Pre-bake cycle to release trapped gases from castings before coating", DisplayOrder = 8, IsActive = true, CompanyId = company.Id, CreatedAt = DateTime.UtcNow } }; await _context.Set().AddRangeAsync(services); await _context.SaveChangesAsync(); return services.Count; } }