842 lines
34 KiB
C#
842 lines
34 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using PowderCoating.Core.Entities;
|
|
|
|
namespace PowderCoating.Infrastructure.Services;
|
|
|
|
public partial class SeedDataService
|
|
{
|
|
/// <summary>
|
|
/// Seeds the 16 standard job status lookup rows for a company, covering the full
|
|
/// powder-coating workflow from Pending through Delivered/Cancelled.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Job status is a lookup table (<see cref="JobStatusLookup"/>) rather than a C# enum so
|
|
/// that operators can customise display names, colours, and ordering without a code deploy.
|
|
/// The <c>StatusCode</c> 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:
|
|
/// - <c>IsTerminalStatus</c> = true for Completed, Delivered, Cancelled — the AI
|
|
/// accounting service uses this to distinguish active vs closed jobs.
|
|
/// - <c>IsWorkInProgressStatus</c> = true for production stages (In Preparation through
|
|
/// Quality Check) — used by dashboard KPIs and scheduling views.
|
|
/// - <c>IsSystemDefined</c> = true for statuses that drive automated workflows
|
|
/// (Pending, Completed, Cancelled) — operators cannot delete these.
|
|
/// - <c>WorkflowCategory</c> groups statuses for reporting (Pre-Production, Production,
|
|
/// Post-Production, Other).
|
|
/// </remarks>
|
|
/// <param name="company">The tenant company to seed statuses for.</param>
|
|
/// <returns>The number of status rows created (16), or 0 if already seeded.</returns>
|
|
private async Task<int> SeedJobStatusLookupsAsync(Company company)
|
|
{
|
|
// Check if job statuses already exist for this company
|
|
var existingCount = await _context.Set<JobStatusLookup>()
|
|
.IgnoreQueryFilters()
|
|
.CountAsync(s => s.CompanyId == company.Id && !s.IsDeleted);
|
|
|
|
if (existingCount >= 16)
|
|
{
|
|
return 0; // Already seeded
|
|
}
|
|
|
|
var statuses = new List<JobStatusLookup>
|
|
{
|
|
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<JobStatusLookup>().AddRangeAsync(statuses);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return statuses.Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Like job statuses, priorities are a lookup table so display names and colours can be
|
|
/// adjusted per tenant without a code change. The <c>PriorityCode</c> 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 <see cref="CompanyOperatingCosts"/>.
|
|
/// </remarks>
|
|
/// <param name="company">The tenant company to seed priorities for.</param>
|
|
/// <returns>The number of priority rows created (5), or 0 if already seeded.</returns>
|
|
private async Task<int> SeedJobPriorityLookupsAsync(Company company)
|
|
{
|
|
// Check if job priorities already exist for this company
|
|
var existingCount = await _context.Set<JobPriorityLookup>()
|
|
.IgnoreQueryFilters()
|
|
.CountAsync(p => p.CompanyId == company.Id && !p.IsDeleted);
|
|
|
|
if (existingCount >= 5)
|
|
{
|
|
return 0; // Already seeded
|
|
}
|
|
|
|
var priorities = new List<JobPriorityLookup>
|
|
{
|
|
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<JobPriorityLookup>().AddRangeAsync(priorities);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return priorities.Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seeds the seven standard quote status lookup rows for a company
|
|
/// (Draft, Sent, Approved, Rejected, Expired, Converted, Revised).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Quote status uses a lookup table (like job status) so operators can adjust display
|
|
/// names and colours. The <c>StatusCode</c> strings and the boolean semantic flags
|
|
/// (<c>IsApprovedStatus</c>, <c>IsConvertedStatus</c>, <c>IsDraftStatus</c>,
|
|
/// <c>IsRejectedStatus</c>) 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
|
|
/// <c>IsRejectedStatus = false</c> 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:
|
|
/// - <c>IsApprovedStatus</c>: only APPROVED — triggers quote-to-job conversion eligibility.
|
|
/// - <c>IsConvertedStatus</c>: only CONVERTED — marks quotes that have already become jobs.
|
|
/// - <c>IsDraftStatus</c>: only DRAFT — controls edit permissions (draft quotes are fully editable).
|
|
/// - <c>IsRejectedStatus</c>: only REJECTED — prevents the customer approval portal from
|
|
/// re-activating a quote the customer already declined.
|
|
/// </remarks>
|
|
/// <param name="company">The tenant company to seed quote statuses for.</param>
|
|
/// <returns>The number of status rows created (7), or 0 if already seeded (retroactive fix may still apply).</returns>
|
|
private async Task<int> SeedQuoteStatusLookupsAsync(Company company)
|
|
{
|
|
// Check if quote statuses already exist for this company
|
|
var existingCount = await _context.Set<QuoteStatusLookup>()
|
|
.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<QuoteStatusLookup>()
|
|
.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<QuoteStatusLookup>
|
|
{
|
|
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<QuoteStatusLookup>().AddRangeAsync(statuses);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return statuses.Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seeds nine inventory category lookup rows for a company: Powder, Primer, Cleaner,
|
|
/// Masking Supplies, Abrasive Media, Chemicals, Consumables, Tools, and Other.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Inventory categories are a lookup table so operators can rename them or add custom
|
|
/// categories. The <c>CategoryCode</c> strings (e.g., "POWDER", "CLEANER") are used by
|
|
/// <see cref="SeedInventoryItemsAsync"/> to resolve the correct category FK when creating
|
|
/// demo inventory items — they must not be changed without also updating that seeder.
|
|
///
|
|
/// The <c>IsCoating = true</c> 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.
|
|
/// </remarks>
|
|
/// <param name="company">The tenant company to seed inventory categories for.</param>
|
|
/// <returns>The number of category rows created (9), or 0 if already seeded.</returns>
|
|
private async Task<int> SeedInventoryCategoryLookupsAsync(Company company)
|
|
{
|
|
// Check if categories already exist for this company
|
|
var existingCount = await _context.Set<InventoryCategoryLookup>()
|
|
.IgnoreQueryFilters()
|
|
.CountAsync(c => c.CompanyId == company.Id && !c.IsDeleted);
|
|
|
|
if (existingCount >= 9)
|
|
{
|
|
return 0; // Already seeded
|
|
}
|
|
|
|
var categories = new List<InventoryCategoryLookup>
|
|
{
|
|
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<InventoryCategoryLookup>().AddRangeAsync(categories);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return categories.Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seeds eight standard surface-preparation services for a company: Sandblasting, Chemical
|
|
/// Stripping, Hand Sanding, Media Blasting, Iron Phosphate Wash, Degreasing, Masking, and
|
|
/// Outgassing.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
/// <param name="company">The tenant company to seed prep services for.</param>
|
|
/// <returns>The number of prep service rows created (8), or 0 if any already existed.</returns>
|
|
private async Task<int> SeedPrepServicesAsync(Company company)
|
|
{
|
|
var existingCount = await _context.Set<PrepService>()
|
|
.IgnoreQueryFilters()
|
|
.CountAsync(s => s.CompanyId == company.Id && !s.IsDeleted);
|
|
|
|
if (existingCount > 0)
|
|
return 0; // Already seeded
|
|
|
|
var services = new List<PrepService>
|
|
{
|
|
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<PrepService>().AddRangeAsync(services);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return services.Count;
|
|
}
|
|
}
|