Initial commit
This commit is contained in:
@@ -0,0 +1,841 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user