diff --git a/src/PowderCoating.Application/DTOs/Common/PagedResult.cs b/src/PowderCoating.Application/DTOs/Common/PagedResult.cs index 76f8a45..94aacdb 100644 --- a/src/PowderCoating.Application/DTOs/Common/PagedResult.cs +++ b/src/PowderCoating.Application/DTOs/Common/PagedResult.cs @@ -3,6 +3,22 @@ namespace PowderCoating.Application.DTOs.Common; public class PagedResult { public IEnumerable Items { get; set; } = new List(); + + /// + /// Creates a PagedResult populated from a GridRequest, avoiding repetitive property + /// assignments across every Index action. SortColumn, SortDirection, and SearchTerm + /// are copied from the grid so the model carries full state for view binding. + /// + public static PagedResult From(GridRequest grid, IEnumerable items, int totalCount) => new() + { + Items = items, + PageNumber = grid.PageNumber, + PageSize = grid.PageSize, + TotalCount = totalCount, + SortColumn = grid.SortColumn, + SortDirection = grid.SortDirection, + SearchTerm = grid.SearchTerm + }; public int PageNumber { get; set; } public int PageSize { get; set; } public int TotalCount { get; set; } diff --git a/src/PowderCoating.Application/Interfaces/IJobItemAssemblyService.cs b/src/PowderCoating.Application/Interfaces/IJobItemAssemblyService.cs new file mode 100644 index 0000000..b593944 --- /dev/null +++ b/src/PowderCoating.Application/Interfaces/IJobItemAssemblyService.cs @@ -0,0 +1,19 @@ +using PowderCoating.Application.DTOs.Quote; +using PowderCoating.Core.Entities; + +namespace PowderCoating.Application.Interfaces; + +public interface IJobItemAssemblyService +{ + JobItem CreateJobItem(CreateQuoteItemDto source, int jobId, int companyId, QuoteItemPricingResult pricing, DateTime createdAtUtc); + IReadOnlyList CreateJobItemCoats(CreateQuoteItemDto source, int jobItemId, int companyId, DateTime createdAtUtc); + IReadOnlyList CreateJobItemPrepServices(CreateQuoteItemDto source, int jobItemId, int companyId, DateTime createdAtUtc); + + JobItem CreateJobItem(QuoteItem source, int jobId, int companyId, DateTime createdAtUtc); + IReadOnlyList CreateJobItemCoats(QuoteItem source, int jobItemId, int companyId, DateTime createdAtUtc); + IReadOnlyList CreateJobItemPrepServices(QuoteItem source, int jobItemId, int companyId, DateTime createdAtUtc); + + JobItem CreateJobItem(JobItem source, int jobId, int companyId, DateTime createdAtUtc); + IReadOnlyList CreateJobItemCoats(JobItem source, int jobItemId, int companyId, DateTime createdAtUtc); + IReadOnlyList CreateJobItemPrepServices(JobItem source, int jobItemId, int companyId, DateTime createdAtUtc); +} diff --git a/src/PowderCoating.Application/Interfaces/IQuotePricingAssemblyService.cs b/src/PowderCoating.Application/Interfaces/IQuotePricingAssemblyService.cs new file mode 100644 index 0000000..24e5af8 --- /dev/null +++ b/src/PowderCoating.Application/Interfaces/IQuotePricingAssemblyService.cs @@ -0,0 +1,16 @@ +using PowderCoating.Application.DTOs.Quote; +using PowderCoating.Core.Entities; + +namespace PowderCoating.Application.Interfaces; + +public interface IQuotePricingAssemblyService +{ + void ApplyPricingSnapshot(Quote quote, QuotePricingResult pricingResult); + + Task> CreateQuoteItemsAsync( + IEnumerable itemDtos, + int quoteId, + int companyId, + decimal? ovenRateOverride, + DateTime createdAtUtc); +} diff --git a/src/PowderCoating.Application/Services/BlobFileHelper.cs b/src/PowderCoating.Application/Services/BlobFileHelper.cs new file mode 100644 index 0000000..3978665 --- /dev/null +++ b/src/PowderCoating.Application/Services/BlobFileHelper.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Http; + +namespace PowderCoating.Application.Services; + +/// +/// Shared file validation and content-type resolution used across all blob storage services. +/// +public static class BlobFileHelper +{ + /// + /// Validates an uploaded file against an extension allowlist and a maximum size. + /// Returns the normalized (lowercase) extension on success so callers do not re-derive it. + /// + public static (bool IsValid, string Extension, string Error) ValidateUpload( + IFormFile? file, + string[] allowedExtensions, + long maxBytes) + { + if (file == null || file.Length == 0) + return (false, string.Empty, "No file provided."); + + if (file.Length > maxBytes) + return (false, string.Empty, $"File exceeds the {maxBytes / 1024 / 1024} MB limit."); + + var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); + if (string.IsNullOrEmpty(extension) || !allowedExtensions.Contains(extension)) + return (false, string.Empty, $"File type not allowed. Allowed: {string.Join(", ", allowedExtensions)}."); + + return (true, extension, string.Empty); + } + + /// + /// Maps a file extension to its MIME content type, covering common image formats and + /// document types. Falls back to application/octet-stream. + /// + public static string GetContentType(string extension) => extension switch + { + ".jpg" or ".jpeg" => "image/jpeg", + ".png" => "image/png", + ".gif" => "image/gif", + ".webp" => "image/webp", + ".svg" => "image/svg+xml", + ".pdf" => "application/pdf", + ".doc" => "application/msword", + ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".txt" => "text/plain", + _ => "application/octet-stream" + }; + + /// + /// Strips OS-invalid filename characters from a base filename (no extension), replacing + /// them with underscores to produce a safe blob path segment. + /// + public static string SanitizeFileName(string fileName) + { + var sanitized = string.Join("_", fileName.Split(Path.GetInvalidFileNameChars())); + return string.IsNullOrWhiteSpace(sanitized) ? "file" : sanitized; + } +} diff --git a/src/PowderCoating.Application/Services/CatalogImageService.cs b/src/PowderCoating.Application/Services/CatalogImageService.cs index c0b8132..6369f87 100644 --- a/src/PowderCoating.Application/Services/CatalogImageService.cs +++ b/src/PowderCoating.Application/Services/CatalogImageService.cs @@ -47,15 +47,9 @@ public class CatalogImageService : ICatalogImageService string? existingImagePath, string? existingThumbnailPath) { - if (file == null || file.Length == 0) - return (false, string.Empty, string.Empty, "No file provided."); - - if (file.Length > MaxFileSizeBytes) - return (false, string.Empty, string.Empty, "File exceeds the 10 MB limit."); - - var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); - if (!AllowedExtensions.Contains(ext)) - return (false, string.Empty, string.Empty, $"File type '{ext}' is not allowed. Accepted types: jpg, jpeg, png, gif, webp."); + var (isValid, ext, validationError) = BlobFileHelper.ValidateUpload(file, AllowedExtensions, MaxFileSizeBytes); + if (!isValid) + return (false, string.Empty, string.Empty, validationError); var container = _settings.Containers.CatalogImages; var blobId = Guid.NewGuid().ToString("N"); diff --git a/src/PowderCoating.Application/Services/CompanyLogoService.cs b/src/PowderCoating.Application/Services/CompanyLogoService.cs index 20b89df..1d38e57 100644 --- a/src/PowderCoating.Application/Services/CompanyLogoService.cs +++ b/src/PowderCoating.Application/Services/CompanyLogoService.cs @@ -67,21 +67,15 @@ public class CompanyLogoService : ICompanyLogoService /// public async Task<(bool Success, string FilePath, string ErrorMessage)> SaveCompanyLogoAsync(IFormFile file, int companyId) { - if (file == null || file.Length == 0) - return (false, string.Empty, "No file provided"); - - if (file.Length > MaxFileSize) - return (false, string.Empty, $"File size exceeds maximum allowed size of {MaxFileSize / 1024 / 1024} MB"); - - var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); - if (!AllowedExtensions.Contains(extension)) - return (false, string.Empty, $"File type not allowed. Allowed types: {string.Join(", ", AllowedExtensions)}"); + var (isValid, extension, error) = BlobFileHelper.ValidateUpload(file, AllowedExtensions, MaxFileSize); + if (!isValid) + return (false, string.Empty, error); // Delete old logo (any extension) before saving new one await DeleteOldLogosAsync(companyId, extension); var blobName = GetCompanyLogoPath(companyId, extension); - var contentType = GetContentType(extension); + var contentType = BlobFileHelper.GetContentType(extension); using var stream = file.OpenReadStream(); var result = await _blobService.UploadAsync(_settings.Containers.CompanyLogos, blobName, stream, contentType); @@ -158,20 +152,4 @@ public class CompanyLogoService : ICompanyLogoService } } - /// - /// Maps a lowercase file extension to its canonical MIME content type. - /// The correct content type is required so that browsers render the image - /// inline rather than triggering a download. - /// - /// Lowercase file extension including the leading dot. - /// MIME type string, or application/octet-stream as a safe fallback. - private static string GetContentType(string extension) => extension switch - { - ".jpg" or ".jpeg" => "image/jpeg", - ".png" => "image/png", - ".gif" => "image/gif", - ".webp" => "image/webp", - ".svg" => "image/svg+xml", - _ => "application/octet-stream" - }; } diff --git a/src/PowderCoating.Application/Services/EquipmentManualService.cs b/src/PowderCoating.Application/Services/EquipmentManualService.cs index 61e9296..90aaccf 100644 --- a/src/PowderCoating.Application/Services/EquipmentManualService.cs +++ b/src/PowderCoating.Application/Services/EquipmentManualService.cs @@ -56,25 +56,16 @@ public class EquipmentManualService : IEquipmentManualService /// public async Task<(bool Success, string FilePath, string ErrorMessage)> SaveEquipmentManualAsync(IFormFile file, int companyId, int equipmentId) { - if (file == null || file.Length == 0) - return (false, string.Empty, "No file provided"); - - if (file.Length > MaxFileSize) - return (false, string.Empty, $"File size exceeds maximum allowed size of {MaxFileSize / 1024 / 1024} MB"); - - var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); - if (!AllowedExtensions.Contains(extension)) - return (false, string.Empty, $"File type not allowed. Allowed types: {string.Join(", ", AllowedExtensions)}"); + var (isValid, extension, error) = BlobFileHelper.ValidateUpload(file, AllowedExtensions, MaxFileSize); + if (!isValid) + return (false, string.Empty, error); // Sanitize filename — replace OS-invalid characters with underscores to // prevent path traversal and blob naming errors in Azure. - var fileName = Path.GetFileNameWithoutExtension(file.FileName); - fileName = string.Join("_", fileName.Split(Path.GetInvalidFileNameChars())); - if (string.IsNullOrWhiteSpace(fileName)) - fileName = "manual"; + var fileName = BlobFileHelper.SanitizeFileName(Path.GetFileNameWithoutExtension(file.FileName)); var blobName = $"{companyId}/equipment-manuals/{equipmentId}/{fileName}{extension}"; - var contentType = GetContentType(extension); + var contentType = BlobFileHelper.GetContentType(extension); using var stream = file.OpenReadStream(); var result = await _blobService.UploadAsync(_settings.Containers.Manuals, blobName, stream, contentType); @@ -130,19 +121,4 @@ public class EquipmentManualService : IEquipmentManualService return await _blobService.ExistsAsync(_settings.Containers.Manuals, filePath); } - /// - /// Maps a lowercase file extension to its canonical MIME content type. - /// Correct MIME types are required so browsers open PDFs inline and - /// Word documents prompt a compatible application rather than a raw download. - /// - /// Lowercase file extension including the leading dot. - /// MIME type string, or application/octet-stream as a safe fallback. - private static string GetContentType(string extension) => extension switch - { - ".pdf" => "application/pdf", - ".doc" => "application/msword", - ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - ".txt" => "text/plain", - _ => "application/octet-stream" - }; } diff --git a/src/PowderCoating.Application/Services/JobItemAssemblyService.cs b/src/PowderCoating.Application/Services/JobItemAssemblyService.cs new file mode 100644 index 0000000..e9032f0 --- /dev/null +++ b/src/PowderCoating.Application/Services/JobItemAssemblyService.cs @@ -0,0 +1,405 @@ +using PowderCoating.Application.DTOs.Quote; +using PowderCoating.Application.Interfaces; +using PowderCoating.Core.Entities; + +namespace PowderCoating.Application.Services; + +public class JobItemAssemblyService : IJobItemAssemblyService +{ + public JobItem CreateJobItem(CreateQuoteItemDto source, int jobId, int companyId, QuoteItemPricingResult pricing, DateTime createdAtUtc) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(pricing); + + return BuildJobItem( + new JobItemSeed + { + Description = source.Description, + Quantity = source.Quantity, + SurfaceAreaSqFt = source.SurfaceAreaSqFt, + CatalogItemId = source.CatalogItemId, + IsGenericItem = source.IsGenericItem, + IsLaborItem = source.IsLaborItem, + IsSalesItem = source.IsSalesItem, + Sku = source.Sku, + ManualUnitPrice = source.ManualUnitPrice, + PowderCostOverride = source.PowderCostOverride, + UnitPrice = pricing.UnitPrice, + TotalPrice = pricing.TotalPrice, + LaborCost = pricing.TotalPrice * 0.4m, + RequiresSandblasting = source.RequiresSandblasting, + RequiresMasking = source.RequiresMasking, + EstimatedMinutes = source.EstimatedMinutes, + Notes = source.Notes, + IncludePrepCost = source.IncludePrepCost, + Complexity = source.Complexity, + AiTags = source.AiTags, + AiPredictionId = source.AiPredictionId + }, + jobId, + companyId, + createdAtUtc); + } + + public IReadOnlyList CreateJobItemCoats(CreateQuoteItemDto source, int jobItemId, int companyId, DateTime createdAtUtc) + { + ArgumentNullException.ThrowIfNull(source); + + return source.Coats? + .OrderBy(c => c.Sequence) + .Select(c => BuildJobItemCoat( + new JobItemCoatSeed + { + CoatName = c.CoatName, + Sequence = c.Sequence, + InventoryItemId = c.InventoryItemId, + ColorName = c.ColorName, + VendorId = c.VendorId, + ColorCode = c.ColorCode, + Finish = c.Finish, + CoverageSqFtPerLb = c.CoverageSqFtPerLb, + TransferEfficiency = c.TransferEfficiency, + PowderCostPerLb = c.PowderCostPerLb, + PowderToOrder = CalculatePowderToOrder(c.PowderToOrder, source.SurfaceAreaSqFt, source.Quantity, c.CoverageSqFtPerLb, c.TransferEfficiency), + Notes = c.Notes + }, + jobItemId, + companyId, + createdAtUtc)) + .ToList() ?? []; + } + + public IReadOnlyList CreateJobItemPrepServices(CreateQuoteItemDto source, int jobItemId, int companyId, DateTime createdAtUtc) + { + ArgumentNullException.ThrowIfNull(source); + return BuildJobItemPrepServices( + source.PrepServices?.Select(p => new JobItemPrepServiceSeed + { + PrepServiceId = p.PrepServiceId, + EstimatedMinutes = p.EstimatedMinutes, + BlastSetupId = p.BlastSetupId + }), + jobItemId, + companyId, + createdAtUtc); + } + + public JobItem CreateJobItem(QuoteItem source, int jobId, int companyId, DateTime createdAtUtc) + { + ArgumentNullException.ThrowIfNull(source); + + var firstCoat = source.Coats? + .OrderBy(c => c.Sequence) + .FirstOrDefault(); + + return BuildJobItem( + new JobItemSeed + { + Description = source.Description, + Quantity = source.Quantity, + ColorName = firstCoat?.ColorName, + ColorCode = firstCoat?.ColorCode, + Finish = firstCoat?.Finish, + SurfaceArea = source.SurfaceAreaSqFt, + SurfaceAreaSqFt = source.SurfaceAreaSqFt, + CatalogItemId = source.CatalogItemId, + IsGenericItem = source.IsGenericItem, + IsLaborItem = source.IsLaborItem, + IsSalesItem = source.IsSalesItem, + Sku = source.Sku, + ManualUnitPrice = source.ManualUnitPrice, + PowderCostOverride = source.PowderCostOverride, + UnitPrice = source.UnitPrice, + TotalPrice = source.TotalPrice, + LaborCost = source.TotalPrice * 0.4m, + RequiresSandblasting = source.RequiresSandblasting, + RequiresMasking = source.RequiresMasking, + EstimatedMinutes = source.EstimatedMinutes, + Notes = source.Notes, + IncludePrepCost = source.IncludePrepCost, + Complexity = source.Complexity, + AiTags = source.AiTags, + AiPredictionId = source.AiPredictionId + }, + jobId, + companyId, + createdAtUtc); + } + + public IReadOnlyList CreateJobItemCoats(QuoteItem source, int jobItemId, int companyId, DateTime createdAtUtc) + { + ArgumentNullException.ThrowIfNull(source); + + return source.Coats? + .OrderBy(c => c.Sequence) + .Select(c => + { + var appearance = ResolveCoatAppearance(c.ColorName, c.ColorCode, c.Finish, c.InventoryItem); + return BuildJobItemCoat( + new JobItemCoatSeed + { + CoatName = c.CoatName, + Sequence = c.Sequence, + InventoryItemId = c.InventoryItemId, + ColorName = appearance.ColorName, + VendorId = c.VendorId, + ColorCode = appearance.ColorCode, + Finish = appearance.Finish, + CoverageSqFtPerLb = c.CoverageSqFtPerLb, + TransferEfficiency = c.TransferEfficiency, + PowderCostPerLb = c.PowderCostPerLb, + PowderToOrder = CalculatePowderToOrder(c.PowderToOrder, source.SurfaceAreaSqFt, source.Quantity, c.CoverageSqFtPerLb, c.TransferEfficiency), + Notes = c.Notes + }, + jobItemId, + companyId, + createdAtUtc); + }) + .ToList() ?? []; + } + + public IReadOnlyList CreateJobItemPrepServices(QuoteItem source, int jobItemId, int companyId, DateTime createdAtUtc) + { + ArgumentNullException.ThrowIfNull(source); + return BuildJobItemPrepServices( + source.PrepServices?.Select(p => new JobItemPrepServiceSeed + { + PrepServiceId = p.PrepServiceId, + EstimatedMinutes = p.EstimatedMinutes, + BlastSetupId = p.BlastSetupId + }), + jobItemId, + companyId, + createdAtUtc); + } + + public JobItem CreateJobItem(JobItem source, int jobId, int companyId, DateTime createdAtUtc) + { + ArgumentNullException.ThrowIfNull(source); + + return BuildJobItem( + new JobItemSeed + { + Description = source.Description, + Quantity = source.Quantity, + ColorName = source.ColorName, + ColorCode = source.ColorCode, + Finish = source.Finish, + SurfaceArea = source.SurfaceArea, + SurfaceAreaSqFt = source.SurfaceAreaSqFt, + CatalogItemId = source.CatalogItemId, + IsGenericItem = source.IsGenericItem, + IsLaborItem = source.IsLaborItem, + IsSalesItem = source.IsSalesItem, + Sku = source.Sku, + ManualUnitPrice = source.ManualUnitPrice, + PowderCostOverride = source.PowderCostOverride, + UnitPrice = source.UnitPrice, + TotalPrice = source.TotalPrice, + LaborCost = source.LaborCost, + RequiresSandblasting = source.RequiresSandblasting, + RequiresMasking = source.RequiresMasking, + EstimatedMinutes = source.EstimatedMinutes, + Notes = source.Notes, + IncludePrepCost = source.IncludePrepCost, + Complexity = source.Complexity, + AiTags = source.AiTags, + AiPredictionId = source.AiPredictionId + }, + jobId, + companyId, + createdAtUtc); + } + + public IReadOnlyList CreateJobItemCoats(JobItem source, int jobItemId, int companyId, DateTime createdAtUtc) + { + ArgumentNullException.ThrowIfNull(source); + + return source.Coats? + .OrderBy(c => c.Sequence) + .Select(c => BuildJobItemCoat( + new JobItemCoatSeed + { + CoatName = c.CoatName, + Sequence = c.Sequence, + InventoryItemId = c.InventoryItemId, + ColorName = c.ColorName, + VendorId = c.VendorId, + ColorCode = c.ColorCode, + Finish = c.Finish, + CoverageSqFtPerLb = c.CoverageSqFtPerLb, + TransferEfficiency = c.TransferEfficiency, + PowderCostPerLb = c.PowderCostPerLb, + PowderToOrder = c.PowderToOrder, + Notes = c.Notes + }, + jobItemId, + companyId, + createdAtUtc)) + .ToList() ?? []; + } + + public IReadOnlyList CreateJobItemPrepServices(JobItem source, int jobItemId, int companyId, DateTime createdAtUtc) + { + ArgumentNullException.ThrowIfNull(source); + return BuildJobItemPrepServices( + source.PrepServices?.Select(p => new JobItemPrepServiceSeed + { + PrepServiceId = p.PrepServiceId, + EstimatedMinutes = p.EstimatedMinutes, + BlastSetupId = p.BlastSetupId + }), + jobItemId, + companyId, + createdAtUtc); + } + + private static JobItem BuildJobItem(JobItemSeed seed, int jobId, int companyId, DateTime createdAtUtc) + { + return new JobItem + { + JobId = jobId, + Description = seed.Description, + Quantity = seed.Quantity, + ColorName = seed.ColorName, + ColorCode = seed.ColorCode, + Finish = seed.Finish, + SurfaceArea = seed.SurfaceArea, + SurfaceAreaSqFt = seed.SurfaceAreaSqFt, + CatalogItemId = seed.CatalogItemId, + IsGenericItem = seed.IsGenericItem, + IsLaborItem = seed.IsLaborItem, + IsSalesItem = seed.IsSalesItem, + Sku = seed.Sku, + ManualUnitPrice = seed.ManualUnitPrice, + PowderCostOverride = seed.PowderCostOverride, + UnitPrice = seed.UnitPrice, + TotalPrice = seed.TotalPrice, + LaborCost = seed.LaborCost, + RequiresSandblasting = seed.RequiresSandblasting, + RequiresMasking = seed.RequiresMasking, + EstimatedMinutes = seed.EstimatedMinutes, + Notes = seed.Notes, + IncludePrepCost = seed.IncludePrepCost, + Complexity = seed.Complexity, + AiTags = seed.AiTags, + AiPredictionId = seed.AiPredictionId, + CompanyId = companyId, + CreatedAt = createdAtUtc + }; + } + + private static JobItemCoat BuildJobItemCoat(JobItemCoatSeed seed, int jobItemId, int companyId, DateTime createdAtUtc) + { + return new JobItemCoat + { + JobItemId = jobItemId, + CoatName = seed.CoatName, + Sequence = seed.Sequence, + InventoryItemId = seed.InventoryItemId, + ColorName = seed.ColorName, + VendorId = seed.VendorId, + ColorCode = seed.ColorCode, + Finish = seed.Finish, + CoverageSqFtPerLb = seed.CoverageSqFtPerLb, + TransferEfficiency = seed.TransferEfficiency, + PowderCostPerLb = seed.PowderCostPerLb, + PowderToOrder = seed.PowderToOrder, + Notes = seed.Notes, + CompanyId = companyId, + CreatedAt = createdAtUtc + }; + } + + private static IReadOnlyList BuildJobItemPrepServices(IEnumerable? seeds, int jobItemId, int companyId, DateTime createdAtUtc) + { + return seeds? + .Select(seed => new JobItemPrepService + { + JobItemId = jobItemId, + PrepServiceId = seed.PrepServiceId, + EstimatedMinutes = seed.EstimatedMinutes, + BlastSetupId = seed.BlastSetupId, + CompanyId = companyId, + CreatedAt = createdAtUtc + }) + .ToList() ?? []; + } + + private static decimal? CalculatePowderToOrder(decimal? storedPowderToOrder, decimal surfaceAreaSqFt, decimal quantity, decimal coverageSqFtPerLb, decimal transferEfficiency) + { + if (storedPowderToOrder.HasValue && storedPowderToOrder.Value > 0) + return storedPowderToOrder; + + if (surfaceAreaSqFt <= 0) + return null; + + var coverage = coverageSqFtPerLb > 0 ? coverageSqFtPerLb : 30m; + var efficiency = transferEfficiency > 0 ? transferEfficiency / 100m : 0.65m; + return Math.Round((surfaceAreaSqFt * quantity) / (coverage * efficiency), 2); + } + + private static (string? ColorName, string? ColorCode, string? Finish) ResolveCoatAppearance( + string? colorName, + string? colorCode, + string? finish, + InventoryItem? inventoryItem) + { + if (inventoryItem == null) + return (colorName, colorCode, finish); + + return (inventoryItem.Name, inventoryItem.ColorCode, inventoryItem.Finish); + } + + private sealed class JobItemSeed + { + public string Description { get; init; } = string.Empty; + public decimal Quantity { get; init; } + public string? ColorName { get; init; } + public string? ColorCode { get; init; } + public string? Finish { get; init; } + public decimal? SurfaceArea { get; init; } + public decimal SurfaceAreaSqFt { get; init; } + public int? CatalogItemId { get; init; } + public bool IsGenericItem { get; init; } + public bool IsLaborItem { get; init; } + public bool IsSalesItem { get; init; } + public string? Sku { get; init; } + public decimal? ManualUnitPrice { get; init; } + public decimal? PowderCostOverride { get; init; } + public decimal UnitPrice { get; init; } + public decimal TotalPrice { get; init; } + public decimal LaborCost { get; init; } + public bool RequiresSandblasting { get; init; } + public bool RequiresMasking { get; init; } + public int EstimatedMinutes { get; init; } + public string? Notes { get; init; } + public bool IncludePrepCost { get; init; } + public string? Complexity { get; init; } + public string? AiTags { get; init; } + public int? AiPredictionId { get; init; } + } + + private sealed class JobItemCoatSeed + { + public string CoatName { get; init; } = string.Empty; + public int Sequence { get; init; } + public int? InventoryItemId { get; init; } + public string? ColorName { get; init; } + public int? VendorId { get; init; } + public string? ColorCode { get; init; } + public string? Finish { get; init; } + public decimal CoverageSqFtPerLb { get; init; } + public decimal TransferEfficiency { get; init; } + public decimal? PowderCostPerLb { get; init; } + public decimal? PowderToOrder { get; init; } + public string? Notes { get; init; } + } + + private sealed class JobItemPrepServiceSeed + { + public int PrepServiceId { get; init; } + public int EstimatedMinutes { get; init; } + public int? BlastSetupId { get; init; } + } +} diff --git a/src/PowderCoating.Application/Services/JobPhotoService.cs b/src/PowderCoating.Application/Services/JobPhotoService.cs index a6106a6..1cab03d 100644 --- a/src/PowderCoating.Application/Services/JobPhotoService.cs +++ b/src/PowderCoating.Application/Services/JobPhotoService.cs @@ -69,19 +69,13 @@ public class JobPhotoService : IJobPhotoService string? caption = null, JobPhotoType photoType = JobPhotoType.Progress) { - if (file == null || file.Length == 0) - return (false, string.Empty, "No file was uploaded."); - - if (file.Length > MaxPhotoSize) - return (false, string.Empty, "Photo must be smaller than 10 MB."); - - var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); - if (string.IsNullOrEmpty(extension) || !AllowedImageTypes.Contains(extension)) - return (false, string.Empty, "Only JPG, PNG, GIF, and WebP images are allowed."); + var (isValid, extension, error) = BlobFileHelper.ValidateUpload(file, AllowedImageTypes, MaxPhotoSize); + if (!isValid) + return (false, string.Empty, error); // SECURITY: Use GUID for blob name to prevent enumeration var blobName = $"{companyId}/job-photos/{jobId}/{Guid.NewGuid()}{extension}"; - var contentType = GetContentType(extension); + var contentType = BlobFileHelper.GetContentType(extension); using var stream = file.OpenReadStream(); var result = await _blobService.UploadAsync(_settings.Containers.JobImages, blobName, stream, contentType); @@ -137,19 +131,4 @@ public class JobPhotoService : IJobPhotoService return await _blobService.ExistsAsync(_settings.Containers.JobImages, filePath); } - /// - /// Maps a lowercase file extension to its canonical MIME content type. - /// Falls back to image/jpeg (rather than octet-stream) because all - /// allowed extensions are image types and browsers will render them correctly. - /// - /// Lowercase file extension including the leading dot. - /// MIME type string. - private static string GetContentType(string extension) => extension switch - { - ".jpg" or ".jpeg" => "image/jpeg", - ".png" => "image/png", - ".gif" => "image/gif", - ".webp" => "image/webp", - _ => "image/jpeg" - }; } diff --git a/src/PowderCoating.Application/Services/ProfilePhotoService.cs b/src/PowderCoating.Application/Services/ProfilePhotoService.cs index b4b24a6..52d3cfc 100644 --- a/src/PowderCoating.Application/Services/ProfilePhotoService.cs +++ b/src/PowderCoating.Application/Services/ProfilePhotoService.cs @@ -66,22 +66,16 @@ public class ProfilePhotoService : IProfilePhotoService string userId, int companyId) { - if (file == null || file.Length == 0) - return (false, string.Empty, "No file was uploaded."); - - if (file.Length > MaxPhotoSize) - return (false, string.Empty, "Photo must be smaller than 10 MB."); - - var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); - if (string.IsNullOrEmpty(extension) || !AllowedImageTypes.Contains(extension)) - return (false, string.Empty, "Only JPG, PNG, GIF, and WebP images are allowed."); + var (isValid, extension, error) = BlobFileHelper.ValidateUpload(file, AllowedImageTypes, MaxPhotoSize); + if (!isValid) + return (false, string.Empty, error); // Delete old photos for this user with different extensions await DeleteOldPhotosForUserAsync(companyId, userId, extension); // Blob path mirrors former filesystem path var blobName = $"{companyId}/profile-photos/{userId}{extension}"; - var contentType = GetContentType(extension); + var contentType = BlobFileHelper.GetContentType(extension); using var stream = file.OpenReadStream(); var result = await _blobService.UploadAsync(_settings.Containers.ProfileImages, blobName, stream, contentType); @@ -172,19 +166,4 @@ public class ProfilePhotoService : IProfilePhotoService } } - /// - /// Maps a lowercase file extension to its canonical MIME content type. - /// Falls back to image/jpeg (rather than octet-stream) because all - /// allowed extensions are image types and browsers will render them correctly. - /// - /// Lowercase file extension including the leading dot. - /// MIME type string. - private static string GetContentType(string extension) => extension switch - { - ".jpg" or ".jpeg" => "image/jpeg", - ".png" => "image/png", - ".gif" => "image/gif", - ".webp" => "image/webp", - _ => "image/jpeg" - }; } diff --git a/src/PowderCoating.Application/Services/QuotePhotoService.cs b/src/PowderCoating.Application/Services/QuotePhotoService.cs index 4ff475f..bddeaa5 100644 --- a/src/PowderCoating.Application/Services/QuotePhotoService.cs +++ b/src/PowderCoating.Application/Services/QuotePhotoService.cs @@ -50,19 +50,13 @@ public class QuotePhotoService : IQuotePhotoService public async Task<(bool Success, string TempId, string FilePath, string ErrorMessage)> SaveTempPhotoAsync( IFormFile file, int companyId) { - if (file == null || file.Length == 0) - return (false, string.Empty, string.Empty, "No file provided."); - - if (file.Length > MaxFileSizeBytes) - return (false, string.Empty, string.Empty, "File exceeds the 10 MB limit."); - - var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); - if (!AllowedExtensions.Contains(ext)) - return (false, string.Empty, string.Empty, $"File type '{ext}' is not allowed."); + var (isValid, ext, validationError) = BlobFileHelper.ValidateUpload(file, AllowedExtensions, MaxFileSizeBytes); + if (!isValid) + return (false, string.Empty, string.Empty, validationError); var tempId = Guid.NewGuid().ToString("N"); var blobName = $"temp/{tempId}/{Guid.NewGuid():N}{ext}"; - var contentType = GetContentType(ext); + var contentType = BlobFileHelper.GetContentType(ext); using var stream = file.OpenReadStream(); var result = await _blobService.UploadAsync(_settings.Containers.QuoteImages, blobName, stream, contentType); @@ -100,7 +94,7 @@ public class QuotePhotoService : IQuotePhotoService return (false, string.Empty, "Failed to read temp photo."); using var ms = new MemoryStream(download.Content); - var upload = await _blobService.UploadAsync(_settings.Containers.QuoteImages, destBlob, ms, GetContentType(ext)); + var upload = await _blobService.UploadAsync(_settings.Containers.QuoteImages, destBlob, ms, BlobFileHelper.GetContentType(ext)); if (!upload.Success) return (false, string.Empty, "Failed to save permanent photo."); @@ -173,12 +167,4 @@ public class QuotePhotoService : IQuotePhotoService } } - private static string GetContentType(string ext) => ext switch - { - ".jpg" or ".jpeg" => "image/jpeg", - ".png" => "image/png", - ".gif" => "image/gif", - ".webp" => "image/webp", - _ => "image/jpeg" - }; } diff --git a/src/PowderCoating.Application/Services/QuotePricingAssemblyService.cs b/src/PowderCoating.Application/Services/QuotePricingAssemblyService.cs new file mode 100644 index 0000000..bf5a2f6 --- /dev/null +++ b/src/PowderCoating.Application/Services/QuotePricingAssemblyService.cs @@ -0,0 +1,369 @@ +using PowderCoating.Application.DTOs.Quote; +using PowderCoating.Application.Interfaces; +using PowderCoating.Core.Entities; +using PowderCoating.Core.Interfaces; +using Microsoft.Extensions.Logging; + +namespace PowderCoating.Application.Services; + +public class QuotePricingAssemblyService : IQuotePricingAssemblyService +{ + private readonly IUnitOfWork _unitOfWork; + private readonly IPricingCalculationService _pricingService; + private readonly IInventoryAiLookupService _aiLookupService; + private readonly ILogger _logger; + + public QuotePricingAssemblyService( + IUnitOfWork unitOfWork, + IPricingCalculationService pricingService, + IInventoryAiLookupService aiLookupService, + ILogger logger) + { + _unitOfWork = unitOfWork; + _pricingService = pricingService; + _aiLookupService = aiLookupService; + _logger = logger; + } + + public void ApplyPricingSnapshot(Quote quote, QuotePricingResult pricingResult) + { + ArgumentNullException.ThrowIfNull(quote); + ArgumentNullException.ThrowIfNull(pricingResult); + + quote.MaterialCosts = pricingResult.MaterialCosts; + quote.LaborCosts = pricingResult.LaborCosts; + quote.EquipmentCosts = pricingResult.EquipmentCosts; + quote.ItemsSubtotal = pricingResult.ItemsSubtotal; + quote.OvenBatchCost = pricingResult.OvenBatchCost; + quote.ShopSuppliesAmount = pricingResult.ShopSuppliesAmount; + quote.ShopSuppliesPercent = pricingResult.ShopSuppliesPercent; + quote.OverheadAmount = pricingResult.OverheadCosts; + quote.OverheadPercent = pricingResult.OverheadPercent; + quote.ProfitMargin = pricingResult.ProfitMargin; + quote.ProfitPercent = pricingResult.ProfitPercent; + quote.SubTotal = pricingResult.SubtotalBeforeDiscount; + quote.DiscountPercent = pricingResult.DiscountPercent; + quote.DiscountAmount = pricingResult.DiscountAmount; + quote.RushFee = pricingResult.RushFee; + quote.TaxAmount = pricingResult.TaxAmount; + quote.Total = pricingResult.Total; + } + + public async Task> CreateQuoteItemsAsync( + IEnumerable itemDtos, + int quoteId, + int companyId, + decimal? ovenRateOverride, + DateTime createdAtUtc) + { + ArgumentNullException.ThrowIfNull(itemDtos); + + var items = new List(); + foreach (var itemDto in itemDtos) + { + var item = BuildQuoteItem(itemDto, quoteId, companyId, createdAtUtc); + await ApplyPricingAsync(item, itemDto, companyId, ovenRateOverride); + await UpdateAiPredictionOverrideAsync(itemDto, item.UnitPrice); + + item.Coats = await BuildQuoteItemCoatsAsync(itemDto, companyId, createdAtUtc); + item.PrepServices = BuildQuoteItemPrepServices(itemDto, companyId, createdAtUtc); + items.Add(item); + } + + return items; + } + + private async Task ApplyPricingAsync(QuoteItem item, CreateQuoteItemDto itemDto, int companyId, decimal? ovenRateOverride) + { + if (itemDto.IsAiItem && itemDto.ManualUnitPrice.HasValue && itemDto.ManualUnitPrice.Value > 0) + { + item.UnitPrice = itemDto.ManualUnitPrice.Value; + item.TotalPrice = itemDto.ManualUnitPrice.Value * itemDto.Quantity; + _logger.LogInformation("AI item price: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); + return; + } + + if (itemDto.IsSalesItem && itemDto.ManualUnitPrice.HasValue) + { + item.UnitPrice = itemDto.ManualUnitPrice.Value; + item.TotalPrice = itemDto.ManualUnitPrice.Value * itemDto.Quantity; + _logger.LogInformation("Sales item price: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); + return; + } + + if (itemDto.CatalogItemId.HasValue) + { + if (itemDto.Coats != null && itemDto.Coats.Any()) + { + _logger.LogInformation("Calculating catalog item with {CoatCount} coats", itemDto.Coats.Count); + var itemPricing = await _pricingService.CalculateQuoteItemPriceAsync(itemDto, companyId, ovenRateOverride); + ApplyCalculatedPricing(item, itemPricing); + return; + } + + var catalogItem = await _unitOfWork.CatalogItems.GetByIdAsync(itemDto.CatalogItemId.Value); + if (catalogItem != null) + { + item.UnitPrice = catalogItem.DefaultPrice; + item.TotalPrice = catalogItem.DefaultPrice * itemDto.Quantity; + _logger.LogInformation("Catalog item no coats: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); + } + + return; + } + + _logger.LogInformation("Calculating custom item with {CoatCount} coats", itemDto.Coats?.Count ?? 0); + var pricing = await _pricingService.CalculateQuoteItemPriceAsync(itemDto, companyId, ovenRateOverride); + ApplyCalculatedPricing(item, pricing); + } + + private async Task> BuildQuoteItemCoatsAsync(CreateQuoteItemDto itemDto, int companyId, DateTime createdAtUtc) + { + if (itemDto.Coats == null || itemDto.Coats.Count == 0) + return []; + + var coats = new List(); + for (var coatIndex = 0; coatIndex < itemDto.Coats.Count; coatIndex++) + { + var coatDto = itemDto.Coats[coatIndex]; + + if (coatDto.AddAsIncoming && coatDto.CatalogItemId.HasValue && !coatDto.InventoryItemId.HasValue) + coatDto.InventoryItemId = await CreateIncomingInventoryItemAsync(coatDto, companyId); + + var coat = BuildQuoteItemCoat(coatDto, companyId, createdAtUtc); + var coatPricing = await _pricingService.CalculateCoatPriceAsync( + coatDto, + itemDto.SurfaceAreaSqFt, + itemDto.Quantity, + coatIndex, + itemDto.EstimatedMinutes, + companyId); + + coat.CoatMaterialCost = coatPricing.CoatMaterialCost; + coat.CoatLaborCost = coatPricing.CoatLaborCost; + coat.CoatTotalCost = coatPricing.CoatTotalCost; + coats.Add(coat); + } + + return coats; + } + + private static List BuildQuoteItemPrepServices(CreateQuoteItemDto itemDto, int companyId, DateTime createdAtUtc) + { + if (itemDto.PrepServices == null || itemDto.PrepServices.Count == 0) + return []; + + return itemDto.PrepServices + .Select(ps => new QuoteItemPrepService + { + PrepServiceId = ps.PrepServiceId, + EstimatedMinutes = ps.EstimatedMinutes, + BlastSetupId = ps.BlastSetupId, + CompanyId = companyId, + CreatedAt = createdAtUtc + }) + .ToList(); + } + + private static QuoteItem BuildQuoteItem(CreateQuoteItemDto itemDto, int quoteId, int companyId, DateTime createdAtUtc) + { + return new QuoteItem + { + QuoteId = quoteId, + Description = itemDto.Description, + Quantity = itemDto.Quantity, + SurfaceAreaSqFt = itemDto.SurfaceAreaSqFt, + CatalogItemId = itemDto.CatalogItemId, + IsGenericItem = itemDto.IsGenericItem, + ManualUnitPrice = itemDto.ManualUnitPrice, + PowderCostOverride = itemDto.PowderCostOverride, + IsLaborItem = itemDto.IsLaborItem, + IsSalesItem = itemDto.IsSalesItem, + Sku = itemDto.Sku, + RequiresSandblasting = itemDto.RequiresSandblasting, + RequiresMasking = itemDto.RequiresMasking, + EstimatedMinutes = itemDto.EstimatedMinutes, + IncludePrepCost = itemDto.IncludePrepCost, + Notes = itemDto.Notes, + Complexity = itemDto.Complexity, + IsAiItem = itemDto.IsAiItem, + AiTags = itemDto.AiTags, + AiPredictionId = itemDto.AiPredictionId, + CompanyId = companyId, + CreatedAt = createdAtUtc + }; + } + + private static QuoteItemCoat BuildQuoteItemCoat(CreateQuoteItemCoatDto coatDto, int companyId, DateTime createdAtUtc) + { + return new QuoteItemCoat + { + CoatName = coatDto.CoatName, + Sequence = coatDto.Sequence, + InventoryItemId = coatDto.InventoryItemId, + ColorName = coatDto.ColorName, + VendorId = coatDto.VendorId, + ColorCode = coatDto.ColorCode, + Finish = coatDto.Finish, + CoverageSqFtPerLb = coatDto.CoverageSqFtPerLb, + TransferEfficiency = coatDto.TransferEfficiency, + PowderCostPerLb = coatDto.PowderCostPerLb, + PowderToOrder = coatDto.PowderToOrder, + Notes = coatDto.Notes, + CompanyId = companyId, + CreatedAt = createdAtUtc + }; + } + + private static void ApplyCalculatedPricing(QuoteItem item, QuoteItemPricingResult pricing) + { + item.UnitPrice = pricing.UnitPrice; + item.TotalPrice = pricing.TotalPrice; + item.ItemMaterialCost = pricing.MaterialCost; + item.ItemLaborCost = pricing.LaborCost; + item.ItemEquipmentCost = pricing.EquipmentCost; + } + + private async Task UpdateAiPredictionOverrideAsync(CreateQuoteItemDto itemDto, decimal finalUnitPrice) + { + if (!itemDto.AiPredictionId.HasValue) return; + + var prediction = await _unitOfWork.AiItemPredictions.GetByIdAsync(itemDto.AiPredictionId.Value); + if (prediction == null) return; + + var sqftDiff = Math.Abs(prediction.PredictedSurfaceAreaSqFt - itemDto.SurfaceAreaSqFt); + var priceDiff = Math.Abs(prediction.PredictedUnitPrice - finalUnitPrice); + prediction.UserOverrodeEstimate = sqftDiff > 0.01m || priceDiff > 0.01m; + prediction.UpdatedAt = DateTime.UtcNow; + } + + private async Task CreateIncomingInventoryItemAsync(CreateQuoteItemCoatDto coatDto, int companyId) + { + try + { + var catalogItem = await _unitOfWork.PowderCatalog.GetByIdAsync(coatDto.CatalogItemId!.Value); + if (catalogItem == null) return null; + + var categories = await _unitOfWork.InventoryCategoryLookups.GetAllAsync(); + var coatingCategory = categories + .Where(c => c.IsActive && c.IsCoating) + .OrderBy(c => c.DisplayOrder) + .FirstOrDefault(); + + var vendors = await _unitOfWork.Vendors.GetAllAsync(); + var vendorNameLower = catalogItem.VendorName.ToLower(); + var matchedVendor = vendors.FirstOrDefault(v => + v.CompanyName.ToLower().Contains(vendorNameLower) || + vendorNameLower.Contains(v.CompanyName.ToLower())); + + var code = coatingCategory != null + ? (coatingCategory.CategoryCode.Length >= 4 + ? coatingCategory.CategoryCode[..4].ToUpperInvariant() + : coatingCategory.CategoryCode.ToUpperInvariant().PadRight(4, 'X')) + : "POWD"; + var prefix = $"{code}-{DateTime.Now:yyMM}-"; + var allItems = await _unitOfWork.InventoryItems.GetAllAsync(ignoreQueryFilters: true); + var maxSeq = allItems + .Where(i => i.SKU.StartsWith(prefix)) + .Select(i => int.TryParse(i.SKU[prefix.Length..], out var n) ? n : 0) + .DefaultIfEmpty(0) + .Max(); + var sku = $"{prefix}{(maxSeq + 1):D4}"; + + var name = System.Globalization.CultureInfo.CurrentCulture.TextInfo + .ToTitleCase(catalogItem.ColorName.Trim().ToLower()); + + var description = catalogItem.Description; + var finish = catalogItem.Finish; + var colorFamilies = catalogItem.ColorFamilies; + var cureTemp = catalogItem.CureTemperatureF; + var cureTime = catalogItem.CureTimeMinutes; + var coverage = catalogItem.CoverageSqFtPerLb; + var transferEff = catalogItem.TransferEfficiency; + var specificGravity = catalogItem.SpecificGravity; + var imageUrl = catalogItem.ImageUrl; + var sdsUrl = catalogItem.SdsUrl; + var tdsUrl = catalogItem.TdsUrl; + + var needsAugment = !string.IsNullOrWhiteSpace(catalogItem.ProductUrl) && + (string.IsNullOrWhiteSpace(description) || + string.IsNullOrWhiteSpace(colorFamilies) || + cureTemp == null || cureTime == null); + if (needsAugment) + { + try + { + var augmented = await _aiLookupService.LookupByUrlAsync(catalogItem.ProductUrl!, catalogItem.ColorName, catalogItem.TdsUrl); + if (augmented.Success) + { + description = string.IsNullOrWhiteSpace(description) ? augmented.Description : description; + finish = string.IsNullOrWhiteSpace(finish) ? augmented.Finish : finish; + colorFamilies = string.IsNullOrWhiteSpace(colorFamilies) ? augmented.ColorFamilies : colorFamilies; + cureTemp ??= augmented.CureTemperatureF; + cureTime ??= augmented.CureTimeMinutes; + coverage ??= augmented.CoverageSqFtPerLb; + transferEff ??= augmented.TransferEfficiency; + specificGravity ??= augmented.SpecificGravity; + imageUrl = string.IsNullOrWhiteSpace(imageUrl) ? augmented.ImageUrl : imageUrl; + sdsUrl = string.IsNullOrWhiteSpace(sdsUrl) ? augmented.SdsUrl : sdsUrl; + tdsUrl = string.IsNullOrWhiteSpace(tdsUrl) ? augmented.TdsUrl : tdsUrl; + _logger.LogInformation("AI-augmented incoming inventory item for catalog {CatalogId}", catalogItem.Id); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "AI augment failed for catalog {CatalogId}, continuing with catalog data", catalogItem.Id); + } + } + + var item = new InventoryItem + { + SKU = sku, + Name = name, + Description = description, + ColorName = catalogItem.ColorName, + Manufacturer = catalogItem.VendorName, + ManufacturerPartNumber = catalogItem.Sku, + Finish = finish, + ColorFamilies = colorFamilies, + RequiresClearCoat = catalogItem.RequiresClearCoat ?? false, + CoverageSqFtPerLb = coverage ?? 30m, + TransferEfficiency = transferEff ?? 65m, + CureTemperatureF = cureTemp, + CureTimeMinutes = cureTime, + SpecificGravity = specificGravity, + SpecPageUrl = catalogItem.ProductUrl, + ImageUrl = imageUrl, + SdsUrl = sdsUrl, + TdsUrl = tdsUrl, + UnitCost = catalogItem.UnitPrice, + AverageCost = catalogItem.UnitPrice, + LastPurchasePrice = catalogItem.UnitPrice, + QuantityOnHand = 0, + UnitOfMeasure = "lbs", + PrimaryVendorId = matchedVendor?.Id, + InventoryCategoryId = coatingCategory?.Id, + Category = coatingCategory?.DisplayName ?? "Powder Coating", + IsActive = true, + IsIncoming = true, + CompanyId = companyId, + CreatedAt = DateTime.UtcNow, + }; + + await _unitOfWork.InventoryItems.AddAsync(item); + await _unitOfWork.SaveChangesAsync(); + + coatDto.PowderCostPerLb = null; + _logger.LogInformation("Created incoming inventory item {Id} ({Name}) from catalog {CatalogId} via quote coat", + item.Id, item.Name, coatDto.CatalogItemId); + + return item.Id; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to create incoming inventory item from catalog {CatalogId}, continuing without inventory link", + coatDto.CatalogItemId); + return null; + } + } +} diff --git a/src/PowderCoating.Core/Entities/JobTemplateItem.cs b/src/PowderCoating.Core/Entities/JobTemplateItem.cs index c61e9c2..49e6ebf 100644 --- a/src/PowderCoating.Core/Entities/JobTemplateItem.cs +++ b/src/PowderCoating.Core/Entities/JobTemplateItem.cs @@ -9,6 +9,8 @@ public class JobTemplateItem : BaseEntity public int? CatalogItemId { get; set; } public bool IsGenericItem { get; set; } public bool IsLaborItem { get; set; } + public bool IsSalesItem { get; set; } + public string? Sku { get; set; } public decimal? ManualUnitPrice { get; set; } public bool RequiresSandblasting { get; set; } public bool RequiresMasking { get; set; } diff --git a/src/PowderCoating.Infrastructure/Migrations/20260510011252_AddJobTemplateItemSalesFields.Designer.cs b/src/PowderCoating.Infrastructure/Migrations/20260510011252_AddJobTemplateItemSalesFields.Designer.cs new file mode 100644 index 0000000..8624c18 --- /dev/null +++ b/src/PowderCoating.Infrastructure/Migrations/20260510011252_AddJobTemplateItemSalesFields.Designer.cs @@ -0,0 +1,9552 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PowderCoating.Infrastructure.Data; + +#nullable disable + +namespace PowderCoating.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260510011252_AddJobTemplateItemSalesFields")] + partial class AddJobTemplateItemSalesFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("nvarchar(max)"); + + b.Property("Xml") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AccountSubType") + .HasColumnType("int"); + + b.Property("AccountType") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CurrentBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OpeningBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("OpeningBalanceDate") + .HasColumnType("datetime2"); + + b.Property("ParentAccountId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ParentAccountId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AiItemPrediction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AiTags") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConversationRounds") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PredictedComplexity") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PredictedMinutes") + .HasColumnType("int"); + + b.Property("PredictedSurfaceAreaSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("PredictedUnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Reasoning") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UserOverrodeEstimate") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("AiItemPredictions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AiUsageLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CalledAt") + .HasColumnType("datetime2"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("Feature") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InputLength") + .HasColumnType("int"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId", "CalledAt") + .HasDatabaseName("IX_AiUsageLogs_CompanyId_CalledAt"); + + b.ToTable("AiUsageLogs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Announcement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedByUserName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDismissible") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartsAt") + .HasColumnType("datetime2"); + + b.Property("Target") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TargetCompanyId") + .HasColumnType("int"); + + b.Property("TargetPlan") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Announcements"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AnnouncementDismissal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnnouncementId") + .HasColumnType("int"); + + b.Property("DismissedAt") + .HasColumnType("datetime2"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("AnnouncementId", "UserId") + .IsUnique(); + + b.ToTable("AnnouncementDismissals"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("BanReason") + .HasColumnType("nvarchar(max)"); + + b.Property("BannedAt") + .HasColumnType("datetime2"); + + b.Property("BannedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("CanApproveQuotes") + .HasColumnType("bit"); + + b.Property("CanCreateQuotes") + .HasColumnType("bit"); + + b.Property("CanManageCalendar") + .HasColumnType("bit"); + + b.Property("CanManageCustomers") + .HasColumnType("bit"); + + b.Property("CanManageEquipment") + .HasColumnType("bit"); + + b.Property("CanManageInventory") + .HasColumnType("bit"); + + b.Property("CanManageInvoices") + .HasColumnType("bit"); + + b.Property("CanManageJobs") + .HasColumnType("bit"); + + b.Property("CanManageMaintenance") + .HasColumnType("bit"); + + b.Property("CanManageProducts") + .HasColumnType("bit"); + + b.Property("CanManageVendors") + .HasColumnType("bit"); + + b.Property("CanViewCalendar") + .HasColumnType("bit"); + + b.Property("CanViewProducts") + .HasColumnType("bit"); + + b.Property("CanViewReports") + .HasColumnType("bit"); + + b.Property("CanViewShopFloor") + .HasColumnType("bit"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyRole") + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DashboardLayout") + .HasColumnType("int"); + + b.Property("DateFormat") + .HasColumnType("nvarchar(max)"); + + b.Property("Department") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("EmployeeNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("HireDate") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsBanned") + .HasColumnType("bit"); + + b.Property("LastLoginDate") + .HasColumnType("datetime2"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PasskeyPromptDismissed") + .HasColumnType("bit"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("Position") + .HasColumnType("nvarchar(max)"); + + b.Property("ProfilePictureFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("SidebarColor") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("TerminationDate") + .HasColumnType("datetime2"); + + b.Property("Theme") + .HasColumnType("nvarchar(max)"); + + b.Property("TimeZone") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ZipCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Appointment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualEndTime") + .HasColumnType("datetime2"); + + b.Property("ActualStartTime") + .HasColumnType("datetime2"); + + b.Property("AppointmentNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AppointmentStatusId") + .HasColumnType("int"); + + b.Property("AppointmentTypeId") + .HasColumnType("int"); + + b.Property("AssignedUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsAllDay") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsReminderEnabled") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Location") + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("ReminderMinutesBefore") + .HasColumnType("int"); + + b.Property("ScheduledEndTime") + .HasColumnType("datetime2"); + + b.Property("ScheduledStartTime") + .HasColumnType("datetime2"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AppointmentStatusId"); + + b.HasIndex("AppointmentTypeId"); + + b.HasIndex("AssignedUserId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("JobId"); + + b.HasIndex("ScheduledStartTime"); + + b.HasIndex("CompanyId", "AppointmentStatusId") + .HasDatabaseName("IX_Appointments_CompanyId_AppointmentStatusId"); + + b.HasIndex("CompanyId", "ScheduledStartTime") + .HasDatabaseName("IX_Appointments_CompanyId_ScheduledStartTime"); + + b.ToTable("Appointments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AppointmentStatusLookup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ColorClass") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IconClass") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsSystemDefined") + .HasColumnType("bit"); + + b.Property("IsTerminalStatus") + .HasColumnType("bit"); + + b.Property("StatusCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AppointmentStatusLookups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AppointmentTypeLookup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ColorClass") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IconClass") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsSystemDefined") + .HasColumnType("bit"); + + b.Property("RequiresJobLink") + .HasColumnType("bit"); + + b.Property("TypeCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("AppointmentTypeLookups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .HasColumnType("nvarchar(max)"); + + b.Property("EntityDescription") + .HasColumnType("nvarchar(max)"); + + b.Property("EntityId") + .HasColumnType("nvarchar(450)"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("NewValues") + .HasColumnType("nvarchar(max)"); + + b.Property("OldValues") + .HasColumnType("nvarchar(max)"); + + b.Property("Timestamp") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("nvarchar(max)"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId", "Timestamp"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BannedIp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannedAt") + .HasColumnType("datetime2"); + + b.Property("BannedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Reason") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("BannedIps"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Bill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("APAccountId") + .HasColumnType("int"); + + b.Property("AmountPaid") + .HasColumnType("decimal(18,2)"); + + b.Property("BillDate") + .HasColumnType("datetime2"); + + b.Property("BillNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Memo") + .HasColumnType("nvarchar(max)"); + + b.Property("ReceiptFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("Terms") + .HasColumnType("nvarchar(max)"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("VendorInvoiceNumber") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("APAccountId"); + + b.HasIndex("DueDate"); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.HasIndex("CompanyId", "Status"); + + b.ToTable("Bills"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BillLineItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("BillId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("BillId"); + + b.HasIndex("JobId"); + + b.ToTable("BillLineItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BillPayment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("BankAccountId") + .HasColumnType("int"); + + b.Property("BillId") + .HasColumnType("int"); + + b.Property("CheckNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Memo") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentDate") + .HasColumnType("datetime2"); + + b.Property("PaymentMethod") + .HasColumnType("int"); + + b.Property("PaymentNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BankAccountId"); + + b.HasIndex("BillId"); + + b.HasIndex("VendorId"); + + b.ToTable("BillPayments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BugReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("ResolutionNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("ResolvedAt") + .HasColumnType("datetime2"); + + b.Property("ResolvedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubmittedByUserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SubmittedByUserName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("BugReports"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BugReportAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BlobPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BugReportId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BugReportId"); + + b.ToTable("BugReportAttachments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CatalogCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsMerchandise") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ParentCategoryId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("ParentCategoryId"); + + b.ToTable("CatalogCategories"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CatalogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApproximateArea") + .HasColumnType("decimal(18,2)"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("CogsAccountId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultEstimatedMinutes") + .HasColumnType("int"); + + b.Property("DefaultPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("DefaultRequiresMasking") + .HasColumnType("bit"); + + b.Property("DefaultRequiresSandblasting") + .HasColumnType("bit"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("ImagePath") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsMerchandise") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RevenueAccountId") + .HasColumnType("int"); + + b.Property("SKU") + .HasColumnType("nvarchar(max)"); + + b.Property("ThumbnailPath") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("CogsAccountId"); + + b.HasIndex("CompanyId"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("RevenueAccountId"); + + b.ToTable("CatalogItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CatalogPriceCheckReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ItemsChecked") + .HasColumnType("int"); + + b.Property("OperatingCostsSummary") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RunAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CatalogPriceCheckReports"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountingOverride") + .HasColumnType("bit"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("AiCatalogPriceCheckEnabled") + .HasColumnType("bit"); + + b.Property("AiInventoryAssistEnabled") + .HasColumnType("bit"); + + b.Property("AiPhotoQuotesEnabled") + .HasColumnType("bit"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyCode") + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsAnnualBilling") + .HasColumnType("bit"); + + b.Property("IsComped") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LogoContentType") + .HasColumnType("nvarchar(max)"); + + b.Property("LogoData") + .HasColumnType("varbinary(max)"); + + b.Property("LogoFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("MarketingEmailOptOut") + .HasColumnType("bit"); + + b.Property("MarketingUnsubscribeToken") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("MaxActiveJobsOverride") + .HasColumnType("int"); + + b.Property("MaxAiPhotoQuotesPerMonthOverride") + .HasColumnType("int"); + + b.Property("MaxCatalogItemsOverride") + .HasColumnType("int"); + + b.Property("MaxCustomersOverride") + .HasColumnType("int"); + + b.Property("MaxJobPhotosOverride") + .HasColumnType("int"); + + b.Property("MaxQuotePhotosOverride") + .HasColumnType("int"); + + b.Property("MaxQuotesOverride") + .HasColumnType("int"); + + b.Property("MaxUsersOverride") + .HasColumnType("int"); + + b.Property("OnlinePaymentSurchargeType") + .HasColumnType("int"); + + b.Property("OnlinePaymentSurchargeValue") + .HasColumnType("decimal(18,2)"); + + b.Property("OnlinePaymentsOverride") + .HasColumnType("bit"); + + b.Property("OnlineSurchargeAcknowledged") + .HasColumnType("bit"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("PrimaryContactEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PrimaryContactName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SmsDisabledByAdmin") + .HasColumnType("bit"); + + b.Property("SmsEnabled") + .HasColumnType("bit"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("StripeAccountId") + .HasColumnType("nvarchar(max)"); + + b.Property("StripeConnectStatus") + .HasColumnType("int"); + + b.Property("StripeCustomerId") + .HasColumnType("nvarchar(max)"); + + b.Property("StripeSubscriptionId") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionEndDate") + .HasColumnType("datetime2"); + + b.Property("SubscriptionNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionPlan") + .HasColumnType("int"); + + b.Property("SubscriptionStartDate") + .HasColumnType("datetime2"); + + b.Property("SubscriptionStatus") + .HasColumnType("int"); + + b.Property("TimeZone") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ZipCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyCode") + .IsUnique() + .HasFilter("[CompanyCode] IS NOT NULL"); + + b.ToTable("Companies"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanyBlastSetup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BlastNozzleSize") + .HasColumnType("int"); + + b.Property("BlastRateSqFtPerHourOverride") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompressorCfm") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDefault") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PrimarySubstrate") + .HasColumnType("int"); + + b.Property("SetupType") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("CompanyBlastSetups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanyOperatingCosts", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdditionalCoatLaborPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("AiContextProfile") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("BlastNozzleSize") + .HasColumnType("int"); + + b.Property("BlastRateSqFtPerHourOverride") + .HasColumnType("decimal(18,2)"); + + b.Property("BlastSetupType") + .HasColumnType("int"); + + b.Property("CoatingBoothCostPerHour") + .HasColumnType("decimal(18,2)"); + + b.Property("CoatingGunType") + .HasColumnType("int"); + + b.Property("CoatingRateSqFtPerHourOverride") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ComplexityComplexPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("ComplexityExtremePercent") + .HasColumnType("decimal(18,2)"); + + b.Property("ComplexityModeratePercent") + .HasColumnType("decimal(18,2)"); + + b.Property("ComplexitySimplePercent") + .HasColumnType("decimal(18,2)"); + + b.Property("CompressorCfm") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultOvenCycleMinutes") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("GeneralMarkupPercentage") + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MonthlyBillableHours") + .HasColumnType("int"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("MonthlyUtilities") + .HasColumnType("decimal(18,2)"); + + b.Property("OvenOperatingCostPerHour") + .HasColumnType("decimal(18,2)"); + + b.Property("PowderCoatingCostPerSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("PricingMode") + .HasColumnType("int"); + + b.Property("PrimaryBlastSubstrate") + .HasColumnType("int"); + + b.Property("RushChargeFixedAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("RushChargePercentage") + .HasColumnType("decimal(18,2)"); + + b.Property("RushChargeType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SandblasterCostPerHour") + .HasColumnType("decimal(18,2)"); + + b.Property("ShopCapabilityTier") + .HasColumnType("int"); + + b.Property("ShopMinimumCharge") + .HasColumnType("decimal(18,2)"); + + b.Property("ShopSuppliesRate") + .HasColumnType("decimal(18,2)"); + + b.Property("StandardLaborRate") + .HasColumnType("decimal(18,2)"); + + b.Property("TargetMarginPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId") + .IsUnique(); + + b.ToTable("CompanyOperatingCosts"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanyPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AllowCustomerApproval") + .HasColumnType("bit"); + + b.Property("AutoArchiveJobsDays") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultCurrency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultDateFormat") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultJobPriority") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultPaymentTerms") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultQuoteValidityDays") + .HasColumnType("int"); + + b.Property("DefaultTimeFormat") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultTurnaroundDays") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedRecordRetentionDays") + .HasColumnType("int"); + + b.Property("DueDateWarningDays") + .HasColumnType("int"); + + b.Property("EmailFromAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("EmailFromName") + .HasColumnType("nvarchar(max)"); + + b.Property("EmailNotificationsEnabled") + .HasColumnType("bit"); + + b.Property("FirstInvoiceCreatedAt") + .HasColumnType("datetime2"); + + b.Property("FirstJobCreatedAt") + .HasColumnType("datetime2"); + + b.Property("FirstQuoteCreatedAt") + .HasColumnType("datetime2"); + + b.Property("FirstWorkflowCompleted") + .HasColumnType("bit"); + + b.Property("FirstWorkflowCompletedAt") + .HasColumnType("datetime2"); + + b.Property("GuidedActivationDismissedAt") + .HasColumnType("datetime2"); + + b.Property("InAccentColor") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InDefaultTerms") + .HasColumnType("nvarchar(max)"); + + b.Property("InFooterNote") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceNumberPrefix") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobNumberPrefix") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JobRetentionYears") + .HasColumnType("int"); + + b.Property("LogRetentionDays") + .HasColumnType("int"); + + b.Property("MaintenanceAlertDays") + .HasColumnType("int"); + + b.Property("MigratingFromQuickBooks") + .HasColumnType("bit"); + + b.Property("NotifyOnJobStatusChange") + .HasColumnType("bit"); + + b.Property("NotifyOnNewJob") + .HasColumnType("bit"); + + b.Property("NotifyOnNewQuote") + .HasColumnType("bit"); + + b.Property("NotifyOnPaymentReceived") + .HasColumnType("bit"); + + b.Property("NotifyOnQuoteApproval") + .HasColumnType("bit"); + + b.Property("OnboardingPath") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentReminderDays") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentRemindersEnabled") + .HasColumnType("bit"); + + b.Property("QbMigrationStateJson") + .HasColumnType("nvarchar(max)"); + + b.Property("QtAccentColor") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("QtDefaultTerms") + .HasColumnType("nvarchar(max)"); + + b.Property("QtFooterNote") + .HasColumnType("nvarchar(max)"); + + b.Property("QuoteExpiryWarningDays") + .HasColumnType("int"); + + b.Property("QuoteNumberPrefix") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("QuoteRetentionYears") + .HasColumnType("int"); + + b.Property("RequireCustomerPO") + .HasColumnType("bit"); + + b.Property("SetupWizardCompleted") + .HasColumnType("bit"); + + b.Property("SetupWizardCompletedAt") + .HasColumnType("datetime2"); + + b.Property("SetupWizardCompletedByName") + .HasColumnType("nvarchar(max)"); + + b.Property("SetupWizardCompletedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("SetupWizardDoneSteps") + .HasColumnType("nvarchar(max)"); + + b.Property("SetupWizardSkippedSteps") + .HasColumnType("nvarchar(max)"); + + b.Property("SetupWizardStarted") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UseMetricSystem") + .HasColumnType("bit"); + + b.Property("WoAccentColor") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WoTerms") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId") + .IsUnique(); + + b.ToTable("CompanyPreferences"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanySmsAgreement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AgreedAt") + .HasColumnType("datetime2"); + + b.Property("AgreedByUserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AgreedByUserName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("TermsVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UserAgent") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CompanySmsAgreements"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ContactSubmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("Category") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReadAt") + .HasColumnType("datetime2"); + + b.Property("ReadByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("ReadByUserName") + .HasColumnType("nvarchar(max)"); + + b.Property("SenderEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SenderName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ContactSubmissions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CreditMemo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("AmountApplied") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IssueDate") + .HasColumnType("datetime2"); + + b.Property("IssuedById") + .HasColumnType("nvarchar(450)"); + + b.Property("MemoNumber") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OriginalInvoiceId") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReworkRecordId") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("IssuedById"); + + b.HasIndex("OriginalInvoiceId"); + + b.HasIndex("ReworkRecordId"); + + b.HasIndex("CompanyId", "MemoNumber") + .IsUnique() + .HasDatabaseName("IX_CreditMemos_CompanyId_MemoNumber"); + + b.ToTable("CreditMemos"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CreditMemoApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AmountApplied") + .HasColumnType("decimal(18,2)"); + + b.Property("AppliedById") + .HasColumnType("nvarchar(450)"); + + b.Property("AppliedDate") + .HasColumnType("datetime2"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreditMemoId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AppliedById"); + + b.HasIndex("CreditMemoId"); + + b.HasIndex("InvoiceId"); + + b.ToTable("CreditMemoApplications"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("BillingEmail") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .HasColumnType("nvarchar(450)"); + + b.Property("ContactFirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactLastName") + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreditBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("CreditLimit") + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasColumnType("nvarchar(450)"); + + b.Property("GeneralNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsCommercial") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsTaxExempt") + .HasColumnType("bit"); + + b.Property("LastContactDate") + .HasColumnType("datetime2"); + + b.Property("MobilePhone") + .HasColumnType("nvarchar(max)"); + + b.Property("NotifyByEmail") + .HasColumnType("bit"); + + b.Property("NotifyBySms") + .HasColumnType("bit"); + + b.Property("PaymentTerms") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("PricingTierId") + .HasColumnType("int"); + + b.Property("SmsConsentMethod") + .HasColumnType("nvarchar(max)"); + + b.Property("SmsConsentedAt") + .HasColumnType("datetime2"); + + b.Property("SmsOptedOutAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("TaxExemptCertificateContentType") + .HasColumnType("nvarchar(max)"); + + b.Property("TaxExemptCertificateData") + .HasColumnType("varbinary(max)"); + + b.Property("TaxExemptCertificateFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("TaxId") + .HasColumnType("nvarchar(max)"); + + b.Property("UnsubscribeToken") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)") + .HasDefaultValueSql("REPLACE(NEWID(),'-','')"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ZipCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CompanyName"); + + b.HasIndex("PricingTierId"); + + b.HasIndex("UnsubscribeToken") + .IsUnique() + .HasDatabaseName("IX_Customers_UnsubscribeToken"); + + b.HasIndex("CompanyId", "Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CustomerNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsImportant") + .HasColumnType("bit"); + + b.Property("Note") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId", "CreatedAt") + .HasDatabaseName("IX_CustomerNotes_CustomerId_CreatedAt"); + + b.ToTable("CustomerNotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.DashboardTip", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("TipText") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DashboardTips"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Deposit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("AppliedDate") + .HasColumnType("datetime2"); + + b.Property("AppliedToInvoiceId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentMethod") + .HasColumnType("int"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("ReceiptNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReceivedDate") + .HasColumnType("datetime2"); + + b.Property("RecordedById") + .HasColumnType("nvarchar(450)"); + + b.Property("Reference") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AppliedToInvoiceId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("JobId"); + + b.HasIndex("QuoteId"); + + b.HasIndex("RecordedById"); + + b.ToTable("Deposits"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Equipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastMaintenanceDate") + .HasColumnType("datetime2"); + + b.Property("Location") + .HasColumnType("nvarchar(max)"); + + b.Property("ManualContentType") + .HasColumnType("nvarchar(max)"); + + b.Property("ManualFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("ManualFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("ManualFileSize") + .HasColumnType("bigint"); + + b.Property("ManualUploadedDate") + .HasColumnType("datetime2"); + + b.Property("Manufacturer") + .HasColumnType("nvarchar(max)"); + + b.Property("MaxLoadSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("NextScheduledMaintenance") + .HasColumnType("datetime2"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OvenCycleMinutes") + .HasColumnType("int"); + + b.Property("PurchaseDate") + .HasColumnType("datetime2"); + + b.Property("PurchasePrice") + .HasColumnType("decimal(18,2)"); + + b.Property("RecommendedMaintenanceIntervalDays") + .HasColumnType("int"); + + b.Property("SerialNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("WarrantyExpiration") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CompanyId", "Status") + .HasDatabaseName("IX_Equipment_CompanyId_Status"); + + b.ToTable("Equipment"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Expense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpenseAccountId") + .HasColumnType("int"); + + b.Property("ExpenseNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Memo") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentAccountId") + .HasColumnType("int"); + + b.Property("PaymentMethod") + .HasColumnType("int"); + + b.Property("ReceiptFilePath") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ExpenseAccountId"); + + b.HasIndex("JobId"); + + b.HasIndex("PaymentAccountId"); + + b.HasIndex("VendorId"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.GiftCertificate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CertificateCode") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IssueDate") + .HasColumnType("datetime2"); + + b.Property("IssuedById") + .HasColumnType("nvarchar(450)"); + + b.Property("IssuedReason") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OriginalAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("PurchasePrice") + .HasColumnType("decimal(18,2)"); + + b.Property("PurchasingCustomerId") + .HasColumnType("int"); + + b.Property("RecipientCustomerId") + .HasColumnType("int"); + + b.Property("RecipientEmail") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipientName") + .HasColumnType("nvarchar(max)"); + + b.Property("RedeemedAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("SourceInvoiceItemId") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("IssuedById"); + + b.HasIndex("PurchasingCustomerId"); + + b.HasIndex("RecipientCustomerId"); + + b.HasIndex("CompanyId", "CertificateCode") + .IsUnique(); + + b.ToTable("GiftCertificates"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.GiftCertificateRedemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AmountRedeemed") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("GiftCertificateId") + .HasColumnType("int"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("RedeemedById") + .HasColumnType("nvarchar(450)"); + + b.Property("RedeemedDate") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("GiftCertificateId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("RedeemedById"); + + b.ToTable("GiftCertificateRedemptions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InAppNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Link") + .HasColumnType("nvarchar(max)"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NotificationType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("ReadAt") + .HasColumnType("datetime2"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("QuoteId"); + + b.ToTable("InAppNotifications"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryCategoryLookup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryCode") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsCoating") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsSystemDefined") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CompanyId", "CategoryCode") + .IsUnique(); + + b.ToTable("InventoryCategoryLookups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AverageCost") + .HasColumnType("decimal(18,2)"); + + b.Property("Category") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CogsAccountId") + .HasColumnType("int"); + + b.Property("ColorCode") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorFamilies") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CoverageSqFtPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CureTemperatureF") + .HasColumnType("decimal(18,2)"); + + b.Property("CureTimeMinutes") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscontinuedDate") + .HasColumnType("datetime2"); + + b.Property("Finish") + .HasColumnType("nvarchar(max)"); + + b.Property("HasSamplePanel") + .HasColumnType("bit"); + + b.Property("ImageUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryAccountId") + .HasColumnType("int"); + + b.Property("InventoryCategoryId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsIncoming") + .HasColumnType("bit"); + + b.Property("LastPurchaseDate") + .HasColumnType("datetime2"); + + b.Property("LastPurchasePrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Location") + .HasColumnType("nvarchar(max)"); + + b.Property("Manufacturer") + .HasColumnType("nvarchar(max)"); + + b.Property("ManufacturerPartNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("MaximumStock") + .HasColumnType("decimal(18,2)"); + + b.Property("MinimumStock") + .HasColumnType("decimal(18,2)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PrimaryVendorId") + .HasColumnType("int"); + + b.Property("QuantityOnHand") + .HasColumnType("decimal(18,2)"); + + b.Property("ReorderPoint") + .HasColumnType("decimal(18,2)"); + + b.Property("ReorderQuantity") + .HasColumnType("decimal(18,2)"); + + b.Property("RequiresClearCoat") + .HasColumnType("bit"); + + b.Property("SKU") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SdsUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("SpecPageUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("SpecificGravity") + .HasColumnType("decimal(18,2)"); + + b.Property("TdsUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("TransferEfficiency") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitCost") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitOfMeasure") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorPartNumber") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CogsAccountId"); + + b.HasIndex("CompanyId"); + + b.HasIndex("InventoryAccountId"); + + b.HasIndex("InventoryCategoryId"); + + b.HasIndex("IsActive"); + + b.HasIndex("PrimaryVendorId"); + + b.HasIndex("CompanyId", "IsActive"); + + b.HasIndex("CompanyId", "SKU") + .IsUnique() + .HasDatabaseName("IX_InventoryItems_CompanyId_SKU"); + + b.HasIndex("CompanyId", "QuantityOnHand", "ReorderPoint") + .HasDatabaseName("IX_InventoryItems_CompanyId_Quantity_Reorder"); + + b.ToTable("InventoryItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BalanceAfter") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PurchaseOrderId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("Reference") + .HasColumnType("nvarchar(max)"); + + b.Property("TotalCost") + .HasColumnType("decimal(18,2)"); + + b.Property("TransactionDate") + .HasColumnType("datetime2"); + + b.Property("TransactionType") + .HasColumnType("int"); + + b.Property("UnitCost") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("JobId"); + + b.HasIndex("PurchaseOrderId"); + + b.HasIndex("TransactionType", "TransactionDate"); + + b.ToTable("InventoryTransactions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Invoice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AmountPaid") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreditApplied") + .HasColumnType("decimal(18,2)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("CustomerPO") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("ExternalReference") + .HasColumnType("nvarchar(450)"); + + b.Property("GiftCertificateRedeemed") + .HasColumnType("decimal(18,2)"); + + b.Property("InternalNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceDate") + .HasColumnType("datetime2"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OnlineAmountPaid") + .HasColumnType("decimal(18,2)"); + + b.Property("OnlinePaymentStatus") + .HasColumnType("int"); + + b.Property("OnlineSurchargeCollected") + .HasColumnType("decimal(18,2)"); + + b.Property("PaidDate") + .HasColumnType("datetime2"); + + b.Property("PaymentLinkExpiresAt") + .HasColumnType("datetime2"); + + b.Property("PaymentLinkToken") + .HasColumnType("nvarchar(max)"); + + b.Property("PreparedById") + .HasColumnType("nvarchar(450)"); + + b.Property("SalesTaxAccountId") + .HasColumnType("int"); + + b.Property("SentDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StripePaymentIntentId") + .HasColumnType("nvarchar(max)"); + + b.Property("SubTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("Terms") + .HasColumnType("nvarchar(max)"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DueDate"); + + b.HasIndex("InvoiceDate"); + + b.HasIndex("JobId") + .IsUnique() + .HasFilter("[JobId] IS NOT NULL"); + + b.HasIndex("PreparedById"); + + b.HasIndex("SalesTaxAccountId"); + + b.HasIndex("Status"); + + b.HasIndex("CompanyId", "CustomerId") + .HasDatabaseName("IX_Invoices_CompanyId_CustomerId"); + + b.HasIndex("CompanyId", "DueDate") + .HasDatabaseName("IX_Invoices_CompanyId_DueDate"); + + b.HasIndex("CompanyId", "ExternalReference") + .HasDatabaseName("IX_Invoices_CompanyId_ExternalReference"); + + b.HasIndex("CompanyId", "InvoiceNumber") + .IsUnique() + .HasDatabaseName("IX_Invoices_CompanyId_InvoiceNumber"); + + b.HasIndex("CompanyId", "IsDeleted"); + + b.HasIndex("CompanyId", "JobId") + .IsUnique() + .HasDatabaseName("IX_Invoices_CompanyId_JobId") + .HasFilter("[JobId] IS NOT NULL"); + + b.HasIndex("CompanyId", "Status") + .HasDatabaseName("IX_Invoices_CompanyId_Status"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InvoiceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CatalogItemId") + .HasColumnType("int"); + + b.Property("ColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("GcExpiryDate") + .HasColumnType("datetime2"); + + b.Property("GcRecipientEmail") + .HasColumnType("nvarchar(max)"); + + b.Property("GcRecipientName") + .HasColumnType("nvarchar(max)"); + + b.Property("GeneratedGiftCertificateId") + .HasColumnType("int"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsGiftCertificate") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("RevenueAccountId") + .HasColumnType("int"); + + b.Property("SourceJobItemId") + .HasColumnType("int"); + + b.Property("TotalPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CatalogItemId"); + + b.HasIndex("CompanyId"); + + b.HasIndex("GeneratedGiftCertificateId"); + + b.HasIndex("InvoiceId") + .HasDatabaseName("IX_InvoiceItems_InvoiceId"); + + b.HasIndex("RevenueAccountId"); + + b.HasIndex("SourceJobItemId"); + + b.ToTable("InvoiceItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualTimeSpentHours") + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompletedDate") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("CustomerPO") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountReason") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountType") + .HasColumnType("int"); + + b.Property("DiscountValue") + .HasColumnType("decimal(18,2)"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("FinalPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("IntakeCheckedByUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("IntakeConditionNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("IntakeDate") + .HasColumnType("datetime2"); + + b.Property("IntakePartCount") + .HasColumnType("int"); + + b.Property("InternalNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("IsCustomerApproved") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsReworkJob") + .HasColumnType("bit"); + + b.Property("IsRushJob") + .HasColumnType("bit"); + + b.Property("JobNumber") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("JobPriorityId") + .HasColumnType("int"); + + b.Property("JobStatusId") + .HasColumnType("int"); + + b.Property("OriginalJobId") + .HasColumnType("int"); + + b.Property("OvenCostId") + .HasColumnType("int"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("QuoteSnapshotUpdatedAt") + .HasColumnType("datetime2"); + + b.Property("QuotedPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("RequiresCustomerApproval") + .HasColumnType("bit"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("ShopAccessCode") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("ShopSuppliesAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("ShopSuppliesPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("ShopWorkerId") + .HasColumnType("int"); + + b.Property("SpecialInstructions") + .HasColumnType("nvarchar(max)"); + + b.Property("StartedDate") + .HasColumnType("datetime2"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AssignedUserId"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DueDate"); + + b.HasIndex("IntakeCheckedByUserId"); + + b.HasIndex("JobPriorityId"); + + b.HasIndex("JobStatusId"); + + b.HasIndex("OriginalJobId"); + + b.HasIndex("OvenCostId"); + + b.HasIndex("QuoteId") + .IsUnique() + .HasFilter("[QuoteId] IS NOT NULL"); + + b.HasIndex("ScheduledDate"); + + b.HasIndex("ShopWorkerId"); + + b.HasIndex("CompanyId", "CustomerId") + .HasDatabaseName("IX_Jobs_CompanyId_CustomerId"); + + b.HasIndex("CompanyId", "DueDate") + .HasDatabaseName("IX_Jobs_CompanyId_DueDate"); + + b.HasIndex("CompanyId", "IsDeleted"); + + b.HasIndex("CompanyId", "JobNumber") + .IsUnique() + .HasDatabaseName("IX_Jobs_CompanyId_JobNumber"); + + b.HasIndex("CompanyId", "JobPriorityId") + .HasDatabaseName("IX_Jobs_CompanyId_JobPriorityId"); + + b.HasIndex("CompanyId", "JobStatusId") + .HasDatabaseName("IX_Jobs_CompanyId_JobStatusId"); + + b.HasIndex("CompanyId", "ScheduledDate") + .HasDatabaseName("IX_Jobs_CompanyId_ScheduledDate"); + + b.HasIndex("CompanyId", "ShopAccessCode") + .IsUnique() + .HasDatabaseName("IX_Jobs_CompanyId_ShopAccessCode"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobChangeHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChangeDescription") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangedAt") + .HasColumnType("datetime2"); + + b.Property("ChangedByUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("FieldName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("NewValue") + .HasColumnType("nvarchar(max)"); + + b.Property("OldValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ChangedByUserId"); + + b.HasIndex("CompanyId"); + + b.HasIndex("JobId"); + + b.ToTable("JobChangeHistories"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobDailyPriority", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.ToTable("JobDailyPriorities"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AiPredictionId") + .HasColumnType("int"); + + b.Property("AiTags") + .HasColumnType("nvarchar(max)"); + + b.Property("CatalogItemId") + .HasColumnType("int"); + + b.Property("ColorCode") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("Complexity") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("Finish") + .HasColumnType("nvarchar(max)"); + + b.Property("IncludePrepCost") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsGenericItem") + .HasColumnType("bit"); + + b.Property("IsLaborItem") + .HasColumnType("bit"); + + b.Property("IsSalesItem") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("LaborCost") + .HasColumnType("decimal(18,2)"); + + b.Property("ManualUnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderCostOverride") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("RequiresMasking") + .HasColumnType("bit"); + + b.Property("RequiresSandblasting") + .HasColumnType("bit"); + + b.Property("Sku") + .HasColumnType("nvarchar(max)"); + + b.Property("SurfaceArea") + .HasColumnType("decimal(18,2)"); + + b.Property("SurfaceAreaSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AiPredictionId"); + + b.HasIndex("CatalogItemId"); + + b.HasIndex("JobId") + .HasDatabaseName("IX_JobItems_JobId"); + + b.HasIndex("JobId", "IsDeleted") + .HasDatabaseName("IX_JobItems_JobId_IsDeleted"); + + b.ToTable("JobItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItemCoat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualPowderUsedLbs") + .HasColumnType("decimal(18,2)"); + + b.Property("CoatName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ColorCode") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CoverageSqFtPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Finish") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobItemId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderCostPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("PowderOrdered") + .HasColumnType("bit"); + + b.Property("PowderOrderedAt") + .HasColumnType("datetime2"); + + b.Property("PowderOrderedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderReceived") + .HasColumnType("bit"); + + b.Property("PowderReceivedAt") + .HasColumnType("datetime2"); + + b.Property("PowderReceivedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderReceivedLbs") + .HasColumnType("decimal(18,2)"); + + b.Property("PowderToOrder") + .HasColumnType("decimal(18,2)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("TransferEfficiency") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("JobItemId"); + + b.HasIndex("VendorId"); + + b.ToTable("JobItemCoats"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItemPrepService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BlastSetupId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobItemId") + .HasColumnType("int"); + + b.Property("PrepServiceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BlastSetupId"); + + b.HasIndex("CompanyId") + .HasDatabaseName("IX_JobItemPrepServices_CompanyId"); + + b.HasIndex("JobItemId") + .HasDatabaseName("IX_JobItemPrepServices_JobItemId"); + + b.HasIndex("PrepServiceId") + .HasDatabaseName("IX_JobItemPrepServices_PrepServiceId"); + + b.ToTable("JobItemPrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsImportant") + .HasColumnType("bit"); + + b.Property("IsInternal") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Note") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId", "CreatedAt") + .HasDatabaseName("IX_JobNotes_JobId_CreatedAt"); + + b.ToTable("JobNotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Caption") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsAiAnalysisPhoto") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("PhotoType") + .HasColumnType("int"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UploadedById") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("UploadedDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("UploadedById"); + + b.HasIndex("JobId", "IsDeleted", "DisplayOrder") + .HasDatabaseName("IX_JobPhotos_JobId_IsDeleted_DisplayOrder"); + + b.ToTable("JobPhotos"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPrepService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("PrepServiceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("PrepServiceId"); + + b.ToTable("JobPrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPriorityLookup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ColorClass") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IconClass") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsSystemDefined") + .HasColumnType("bit"); + + b.Property("PriorityCode") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CompanyId", "PriorityCode") + .IsUnique(); + + b.ToTable("JobPriorityLookups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobStatusHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChangedDate") + .HasColumnType("datetime2"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("FromStatusId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("ToStatusId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("FromStatusId"); + + b.HasIndex("JobId"); + + b.HasIndex("ToStatusId"); + + b.ToTable("JobStatusHistory"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobStatusLookup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ColorClass") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IconClass") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsSystemDefined") + .HasColumnType("bit"); + + b.Property("IsTerminalStatus") + .HasColumnType("bit"); + + b.Property("IsWorkInProgressStatus") + .HasColumnType("bit"); + + b.Property("StatusCode") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkflowCategory") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CompanyId", "StatusCode") + .IsUnique(); + + b.ToTable("JobStatusLookups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SpecialInstructions") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UsageCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("JobTemplates"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CatalogItemId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("Complexity") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("IncludePrepCost") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsGenericItem") + .HasColumnType("bit"); + + b.Property("IsLaborItem") + .HasColumnType("bit"); + + b.Property("IsSalesItem") + .HasColumnType("bit"); + + b.Property("JobTemplateId") + .HasColumnType("int"); + + b.Property("ManualUnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("RequiresMasking") + .HasColumnType("bit"); + + b.Property("RequiresSandblasting") + .HasColumnType("bit"); + + b.Property("Sku") + .HasColumnType("nvarchar(max)"); + + b.Property("SurfaceAreaSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CatalogItemId"); + + b.HasIndex("JobTemplateId"); + + b.ToTable("JobTemplateItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItemCoat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CoatName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ColorCode") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CoverageSqFtPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Finish") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobTemplateItemId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderCostPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("TransferEfficiency") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("JobTemplateItemId"); + + b.HasIndex("VendorId"); + + b.ToTable("JobTemplateItemCoats"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItemPrepService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobTemplateItemId") + .HasColumnType("int"); + + b.Property("PrepServiceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobTemplateItemId"); + + b.HasIndex("PrepServiceId"); + + b.ToTable("JobTemplateItemPrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTimeEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("HoursWorked") + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("ShopWorkerId") + .HasColumnType("int"); + + b.Property("Stage") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UserDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("ShopWorkerId"); + + b.ToTable("JobTimeEntries"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.MaintenanceRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AssignedUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompletedDate") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DowntimeHours") + .HasColumnType("decimal(18,2)"); + + b.Property("EquipmentId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsRecurring") + .HasColumnType("bit"); + + b.Property("LaborCost") + .HasColumnType("decimal(18,2)"); + + b.Property("MaintenanceType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PartsCost") + .HasColumnType("decimal(18,2)"); + + b.Property("PartsReplaced") + .HasColumnType("nvarchar(max)"); + + b.Property("PerformedById") + .HasColumnType("nvarchar(450)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("RecurrenceEndDate") + .HasColumnType("datetime2"); + + b.Property("RecurrenceFrequency") + .HasColumnType("int"); + + b.Property("RecurrenceGroupId") + .HasColumnType("nvarchar(max)"); + + b.Property("RecurrenceParentId") + .HasColumnType("int"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("ShopWorkerId") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TechnicianNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("TotalCost") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkPerformed") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AssignedUserId"); + + b.HasIndex("EquipmentId"); + + b.HasIndex("PerformedById"); + + b.HasIndex("RecurrenceParentId"); + + b.HasIndex("ScheduledDate"); + + b.HasIndex("ShopWorkerId"); + + b.HasIndex("Status"); + + b.HasIndex("CompanyId", "ScheduledDate") + .HasDatabaseName("IX_MaintenanceRecords_CompanyId_ScheduledDate"); + + b.HasIndex("CompanyId", "Status") + .HasDatabaseName("IX_MaintenanceRecords_CompanyId_Status"); + + b.ToTable("MaintenanceRecords"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ManufacturerLookupPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Domain") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ManufacturerName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductUrlTemplate") + .HasColumnType("nvarchar(max)"); + + b.Property("SlugTransform") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ManufacturerLookupPatterns"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.NotificationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Channel") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ErrorMessage") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("Recipient") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RecipientName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SentAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Subject") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("JobId"); + + b.HasIndex("QuoteId"); + + b.HasIndex("CompanyId", "SentAt") + .HasDatabaseName("IX_NotificationLogs_CompanyId_SentAt"); + + b.HasIndex("CompanyId", "Status") + .HasDatabaseName("IX_NotificationLogs_CompanyId_Status"); + + b.ToTable("NotificationLogs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.NotificationTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Channel") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("Subject") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId", "NotificationType", "Channel") + .IsUnique() + .HasDatabaseName("IX_NotificationTemplates_Company_Type_Channel"); + + b.ToTable("NotificationTemplates"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualEndTime") + .HasColumnType("datetime2"); + + b.Property("ActualStartTime") + .HasColumnType("datetime2"); + + b.Property("AiReasoningJson") + .HasColumnType("nvarchar(max)"); + + b.Property("AiSuggested") + .HasColumnType("bit"); + + b.Property("BatchNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CureTemperatureF") + .HasColumnType("decimal(18,2)"); + + b.Property("CycleMinutes") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EquipmentId") + .HasColumnType("int"); + + b.Property("EstimatedEndTime") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OvenCostId") + .HasColumnType("int"); + + b.Property("PrimaryColorCode") + .HasColumnType("nvarchar(max)"); + + b.Property("PrimaryColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("ScheduledDate") + .HasColumnType("datetime2"); + + b.Property("ScheduledStartTime") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalSurfaceAreaSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("EquipmentId"); + + b.HasIndex("OvenCostId"); + + b.HasIndex("ScheduledDate", "Status"); + + b.ToTable("OvenBatches"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenBatchItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CoatPassNumber") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("JobItemCoatId") + .HasColumnType("int"); + + b.Property("JobItemId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OvenBatchId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SurfaceAreaContribution") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("JobItemCoatId"); + + b.HasIndex("JobItemId"); + + b.HasIndex("OvenBatchId"); + + b.ToTable("OvenBatchItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenCost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CostPerHour") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultCycleMinutes") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MaxLoadSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("OvenCosts"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DepositAccountId") + .HasColumnType("int"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentDate") + .HasColumnType("datetime2"); + + b.Property("PaymentMethod") + .HasColumnType("int"); + + b.Property("RecordedById") + .HasColumnType("nvarchar(450)"); + + b.Property("Reference") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DepositAccountId"); + + b.HasIndex("InvoiceId") + .HasDatabaseName("IX_Payments_InvoiceId"); + + b.HasIndex("PaymentDate"); + + b.HasIndex("RecordedById"); + + b.HasIndex("CompanyId", "PaymentDate") + .HasDatabaseName("IX_Payments_CompanyId_PaymentDate"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PendingRegistrationSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsAnnual") + .HasColumnType("bit"); + + b.Property("IsCompleted") + .HasColumnType("bit"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Plan") + .HasColumnType("int"); + + b.Property("Token") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("PendingRegistrationSessions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("GroupName") + .HasColumnType("nvarchar(max)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Label") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PowderCatalogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApplicationGuideUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorFamilies") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CoverageSqFtPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CureTemperatureF") + .HasColumnType("decimal(18,2)"); + + b.Property("CureTimeMinutes") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Finish") + .HasColumnType("nvarchar(max)"); + + b.Property("ImageUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDiscontinued") + .HasColumnType("bit"); + + b.Property("IsUserContributed") + .HasColumnType("bit"); + + b.Property("LastSyncedAt") + .HasColumnType("datetime2"); + + b.Property("PriceTiersJson") + .HasColumnType("nvarchar(max)"); + + b.Property("ProductUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("RequiresClearCoat") + .HasColumnType("bit"); + + b.Property("SdsUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SpecificGravity") + .HasColumnType("decimal(18,2)"); + + b.Property("TdsUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("TransferEfficiency") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("VendorName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("ColorName") + .HasDatabaseName("IX_PowderCatalogItems_ColorName"); + + b.HasIndex("VendorName", "Sku") + .IsUnique() + .HasDatabaseName("IX_PowderCatalogItems_Vendor_Sku"); + + b.ToTable("PowderCatalogItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PowderUsageLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualLbsUsed") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EstimatedLbs") + .HasColumnType("decimal(18,2)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("InventoryTransactionId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("JobItemCoatId") + .HasColumnType("int"); + + b.Property("JobItemId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("RecordedAt") + .HasColumnType("datetime2"); + + b.Property("RecordedByUserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VarianceLbs") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("InventoryTransactionId"); + + b.HasIndex("JobId"); + + b.HasIndex("JobItemCoatId"); + + b.HasIndex("JobItemId"); + + b.ToTable("PowderUsageLogs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PrepService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("RequiresBlastSetup") + .HasColumnType("bit"); + + b.Property("ServiceName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("PrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PricingTier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("TierName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("PricingTiers"); + + b.HasData( + new + { + Id = 1, + CompanyId = 0, + CreatedAt = new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2249), + Description = "Standard pricing for regular customers", + DiscountPercent = 0m, + IsActive = true, + IsDeleted = false, + TierName = "Standard" + }, + new + { + Id = 2, + CompanyId = 0, + CreatedAt = new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2260), + Description = "5% discount for preferred customers", + DiscountPercent = 5m, + IsActive = true, + IsDeleted = false, + TierName = "Preferred" + }, + new + { + Id = 3, + CompanyId = 0, + CreatedAt = new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2261), + Description = "10% discount for premium customers", + DiscountPercent = 10m, + IsActive = true, + IsDeleted = false, + TierName = "Premium" + }); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PurchaseOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BillId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectedDeliveryDate") + .HasColumnType("datetime2"); + + b.Property("InternalNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OrderDate") + .HasColumnType("datetime2"); + + b.Property("PoNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReceivedDate") + .HasColumnType("datetime2"); + + b.Property("ShippingCost") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BillId"); + + b.HasIndex("VendorId"); + + b.ToTable("PurchaseOrders"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PurchaseOrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LineTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PurchaseOrderId") + .HasColumnType("int"); + + b.Property("QuantityOrdered") + .HasColumnType("decimal(18,2)"); + + b.Property("QuantityReceived") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitCost") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitOfMeasure") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("PurchaseOrderId"); + + b.ToTable("PurchaseOrderItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApprovalToken") + .HasColumnType("nvarchar(450)"); + + b.Property("ApprovalTokenExpiresAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalTokenUsedAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedDate") + .HasColumnType("datetime2"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ConvertedDate") + .HasColumnType("datetime2"); + + b.Property("ConvertedToJobId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("CustomerPO") + .HasColumnType("nvarchar(max)"); + + b.Property("DeclineReason") + .HasColumnType("nvarchar(max)"); + + b.Property("DeclinedByIp") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DepositAmountPaid") + .HasColumnType("decimal(18,2)"); + + b.Property("DepositPaymentIntentId") + .HasColumnType("nvarchar(max)"); + + b.Property("DepositPaymentLinkExpiresAt") + .HasColumnType("datetime2"); + + b.Property("DepositPaymentLinkToken") + .HasColumnType("nvarchar(max)"); + + b.Property("DepositPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("DiscountPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("DiscountReason") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscountType") + .HasColumnType("int"); + + b.Property("DiscountValue") + .HasColumnType("decimal(18,2)"); + + b.Property("EquipmentCosts") + .HasColumnType("decimal(18,2)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("HideDiscountFromCustomer") + .HasColumnType("bit"); + + b.Property("IsCommercial") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsRushJob") + .HasColumnType("bit"); + + b.Property("ItemsSubtotal") + .HasColumnType("decimal(18,2)"); + + b.Property("LaborCosts") + .HasColumnType("decimal(18,2)"); + + b.Property("MaterialCosts") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OvenBatchCost") + .HasColumnType("decimal(18,2)"); + + b.Property("OvenBatches") + .HasColumnType("int"); + + b.Property("OvenCostId") + .HasColumnType("int"); + + b.Property("OvenCycleMinutes") + .HasColumnType("int"); + + b.Property("OverheadAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("OverheadPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("PreparedById") + .HasColumnType("nvarchar(450)"); + + b.Property("ProfitMargin") + .HasColumnType("decimal(18,2)"); + + b.Property("ProfitPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("ProspectAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectCity") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectCompanyName") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectContactName") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectEmail") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectSmsConsent") + .HasColumnType("bit"); + + b.Property("ProspectSmsConsentedAt") + .HasColumnType("datetime2"); + + b.Property("ProspectState") + .HasColumnType("nvarchar(max)"); + + b.Property("ProspectZipCode") + .HasColumnType("nvarchar(max)"); + + b.Property("QuoteDate") + .HasColumnType("datetime2"); + + b.Property("QuoteNumber") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("QuoteStatusId") + .HasColumnType("int"); + + b.Property("RequiresDeposit") + .HasColumnType("bit"); + + b.Property("RushFee") + .HasColumnType("decimal(18,2)"); + + b.Property("SentDate") + .HasColumnType("datetime2"); + + b.Property("ShopSuppliesAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("ShopSuppliesPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("SubTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("TaxAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("Terms") + .HasColumnType("nvarchar(max)"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalToken") + .IsUnique() + .HasDatabaseName("IX_Quotes_ApprovalToken") + .HasFilter("[ApprovalToken] IS NOT NULL"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ExpirationDate"); + + b.HasIndex("OvenCostId"); + + b.HasIndex("PreparedById"); + + b.HasIndex("QuoteStatusId"); + + b.HasIndex("CompanyId", "ExpirationDate") + .HasDatabaseName("IX_Quotes_CompanyId_ExpirationDate"); + + b.HasIndex("CompanyId", "IsDeleted"); + + b.HasIndex("CompanyId", "QuoteNumber") + .IsUnique() + .HasDatabaseName("IX_Quotes_CompanyId_QuoteNumber"); + + b.HasIndex("CompanyId", "QuoteStatusId") + .HasDatabaseName("IX_Quotes_CompanyId_QuoteStatusId"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteChangeHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChangeDescription") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ChangedAt") + .HasColumnType("datetime2"); + + b.Property("ChangedByUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("FieldName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("NewValue") + .HasColumnType("nvarchar(max)"); + + b.Property("OldValue") + .HasColumnType("nvarchar(max)"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ChangedByUserId"); + + b.HasIndex("CompanyId"); + + b.HasIndex("QuoteId"); + + b.ToTable("QuoteChangeHistories"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AiPredictionId") + .HasColumnType("int"); + + b.Property("AiTags") + .HasColumnType("nvarchar(max)"); + + b.Property("CatalogItemId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("Complexity") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("IncludePrepCost") + .HasColumnType("bit"); + + b.Property("IsAiItem") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsGenericItem") + .HasColumnType("bit"); + + b.Property("IsLaborItem") + .HasColumnType("bit"); + + b.Property("IsSalesItem") + .HasColumnType("bit"); + + b.Property("ItemEquipmentCost") + .HasColumnType("decimal(18,2)"); + + b.Property("ItemLaborCost") + .HasColumnType("decimal(18,2)"); + + b.Property("ItemMaterialCost") + .HasColumnType("decimal(18,2)"); + + b.Property("ManualUnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderCostOverride") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("RequiresMasking") + .HasColumnType("bit"); + + b.Property("RequiresSandblasting") + .HasColumnType("bit"); + + b.Property("Sku") + .HasColumnType("nvarchar(max)"); + + b.Property("SurfaceArea") + .HasColumnType("decimal(18,2)"); + + b.Property("SurfaceAreaSqFt") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AiPredictionId"); + + b.HasIndex("CatalogItemId"); + + b.HasIndex("QuoteId") + .HasDatabaseName("IX_QuoteItems_QuoteId"); + + b.ToTable("QuoteItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItemCoat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CoatLaborCost") + .HasColumnType("decimal(18,2)"); + + b.Property("CoatMaterialCost") + .HasColumnType("decimal(18,2)"); + + b.Property("CoatName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CoatTotalCost") + .HasColumnType("decimal(18,2)"); + + b.Property("ColorCode") + .HasColumnType("nvarchar(max)"); + + b.Property("ColorName") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CoverageSqFtPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Finish") + .HasColumnType("nvarchar(max)"); + + b.Property("InventoryItemId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PowderCostPerLb") + .HasColumnType("decimal(18,2)"); + + b.Property("PowderToOrder") + .HasColumnType("decimal(18,2)"); + + b.Property("QuoteItemId") + .HasColumnType("int"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("TransferEfficiency") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId") + .HasDatabaseName("IX_QuoteItemCoats_CompanyId"); + + b.HasIndex("InventoryItemId") + .HasDatabaseName("IX_QuoteItemCoats_InventoryItemId"); + + b.HasIndex("QuoteItemId") + .HasDatabaseName("IX_QuoteItemCoats_QuoteItemId"); + + b.HasIndex("VendorId"); + + b.ToTable("QuoteItemCoats"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItemPrepService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BlastSetupId") + .HasColumnType("int"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PrepServiceId") + .HasColumnType("int"); + + b.Property("QuoteItemId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BlastSetupId"); + + b.HasIndex("CompanyId") + .HasDatabaseName("IX_QuoteItemPrepServices_CompanyId"); + + b.HasIndex("PrepServiceId") + .HasDatabaseName("IX_QuoteItemPrepServices_PrepServiceId"); + + b.HasIndex("QuoteItemId") + .HasDatabaseName("IX_QuoteItemPrepServices_QuoteItemId"); + + b.ToTable("QuoteItemPrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuotePhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Caption") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsAiAnalysisPhoto") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("TempId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UploadedById") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("QuoteId"); + + b.HasIndex("UploadedById"); + + b.ToTable("QuotePhotos"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuotePrepService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PrepServiceId") + .HasColumnType("int"); + + b.Property("QuoteId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PrepServiceId"); + + b.HasIndex("QuoteId"); + + b.ToTable("QuotePrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteStatusLookup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ColorClass") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("IconClass") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsApprovedStatus") + .HasColumnType("bit"); + + b.Property("IsConvertedStatus") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsDraftStatus") + .HasColumnType("bit"); + + b.Property("IsRejectedStatus") + .HasColumnType("bit"); + + b.Property("IsSystemDefined") + .HasColumnType("bit"); + + b.Property("StatusCode") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("CompanyId", "StatusCode") + .IsUnique(); + + b.ToTable("QuoteStatusLookups"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreditMemoId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IssuedById") + .HasColumnType("nvarchar(450)"); + + b.Property("IssuedDate") + .HasColumnType("datetime2"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PaymentId") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Reference") + .HasColumnType("nvarchar(max)"); + + b.Property("RefundDate") + .HasColumnType("datetime2"); + + b.Property("RefundMethod") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreditMemoId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("IssuedById"); + + b.HasIndex("PaymentId"); + + b.ToTable("Refunds"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ReleaseNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByUserId") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedByUserName") + .HasColumnType("nvarchar(max)"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("ReleasedAt") + .HasColumnType("datetime2"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("Version") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ReleaseNotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ReworkRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualReworkCost") + .HasColumnType("decimal(18,2)"); + + b.Property("BillingNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DefectDescription") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DiscoveredBy") + .HasColumnType("int"); + + b.Property("DiscoveredDate") + .HasColumnType("datetime2"); + + b.Property("EstimatedReworkCost") + .HasColumnType("decimal(18,2)"); + + b.Property("IsBillableToCustomer") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("int"); + + b.Property("JobItemId") + .HasColumnType("int"); + + b.Property("Reason") + .HasColumnType("int"); + + b.Property("ReportedByName") + .HasColumnType("nvarchar(max)"); + + b.Property("Resolution") + .HasColumnType("int"); + + b.Property("ResolutionNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("ResolvedDate") + .HasColumnType("datetime2"); + + b.Property("ReworkJobId") + .HasColumnType("int"); + + b.Property("ReworkType") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("JobItemId"); + + b.HasIndex("ReworkJobId"); + + b.ToTable("ReworkRecords"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ShopWorker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("ShopWorkers"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ShopWorkerRoleCost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("HourlyRate") + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId", "Role") + .IsUnique() + .HasDatabaseName("IX_ShopWorkerRoleCosts_CompanyId_Role"); + + b.ToTable("ShopWorkerRoleCosts"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.StripeWebhookEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("RawJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("StripeWebhookEvents"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.SubscriptionPlanConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AllowAccounting") + .HasColumnType("bit"); + + b.Property("AllowAiCatalogPriceCheck") + .HasColumnType("bit"); + + b.Property("AllowAiInventoryAssist") + .HasColumnType("bit"); + + b.Property("AllowAiPhotoQuotes") + .HasColumnType("bit"); + + b.Property("AllowOnlinePayments") + .HasColumnType("bit"); + + b.Property("AllowSms") + .HasColumnType("bit"); + + b.Property("AnnualPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaxActiveJobs") + .HasColumnType("int"); + + b.Property("MaxAiPhotoQuotesPerMonth") + .HasColumnType("int"); + + b.Property("MaxCatalogItems") + .HasColumnType("int"); + + b.Property("MaxCustomers") + .HasColumnType("int"); + + b.Property("MaxJobPhotos") + .HasColumnType("int"); + + b.Property("MaxQuotePhotos") + .HasColumnType("int"); + + b.Property("MaxQuotes") + .HasColumnType("int"); + + b.Property("MaxUsers") + .HasColumnType("int"); + + b.Property("MonthlyPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Plan") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("StripePriceIdAnnual") + .HasColumnType("nvarchar(max)"); + + b.Property("StripePriceIdMonthly") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionPlanConfigs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.TermsAcceptance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AcceptedAt") + .HasColumnType("datetime2"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("TosVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserAgent") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TermsAcceptances"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.UserPasskey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CredentialId") + .IsRequired() + .HasColumnType("varbinary(900)"); + + b.Property("DeviceFriendlyName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastUsedAt") + .HasColumnType("datetime2"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("SignCount") + .HasColumnType("bigint"); + + b.Property("UserHandle") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CredentialId") + .IsUnique(); + + b.ToTable("UserPasskeys"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Vendor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ContactName") + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreditLimit") + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("DefaultExpenseAccountId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsPreferred") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OpeningBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("OpeningBalanceDate") + .HasColumnType("datetime2"); + + b.Property("PaymentTerms") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("TaxId") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Website") + .HasColumnType("nvarchar(max)"); + + b.Property("ZipCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.HasIndex("DefaultExpenseAccountId"); + + b.ToTable("Vendors"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Account", b => + { + b.HasOne("PowderCoating.Core.Entities.Account", "ParentAccount") + .WithMany("SubAccounts") + .HasForeignKey("ParentAccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ParentAccount"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AiUsageLog", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AnnouncementDismissal", b => + { + b.HasOne("PowderCoating.Core.Entities.Announcement", "Announcement") + .WithMany("Dismissals") + .HasForeignKey("AnnouncementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Announcement"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ApplicationUser", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithMany("Users") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Appointment", b => + { + b.HasOne("PowderCoating.Core.Entities.AppointmentStatusLookup", "AppointmentStatus") + .WithMany("Appointments") + .HasForeignKey("AppointmentStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.AppointmentTypeLookup", "AppointmentType") + .WithMany("Appointments") + .HasForeignKey("AppointmentTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "AssignedUser") + .WithMany() + .HasForeignKey("AssignedUserId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId"); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId"); + + b.Navigation("AppointmentStatus"); + + b.Navigation("AppointmentType"); + + b.Navigation("AssignedUser"); + + b.Navigation("Customer"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Bill", b => + { + b.HasOne("PowderCoating.Core.Entities.Account", "APAccount") + .WithMany("Bills") + .HasForeignKey("APAccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany("Bills") + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("APAccount"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BillLineItem", b => + { + b.HasOne("PowderCoating.Core.Entities.Account", "Account") + .WithMany("BillLineItems") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.Bill", "Bill") + .WithMany("LineItems") + .HasForeignKey("BillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId"); + + b.Navigation("Account"); + + b.Navigation("Bill"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BillPayment", b => + { + b.HasOne("PowderCoating.Core.Entities.Account", "BankAccount") + .WithMany("BillPayments") + .HasForeignKey("BankAccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Bill", "Bill") + .WithMany("Payments") + .HasForeignKey("BillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany("BillPayments") + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BankAccount"); + + b.Navigation("Bill"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BugReportAttachment", b => + { + b.HasOne("PowderCoating.Core.Entities.BugReport", "BugReport") + .WithMany("Attachments") + .HasForeignKey("BugReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BugReport"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CatalogCategory", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.CatalogCategory", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CatalogItem", b => + { + b.HasOne("PowderCoating.Core.Entities.CatalogCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Account", "CogsAccount") + .WithMany() + .HasForeignKey("CogsAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId"); + + b.HasOne("PowderCoating.Core.Entities.Account", "RevenueAccount") + .WithMany() + .HasForeignKey("RevenueAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Category"); + + b.Navigation("CogsAccount"); + + b.Navigation("InventoryItem"); + + b.Navigation("RevenueAccount"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanyBlastSetup", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanyOperatingCosts", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithOne("OperatingCosts") + .HasForeignKey("PowderCoating.Core.Entities.CompanyOperatingCosts", "CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CompanyPreferences", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithOne("Preferences") + .HasForeignKey("PowderCoating.Core.Entities.CompanyPreferences", "CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CreditMemo", b => + { + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "IssuedBy") + .WithMany() + .HasForeignKey("IssuedById"); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "OriginalInvoice") + .WithMany() + .HasForeignKey("OriginalInvoiceId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.ReworkRecord", "ReworkRecord") + .WithMany() + .HasForeignKey("ReworkRecordId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Customer"); + + b.Navigation("IssuedBy"); + + b.Navigation("OriginalInvoice"); + + b.Navigation("ReworkRecord"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CreditMemoApplication", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "AppliedBy") + .WithMany() + .HasForeignKey("AppliedById"); + + b.HasOne("PowderCoating.Core.Entities.CreditMemo", "CreditMemo") + .WithMany("Applications") + .HasForeignKey("CreditMemoId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany("CreditApplications") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AppliedBy"); + + b.Navigation("CreditMemo"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Customer", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("Customers") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.PricingTier", "PricingTier") + .WithMany("Customers") + .HasForeignKey("PricingTierId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("PricingTier"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CustomerNote", b => + { + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany("CustomerNotes") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Deposit", b => + { + b.HasOne("PowderCoating.Core.Entities.Invoice", "AppliedToInvoice") + .WithMany() + .HasForeignKey("AppliedToInvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId"); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany() + .HasForeignKey("QuoteId"); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "RecordedBy") + .WithMany() + .HasForeignKey("RecordedById"); + + b.Navigation("AppliedToInvoice"); + + b.Navigation("Customer"); + + b.Navigation("Job"); + + b.Navigation("Quote"); + + b.Navigation("RecordedBy"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Equipment", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("Equipment") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Expense", b => + { + b.HasOne("PowderCoating.Core.Entities.Account", "ExpenseAccount") + .WithMany("Expenses") + .HasForeignKey("ExpenseAccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId"); + + b.HasOne("PowderCoating.Core.Entities.Account", "PaymentAccount") + .WithMany("ExpensePaymentAccounts") + .HasForeignKey("PaymentAccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany("Expenses") + .HasForeignKey("VendorId"); + + b.Navigation("ExpenseAccount"); + + b.Navigation("Job"); + + b.Navigation("PaymentAccount"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.GiftCertificate", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "IssuedBy") + .WithMany() + .HasForeignKey("IssuedById"); + + b.HasOne("PowderCoating.Core.Entities.Customer", "PurchasingCustomer") + .WithMany() + .HasForeignKey("PurchasingCustomerId"); + + b.HasOne("PowderCoating.Core.Entities.Customer", "RecipientCustomer") + .WithMany() + .HasForeignKey("RecipientCustomerId"); + + b.Navigation("IssuedBy"); + + b.Navigation("PurchasingCustomer"); + + b.Navigation("RecipientCustomer"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.GiftCertificateRedemption", b => + { + b.HasOne("PowderCoating.Core.Entities.GiftCertificate", "GiftCertificate") + .WithMany("Redemptions") + .HasForeignKey("GiftCertificateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany("GiftCertificateRedemptions") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "RedeemedBy") + .WithMany() + .HasForeignKey("RedeemedById"); + + b.Navigation("GiftCertificate"); + + b.Navigation("Invoice"); + + b.Navigation("RedeemedBy"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InAppNotification", b => + { + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId"); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId"); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany() + .HasForeignKey("QuoteId"); + + b.Navigation("Customer"); + + b.Navigation("Invoice"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryCategoryLookup", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryItem", b => + { + b.HasOne("PowderCoating.Core.Entities.Account", "CogsAccount") + .WithMany() + .HasForeignKey("CogsAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("InventoryItems") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Account", "InventoryAccount") + .WithMany() + .HasForeignKey("InventoryAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.InventoryCategoryLookup", "InventoryCategory") + .WithMany("InventoryItems") + .HasForeignKey("InventoryCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "PrimaryVendor") + .WithMany("InventoryItems") + .HasForeignKey("PrimaryVendorId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("CogsAccount"); + + b.Navigation("InventoryAccount"); + + b.Navigation("InventoryCategory"); + + b.Navigation("PrimaryVendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryTransaction", b => + { + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany("Transactions") + .HasForeignKey("InventoryItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("PurchaseOrderId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("InventoryItem"); + + b.Navigation("Job"); + + b.Navigation("PurchaseOrder"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Invoice", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany("Invoices") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithOne("Invoice") + .HasForeignKey("PowderCoating.Core.Entities.Invoice", "JobId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "PreparedBy") + .WithMany() + .HasForeignKey("PreparedById") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Account", "SalesTaxAccount") + .WithMany() + .HasForeignKey("SalesTaxAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Customer"); + + b.Navigation("Job"); + + b.Navigation("PreparedBy"); + + b.Navigation("SalesTaxAccount"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InvoiceItem", b => + { + b.HasOne("PowderCoating.Core.Entities.CatalogItem", "CatalogItem") + .WithMany() + .HasForeignKey("CatalogItemId"); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.GiftCertificate", "GeneratedGiftCertificate") + .WithMany() + .HasForeignKey("GeneratedGiftCertificateId"); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany("InvoiceItems") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Account", "RevenueAccount") + .WithMany() + .HasForeignKey("RevenueAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.JobItem", "SourceJobItem") + .WithMany() + .HasForeignKey("SourceJobItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("CatalogItem"); + + b.Navigation("GeneratedGiftCertificate"); + + b.Navigation("Invoice"); + + b.Navigation("RevenueAccount"); + + b.Navigation("SourceJobItem"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Job", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "AssignedUser") + .WithMany() + .HasForeignKey("AssignedUserId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("Jobs") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany("Jobs") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "IntakeCheckedBy") + .WithMany() + .HasForeignKey("IntakeCheckedByUserId"); + + b.HasOne("PowderCoating.Core.Entities.JobPriorityLookup", "JobPriority") + .WithMany("Jobs") + .HasForeignKey("JobPriorityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobStatusLookup", "JobStatus") + .WithMany("Jobs") + .HasForeignKey("JobStatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "OriginalJob") + .WithMany() + .HasForeignKey("OriginalJobId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.OvenCost", "OvenCost") + .WithMany("Jobs") + .HasForeignKey("OvenCostId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithOne("ConvertedToJob") + .HasForeignKey("PowderCoating.Core.Entities.Job", "QuoteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.ShopWorker", null) + .WithMany("AssignedJobs") + .HasForeignKey("ShopWorkerId"); + + b.Navigation("AssignedUser"); + + b.Navigation("Customer"); + + b.Navigation("IntakeCheckedBy"); + + b.Navigation("JobPriority"); + + b.Navigation("JobStatus"); + + b.Navigation("OriginalJob"); + + b.Navigation("OvenCost"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobChangeHistory", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "ChangedBy") + .WithMany() + .HasForeignKey("ChangedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChangedBy"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobDailyPriority", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItem", b => + { + b.HasOne("PowderCoating.Core.Entities.AiItemPrediction", "AiPrediction") + .WithMany() + .HasForeignKey("AiPredictionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.CatalogItem", "CatalogItem") + .WithMany() + .HasForeignKey("CatalogItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("JobItems") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AiPrediction"); + + b.Navigation("CatalogItem"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItemCoat", b => + { + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId"); + + b.HasOne("PowderCoating.Core.Entities.JobItem", "JobItem") + .WithMany("Coats") + .HasForeignKey("JobItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId"); + + b.Navigation("InventoryItem"); + + b.Navigation("JobItem"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItemPrepService", b => + { + b.HasOne("PowderCoating.Core.Entities.CompanyBlastSetup", "BlastSetup") + .WithMany() + .HasForeignKey("BlastSetupId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobItem", "JobItem") + .WithMany("PrepServices") + .HasForeignKey("JobItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.PrepService", "PrepService") + .WithMany() + .HasForeignKey("PrepServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BlastSetup"); + + b.Navigation("JobItem"); + + b.Navigation("PrepService"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobNote", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("Notes") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPhoto", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("Photos") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "UploadedBy") + .WithMany() + .HasForeignKey("UploadedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("UploadedBy"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPrepService", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("JobPrepServices") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.PrepService", "PrepService") + .WithMany() + .HasForeignKey("PrepServiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("PrepService"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPriorityLookup", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobStatusHistory", b => + { + b.HasOne("PowderCoating.Core.Entities.JobStatusLookup", "FromStatus") + .WithMany("FromStatusHistory") + .HasForeignKey("FromStatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("StatusHistory") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobStatusLookup", "ToStatus") + .WithMany("ToStatusHistory") + .HasForeignKey("ToStatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("FromStatus"); + + b.Navigation("Job"); + + b.Navigation("ToStatus"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobStatusLookup", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplate", b => + { + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItem", b => + { + b.HasOne("PowderCoating.Core.Entities.CatalogItem", "CatalogItem") + .WithMany() + .HasForeignKey("CatalogItemId"); + + b.HasOne("PowderCoating.Core.Entities.JobTemplate", "JobTemplate") + .WithMany("Items") + .HasForeignKey("JobTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CatalogItem"); + + b.Navigation("JobTemplate"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItemCoat", b => + { + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId"); + + b.HasOne("PowderCoating.Core.Entities.JobTemplateItem", "JobTemplateItem") + .WithMany("Coats") + .HasForeignKey("JobTemplateItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId"); + + b.Navigation("InventoryItem"); + + b.Navigation("JobTemplateItem"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItemPrepService", b => + { + b.HasOne("PowderCoating.Core.Entities.JobTemplateItem", "JobTemplateItem") + .WithMany("PrepServices") + .HasForeignKey("JobTemplateItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.PrepService", "PrepService") + .WithMany() + .HasForeignKey("PrepServiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobTemplateItem"); + + b.Navigation("PrepService"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTimeEntry", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("TimeEntries") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ShopWorker", "Worker") + .WithMany("TimeEntries") + .HasForeignKey("ShopWorkerId"); + + b.Navigation("Job"); + + b.Navigation("Worker"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.MaintenanceRecord", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "AssignedUser") + .WithMany() + .HasForeignKey("AssignedUserId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Equipment", "Equipment") + .WithMany("MaintenanceRecords") + .HasForeignKey("EquipmentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "PerformedBy") + .WithMany("PerformedMaintenances") + .HasForeignKey("PerformedById") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.MaintenanceRecord", "RecurrenceParent") + .WithMany() + .HasForeignKey("RecurrenceParentId"); + + b.HasOne("PowderCoating.Core.Entities.ShopWorker", null) + .WithMany("AssignedMaintenanceTasks") + .HasForeignKey("ShopWorkerId"); + + b.Navigation("AssignedUser"); + + b.Navigation("Equipment"); + + b.Navigation("PerformedBy"); + + b.Navigation("RecurrenceParent"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.NotificationLog", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany("NotificationLogs") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany() + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Customer"); + + b.Navigation("Invoice"); + + b.Navigation("Job"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.NotificationTemplate", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenBatch", b => + { + b.HasOne("PowderCoating.Core.Entities.Equipment", "Equipment") + .WithMany("OvenBatches") + .HasForeignKey("EquipmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.OvenCost", "OvenCost") + .WithMany() + .HasForeignKey("OvenCostId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Equipment"); + + b.Navigation("OvenCost"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenBatchItem", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobItemCoat", "JobItemCoat") + .WithMany() + .HasForeignKey("JobItemCoatId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobItem", "JobItem") + .WithMany() + .HasForeignKey("JobItemId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.OvenBatch", "Batch") + .WithMany("Items") + .HasForeignKey("OvenBatchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Batch"); + + b.Navigation("Job"); + + b.Navigation("JobItem"); + + b.Navigation("JobItemCoat"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenCost", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", "Company") + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Payment", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Account", "DepositAccount") + .WithMany() + .HasForeignKey("DepositAccountId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "RecordedBy") + .WithMany() + .HasForeignKey("RecordedById") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DepositAccount"); + + b.Navigation("Invoice"); + + b.Navigation("RecordedBy"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PowderUsageLog", b => + { + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.InventoryTransaction", "InventoryTransaction") + .WithMany() + .HasForeignKey("InventoryTransactionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobItemCoat", "JobItemCoat") + .WithMany() + .HasForeignKey("JobItemCoatId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobItem", "JobItem") + .WithMany() + .HasForeignKey("JobItemId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("InventoryItem"); + + b.Navigation("InventoryTransaction"); + + b.Navigation("Job"); + + b.Navigation("JobItem"); + + b.Navigation("JobItemCoat"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PricingTier", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("PricingTiers") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PurchaseOrder", b => + { + b.HasOne("PowderCoating.Core.Entities.Bill", "Bill") + .WithMany() + .HasForeignKey("BillId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Bill"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PurchaseOrderItem", b => + { + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.PurchaseOrder", "PurchaseOrder") + .WithMany("Items") + .HasForeignKey("PurchaseOrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InventoryItem"); + + b.Navigation("PurchaseOrder"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Quote", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("Quotes") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Customer", "Customer") + .WithMany("Quotes") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.OvenCost", "OvenCost") + .WithMany("Quotes") + .HasForeignKey("OvenCostId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "PreparedBy") + .WithMany("PreparedQuotes") + .HasForeignKey("PreparedById") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.QuoteStatusLookup", "QuoteStatus") + .WithMany("Quotes") + .HasForeignKey("QuoteStatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("OvenCost"); + + b.Navigation("PreparedBy"); + + b.Navigation("QuoteStatus"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteChangeHistory", b => + { + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "ChangedBy") + .WithMany() + .HasForeignKey("ChangedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany() + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChangedBy"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItem", b => + { + b.HasOne("PowderCoating.Core.Entities.AiItemPrediction", "AiPrediction") + .WithMany() + .HasForeignKey("AiPredictionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.CatalogItem", "CatalogItem") + .WithMany() + .HasForeignKey("CatalogItemId"); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany("QuoteItems") + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AiPrediction"); + + b.Navigation("CatalogItem"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItemCoat", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PowderCoating.Core.Entities.QuoteItem", "QuoteItem") + .WithMany("Coats") + .HasForeignKey("QuoteItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId"); + + b.Navigation("InventoryItem"); + + b.Navigation("QuoteItem"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItemPrepService", b => + { + b.HasOne("PowderCoating.Core.Entities.CompanyBlastSetup", "BlastSetup") + .WithMany() + .HasForeignKey("BlastSetupId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.PrepService", "PrepService") + .WithMany() + .HasForeignKey("PrepServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.QuoteItem", "QuoteItem") + .WithMany("PrepServices") + .HasForeignKey("QuoteItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlastSetup"); + + b.Navigation("PrepService"); + + b.Navigation("QuoteItem"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuotePhoto", b => + { + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany("QuotePhotos") + .HasForeignKey("QuoteId"); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "UploadedBy") + .WithMany() + .HasForeignKey("UploadedById"); + + b.Navigation("Quote"); + + b.Navigation("UploadedBy"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuotePrepService", b => + { + b.HasOne("PowderCoating.Core.Entities.PrepService", "PrepService") + .WithMany() + .HasForeignKey("PrepServiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Quote", "Quote") + .WithMany("QuotePrepServices") + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PrepService"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteStatusLookup", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany() + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Refund", b => + { + b.HasOne("PowderCoating.Core.Entities.CreditMemo", "CreditMemo") + .WithMany() + .HasForeignKey("CreditMemoId"); + + b.HasOne("PowderCoating.Core.Entities.Invoice", "Invoice") + .WithMany("Refunds") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.ApplicationUser", "IssuedBy") + .WithMany() + .HasForeignKey("IssuedById"); + + b.HasOne("PowderCoating.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("CreditMemo"); + + b.Navigation("Invoice"); + + b.Navigation("IssuedBy"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ReworkRecord", b => + { + b.HasOne("PowderCoating.Core.Entities.Job", "Job") + .WithMany("ReworkRecords") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.JobItem", "JobItem") + .WithMany() + .HasForeignKey("JobItemId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PowderCoating.Core.Entities.Job", "ReworkJob") + .WithMany() + .HasForeignKey("ReworkJobId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Job"); + + b.Navigation("JobItem"); + + b.Navigation("ReworkJob"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ShopWorker", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("ShopWorkers") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Vendor", b => + { + b.HasOne("PowderCoating.Core.Entities.Company", null) + .WithMany("Vendors") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PowderCoating.Core.Entities.Account", "DefaultExpenseAccount") + .WithMany() + .HasForeignKey("DefaultExpenseAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DefaultExpenseAccount"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Account", b => + { + b.Navigation("BillLineItems"); + + b.Navigation("BillPayments"); + + b.Navigation("Bills"); + + b.Navigation("ExpensePaymentAccounts"); + + b.Navigation("Expenses"); + + b.Navigation("SubAccounts"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Announcement", b => + { + b.Navigation("Dismissals"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ApplicationUser", b => + { + b.Navigation("PerformedMaintenances"); + + b.Navigation("PreparedQuotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AppointmentStatusLookup", b => + { + b.Navigation("Appointments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.AppointmentTypeLookup", b => + { + b.Navigation("Appointments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Bill", b => + { + b.Navigation("LineItems"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.BugReport", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CatalogCategory", b => + { + b.Navigation("Items"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Company", b => + { + b.Navigation("Customers"); + + b.Navigation("Equipment"); + + b.Navigation("InventoryItems"); + + b.Navigation("Jobs"); + + b.Navigation("OperatingCosts"); + + b.Navigation("Preferences"); + + b.Navigation("PricingTiers"); + + b.Navigation("Quotes"); + + b.Navigation("ShopWorkers"); + + b.Navigation("Users"); + + b.Navigation("Vendors"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.CreditMemo", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Customer", b => + { + b.Navigation("CustomerNotes"); + + b.Navigation("Invoices"); + + b.Navigation("Jobs"); + + b.Navigation("NotificationLogs"); + + b.Navigation("Quotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Equipment", b => + { + b.Navigation("MaintenanceRecords"); + + b.Navigation("OvenBatches"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.GiftCertificate", b => + { + b.Navigation("Redemptions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryCategoryLookup", b => + { + b.Navigation("InventoryItems"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.InventoryItem", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Invoice", b => + { + b.Navigation("CreditApplications"); + + b.Navigation("GiftCertificateRedemptions"); + + b.Navigation("InvoiceItems"); + + b.Navigation("Payments"); + + b.Navigation("Refunds"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Job", b => + { + b.Navigation("Invoice"); + + b.Navigation("JobItems"); + + b.Navigation("JobPrepServices"); + + b.Navigation("Notes"); + + b.Navigation("Photos"); + + b.Navigation("ReworkRecords"); + + b.Navigation("StatusHistory"); + + b.Navigation("TimeEntries"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobItem", b => + { + b.Navigation("Coats"); + + b.Navigation("PrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobPriorityLookup", b => + { + b.Navigation("Jobs"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobStatusLookup", b => + { + b.Navigation("FromStatusHistory"); + + b.Navigation("Jobs"); + + b.Navigation("ToStatusHistory"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplate", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.JobTemplateItem", b => + { + b.Navigation("Coats"); + + b.Navigation("PrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenBatch", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.OvenCost", b => + { + b.Navigation("Jobs"); + + b.Navigation("Quotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PricingTier", b => + { + b.Navigation("Customers"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.PurchaseOrder", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Quote", b => + { + b.Navigation("ConvertedToJob"); + + b.Navigation("QuoteItems"); + + b.Navigation("QuotePhotos"); + + b.Navigation("QuotePrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteItem", b => + { + b.Navigation("Coats"); + + b.Navigation("PrepServices"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.QuoteStatusLookup", b => + { + b.Navigation("Quotes"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.ShopWorker", b => + { + b.Navigation("AssignedJobs"); + + b.Navigation("AssignedMaintenanceTasks"); + + b.Navigation("TimeEntries"); + }); + + modelBuilder.Entity("PowderCoating.Core.Entities.Vendor", b => + { + b.Navigation("BillPayments"); + + b.Navigation("Bills"); + + b.Navigation("Expenses"); + + b.Navigation("InventoryItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/PowderCoating.Infrastructure/Migrations/20260510011252_AddJobTemplateItemSalesFields.cs b/src/PowderCoating.Infrastructure/Migrations/20260510011252_AddJobTemplateItemSalesFields.cs new file mode 100644 index 0000000..bdf26b4 --- /dev/null +++ b/src/PowderCoating.Infrastructure/Migrations/20260510011252_AddJobTemplateItemSalesFields.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PowderCoating.Infrastructure.Migrations +{ + /// + public partial class AddJobTemplateItemSalesFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsSalesItem", + table: "JobTemplateItems", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "Sku", + table: "JobTemplateItems", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.UpdateData( + table: "PricingTiers", + keyColumn: "Id", + keyValue: 1, + column: "CreatedAt", + value: new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2249)); + + migrationBuilder.UpdateData( + table: "PricingTiers", + keyColumn: "Id", + keyValue: 2, + column: "CreatedAt", + value: new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2260)); + + migrationBuilder.UpdateData( + table: "PricingTiers", + keyColumn: "Id", + keyValue: 3, + column: "CreatedAt", + value: new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2261)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsSalesItem", + table: "JobTemplateItems"); + + migrationBuilder.DropColumn( + name: "Sku", + table: "JobTemplateItems"); + + migrationBuilder.UpdateData( + table: "PricingTiers", + keyColumn: "Id", + keyValue: 1, + column: "CreatedAt", + value: new DateTime(2026, 5, 8, 14, 21, 51, 589, DateTimeKind.Utc).AddTicks(4358)); + + migrationBuilder.UpdateData( + table: "PricingTiers", + keyColumn: "Id", + keyValue: 2, + column: "CreatedAt", + value: new DateTime(2026, 5, 8, 14, 21, 51, 589, DateTimeKind.Utc).AddTicks(4424)); + + migrationBuilder.UpdateData( + table: "PricingTiers", + keyColumn: "Id", + keyValue: 3, + column: "CreatedAt", + value: new DateTime(2026, 5, 8, 14, 21, 51, 589, DateTimeKind.Utc).AddTicks(4426)); + } + } +} diff --git a/src/PowderCoating.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/PowderCoating.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 19a5e7b..4f9ceec 100644 --- a/src/PowderCoating.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/PowderCoating.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -4833,6 +4833,9 @@ namespace PowderCoating.Infrastructure.Migrations b.Property("IsLaborItem") .HasColumnType("bit"); + b.Property("IsSalesItem") + .HasColumnType("bit"); + b.Property("JobTemplateId") .HasColumnType("int"); @@ -4851,6 +4854,9 @@ namespace PowderCoating.Infrastructure.Migrations b.Property("RequiresSandblasting") .HasColumnType("bit"); + b.Property("Sku") + .HasColumnType("nvarchar(max)"); + b.Property("SurfaceAreaSqFt") .HasColumnType("decimal(18,2)"); @@ -6071,7 +6077,7 @@ namespace PowderCoating.Infrastructure.Migrations { Id = 1, CompanyId = 0, - CreatedAt = new DateTime(2026, 5, 8, 14, 21, 51, 589, DateTimeKind.Utc).AddTicks(4358), + CreatedAt = new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2249), Description = "Standard pricing for regular customers", DiscountPercent = 0m, IsActive = true, @@ -6082,7 +6088,7 @@ namespace PowderCoating.Infrastructure.Migrations { Id = 2, CompanyId = 0, - CreatedAt = new DateTime(2026, 5, 8, 14, 21, 51, 589, DateTimeKind.Utc).AddTicks(4424), + CreatedAt = new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2260), Description = "5% discount for preferred customers", DiscountPercent = 5m, IsActive = true, @@ -6093,7 +6099,7 @@ namespace PowderCoating.Infrastructure.Migrations { Id = 3, CompanyId = 0, - CreatedAt = new DateTime(2026, 5, 8, 14, 21, 51, 589, DateTimeKind.Utc).AddTicks(4426), + CreatedAt = new DateTime(2026, 5, 10, 1, 12, 48, 386, DateTimeKind.Utc).AddTicks(2261), Description = "10% discount for premium customers", DiscountPercent = 10m, IsActive = true, diff --git a/src/PowderCoating.Web/Controllers/AppointmentsController.cs b/src/PowderCoating.Web/Controllers/AppointmentsController.cs index bf58726..fddcb6e 100644 --- a/src/PowderCoating.Web/Controllers/AppointmentsController.cs +++ b/src/PowderCoating.Web/Controllers/AppointmentsController.cs @@ -117,14 +117,7 @@ public class AppointmentsController : Controller // Map to DTOs var appointmentDtos = _mapper.Map>(items); - // Create paged result - var pagedResult = new PagedResult - { - Items = appointmentDtos, - PageNumber = gridRequest.PageNumber, - PageSize = gridRequest.PageSize, - TotalCount = totalCount - }; + var pagedResult = PagedResult.From(gridRequest, appointmentDtos, totalCount); // Set ViewBag ViewBag.SearchTerm = searchTerm; diff --git a/src/PowderCoating.Web/Controllers/BillsController.cs b/src/PowderCoating.Web/Controllers/BillsController.cs index 71e61e1..5464c9c 100644 --- a/src/PowderCoating.Web/Controllers/BillsController.cs +++ b/src/PowderCoating.Web/Controllers/BillsController.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using PowderCoating.Application.Configuration; +using PowderCoating.Application.Services; using PowderCoating.Application.DTOs.Accounting; using PowderCoating.Application.DTOs.AI; using PowderCoating.Application.Interfaces; @@ -15,6 +16,7 @@ using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; using PowderCoating.Core.Interfaces; using PowderCoating.Application.DTOs.PurchaseOrder; +using PowderCoating.Web.Helpers; namespace PowderCoating.Web.Controllers; @@ -345,10 +347,11 @@ public class BillsController : Controller // Attach receipt file if provided if (receiptFile != null && receiptFile.Length > 0) { - if (IsValidReceiptFile(receiptFile, out var fileError)) + var (receiptValid, _, receiptError) = BlobFileHelper.ValidateUpload(receiptFile, AllowedReceiptTypes, MaxReceiptBytes); + if (receiptValid) bill.ReceiptFilePath = await UploadReceiptAsync(receiptFile, bill.Id, currentUser.CompanyId); else - TempData["Warning"] = $"Bill saved but receipt not uploaded: {fileError}"; + TempData["Warning"] = $"Bill saved but receipt not uploaded: {receiptError}"; await _unitOfWork.CompleteAsync(); } @@ -571,7 +574,8 @@ public class BillsController : Controller // Handle receipt file replacement if (receiptFile != null && receiptFile.Length > 0) { - if (IsValidReceiptFile(receiptFile, out var fileError)) + var (receiptValid, _, receiptError) = BlobFileHelper.ValidateUpload(receiptFile, AllowedReceiptTypes, MaxReceiptBytes); + if (receiptValid) { if (!string.IsNullOrEmpty(bill.ReceiptFilePath)) await _blobStorage.DeleteAsync(_storageSettings.Containers.ReceiptImages, bill.ReceiptFilePath); @@ -579,7 +583,7 @@ public class BillsController : Controller } else { - TempData["Warning"] = $"Bill saved but receipt not uploaded: {fileError}"; + TempData["Warning"] = $"Bill saved but receipt not uploaded: {receiptError}"; } } @@ -927,48 +931,13 @@ public class BillsController : Controller /// private async Task PopulateDropdownsAsync() { - var vendors = await _unitOfWork.Vendors.FindAsync(s => s.IsActive); - ViewBag.Vendors = vendors - .OrderBy(s => s.CompanyName) - .Select(s => new SelectListItem(s.CompanyName, s.Id.ToString())) - .ToList(); - - var allAccounts = await _unitOfWork.Accounts.FindAsync(a => a.IsActive); - - ViewBag.APAccounts = allAccounts - .Where(a => a.AccountSubType == AccountSubType.AccountsPayable) - .OrderBy(a => a.AccountNumber) - .Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString())) - .ToList(); - - ViewBag.ExpenseAccounts = allAccounts - .Where(a => a.AccountType == AccountType.Expense || - a.AccountType == AccountType.CostOfGoods || - a.AccountType == AccountType.Asset) - .OrderBy(a => a.AccountNumber) - .Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString())) - .ToList(); - - ViewBag.BankAccounts = allAccounts - .Where(a => a.AccountSubType == AccountSubType.Cash || - a.AccountSubType == AccountSubType.Checking || - a.AccountSubType == AccountSubType.Savings || - a.AccountSubType == AccountSubType.CreditCard) - .OrderBy(a => a.AccountNumber) - .Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString())) - .ToList(); - - ViewBag.PaymentMethods = Enum.GetValues() - .Select(m => new SelectListItem(m.ToString(), ((int)m).ToString())) - .ToList(); - - ViewBag.Jobs = (await _unitOfWork.Jobs.FindAsync(j => - j.JobStatus.StatusCode != "COMPLETED" && - j.JobStatus.StatusCode != "CANCELLED" && - j.JobStatus.StatusCode != "DELIVERED")) - .OrderBy(j => j.JobNumber) - .Select(j => new SelectListItem($"{j.JobNumber} – {j.Description ?? "No description"}", j.Id.ToString())) - .ToList(); + var dd = await AccountingDropdownHelper.LoadAsync(_unitOfWork); + ViewBag.Vendors = dd.Vendors; + ViewBag.APAccounts = dd.ApAccounts; + ViewBag.ExpenseAccounts = dd.ExpenseAndAssetAccounts; + ViewBag.BankAccounts = dd.BankAccounts; + ViewBag.PaymentMethods = dd.PaymentMethods; + ViewBag.Jobs = dd.ActiveJobs; } /// @@ -1023,7 +992,7 @@ public class BillsController : Controller if (!result.Success) return NotFound(); var ext = Path.GetExtension(bill.ReceiptFilePath).ToLowerInvariant(); - var contentType = MimeFromExt(ext); + var contentType = BlobFileHelper.GetContentType(ext); var fileName = $"receipt-{bill.BillNumber}{ext}"; return File(result.Content, contentType, fileName); } @@ -1161,41 +1130,8 @@ public class BillsController : Controller { var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); var blobName = $"{companyId}/bill-receipts/{billId}/{Guid.NewGuid()}{ext}"; - var contentType = MimeFromExt(ext); using var stream = file.OpenReadStream(); - var result = await _blobStorage.UploadAsync(_storageSettings.Containers.ReceiptImages, blobName, stream, contentType); + var result = await _blobStorage.UploadAsync(_storageSettings.Containers.ReceiptImages, blobName, stream, BlobFileHelper.GetContentType(ext)); return result.Success ? blobName : null; } - - /// - /// Validates a receipt file upload against the allowed extension list and the 10 MB size cap. - /// Returns false and populates with a user-friendly message - /// when the file fails either check; returns true and sets to - /// an empty string when the file is acceptable. - /// - private static bool IsValidReceiptFile(IFormFile file, out string error) - { - var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); - if (!AllowedReceiptTypes.Contains(ext)) - { - error = $"File type '{ext}' is not allowed. Accepted: {string.Join(", ", AllowedReceiptTypes)}"; - return false; - } - if (file.Length > MaxReceiptBytes) - { - error = "Receipt file must be 10 MB or smaller."; - return false; - } - error = string.Empty; - return true; - } - - private static string MimeFromExt(string ext) => ext switch - { - ".pdf" => "application/pdf", - ".png" => "image/png", - ".gif" => "image/gif", - ".webp" => "image/webp", - _ => "image/jpeg" - }; } diff --git a/src/PowderCoating.Web/Controllers/CompanyUsersController.cs b/src/PowderCoating.Web/Controllers/CompanyUsersController.cs index 548f0d1..8be50d0 100644 --- a/src/PowderCoating.Web/Controllers/CompanyUsersController.cs +++ b/src/PowderCoating.Web/Controllers/CompanyUsersController.cs @@ -174,14 +174,7 @@ public class CompanyUsersController : Controller LastLoginDate = u.LastLoginDate }).ToList(); - // Create paged result - var pagedResult = new PagedResult - { - Items = userDtos, - PageNumber = gridRequest.PageNumber, - PageSize = gridRequest.PageSize, - TotalCount = totalCount - }; + var pagedResult = PagedResult.From(gridRequest, userDtos, totalCount); // Set ViewBag for sorting and filters ViewBag.SearchTerm = searchTerm; diff --git a/src/PowderCoating.Web/Controllers/CustomersController.cs b/src/PowderCoating.Web/Controllers/CustomersController.cs index c1d3b85..c4b6daf 100644 --- a/src/PowderCoating.Web/Controllers/CustomersController.cs +++ b/src/PowderCoating.Web/Controllers/CustomersController.cs @@ -123,14 +123,7 @@ public class CustomersController : Controller LastContactDate = c.LastContactDate }).ToList(); - // Create paged result - var pagedResult = new PagedResult - { - Items = customerDtos, - PageNumber = gridRequest.PageNumber, - PageSize = gridRequest.PageSize, - TotalCount = totalCount - }; + var pagedResult = PagedResult.From(gridRequest, customerDtos, totalCount); // Set ViewBag for sorting ViewBag.SearchTerm = searchTerm; diff --git a/src/PowderCoating.Web/Controllers/EquipmentController.cs b/src/PowderCoating.Web/Controllers/EquipmentController.cs index 999c78a..5bbc385 100644 --- a/src/PowderCoating.Web/Controllers/EquipmentController.cs +++ b/src/PowderCoating.Web/Controllers/EquipmentController.cs @@ -121,14 +121,7 @@ public class EquipmentController : Controller // Map to DTOs var equipmentDtos = _mapper.Map>(items); - // Create paged result - var pagedResult = new PagedResult - { - Items = equipmentDtos, - PageNumber = gridRequest.PageNumber, - PageSize = gridRequest.PageSize, - TotalCount = totalCount - }; + var pagedResult = PagedResult.From(gridRequest, equipmentDtos, totalCount); // Set ViewBag for sorting and filters ViewBag.SearchTerm = searchTerm; diff --git a/src/PowderCoating.Web/Controllers/ExpensesController.cs b/src/PowderCoating.Web/Controllers/ExpensesController.cs index 9d65386..8fb04a2 100644 --- a/src/PowderCoating.Web/Controllers/ExpensesController.cs +++ b/src/PowderCoating.Web/Controllers/ExpensesController.cs @@ -8,12 +8,14 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using PowderCoating.Application.Configuration; +using PowderCoating.Application.Services; using PowderCoating.Application.DTOs.Accounting; using PowderCoating.Application.DTOs.AI; using PowderCoating.Application.Interfaces; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; using PowderCoating.Core.Interfaces; +using PowderCoating.Web.Helpers; namespace PowderCoating.Web.Controllers; @@ -148,11 +150,15 @@ public class ExpensesController : Controller return View(dto); } - if (receiptFile != null && !IsValidReceiptFile(receiptFile, out var fileError)) + if (receiptFile != null) { - ModelState.AddModelError(string.Empty, fileError); - await PopulateDropdownsAsync(); - return View(dto); + var (receiptValid, _, receiptError) = BlobFileHelper.ValidateUpload(receiptFile, AllowedReceiptTypes, MaxReceiptBytes); + if (!receiptValid) + { + ModelState.AddModelError(string.Empty, receiptError); + await PopulateDropdownsAsync(); + return View(dto); + } } try @@ -228,11 +234,15 @@ public class ExpensesController : Controller return View(dto); } - if (receiptFile != null && !IsValidReceiptFile(receiptFile, out var fileError)) + if (receiptFile != null) { - ModelState.AddModelError(string.Empty, fileError); - await PopulateDropdownsAsync(); - return View(dto); + var (receiptValid, _, receiptError) = BlobFileHelper.ValidateUpload(receiptFile, AllowedReceiptTypes, MaxReceiptBytes); + if (!receiptValid) + { + ModelState.AddModelError(string.Empty, receiptError); + await PopulateDropdownsAsync(); + return View(dto); + } } try @@ -345,7 +355,7 @@ public class ExpensesController : Controller // Inline for images so the browser previews them; attachment for PDFs triggers download var ext = Path.GetExtension(expense.ReceiptFilePath).ToLowerInvariant(); - var contentType = result.ContentType.Length > 0 ? result.ContentType : MimeFromExt(ext); + var contentType = result.ContentType.Length > 0 ? result.ContentType : BlobFileHelper.GetContentType(ext); var filename = $"Receipt-{expense.ExpenseNumber}{ext}"; Response.Headers["Content-Disposition"] = ext == ".pdf" @@ -392,39 +402,12 @@ public class ExpensesController : Controller /// private async Task PopulateDropdownsAsync() { - var allAccounts = await _unitOfWork.Accounts.FindAsync(a => a.IsActive); - - ViewBag.ExpenseAccounts = allAccounts - .Where(a => a.AccountType == AccountType.Expense || a.AccountType == AccountType.CostOfGoods) - .OrderBy(a => a.AccountNumber) - .Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString())) - .ToList(); - - ViewBag.PaymentAccounts = allAccounts - .Where(a => a.AccountSubType == AccountSubType.Cash || - a.AccountSubType == AccountSubType.Checking || - a.AccountSubType == AccountSubType.Savings || - a.AccountSubType == AccountSubType.CreditCard) - .OrderBy(a => a.AccountNumber) - .Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString())) - .ToList(); - - ViewBag.Vendors = (await _unitOfWork.Vendors.FindAsync(s => s.IsActive)) - .OrderBy(s => s.CompanyName) - .Select(s => new SelectListItem(s.CompanyName, s.Id.ToString())) - .ToList(); - - ViewBag.Jobs = (await _unitOfWork.Jobs.FindAsync(j => - j.JobStatus.StatusCode != "COMPLETED" && - j.JobStatus.StatusCode != "CANCELLED" && - j.JobStatus.StatusCode != "DELIVERED")) - .OrderBy(j => j.JobNumber) - .Select(j => new SelectListItem($"{j.JobNumber} – {j.Description ?? "No description"}", j.Id.ToString())) - .ToList(); - - ViewBag.PaymentMethods = Enum.GetValues() - .Select(m => new SelectListItem(m.ToString(), ((int)m).ToString())) - .ToList(); + var dd = await AccountingDropdownHelper.LoadAsync(_unitOfWork); + ViewBag.ExpenseAccounts = dd.ExpenseAccounts; + ViewBag.PaymentAccounts = dd.BankAccounts; + ViewBag.Vendors = dd.Vendors; + ViewBag.Jobs = dd.ActiveJobs; + ViewBag.PaymentMethods = dd.PaymentMethods; } /// @@ -458,10 +441,8 @@ public class ExpensesController : Controller { var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); var blobName = $"{companyId}/expense-receipts/{expenseId}{ext}"; - var contentType = MimeFromExt(ext); - using var stream = file.OpenReadStream(); - var result = await _blobStorage.UploadAsync(_storageSettings.Containers.ReceiptImages, blobName, stream, contentType); + var result = await _blobStorage.UploadAsync(_storageSettings.Containers.ReceiptImages, blobName, stream, BlobFileHelper.GetContentType(ext)); if (!result.Success) { _logger.LogError("Receipt upload failed for expense {Id}: {Error}", expenseId, result.ErrorMessage); @@ -470,35 +451,7 @@ public class ExpensesController : Controller return blobName; } - /// - /// Validates a receipt file against the allowed extension whitelist and the 10 MB size cap. - /// Returns false and sets when validation fails. - /// - private static bool IsValidReceiptFile(IFormFile file, out string error) - { - var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); - if (!AllowedReceiptTypes.Contains(ext)) - { - error = $"File type '{ext}' is not allowed. Accepted types: {string.Join(", ", AllowedReceiptTypes)}"; - return false; - } - if (file.Length > MaxReceiptBytes) - { - error = "Receipt file must be 10 MB or smaller."; - return false; - } - error = string.Empty; - return true; - } - private static string MimeFromExt(string ext) => ext switch - { - ".pdf" => "application/pdf", - ".png" => "image/png", - ".gif" => "image/gif", - ".webp" => "image/webp", - _ => "image/jpeg" - }; // ── AI: Account Suggestion ──────────────────────────────────────────────── diff --git a/src/PowderCoating.Web/Controllers/InventoryController.cs b/src/PowderCoating.Web/Controllers/InventoryController.cs index 6daeeca..63be478 100644 --- a/src/PowderCoating.Web/Controllers/InventoryController.cs +++ b/src/PowderCoating.Web/Controllers/InventoryController.cs @@ -154,14 +154,7 @@ public class InventoryController : Controller // Map to DTOs using AutoMapper var itemDtos = _mapper.Map>(items); - // Create paged result - var pagedResult = new PagedResult - { - Items = itemDtos, - PageNumber = gridRequest.PageNumber, - PageSize = gridRequest.PageSize, - TotalCount = totalCount - }; + var pagedResult = PagedResult.From(gridRequest, itemDtos, totalCount); // Load all items once to compute sidebar stats and category list in memory var allItems = (await _unitOfWork.InventoryItems.GetAllAsync()).ToList(); diff --git a/src/PowderCoating.Web/Controllers/InvoicesController.cs b/src/PowderCoating.Web/Controllers/InvoicesController.cs index 94b9a8c..8c1eb1a 100644 --- a/src/PowderCoating.Web/Controllers/InvoicesController.cs +++ b/src/PowderCoating.Web/Controllers/InvoicesController.cs @@ -208,16 +208,8 @@ public class InvoicesController : Controller var dtos = _mapper.Map>(items); - var pagedResult = new PagedResult - { - Items = dtos, - PageNumber = gridRequest.PageNumber, - PageSize = gridRequest.PageSize, - TotalCount = totalCount, - SortColumn = gridRequest.SortColumn, - SortDirection = gridRequest.SortDirection, - SearchTerm = searchTerm - }; + var pagedResult = PagedResult.From(gridRequest, dtos, totalCount); + pagedResult.SearchTerm = searchTerm; ViewBag.SearchTerm = searchTerm; ViewBag.StatusFilter = statusFilter; diff --git a/src/PowderCoating.Web/Controllers/JobTemplatesController.cs b/src/PowderCoating.Web/Controllers/JobTemplatesController.cs index 5434cb4..9e15bfc 100644 --- a/src/PowderCoating.Web/Controllers/JobTemplatesController.cs +++ b/src/PowderCoating.Web/Controllers/JobTemplatesController.cs @@ -159,6 +159,8 @@ public class JobTemplatesController : Controller CatalogItemId = item.CatalogItemId, IsGenericItem = item.IsGenericItem, IsLaborItem = item.IsLaborItem, + IsSalesItem = item.IsSalesItem, + Sku = item.Sku, ManualUnitPrice = item.ManualUnitPrice, RequiresSandblasting = item.RequiresSandblasting, RequiresMasking = item.RequiresMasking, @@ -248,6 +250,8 @@ public class JobTemplatesController : Controller catalogItemId = i.CatalogItemId, isGenericItem = i.IsGenericItem, isLaborItem = i.IsLaborItem, + isSalesItem = i.IsSalesItem, + sku = i.Sku, manualUnitPrice = i.ManualUnitPrice, requiresSandblasting = i.RequiresSandblasting, requiresMasking = i.RequiresMasking, diff --git a/src/PowderCoating.Web/Controllers/JobsController.cs b/src/PowderCoating.Web/Controllers/JobsController.cs index b1e4ad9..22cd5f1 100644 --- a/src/PowderCoating.Web/Controllers/JobsController.cs +++ b/src/PowderCoating.Web/Controllers/JobsController.cs @@ -34,6 +34,7 @@ public class JobsController : Controller private readonly INotificationService _notificationService; private readonly ISubscriptionService _subscriptionService; private readonly IPricingCalculationService _pricingService; + private readonly IJobItemAssemblyService _jobItemAssemblyService; private readonly IHubContext _hub; private readonly IHubContext _shopHub; @@ -49,6 +50,7 @@ public class JobsController : Controller INotificationService notificationService, ISubscriptionService subscriptionService, IPricingCalculationService pricingService, + IJobItemAssemblyService jobItemAssemblyService, IHubContext hub, IHubContext shopHub) { @@ -63,6 +65,7 @@ public class JobsController : Controller _notificationService = notificationService; _subscriptionService = subscriptionService; _pricingService = pricingService; + _jobItemAssemblyService = jobItemAssemblyService; _hub = hub; _shopHub = shopHub; } @@ -185,14 +188,9 @@ public class JobsController : Controller .Contains(tagLower)).ToList(); } - // Create paged result - var pagedResult = new PagedResult - { - Items = jobDtos, - PageNumber = gridRequest.PageNumber, - PageSize = gridRequest.PageSize, - TotalCount = string.IsNullOrWhiteSpace(tagFilter) ? totalCount : jobDtos.Count - }; + var pagedResult = PagedResult.From( + gridRequest, jobDtos, + string.IsNullOrWhiteSpace(tagFilter) ? totalCount : jobDtos.Count); // Set ViewBag for sorting ViewBag.SearchTerm = searchTerm; @@ -1026,8 +1024,8 @@ public class JobsController : Controller catalogItemId = i.CatalogItemId, isGenericItem = i.IsGenericItem, isLaborItem = i.IsLaborItem, - isSalesItem = false, // JobTemplateItem doesn't have IsSalesItem — default false - sku = (string?)null, + isSalesItem = i.IsSalesItem, + sku = i.Sku, manualUnitPrice = i.ManualUnitPrice, requiresSandblasting = i.RequiresSandblasting, requiresMasking = i.RequiresMasking, @@ -1147,82 +1145,20 @@ public class JobsController : Controller { var itemPricing = await _pricingService.CalculateQuoteItemPriceAsync( itemDto, companyId, null); - - var jobItem = new JobItem - { - JobId = job.Id, - Description = itemDto.Description, - Quantity = itemDto.Quantity, - SurfaceAreaSqFt = itemDto.SurfaceAreaSqFt, - EstimatedMinutes = itemDto.EstimatedMinutes, - CatalogItemId = itemDto.CatalogItemId, - IsGenericItem = itemDto.IsGenericItem, - IsLaborItem = itemDto.IsLaborItem, - IsSalesItem = itemDto.IsSalesItem, - Sku = itemDto.Sku, - ManualUnitPrice = itemDto.ManualUnitPrice, - PowderCostOverride = itemDto.PowderCostOverride, - RequiresSandblasting = itemDto.RequiresSandblasting, - RequiresMasking = itemDto.RequiresMasking, - Notes = itemDto.Notes, - IncludePrepCost = itemDto.IncludePrepCost, - Complexity = itemDto.Complexity, - UnitPrice = itemPricing.UnitPrice, - TotalPrice = itemPricing.TotalPrice, - LaborCost = itemPricing.TotalPrice * 0.4m, - CompanyId = companyId, - CreatedAt = DateTime.UtcNow - }; + var createdAtUtc = DateTime.UtcNow; + var jobItem = _jobItemAssemblyService.CreateJobItem(itemDto, job.Id, companyId, itemPricing, createdAtUtc); await _unitOfWork.JobItems.AddAsync(jobItem); await _unitOfWork.SaveChangesAsync(); - if (itemDto.Coats?.Any() == true) + foreach (var coat in _jobItemAssemblyService.CreateJobItemCoats(itemDto, jobItem.Id, companyId, createdAtUtc)) { - foreach (var coatDto in itemDto.Coats.OrderBy(c => c.Sequence)) - { - decimal? powderToOrder = coatDto.PowderToOrder; - if ((powderToOrder == null || powderToOrder == 0) && itemDto.SurfaceAreaSqFt > 0) - { - var cov = coatDto.CoverageSqFtPerLb > 0 ? coatDto.CoverageSqFtPerLb : 30m; - var eff = coatDto.TransferEfficiency > 0 ? coatDto.TransferEfficiency / 100m : 0.65m; - powderToOrder = Math.Round((itemDto.SurfaceAreaSqFt * itemDto.Quantity) / (cov * eff), 2); - } - await _unitOfWork.JobItemCoats.AddAsync(new JobItemCoat - { - JobItemId = jobItem.Id, - CoatName = coatDto.CoatName, - Sequence = coatDto.Sequence, - InventoryItemId = coatDto.InventoryItemId, - ColorName = coatDto.ColorName, - VendorId = coatDto.VendorId, - ColorCode = coatDto.ColorCode, - Finish = coatDto.Finish, - CoverageSqFtPerLb = coatDto.CoverageSqFtPerLb, - TransferEfficiency = coatDto.TransferEfficiency, - PowderCostPerLb = coatDto.PowderCostPerLb, - PowderToOrder = powderToOrder, - Notes = coatDto.Notes, - CompanyId = companyId, - CreatedAt = DateTime.UtcNow - }); - } + await _unitOfWork.JobItemCoats.AddAsync(coat); } - if (itemDto.PrepServices?.Any() == true) + foreach (var prepService in _jobItemAssemblyService.CreateJobItemPrepServices(itemDto, jobItem.Id, companyId, createdAtUtc)) { - foreach (var psDto in itemDto.PrepServices) - { - await _unitOfWork.JobItemPrepServices.AddAsync(new JobItemPrepService - { - JobItemId = jobItem.Id, - PrepServiceId = psDto.PrepServiceId, - EstimatedMinutes = psDto.EstimatedMinutes, - BlastSetupId = psDto.BlastSetupId, - CompanyId = companyId, - CreatedAt = DateTime.UtcNow - }); - } + await _unitOfWork.JobItemPrepServices.AddAsync(prepService); } } @@ -1416,79 +1352,20 @@ public class JobsController : Controller { var itemPricing = await _pricingService.CalculateQuoteItemPriceAsync( itemDto, companyId, null); - - var jobItem = new JobItem - { - JobId = id, - Description = itemDto.Description, - Quantity = itemDto.Quantity, - SurfaceAreaSqFt = itemDto.SurfaceAreaSqFt, - EstimatedMinutes = itemDto.EstimatedMinutes, - CatalogItemId = itemDto.CatalogItemId, - IsGenericItem = itemDto.IsGenericItem, - IsLaborItem = itemDto.IsLaborItem, - ManualUnitPrice = itemDto.ManualUnitPrice, - PowderCostOverride = itemDto.PowderCostOverride, - RequiresSandblasting = itemDto.RequiresSandblasting, - RequiresMasking = itemDto.RequiresMasking, - Notes = itemDto.Notes, - IncludePrepCost = itemDto.IncludePrepCost, - Complexity = itemDto.Complexity, - UnitPrice = itemPricing.UnitPrice, - TotalPrice = itemPricing.TotalPrice, - LaborCost = itemPricing.TotalPrice * 0.4m, - CompanyId = companyId, - CreatedAt = DateTime.UtcNow - }; + var createdAtUtc = DateTime.UtcNow; + var jobItem = _jobItemAssemblyService.CreateJobItem(itemDto, id, companyId, itemPricing, createdAtUtc); await _unitOfWork.JobItems.AddAsync(jobItem); await _unitOfWork.SaveChangesAsync(); - if (itemDto.Coats?.Any() == true) + foreach (var coat in _jobItemAssemblyService.CreateJobItemCoats(itemDto, jobItem.Id, companyId, createdAtUtc)) { - foreach (var coatDto in itemDto.Coats.OrderBy(c => c.Sequence)) - { - decimal? powderToOrder = coatDto.PowderToOrder; - if ((powderToOrder == null || powderToOrder == 0) && itemDto.SurfaceAreaSqFt > 0) - { - var cov = coatDto.CoverageSqFtPerLb > 0 ? coatDto.CoverageSqFtPerLb : 30m; - var eff = coatDto.TransferEfficiency > 0 ? coatDto.TransferEfficiency / 100m : 0.65m; - powderToOrder = Math.Round((itemDto.SurfaceAreaSqFt * itemDto.Quantity) / (cov * eff), 2); - } - await _unitOfWork.JobItemCoats.AddAsync(new JobItemCoat - { - JobItemId = jobItem.Id, - CoatName = coatDto.CoatName, - Sequence = coatDto.Sequence, - InventoryItemId = coatDto.InventoryItemId, - ColorName = coatDto.ColorName, - VendorId = coatDto.VendorId, - ColorCode = coatDto.ColorCode, - Finish = coatDto.Finish, - CoverageSqFtPerLb = coatDto.CoverageSqFtPerLb, - TransferEfficiency = coatDto.TransferEfficiency, - PowderCostPerLb = coatDto.PowderCostPerLb, - PowderToOrder = powderToOrder, - Notes = coatDto.Notes, - CompanyId = companyId, - CreatedAt = DateTime.UtcNow - }); - } + await _unitOfWork.JobItemCoats.AddAsync(coat); } - if (itemDto.PrepServices?.Any() == true) + foreach (var prepService in _jobItemAssemblyService.CreateJobItemPrepServices(itemDto, jobItem.Id, companyId, createdAtUtc)) { - foreach (var psDto in itemDto.PrepServices) - { - await _unitOfWork.JobItemPrepServices.AddAsync(new JobItemPrepService - { - JobItemId = jobItem.Id, - PrepServiceId = psDto.PrepServiceId, - EstimatedMinutes = psDto.EstimatedMinutes, - CompanyId = companyId, - CreatedAt = DateTime.UtcNow - }); - } + await _unitOfWork.JobItemPrepServices.AddAsync(prepService); } } @@ -3130,86 +3007,20 @@ public class JobsController : Controller { var itemPricing = await _pricingService.CalculateQuoteItemPriceAsync( itemDto, currentUser.CompanyId, null); - - var jobItem = new JobItem - { - JobId = job.Id, - Description = itemDto.Description, - Quantity = itemDto.Quantity, - SurfaceAreaSqFt = itemDto.SurfaceAreaSqFt, - EstimatedMinutes = itemDto.EstimatedMinutes, - CatalogItemId = itemDto.CatalogItemId, - IsGenericItem = itemDto.IsGenericItem, - IsLaborItem = itemDto.IsLaborItem, - ManualUnitPrice = itemDto.ManualUnitPrice, - PowderCostOverride = itemDto.PowderCostOverride, - RequiresSandblasting = itemDto.RequiresSandblasting, - RequiresMasking = itemDto.RequiresMasking, - Notes = itemDto.Notes, - IncludePrepCost = itemDto.IncludePrepCost, - Complexity = itemDto.Complexity, - UnitPrice = itemPricing.UnitPrice, - TotalPrice = itemPricing.TotalPrice, - LaborCost = itemPricing.TotalPrice * 0.4m, - CompanyId = currentUser.CompanyId, - CreatedAt = DateTime.UtcNow - }; + var createdAtUtc = DateTime.UtcNow; + var jobItem = _jobItemAssemblyService.CreateJobItem(itemDto, job.Id, currentUser.CompanyId, itemPricing, createdAtUtc); await _unitOfWork.JobItems.AddAsync(jobItem); await _unitOfWork.SaveChangesAsync(); - // Coats - if (itemDto.Coats?.Any() == true) + foreach (var coat in _jobItemAssemblyService.CreateJobItemCoats(itemDto, jobItem.Id, currentUser.CompanyId, createdAtUtc)) { - foreach (var coatDto in itemDto.Coats.OrderBy(c => c.Sequence)) - { - // Calculate PowderToOrder if not supplied by the client - decimal? powderToOrder = coatDto.PowderToOrder; - if ((powderToOrder == null || powderToOrder == 0) && itemDto.SurfaceAreaSqFt > 0) - { - var cov = coatDto.CoverageSqFtPerLb > 0 ? coatDto.CoverageSqFtPerLb : 30m; - var eff = coatDto.TransferEfficiency > 0 ? coatDto.TransferEfficiency / 100m : 0.65m; - powderToOrder = Math.Round((itemDto.SurfaceAreaSqFt * itemDto.Quantity) / (cov * eff), 2); - } - - var coat = new JobItemCoat - { - JobItemId = jobItem.Id, - CoatName = coatDto.CoatName, - Sequence = coatDto.Sequence, - InventoryItemId = coatDto.InventoryItemId, - ColorName = coatDto.ColorName, - VendorId = coatDto.VendorId, - ColorCode = coatDto.ColorCode, - Finish = coatDto.Finish, - CoverageSqFtPerLb = coatDto.CoverageSqFtPerLb, - TransferEfficiency = coatDto.TransferEfficiency, - PowderCostPerLb = coatDto.PowderCostPerLb, - PowderToOrder = powderToOrder, - Notes = coatDto.Notes, - CompanyId = currentUser.CompanyId, - CreatedAt = DateTime.UtcNow - }; - await _unitOfWork.JobItemCoats.AddAsync(coat); - } + await _unitOfWork.JobItemCoats.AddAsync(coat); } - // Prep services - if (itemDto.PrepServices?.Any() == true) + foreach (var prepService in _jobItemAssemblyService.CreateJobItemPrepServices(itemDto, jobItem.Id, currentUser.CompanyId, createdAtUtc)) { - foreach (var psDto in itemDto.PrepServices) - { - var ps = new JobItemPrepService - { - JobItemId = jobItem.Id, - PrepServiceId = psDto.PrepServiceId, - EstimatedMinutes = psDto.EstimatedMinutes, - BlastSetupId = psDto.BlastSetupId, - CompanyId = currentUser.CompanyId, - CreatedAt = DateTime.UtcNow - }; - await _unitOfWork.JobItemPrepServices.AddAsync(ps); - } + await _unitOfWork.JobItemPrepServices.AddAsync(prepService); } } @@ -3638,60 +3449,20 @@ public class JobsController : Controller foreach (var item in itemsToCopy) { - var newItem = new JobItem - { - JobId = reworkJob.Id, - Description = item.Description, - Quantity = item.Quantity, - SurfaceAreaSqFt = item.SurfaceAreaSqFt, - CatalogItemId = item.CatalogItemId, - IsGenericItem = item.IsGenericItem, - IsLaborItem = item.IsLaborItem, - ManualUnitPrice = item.ManualUnitPrice, - RequiresSandblasting = item.RequiresSandblasting, - RequiresMasking = item.RequiresMasking, - IncludePrepCost = item.IncludePrepCost, - EstimatedMinutes = item.EstimatedMinutes, - Complexity = item.Complexity, - Notes = item.Notes, - CompanyId = companyId, - CreatedAt = DateTime.UtcNow - }; + var createdAtUtc = DateTime.UtcNow; + var newItem = _jobItemAssemblyService.CreateJobItem(item, reworkJob.Id, companyId, createdAtUtc); await _unitOfWork.JobItems.AddAsync(newItem); await _unitOfWork.CompleteAsync(); - foreach (var coat in item.Coats.OrderBy(c => c.Sequence)) + foreach (var coat in _jobItemAssemblyService.CreateJobItemCoats(item, newItem.Id, companyId, createdAtUtc)) { - await _unitOfWork.JobItemCoats.AddAsync(new JobItemCoat - { - JobItemId = newItem.Id, - CoatName = coat.CoatName, - Sequence = coat.Sequence, - InventoryItemId = coat.InventoryItemId, - ColorName = coat.ColorName, - VendorId = coat.VendorId, - ColorCode = coat.ColorCode, - Finish = coat.Finish, - CoverageSqFtPerLb = coat.CoverageSqFtPerLb, - TransferEfficiency = coat.TransferEfficiency, - PowderCostPerLb = coat.PowderCostPerLb, - Notes = coat.Notes, - CompanyId = companyId, - CreatedAt = DateTime.UtcNow - }); + await _unitOfWork.JobItemCoats.AddAsync(coat); } - foreach (var prep in item.PrepServices) + foreach (var prepService in _jobItemAssemblyService.CreateJobItemPrepServices(item, newItem.Id, companyId, createdAtUtc)) { - await _unitOfWork.JobItemPrepServices.AddAsync(new JobItemPrepService - { - JobItemId = newItem.Id, - PrepServiceId = prep.PrepServiceId, - EstimatedMinutes = prep.EstimatedMinutes, - CompanyId = companyId, - CreatedAt = DateTime.UtcNow - }); + await _unitOfWork.JobItemPrepServices.AddAsync(prepService); } } @@ -3910,95 +3681,31 @@ public class JobsController : Controller foreach (var quoteItem in fullItems.Where(qi => !qi.IsDeleted)) { - var firstCoat = quoteItem.Coats?.OrderBy(c => c.Sequence).FirstOrDefault(); - - var jobItem = new JobItem - { - JobId = id, - Description = quoteItem.Description, - Quantity = quoteItem.Quantity, - ColorName = firstCoat?.ColorName, - ColorCode = firstCoat?.ColorCode, - Finish = firstCoat?.Finish, - SurfaceArea = quoteItem.SurfaceAreaSqFt, - SurfaceAreaSqFt = quoteItem.SurfaceAreaSqFt, - CatalogItemId = quoteItem.CatalogItemId, - IsGenericItem = quoteItem.IsGenericItem, - IsLaborItem = quoteItem.IsLaborItem, - IsSalesItem = quoteItem.IsSalesItem, - Sku = quoteItem.Sku, - ManualUnitPrice = quoteItem.ManualUnitPrice, - PowderCostOverride = quoteItem.PowderCostOverride, - UnitPrice = quoteItem.UnitPrice, - TotalPrice = quoteItem.TotalPrice, - LaborCost = quoteItem.TotalPrice * 0.4m, - RequiresSandblasting = quoteItem.RequiresSandblasting, - RequiresMasking = quoteItem.RequiresMasking, - EstimatedMinutes = quoteItem.EstimatedMinutes, - Notes = quoteItem.Notes, - Complexity = quoteItem.Complexity, - AiTags = quoteItem.AiTags, - AiPredictionId = quoteItem.AiPredictionId, - IncludePrepCost = !quoteItem.CatalogItemId.HasValue, - CompanyId = job.CompanyId, - CreatedAt = DateTime.UtcNow - }; + var createdAtUtc = DateTime.UtcNow; + var jobItem = _jobItemAssemblyService.CreateJobItem(quoteItem, id, job.CompanyId, createdAtUtc); await _unitOfWork.JobItems.AddAsync(jobItem); await _unitOfWork.SaveChangesAsync(); - if (quoteItem.Coats != null) + foreach (var coat in _jobItemAssemblyService.CreateJobItemCoats(quoteItem, jobItem.Id, job.CompanyId, createdAtUtc)) { - foreach (var quoteCoat in quoteItem.Coats.OrderBy(c => c.Sequence)) - { - string colorName = quoteCoat.ColorName; - string colorCode = quoteCoat.ColorCode; - string finish = quoteCoat.Finish; + await _unitOfWork.JobItemCoats.AddAsync(coat); + } - if (quoteCoat.InventoryItemId.HasValue && quoteCoat.InventoryItem != null) - { - colorName = quoteCoat.InventoryItem.Name; - colorCode = quoteCoat.InventoryItem.ColorCode; - finish = quoteCoat.InventoryItem.Finish; - } - - var cov = quoteCoat.CoverageSqFtPerLb > 0 ? quoteCoat.CoverageSqFtPerLb : 30m; - var eff = quoteCoat.TransferEfficiency > 0 ? quoteCoat.TransferEfficiency / 100m : 0.65m; - var powderToOrder = (quoteCoat.PowderToOrder > 0) - ? quoteCoat.PowderToOrder - : (quoteItem.SurfaceAreaSqFt > 0 - ? Math.Round((quoteItem.SurfaceAreaSqFt * quoteItem.Quantity) / (cov * eff), 2) - : (decimal?)null); - - await _unitOfWork.JobItemCoats.AddAsync(new JobItemCoat - { - JobItemId = jobItem.Id, - CoatName = quoteCoat.CoatName, - Sequence = quoteCoat.Sequence, - InventoryItemId = quoteCoat.InventoryItemId, - ColorName = colorName, - VendorId = quoteCoat.VendorId, - ColorCode = colorCode, - Finish = finish, - CoverageSqFtPerLb = quoteCoat.CoverageSqFtPerLb, - TransferEfficiency = quoteCoat.TransferEfficiency, - PowderCostPerLb = quoteCoat.PowderCostPerLb, - PowderToOrder = powderToOrder, - Notes = quoteCoat.Notes, - CompanyId = job.CompanyId, - CreatedAt = DateTime.UtcNow - }); - } + foreach (var prepService in _jobItemAssemblyService.CreateJobItemPrepServices(quoteItem, jobItem.Id, job.CompanyId, createdAtUtc)) + { + await _unitOfWork.JobItemPrepServices.AddAsync(prepService); } } await _unitOfWork.SaveChangesAsync(); - // Aggregate prep services from all quote items and copy to job - var quoteItemIds = fullItems.Select(qi => qi.Id).ToList(); - var itemPrepServices = await _unitOfWork.QuoteItemPrepServices.FindAsync( - ps => quoteItemIds.Contains(ps.QuoteItemId)); - foreach (var prepServiceId in itemPrepServices.Select(ps => ps.PrepServiceId).Distinct()) + // Aggregate prep services from the fully-loaded quote items and copy to job + foreach (var prepServiceId in fullItems + .SelectMany(qi => qi.PrepServices) + .Where(ps => !ps.IsDeleted) + .Select(ps => ps.PrepServiceId) + .Distinct()) { await _unitOfWork.JobPrepServices.AddAsync(new JobPrepService { diff --git a/src/PowderCoating.Web/Controllers/MaintenanceController.cs b/src/PowderCoating.Web/Controllers/MaintenanceController.cs index 115b7a6..24e6eca 100644 --- a/src/PowderCoating.Web/Controllers/MaintenanceController.cs +++ b/src/PowderCoating.Web/Controllers/MaintenanceController.cs @@ -156,14 +156,7 @@ public class MaintenanceController : Controller // Map to DTOs var maintenanceDtos = _mapper.Map>(items); - // Create paged result - var pagedResult = new PagedResult - { - Items = maintenanceDtos, - PageNumber = gridRequest.PageNumber, - PageSize = gridRequest.PageSize, - TotalCount = totalCount - }; + var pagedResult = PagedResult.From(gridRequest, maintenanceDtos, totalCount); // Get equipment name if filtering by equipment if (equipmentId.HasValue) diff --git a/src/PowderCoating.Web/Controllers/PlatformUsersController.cs b/src/PowderCoating.Web/Controllers/PlatformUsersController.cs index c2000de..48a2622 100644 --- a/src/PowderCoating.Web/Controllers/PlatformUsersController.cs +++ b/src/PowderCoating.Web/Controllers/PlatformUsersController.cs @@ -170,14 +170,7 @@ public class PlatformUsersController : Controller totalCount = userDtos.Count; // Recalculate total for SuperAdmins } - // Create paged result - var pagedResult = new PagedResult - { - Items = userDtos, - PageNumber = gridRequest.PageNumber, - PageSize = gridRequest.PageSize, - TotalCount = totalCount - }; + var pagedResult = PagedResult.From(gridRequest, userDtos, totalCount); // Set ViewBag for sorting and filters ViewBag.CurrentFilter = filter; diff --git a/src/PowderCoating.Web/Controllers/QuotesController.cs b/src/PowderCoating.Web/Controllers/QuotesController.cs index 91ecaad..32b7105 100644 --- a/src/PowderCoating.Web/Controllers/QuotesController.cs +++ b/src/PowderCoating.Web/Controllers/QuotesController.cs @@ -33,6 +33,8 @@ public class QuotesController : Controller private readonly ILookupCacheService _lookupCache; private readonly INotificationService _notificationService; private readonly ISubscriptionService _subscriptionService; + private readonly IJobItemAssemblyService _jobItemAssemblyService; + private readonly IQuotePricingAssemblyService _quotePricingAssemblyService; private readonly IConfiguration _configuration; private readonly IPlatformSettingsService _platformSettings; private readonly IQuotePhotoService _photoService; @@ -55,6 +57,8 @@ public class QuotesController : Controller ILookupCacheService lookupCache, INotificationService notificationService, ISubscriptionService subscriptionService, + IJobItemAssemblyService jobItemAssemblyService, + IQuotePricingAssemblyService quotePricingAssemblyService, IConfiguration configuration, IPlatformSettingsService platformSettings, IQuotePhotoService photoService, @@ -76,6 +80,8 @@ public class QuotesController : Controller _lookupCache = lookupCache; _notificationService = notificationService; _subscriptionService = subscriptionService; + _jobItemAssemblyService = jobItemAssemblyService; + _quotePricingAssemblyService = quotePricingAssemblyService; _configuration = configuration; _platformSettings = platformSettings; _photoService = photoService; @@ -198,14 +204,9 @@ public class QuotesController : Controller .Contains(tagLower)).ToList(); } - // Create paged result - var pagedResult = new PagedResult - { - Items = quoteDtos, - PageNumber = gridRequest.PageNumber, - PageSize = gridRequest.PageSize, - TotalCount = string.IsNullOrWhiteSpace(tagFilter) ? totalCount : quoteDtos.Count - }; + var pagedResult = PagedResult.From( + gridRequest, quoteDtos, + string.IsNullOrWhiteSpace(tagFilter) ? totalCount : quoteDtos.Count); // Set ViewBag for sorting and filters ViewBag.SearchTerm = searchTerm; @@ -914,140 +915,19 @@ public class QuotesController : Controller } // Set calculated pricing — snapshot at save time; never recalculate on load - quote.MaterialCosts = pricingResult.MaterialCosts; - quote.LaborCosts = pricingResult.LaborCosts; - quote.EquipmentCosts = pricingResult.EquipmentCosts; - quote.ItemsSubtotal = pricingResult.ItemsSubtotal; - quote.OvenBatchCost = pricingResult.OvenBatchCost; - quote.ShopSuppliesAmount = pricingResult.ShopSuppliesAmount; - quote.ShopSuppliesPercent = pricingResult.ShopSuppliesPercent; - quote.OverheadAmount = pricingResult.OverheadCosts; - quote.OverheadPercent = pricingResult.OverheadPercent; - quote.ProfitMargin = pricingResult.ProfitMargin; - quote.ProfitPercent = pricingResult.ProfitPercent; - quote.SubTotal = pricingResult.SubtotalBeforeDiscount; - quote.DiscountPercent = pricingResult.DiscountPercent; - quote.DiscountAmount = pricingResult.DiscountAmount; - quote.RushFee = pricingResult.RushFee; - quote.TaxAmount = pricingResult.TaxAmount; - quote.Total = pricingResult.Total; + _quotePricingAssemblyService.ApplyPricingSnapshot(quote, pricingResult); // Add quote await _unitOfWork.Quotes.AddAsync(quote); await _unitOfWork.CompleteAsync(); // Create quote items with calculated pricing - var itemResults = new List(); - foreach (var itemDto in dto.QuoteItems) - { - var item = _mapper.Map(itemDto); - item.QuoteId = quote.Id; - item.CompanyId = currentUser.CompanyId; - - // AI items: use stored price (AI estimate or user override) — skip the pricing engine - if (itemDto.IsAiItem && itemDto.ManualUnitPrice.HasValue && itemDto.ManualUnitPrice.Value > 0) - { - item.UnitPrice = itemDto.ManualUnitPrice.Value; - item.TotalPrice = itemDto.ManualUnitPrice.Value * itemDto.Quantity; - _logger.LogInformation("AI item price: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); - } - // Sales/merchandise items: use the manually entered price directly — no coating calculation - else if (itemDto.IsSalesItem && itemDto.ManualUnitPrice.HasValue) - { - item.UnitPrice = itemDto.ManualUnitPrice.Value; - item.TotalPrice = itemDto.ManualUnitPrice.Value * itemDto.Quantity; - _logger.LogInformation("Sales item price: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); - } - // Catalog items: if they have coats, calculate with coats; otherwise use default price - else if (itemDto.CatalogItemId.HasValue) - { - // If catalog item has coats, calculate the full price with coat costs - if (itemDto.Coats != null && itemDto.Coats.Any()) - { - _logger.LogInformation("Calculating catalog item with {CoatCount} coats", itemDto.Coats.Count); - var itemPricing = await _pricingService.CalculateQuoteItemPriceAsync(itemDto, currentUser.CompanyId, ovenRateOverride); - item.UnitPrice = itemPricing.UnitPrice; - item.TotalPrice = itemPricing.TotalPrice; - item.ItemMaterialCost = itemPricing.MaterialCost; - item.ItemLaborCost = itemPricing.LaborCost; - item.ItemEquipmentCost = itemPricing.EquipmentCost; - _logger.LogInformation("Set prices: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); - } - else - { - // No coats - use catalog default price - var catalogItem = await _unitOfWork.CatalogItems.GetByIdAsync(itemDto.CatalogItemId.Value); - if (catalogItem != null) - { - item.UnitPrice = catalogItem.DefaultPrice; - item.TotalPrice = catalogItem.DefaultPrice * itemDto.Quantity; - _logger.LogInformation("Catalog item no coats: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); - } - } - } - else - { - // Calculated items use the pricing service - _logger.LogInformation("Calculating custom item with {CoatCount} coats", itemDto.Coats?.Count ?? 0); - var itemPricing = await _pricingService.CalculateQuoteItemPriceAsync(itemDto, currentUser.CompanyId, ovenRateOverride); - item.UnitPrice = itemPricing.UnitPrice; - item.TotalPrice = itemPricing.TotalPrice; - item.ItemMaterialCost = itemPricing.MaterialCost; - item.ItemLaborCost = itemPricing.LaborCost; - item.ItemEquipmentCost = itemPricing.EquipmentCost; - _logger.LogInformation("Set prices: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); - } - - // Flag whether the user overrode the AI's estimates before accepting - await UpdateAiPredictionOverrideAsync(itemDto, item.UnitPrice); - - // Map coats for this item with calculated costs - if (itemDto.Coats != null && itemDto.Coats.Any()) - { - item.Coats = new List(); - for (int coatIndex = 0; coatIndex < itemDto.Coats.Count; coatIndex++) - { - var coatDto = itemDto.Coats[coatIndex]; - - // If "Add to inventory as Incoming" was checked on the custom tab, - // create a 0-balance inventory record so QR codes work on the work order. - if (coatDto.AddAsIncoming && coatDto.CatalogItemId.HasValue && !coatDto.InventoryItemId.HasValue) - coatDto.InventoryItemId = await CreateIncomingInventoryItemAsync(coatDto, currentUser.CompanyId); - - var coat = _mapper.Map(coatDto); - coat.CompanyId = currentUser.CompanyId; - - // Calculate and store the coat costs - var coatPricing = await _pricingService.CalculateCoatPriceAsync( - coatDto, - itemDto.SurfaceAreaSqFt, - itemDto.Quantity, - coatIndex, - itemDto.EstimatedMinutes, - currentUser.CompanyId); - - coat.CoatMaterialCost = coatPricing.CoatMaterialCost; - coat.CoatLaborCost = coatPricing.CoatLaborCost; - coat.CoatTotalCost = coatPricing.CoatTotalCost; - - item.Coats.Add(coat); - } - } - - // Map per-item prep services - if (itemDto.PrepServices != null && itemDto.PrepServices.Any()) - { - item.PrepServices = new List(); - foreach (var psDto in itemDto.PrepServices) - { - var prepService = _mapper.Map(psDto); - prepService.CompanyId = currentUser.CompanyId; - item.PrepServices.Add(prepService); - } - } - - itemResults.Add(item); - } + var itemResults = await _quotePricingAssemblyService.CreateQuoteItemsAsync( + dto.QuoteItems, + quote.Id, + currentUser.CompanyId, + ovenRateOverride, + DateTime.UtcNow); foreach (var item in itemResults) { @@ -1444,23 +1324,7 @@ public class QuotesController : Controller quote.ProspectSmsConsentedAt = null; // Set calculated pricing — snapshot at save time; never recalculate on load - quote.MaterialCosts = pricingResult.MaterialCosts; - quote.LaborCosts = pricingResult.LaborCosts; - quote.EquipmentCosts = pricingResult.EquipmentCosts; - quote.ItemsSubtotal = pricingResult.ItemsSubtotal; - quote.OvenBatchCost = pricingResult.OvenBatchCost; - quote.ShopSuppliesAmount = pricingResult.ShopSuppliesAmount; - quote.ShopSuppliesPercent = pricingResult.ShopSuppliesPercent; - quote.OverheadAmount = pricingResult.OverheadCosts; - quote.OverheadPercent = pricingResult.OverheadPercent; - quote.ProfitMargin = pricingResult.ProfitMargin; - quote.ProfitPercent = pricingResult.ProfitPercent; - quote.SubTotal = pricingResult.SubtotalBeforeDiscount; - quote.DiscountPercent = pricingResult.DiscountPercent; - quote.DiscountAmount = pricingResult.DiscountAmount; - quote.RushFee = pricingResult.RushFee; - quote.TaxAmount = pricingResult.TaxAmount; - quote.Total = pricingResult.Total; + _quotePricingAssemblyService.ApplyPricingSnapshot(quote, pricingResult); // Track changes var changeHistories = new List(); @@ -1704,121 +1568,25 @@ public class QuotesController : Controller // Create new quote items with calculated pricing var newItemsForComparison = new List<(string Description, decimal Quantity, decimal UnitPrice, decimal TotalPrice, bool Sandblasting, bool Masking, decimal? SurfaceArea, string? Notes)>(); - foreach (var itemDto in dto.QuoteItems) + var assembledItems = await _quotePricingAssemblyService.CreateQuoteItemsAsync( + dto.QuoteItems, + quote.Id, + currentUser.CompanyId, + ovenRateOverride, + DateTime.UtcNow); + + foreach (var item in assembledItems) { - var item = _mapper.Map(itemDto); - item.QuoteId = quote.Id; - item.CompanyId = currentUser.CompanyId; - - _logger.LogInformation("Creating item: {Desc}, Sandblasting={Sand}, Masking={Mask} (from DTO: Sand={DtoSand}, Mask={DtoMask})", - item.Description, item.RequiresSandblasting, item.RequiresMasking, - itemDto.RequiresSandblasting, itemDto.RequiresMasking); - - // AI items: use stored price (AI estimate or user override) — skip the pricing engine - if (itemDto.IsAiItem && itemDto.ManualUnitPrice.HasValue && itemDto.ManualUnitPrice.Value > 0) - { - item.UnitPrice = itemDto.ManualUnitPrice.Value; - item.TotalPrice = itemDto.ManualUnitPrice.Value * itemDto.Quantity; - _logger.LogInformation("AI item price: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); - } - // Sales/merchandise items: use the manually entered price directly — no coating calculation - else if (itemDto.IsSalesItem && itemDto.ManualUnitPrice.HasValue) - { - item.UnitPrice = itemDto.ManualUnitPrice.Value; - item.TotalPrice = itemDto.ManualUnitPrice.Value * itemDto.Quantity; - _logger.LogInformation("Sales item price: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); - } - // Catalog items: if they have coats, calculate with coats; otherwise use default price - else if (itemDto.CatalogItemId.HasValue) - { - // If catalog item has coats, calculate the full price with coat costs - if (itemDto.Coats != null && itemDto.Coats.Any()) - { - _logger.LogInformation("Calculating catalog item with {CoatCount} coats", itemDto.Coats.Count); - var itemPricing = await _pricingService.CalculateQuoteItemPriceAsync(itemDto, currentUser.CompanyId, ovenRateOverride); - item.UnitPrice = itemPricing.UnitPrice; - item.TotalPrice = itemPricing.TotalPrice; - item.ItemMaterialCost = itemPricing.MaterialCost; - item.ItemLaborCost = itemPricing.LaborCost; - item.ItemEquipmentCost = itemPricing.EquipmentCost; - _logger.LogInformation("Set prices: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); - } - else - { - // No coats - use catalog default price - var catalogItem = await _unitOfWork.CatalogItems.GetByIdAsync(itemDto.CatalogItemId.Value); - if (catalogItem != null) - { - item.UnitPrice = catalogItem.DefaultPrice; - item.TotalPrice = catalogItem.DefaultPrice * itemDto.Quantity; - _logger.LogInformation("Catalog item no coats: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); - } - } - } - else - { - // Calculated items use the pricing service - _logger.LogInformation("Calculating custom item with {CoatCount} coats", itemDto.Coats?.Count ?? 0); - var itemPricing = await _pricingService.CalculateQuoteItemPriceAsync(itemDto, currentUser.CompanyId, ovenRateOverride); - item.UnitPrice = itemPricing.UnitPrice; - item.TotalPrice = itemPricing.TotalPrice; - item.ItemMaterialCost = itemPricing.MaterialCost; - item.ItemLaborCost = itemPricing.LaborCost; - item.ItemEquipmentCost = itemPricing.EquipmentCost; - _logger.LogInformation("Set prices: UnitPrice={Unit}, TotalPrice={Total}", item.UnitPrice, item.TotalPrice); - } - - // Flag whether the user overrode the AI's estimates before accepting - await UpdateAiPredictionOverrideAsync(itemDto, item.UnitPrice); + _logger.LogInformation("Creating item: {Desc}, Sandblasting={Sand}, Masking={Mask}", + item.Description, item.RequiresSandblasting, item.RequiresMasking); await _unitOfWork.QuoteItems.AddAsync(item); - // Map coats for this item with calculated costs - if (itemDto.Coats != null && itemDto.Coats.Any()) + if (item.Coats?.Any() == true) { - item.Coats = new List(); - for (int coatIndex = 0; coatIndex < itemDto.Coats.Count; coatIndex++) - { - var coatDto = itemDto.Coats[coatIndex]; - - if (coatDto.AddAsIncoming && coatDto.CatalogItemId.HasValue && !coatDto.InventoryItemId.HasValue) - coatDto.InventoryItemId = await CreateIncomingInventoryItemAsync(coatDto, currentUser.CompanyId); - - var coat = _mapper.Map(coatDto); - coat.CompanyId = currentUser.CompanyId; - - // Calculate and store the coat costs - var coatPricing = await _pricingService.CalculateCoatPriceAsync( - coatDto, - itemDto.SurfaceAreaSqFt, - itemDto.Quantity, - coatIndex, - itemDto.EstimatedMinutes, - currentUser.CompanyId); - - coat.CoatMaterialCost = coatPricing.CoatMaterialCost; - coat.CoatLaborCost = coatPricing.CoatLaborCost; - coat.CoatTotalCost = coatPricing.CoatTotalCost; - - item.Coats.Add(coat); - } - _logger.LogInformation("Added {CoatCount} coats to item {Description}", item.Coats.Count, item.Description); } - // Map per-item prep services - if (itemDto.PrepServices != null && itemDto.PrepServices.Any()) - { - item.PrepServices = new List(); - foreach (var psDto in itemDto.PrepServices) - { - var prepService = _mapper.Map(psDto); - prepService.CompanyId = currentUser.CompanyId; - item.PrepServices.Add(prepService); - } - } - - // Track new item for comparison newItemsForComparison.Add(( item.Description ?? "", item.Quantity, @@ -3086,108 +2854,32 @@ public class QuotesController : Controller // Create job items from quote items foreach (var quoteItem in fullItems.Where(qi => !qi.IsDeleted)) { - // Get first coat's color information if available - var firstCoat = quoteItem.Coats?.OrderBy(c => c.Sequence).FirstOrDefault(); - - var jobItem = new JobItem - { - JobId = job.Id, - Description = quoteItem.Description, - Quantity = quoteItem.Quantity, - ColorName = firstCoat?.ColorName, - ColorCode = firstCoat?.ColorCode, - Finish = firstCoat?.Finish, - SurfaceArea = quoteItem.SurfaceAreaSqFt, - SurfaceAreaSqFt = quoteItem.SurfaceAreaSqFt, - CatalogItemId = quoteItem.CatalogItemId, - IsGenericItem = quoteItem.IsGenericItem, - IsLaborItem = quoteItem.IsLaborItem, - IsSalesItem = quoteItem.IsSalesItem, - Sku = quoteItem.Sku, - ManualUnitPrice = quoteItem.ManualUnitPrice, - PowderCostOverride = quoteItem.PowderCostOverride, - UnitPrice = quoteItem.UnitPrice, - TotalPrice = quoteItem.TotalPrice, - LaborCost = quoteItem.TotalPrice * 0.4m, // Estimated 40% labor cost - RequiresSandblasting = quoteItem.RequiresSandblasting, - RequiresMasking = quoteItem.RequiresMasking, - EstimatedMinutes = quoteItem.EstimatedMinutes, - Notes = quoteItem.Notes, - Complexity = quoteItem.Complexity, - AiTags = quoteItem.AiTags, - AiPredictionId = quoteItem.AiPredictionId, // Share the same prediction record — no duplication - // Catalog items are fixed-price — prep services must not add labor cost to them. - // Non-catalog items default to true so prep service labor is included in the calculated price. - IncludePrepCost = !quoteItem.CatalogItemId.HasValue, - CompanyId = quote.CompanyId, - CreatedAt = DateTime.UtcNow - }; + var createdAtUtc = DateTime.UtcNow; + var jobItem = _jobItemAssemblyService.CreateJobItem(quoteItem, job.Id, quote.CompanyId, createdAtUtc); await _unitOfWork.JobItems.AddAsync(jobItem); await _unitOfWork.SaveChangesAsync(); // Save JobItem first to get its ID - // Create JobItemCoat records for all coats from quote - if (quoteItem.Coats != null && quoteItem.Coats.Any()) + foreach (var coat in _jobItemAssemblyService.CreateJobItemCoats(quoteItem, jobItem.Id, quote.CompanyId, createdAtUtc)) { - foreach (var quoteCoat in quoteItem.Coats.OrderBy(c => c.Sequence)) - { - // Get color info from inventory item if available, otherwise use coat fields - string colorName = quoteCoat.ColorName; - string colorCode = quoteCoat.ColorCode; - string finish = quoteCoat.Finish; + await _unitOfWork.JobItemCoats.AddAsync(coat); + _logger.LogInformation("Created JobItemCoat '{CoatName}' (Sequence {Seq}) - Color: {Color} ({Code})", + coat.CoatName, coat.Sequence, coat.ColorName ?? "N/A", coat.ColorCode ?? "N/A"); + } - if (quoteCoat.InventoryItemId.HasValue && quoteCoat.InventoryItem != null) - { - // Use inventory item information (takes precedence) - colorName = quoteCoat.InventoryItem.Name; - colorCode = quoteCoat.InventoryItem.ColorCode; - finish = quoteCoat.InventoryItem.Finish; - } - - // Calculate PowderToOrder if not already stored on the quote coat - var cov = quoteCoat.CoverageSqFtPerLb > 0 ? quoteCoat.CoverageSqFtPerLb : 30m; - var eff = quoteCoat.TransferEfficiency > 0 ? quoteCoat.TransferEfficiency / 100m : 0.65m; - var powderToOrder = (quoteCoat.PowderToOrder > 0) - ? quoteCoat.PowderToOrder - : (quoteItem.SurfaceAreaSqFt > 0 - ? Math.Round((quoteItem.SurfaceAreaSqFt * quoteItem.Quantity) / (cov * eff), 2) - : (decimal?)null); - - var jobCoat = new JobItemCoat - { - JobItemId = jobItem.Id, - CoatName = quoteCoat.CoatName, - Sequence = quoteCoat.Sequence, - InventoryItemId = quoteCoat.InventoryItemId, - ColorName = colorName, - VendorId = quoteCoat.VendorId, - ColorCode = colorCode, - Finish = finish, - CoverageSqFtPerLb = quoteCoat.CoverageSqFtPerLb, - TransferEfficiency = quoteCoat.TransferEfficiency, - PowderCostPerLb = quoteCoat.PowderCostPerLb, - PowderToOrder = powderToOrder, - Notes = quoteCoat.Notes, - CompanyId = quote.CompanyId, - CreatedAt = DateTime.UtcNow - }; - - await _unitOfWork.JobItemCoats.AddAsync(jobCoat); - _logger.LogInformation("Created JobItemCoat '{CoatName}' (Sequence {Seq}) - Color: {Color} ({Code})", - jobCoat.CoatName, jobCoat.Sequence, colorName ?? "N/A", colorCode ?? "N/A"); - } + foreach (var prepService in _jobItemAssemblyService.CreateJobItemPrepServices(quoteItem, jobItem.Id, quote.CompanyId, createdAtUtc)) + { + await _unitOfWork.JobItemPrepServices.AddAsync(prepService); } } await _unitOfWork.SaveChangesAsync(); - // Aggregate unique prep services from all quote items and copy to job - // Load from DB directly to ensure prep services are available regardless of caller's includes - var quoteItemIds = fullItems.Select(qi => qi.Id).ToList(); - var itemPrepServices = (await _unitOfWork.QuoteItemPrepServices.FindAsync( - ps => quoteItemIds.Contains(ps.QuoteItemId))).ToList(); - var uniquePrepServiceIds = itemPrepServices + // Aggregate unique prep services from the fully-loaded quote items and copy to job + var uniquePrepServiceIds = fullItems + .SelectMany(qi => qi.PrepServices) + .Where(ps => !ps.IsDeleted) .Select(ps => ps.PrepServiceId) .Distinct() .ToList(); @@ -3795,160 +3487,6 @@ public class QuotesController : Controller /// Returns the new inventory item ID, or null if creation fails (non-fatal — the coat /// falls back to custom-powder pricing without an inventory link). /// - private async Task CreateIncomingInventoryItemAsync(CreateQuoteItemCoatDto coatDto, int companyId) - { - try - { - var catalogItem = await _unitOfWork.PowderCatalog.GetByIdAsync(coatDto.CatalogItemId!.Value); - if (catalogItem == null) return null; - - var categories = await _unitOfWork.InventoryCategoryLookups.GetAllAsync(); - var coatingCategory = categories - .Where(c => c.IsActive && c.IsCoating) - .OrderBy(c => c.DisplayOrder) - .FirstOrDefault(); - - // Match catalog vendor name to a company vendor record - var vendors = await _unitOfWork.Vendors.GetAllAsync(); - var vendorNameLower = catalogItem.VendorName.ToLower(); - var matchedVendor = vendors.FirstOrDefault(v => - v.CompanyName.ToLower().Contains(vendorNameLower) || - vendorNameLower.Contains(v.CompanyName.ToLower())); - // InventoryCategoryId is nullable — degrade gracefully rather than aborting if the - // company has not yet set up inventory categories (e.g., pre-seed). - var code = coatingCategory != null - ? (coatingCategory.CategoryCode.Length >= 4 - ? coatingCategory.CategoryCode[..4].ToUpperInvariant() - : coatingCategory.CategoryCode.ToUpperInvariant().PadRight(4, 'X')) - : "POWD"; - var prefix = $"{code}-{DateTime.Now:yyMM}-"; - var allItems = await _unitOfWork.InventoryItems.GetAllAsync(ignoreQueryFilters: true); - var maxSeq = allItems - .Where(i => i.SKU.StartsWith(prefix)) - .Select(i => int.TryParse(i.SKU[prefix.Length..], out var n) ? n : 0) - .DefaultIfEmpty(0) - .Max(); - var sku = $"{prefix}{(maxSeq + 1):D4}"; - - var name = System.Globalization.CultureInfo.CurrentCulture.TextInfo - .ToTitleCase(catalogItem.ColorName.Trim().ToLower()); - - // Start with everything the catalog already has, then augment any null - // spec fields by fetching the product URL through the AI lookup service. - var description = catalogItem.Description; - var finish = catalogItem.Finish; - var colorFamilies = catalogItem.ColorFamilies; - var cureTemp = catalogItem.CureTemperatureF; - var cureTime = catalogItem.CureTimeMinutes; - var coverage = catalogItem.CoverageSqFtPerLb; - var transferEff = catalogItem.TransferEfficiency; - var specificGravity = catalogItem.SpecificGravity; - var imageUrl = catalogItem.ImageUrl; - var sdsUrl = catalogItem.SdsUrl; - var tdsUrl = catalogItem.TdsUrl; - - var needsAugment = !string.IsNullOrWhiteSpace(catalogItem.ProductUrl) && - (string.IsNullOrWhiteSpace(description) || - string.IsNullOrWhiteSpace(colorFamilies) || - cureTemp == null || cureTime == null); - if (needsAugment) - { - try - { - var augmented = await _aiLookupService.LookupByUrlAsync(catalogItem.ProductUrl!, catalogItem.ColorName, catalogItem.TdsUrl); - if (augmented.Success) - { - description = string.IsNullOrWhiteSpace(description) ? augmented.Description : description; - finish = string.IsNullOrWhiteSpace(finish) ? augmented.Finish : finish; - colorFamilies = string.IsNullOrWhiteSpace(colorFamilies) ? augmented.ColorFamilies : colorFamilies; - cureTemp ??= augmented.CureTemperatureF; - cureTime ??= augmented.CureTimeMinutes; - coverage ??= augmented.CoverageSqFtPerLb; - transferEff ??= augmented.TransferEfficiency; - specificGravity ??= augmented.SpecificGravity; - imageUrl = string.IsNullOrWhiteSpace(imageUrl) ? augmented.ImageUrl : imageUrl; - sdsUrl = string.IsNullOrWhiteSpace(sdsUrl) ? augmented.SdsUrl : sdsUrl; - tdsUrl = string.IsNullOrWhiteSpace(tdsUrl) ? augmented.TdsUrl : tdsUrl; - _logger.LogInformation("AI-augmented incoming inventory item for catalog {CatalogId}", catalogItem.Id); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "AI augment failed for catalog {CatalogId}, continuing with catalog data", catalogItem.Id); - } - } - - var item = new PowderCoating.Core.Entities.InventoryItem - { - SKU = sku, - Name = name, - Description = description, - ColorName = catalogItem.ColorName, - Manufacturer = catalogItem.VendorName, - ManufacturerPartNumber = catalogItem.Sku, - Finish = finish, - ColorFamilies = colorFamilies, - RequiresClearCoat = catalogItem.RequiresClearCoat ?? false, - CoverageSqFtPerLb = coverage ?? 30m, - TransferEfficiency = transferEff ?? 65m, - CureTemperatureF = cureTemp, - CureTimeMinutes = cureTime, - SpecificGravity = specificGravity, - SpecPageUrl = catalogItem.ProductUrl, - ImageUrl = imageUrl, - SdsUrl = sdsUrl, - TdsUrl = tdsUrl, - UnitCost = catalogItem.UnitPrice, - AverageCost = catalogItem.UnitPrice, - LastPurchasePrice = catalogItem.UnitPrice, - QuantityOnHand = 0, - UnitOfMeasure = "lbs", - PrimaryVendorId = matchedVendor?.Id, - InventoryCategoryId = coatingCategory?.Id, - Category = coatingCategory?.DisplayName ?? "Powder Coating", - IsActive = true, - IsIncoming = true, - CompanyId = companyId, - CreatedAt = DateTime.UtcNow, - }; - - await _unitOfWork.InventoryItems.AddAsync(item); - await _unitOfWork.SaveChangesAsync(); - - // Also update the coat DTO so pricing uses the inventory unit cost - coatDto.PowderCostPerLb = null; // clear manual price; pricing service reads from inventory - _logger.LogInformation("Created incoming inventory item {Id} ({Name}) from catalog {CatalogId} via quote coat", - item.Id, item.Name, coatDto.CatalogItemId); - - return item.Id; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to create incoming inventory item from catalog {CatalogId}, continuing without inventory link", - coatDto.CatalogItemId); - return null; - } - } - - /// - /// After pricing is determined for an AI item, update the prediction record to flag whether - /// the user changed the AI's estimated surface area or unit price before accepting. - /// This data powers the "AI accuracy" reporting queries. - /// - private async Task UpdateAiPredictionOverrideAsync(CreateQuoteItemDto itemDto, decimal finalUnitPrice) - { - if (!itemDto.AiPredictionId.HasValue) return; - - var prediction = await _unitOfWork.AiItemPredictions.GetByIdAsync(itemDto.AiPredictionId.Value); - if (prediction == null) return; - - var sqftDiff = Math.Abs(prediction.PredictedSurfaceAreaSqFt - itemDto.SurfaceAreaSqFt); - var priceDiff = Math.Abs(prediction.PredictedUnitPrice - finalUnitPrice); - prediction.UserOverrodeEstimate = sqftDiff > 0.01m || priceDiff > 0.01m; - prediction.UpdatedAt = DateTime.UtcNow; - // Change is tracked by EF; will be persisted on the next CompleteAsync() - } - /// /// Builds a benchmark summary comparing the AI's estimate to historical completed jobs of /// similar complexity and surface area (±60% sqft range). The benchmark is displayed diff --git a/src/PowderCoating.Web/Controllers/VendorsController.cs b/src/PowderCoating.Web/Controllers/VendorsController.cs index 9b8c433..bf21410 100644 --- a/src/PowderCoating.Web/Controllers/VendorsController.cs +++ b/src/PowderCoating.Web/Controllers/VendorsController.cs @@ -111,14 +111,7 @@ public class VendorsController : Controller InventoryItemCount = s.InventoryItems.Count(i => !i.IsDeleted) }).ToList(); - // Create paged result - var pagedResult = new PagedResult - { - Items = vendorDtos, - PageNumber = gridRequest.PageNumber, - PageSize = gridRequest.PageSize, - TotalCount = totalCount - }; + var pagedResult = PagedResult.From(gridRequest, vendorDtos, totalCount); // Set ViewBag for sorting ViewBag.SearchTerm = searchTerm; diff --git a/src/PowderCoating.Web/Helpers/AccountingDropdownHelper.cs b/src/PowderCoating.Web/Helpers/AccountingDropdownHelper.cs new file mode 100644 index 0000000..392a980 --- /dev/null +++ b/src/PowderCoating.Web/Helpers/AccountingDropdownHelper.cs @@ -0,0 +1,99 @@ +using Microsoft.AspNetCore.Mvc.Rendering; +using PowderCoating.Core.Enums; +using PowderCoating.Core.Interfaces; + +namespace PowderCoating.Web.Helpers; + +/// +/// Centralizes the repeated DB queries and SelectListItem projections used by the accounting +/// controllers (Bills, Expenses). Each controller assigns only the properties it needs to ViewBag, +/// so the naming mismatch between controllers (BankAccounts vs PaymentAccounts) is harmless. +/// +internal static class AccountingDropdownHelper +{ + /// + /// Loads vendors, accounts, payment methods, and active jobs in a single call. + /// Returns pre-projected SelectListItem collections so controllers avoid duplicating the + /// LINQ-to-SelectListItem transform. + /// + internal static async Task LoadAsync(IUnitOfWork unitOfWork) + { + var vendors = await unitOfWork.Vendors.FindAsync(v => v.IsActive); + var allAccounts = await unitOfWork.Accounts.FindAsync(a => a.IsActive); + var jobs = await unitOfWork.Jobs.FindAsync(j => + j.JobStatus.StatusCode != "COMPLETED" && + j.JobStatus.StatusCode != "CANCELLED" && + j.JobStatus.StatusCode != "DELIVERED"); + + var accountLabel = (Core.Entities.Account a) => $"{a.AccountNumber} – {a.Name}"; + + return new AccountingDropdowns + { + Vendors = vendors + .OrderBy(v => v.CompanyName) + .Select(v => new SelectListItem(v.CompanyName, v.Id.ToString())) + .ToList(), + + ExpenseAccounts = allAccounts + .Where(a => a.AccountType == AccountType.Expense || + a.AccountType == AccountType.CostOfGoods) + .OrderBy(a => a.AccountNumber) + .Select(a => new SelectListItem(accountLabel(a), a.Id.ToString())) + .ToList(), + + ExpenseAndAssetAccounts = allAccounts + .Where(a => a.AccountType == AccountType.Expense || + a.AccountType == AccountType.CostOfGoods || + a.AccountType == AccountType.Asset) + .OrderBy(a => a.AccountNumber) + .Select(a => new SelectListItem(accountLabel(a), a.Id.ToString())) + .ToList(), + + ApAccounts = allAccounts + .Where(a => a.AccountSubType == AccountSubType.AccountsPayable) + .OrderBy(a => a.AccountNumber) + .Select(a => new SelectListItem(accountLabel(a), a.Id.ToString())) + .ToList(), + + BankAccounts = allAccounts + .Where(a => a.AccountSubType == AccountSubType.Cash || + a.AccountSubType == AccountSubType.Checking || + a.AccountSubType == AccountSubType.Savings || + a.AccountSubType == AccountSubType.CreditCard) + .OrderBy(a => a.AccountNumber) + .Select(a => new SelectListItem(accountLabel(a), a.Id.ToString())) + .ToList(), + + PaymentMethods = Enum.GetValues() + .Select(m => new SelectListItem(m.ToString(), ((int)m).ToString())) + .ToList(), + + ActiveJobs = jobs + .OrderBy(j => j.JobNumber) + .Select(j => new SelectListItem( + $"{j.JobNumber} – {j.Description ?? "No description"}", + j.Id.ToString())) + .ToList() + }; + } +} + +internal sealed class AccountingDropdowns +{ + public IReadOnlyList Vendors { get; init; } = []; + + /// Expense + Cost of Goods accounts (used by Expenses controller). + public IReadOnlyList ExpenseAccounts { get; init; } = []; + + /// Expense + Cost of Goods + Asset accounts (used by Bills controller). + public IReadOnlyList ExpenseAndAssetAccounts { get; init; } = []; + + /// Accounts Payable accounts (used by Bills controller). + public IReadOnlyList ApAccounts { get; init; } = []; + + /// Cash, Checking, Savings, and Credit Card accounts. + public IReadOnlyList BankAccounts { get; init; } = []; + + public IReadOnlyList PaymentMethods { get; init; } = []; + public IReadOnlyList ActiveJobs { get; init; } = []; +} diff --git a/src/PowderCoating.Web/Program.cs b/src/PowderCoating.Web/Program.cs index 122b883..4b2f2c0 100644 --- a/src/PowderCoating.Web/Program.cs +++ b/src/PowderCoating.Web/Program.cs @@ -211,6 +211,8 @@ builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/tests/PowderCoating.UnitTests/JobItemAssemblyServiceTests.cs b/tests/PowderCoating.UnitTests/JobItemAssemblyServiceTests.cs new file mode 100644 index 0000000..e00b22a --- /dev/null +++ b/tests/PowderCoating.UnitTests/JobItemAssemblyServiceTests.cs @@ -0,0 +1,277 @@ +using PowderCoating.Application.DTOs.Quote; +using PowderCoating.Application.Interfaces; +using PowderCoating.Application.Services; +using PowderCoating.Core.Entities; + +namespace PowderCoating.UnitTests; + +public class JobItemAssemblyServiceTests +{ + private static readonly DateTime CreatedAtUtc = new(2026, 5, 9, 14, 30, 0, DateTimeKind.Utc); + + private readonly IJobItemAssemblyService _service = new JobItemAssemblyService(); + + [Fact] + public void CreateJobItem_FromWizardDto_PreservesSalesFieldsAndCalculatedChildren() + { + var source = new CreateQuoteItemDto + { + Description = "Powder coated tumbler", + Quantity = 2m, + SurfaceAreaSqFt = 12m, + EstimatedMinutes = 18, + CatalogItemId = 44, + IsSalesItem = true, + Sku = "TMB-RED-20", + ManualUnitPrice = 29.99m, + PowderCostOverride = 7.25m, + RequiresSandblasting = true, + RequiresMasking = true, + Notes = "Merch item", + IncludePrepCost = false, + Complexity = "Moderate", + AiTags = "merch,tumbler", + AiPredictionId = 91, + Coats = + [ + new CreateQuoteItemCoatDto + { + CoatName = "Base", + Sequence = 1, + ColorName = "Signal Red", + ColorCode = "RAL3001", + Finish = "Gloss", + CoverageSqFtPerLb = 30m, + TransferEfficiency = 50m + } + ], + PrepServices = + [ + new CreateQuoteItemPrepServiceDto + { + PrepServiceId = 7, + EstimatedMinutes = 12, + BlastSetupId = 88 + } + ] + }; + + var pricing = new QuoteItemPricingResult + { + UnitPrice = 29.99m, + TotalPrice = 59.98m + }; + + var jobItem = _service.CreateJobItem(source, jobId: 10, companyId: 3, pricing: pricing, createdAtUtc: CreatedAtUtc); + var coats = _service.CreateJobItemCoats(source, jobItemId: 25, companyId: 3, CreatedAtUtc); + var prepServices = _service.CreateJobItemPrepServices(source, jobItemId: 25, companyId: 3, CreatedAtUtc); + + Assert.Equal(10, jobItem.JobId); + Assert.Equal("Powder coated tumbler", jobItem.Description); + Assert.True(jobItem.IsSalesItem); + Assert.Equal("TMB-RED-20", jobItem.Sku); + Assert.False(jobItem.IncludePrepCost); + Assert.Equal(91, jobItem.AiPredictionId); + Assert.Equal("merch,tumbler", jobItem.AiTags); + Assert.Equal(59.98m, jobItem.TotalPrice); + Assert.Equal(23.992m, jobItem.LaborCost); + Assert.Equal(CreatedAtUtc, jobItem.CreatedAt); + + var coat = Assert.Single(coats); + Assert.Equal(25, coat.JobItemId); + Assert.Equal(1.6m, coat.PowderToOrder); + Assert.Equal("Signal Red", coat.ColorName); + Assert.Equal(CreatedAtUtc, coat.CreatedAt); + + var prepService = Assert.Single(prepServices); + Assert.Equal(88, prepService.BlastSetupId); + Assert.Equal(12, prepService.EstimatedMinutes); + Assert.Equal(CreatedAtUtc, prepService.CreatedAt); + } + + [Fact] + public void CreateJobItem_FromQuoteItem_PreservesQuoteShapeAndPrepCostFlag() + { + var quoteItem = new QuoteItem + { + Description = "Bracket set", + Quantity = 3m, + SurfaceAreaSqFt = 10m, + CatalogItemId = 14, + IsGenericItem = false, + IsLaborItem = false, + IsSalesItem = true, + Sku = "BRK-SET", + ManualUnitPrice = 18m, + PowderCostOverride = 6m, + UnitPrice = 42m, + TotalPrice = 126m, + RequiresSandblasting = true, + RequiresMasking = false, + EstimatedMinutes = 25, + Notes = "Use existing hang points", + Complexity = "Complex", + IncludePrepCost = true, + AiTags = "bracket,steel", + AiPredictionId = 55, + Coats = + [ + new QuoteItemCoat + { + CoatName = "Top Coat", + Sequence = 1, + InventoryItemId = 12, + ColorName = "Stale Name", + ColorCode = "STALE", + Finish = "Stale", + CoverageSqFtPerLb = 20m, + TransferEfficiency = 80m, + PowderCostPerLb = 5m, + Notes = "Resolved from inventory", + InventoryItem = new InventoryItem + { + Id = 12, + Name = "Gloss Black", + ColorCode = "RAL9005", + Finish = "Gloss" + } + } + ], + PrepServices = + [ + new QuoteItemPrepService + { + PrepServiceId = 4, + EstimatedMinutes = 9, + BlastSetupId = 41 + } + ] + }; + + var jobItem = _service.CreateJobItem(quoteItem, jobId: 99, companyId: 6, createdAtUtc: CreatedAtUtc); + var coats = _service.CreateJobItemCoats(quoteItem, jobItemId: 70, companyId: 6, CreatedAtUtc); + var prepServices = _service.CreateJobItemPrepServices(quoteItem, jobItemId: 70, companyId: 6, CreatedAtUtc); + + Assert.Equal(99, jobItem.JobId); + Assert.True(jobItem.IsSalesItem); + Assert.Equal("BRK-SET", jobItem.Sku); + Assert.True(jobItem.IncludePrepCost); + Assert.Equal(55, jobItem.AiPredictionId); + Assert.Equal("bracket,steel", jobItem.AiTags); + + var coat = Assert.Single(coats); + Assert.Equal("Gloss Black", coat.ColorName); + Assert.Equal("RAL9005", coat.ColorCode); + Assert.Equal("Gloss", coat.Finish); + Assert.Equal(1.88m, coat.PowderToOrder); + + var prepService = Assert.Single(prepServices); + Assert.Equal(41, prepService.BlastSetupId); + } + + [Fact] + public void CreateJobItem_FromQuoteItem_UsesStoredPowderToOrderWhenPresent() + { + var quoteItem = new QuoteItem + { + Description = "Wheel", + Quantity = 4m, + SurfaceAreaSqFt = 15m, + Coats = + [ + new QuoteItemCoat + { + CoatName = "Primer", + Sequence = 1, + CoverageSqFtPerLb = 30m, + TransferEfficiency = 65m, + PowderToOrder = 9.5m + } + ] + }; + + var coat = Assert.Single(_service.CreateJobItemCoats(quoteItem, jobItemId: 5, companyId: 1, CreatedAtUtc)); + + Assert.Equal(9.5m, coat.PowderToOrder); + } + + [Fact] + public void CreateJobItem_FromExistingJobItem_PreservesTransferableShapeForRework() + { + var source = new JobItem + { + Description = "Gate panel", + Quantity = 1m, + ColorName = "Bronze", + ColorCode = "BZ-22", + Finish = "Textured", + SurfaceArea = 22m, + SurfaceAreaSqFt = 22m, + CatalogItemId = 8, + IsGenericItem = false, + IsLaborItem = false, + IsSalesItem = true, + Sku = "GATE-BRZ", + ManualUnitPrice = 140m, + PowderCostOverride = 9m, + UnitPrice = 140m, + TotalPrice = 140m, + LaborCost = 56m, + RequiresSandblasting = true, + RequiresMasking = true, + EstimatedMinutes = 90, + Notes = "Rework copy", + IncludePrepCost = false, + Complexity = "Extreme", + AiTags = "gate,outdoor", + AiPredictionId = 12, + Coats = + [ + new JobItemCoat + { + CoatName = "Top Coat", + Sequence = 1, + InventoryItemId = 21, + ColorName = "Bronze", + VendorId = 13, + ColorCode = "BZ-22", + Finish = "Textured", + CoverageSqFtPerLb = 26m, + TransferEfficiency = 70m, + PowderCostPerLb = 8m, + PowderToOrder = 2.75m, + Notes = "Keep order qty" + } + ], + PrepServices = + [ + new JobItemPrepService + { + PrepServiceId = 3, + EstimatedMinutes = 45, + BlastSetupId = 77 + } + ] + }; + + var jobItem = _service.CreateJobItem(source, jobId: 222, companyId: 9, createdAtUtc: CreatedAtUtc); + var coats = _service.CreateJobItemCoats(source, jobItemId: 333, companyId: 9, CreatedAtUtc); + var prepServices = _service.CreateJobItemPrepServices(source, jobItemId: 333, companyId: 9, CreatedAtUtc); + + Assert.Equal(222, jobItem.JobId); + Assert.Equal("Bronze", jobItem.ColorName); + Assert.True(jobItem.IsSalesItem); + Assert.Equal("GATE-BRZ", jobItem.Sku); + Assert.False(jobItem.IncludePrepCost); + Assert.Equal(56m, jobItem.LaborCost); + Assert.Equal(12, jobItem.AiPredictionId); + + var coat = Assert.Single(coats); + Assert.Equal(2.75m, coat.PowderToOrder); + Assert.Equal("Bronze", coat.ColorName); + + var prepService = Assert.Single(prepServices); + Assert.Equal(77, prepService.BlastSetupId); + Assert.Equal(45, prepService.EstimatedMinutes); + } +} diff --git a/tests/PowderCoating.UnitTests/JobPhotoServiceTests.cs b/tests/PowderCoating.UnitTests/JobPhotoServiceTests.cs index f7c4daf..04e2a4b 100644 --- a/tests/PowderCoating.UnitTests/JobPhotoServiceTests.cs +++ b/tests/PowderCoating.UnitTests/JobPhotoServiceTests.cs @@ -19,7 +19,7 @@ public class JobPhotoServiceTests var result = await service.SaveJobPhotoAsync(null!, 1, 2); Assert.False(result.Success); - Assert.Equal("No file was uploaded.", result.ErrorMessage); + Assert.Equal("No file provided.", result.ErrorMessage); } [Fact] @@ -31,7 +31,7 @@ public class JobPhotoServiceTests var result = await service.SaveJobPhotoAsync(file, 1, 2); Assert.False(result.Success); - Assert.Equal("Photo must be smaller than 10 MB.", result.ErrorMessage); + Assert.Equal("File exceeds the 10 MB limit.", result.ErrorMessage); } [Fact] @@ -43,7 +43,7 @@ public class JobPhotoServiceTests var result = await service.SaveJobPhotoAsync(file, 1, 2); Assert.False(result.Success); - Assert.Equal("Only JPG, PNG, GIF, and WebP images are allowed.", result.ErrorMessage); + Assert.Equal("File type not allowed. Allowed: .jpg, .jpeg, .png, .gif, .webp.", result.ErrorMessage); } [Fact] diff --git a/tests/PowderCoating.UnitTests/QuoteAndReworkControllerFlowTests.cs b/tests/PowderCoating.UnitTests/QuoteAndReworkControllerFlowTests.cs new file mode 100644 index 0000000..7f56144 --- /dev/null +++ b/tests/PowderCoating.UnitTests/QuoteAndReworkControllerFlowTests.cs @@ -0,0 +1,407 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Moq; +using PowderCoating.Application.DTOs.Job; +using PowderCoating.Application.Interfaces; +using PowderCoating.Application.Services; +using PowderCoating.Core.Entities; +using PowderCoating.Core.Enums; +using PowderCoating.Core.Interfaces; +using PowderCoating.Infrastructure.Data; +using PowderCoating.Infrastructure.Repositories; +using PowderCoating.Web.Controllers; +using PowderCoating.Web.Hubs; + +namespace PowderCoating.UnitTests; + +public class QuoteAndReworkControllerFlowTests +{ + [Fact] + public async Task UpdateQuoteStatus_ApprovedQuote_CopiesItemLevelFieldsIntoCreatedJob() + { + await using var context = CreateContext(); + SeedQuoteConversionData(context); + await context.SaveChangesAsync(); + + var quoteStatuses = await context.QuoteStatusLookups.OrderBy(s => s.Id).ToListAsync(); + var lookupCache = new Mock(); + lookupCache + .Setup(x => x.GetQuoteStatusLookupsAsync(1)) + .ReturnsAsync(quoteStatuses); + var tenantContext = CreateTenantContext(); + + var controller = new QuotesController( + new UnitOfWork(context), + Mock.Of(), + Mock.Of(), + CreateUserManager().Object, + Mock.Of>(), + Mock.Of(), + tenantContext.Object, + Mock.Of(), + lookupCache.Object, + Mock.Of(), + Mock.Of(), + new JobItemAssemblyService(), + Mock.Of(), + new ConfigurationBuilder().Build(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of()); + + var result = await controller.UpdateQuoteStatus(new UpdateQuoteStatusRequest + { + QuoteId = 1, + StatusId = 2 + }); + + Assert.IsType(result); + + var quote = await context.Quotes.SingleAsync(); + Assert.Equal(3, quote.QuoteStatusId); + Assert.True(quote.ConvertedToJobId.HasValue); + + var job = await context.Jobs.SingleAsync(); + Assert.Equal(quote.Id, job.QuoteId); + + var jobItem = await context.JobItems.SingleAsync(); + Assert.True(jobItem.IsSalesItem); + Assert.Equal("MUG-01", jobItem.Sku); + Assert.False(jobItem.IncludePrepCost); + + var jobCoat = await context.JobItemCoats.SingleAsync(); + Assert.Equal(1.5m, jobCoat.PowderToOrder); + Assert.Equal(50, jobCoat.InventoryItemId); + + var jobItemPrep = await context.JobItemPrepServices.SingleAsync(); + Assert.Equal(77, jobItemPrep.BlastSetupId); + + var jobPrep = await context.JobPrepServices.SingleAsync(); + Assert.Equal(5, jobPrep.PrepServiceId); + } + + [Fact] + public async Task AddReworkRecord_CopiesFullItemShapeToReworkJob() + { + await using var context = CreateContext(); + SeedReworkData(context); + await context.SaveChangesAsync(); + + var lookupCache = new Mock(); + lookupCache + .Setup(x => x.GetJobStatusLookupsAsync(1)) + .ReturnsAsync(await context.JobStatusLookups.ToListAsync()); + lookupCache + .Setup(x => x.GetJobPriorityLookupsAsync(1)) + .ReturnsAsync(await context.JobPriorityLookups.ToListAsync()); + var tenantContext = CreateTenantContext(); + + var mapper = new Mock(); + mapper + .Setup(x => x.Map(It.IsAny())) + .Returns(source => + { + var record = Assert.IsType(source); + return new ReworkRecordDto + { + Id = record.Id, + JobId = record.JobId, + JobItemId = record.JobItemId, + ReworkJobId = record.ReworkJobId + }; + }); + + var controller = new JobsController( + new UnitOfWork(context), + mapper.Object, + Mock.Of(), + CreateUserManager().Object, + Mock.Of>(), + tenantContext.Object, + Mock.Of(), + lookupCache.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + new JobItemAssemblyService(), + Mock.Of>(), + Mock.Of>()); + + var result = await controller.AddReworkRecord(new CreateReworkRecordDto + { + JobId = 1, + JobItemId = 10, + ReworkType = ReworkType.InternalDefect, + Reason = ReworkReason.InsufficientCoverage, + DefectDescription = "Thin coverage on one edge", + DiscoveredBy = ReworkDiscoveredBy.Internal, + DiscoveredDate = new DateTime(2026, 5, 9), + EstimatedReworkCost = 65m + }); + + Assert.IsType(result); + + var reworkJob = await context.Jobs.SingleAsync(j => j.IsReworkJob); + Assert.Equal(1, reworkJob.OriginalJobId); + + var reworkItem = await context.JobItems.SingleAsync(i => i.JobId == reworkJob.Id); + Assert.True(reworkItem.IsSalesItem); + Assert.Equal("GATE-BRZ", reworkItem.Sku); + Assert.False(reworkItem.IncludePrepCost); + Assert.Equal(140m, reworkItem.UnitPrice); + Assert.Equal(140m, reworkItem.TotalPrice); + + var reworkCoat = await context.JobItemCoats.SingleAsync(c => c.JobItemId == reworkItem.Id); + Assert.Equal(2.75m, reworkCoat.PowderToOrder); + + var reworkPrep = await context.JobItemPrepServices.SingleAsync(p => p.JobItemId == reworkItem.Id); + Assert.Equal(88, reworkPrep.BlastSetupId); + } + + private static void SeedQuoteConversionData(ApplicationDbContext context) + { + context.Customers.Add(new Customer + { + Id = 1, + CompanyId = 1, + CompanyName = "Acme Fabrication" + }); + + context.InventoryItems.Add(new InventoryItem + { + Id = 50, + CompanyId = 1, + SKU = "POW-1", + Name = "Gloss Black", + ColorCode = "RAL9005", + Finish = "Gloss", + Category = "Powder", + UnitOfMeasure = "lbs" + }); + + context.QuoteStatusLookups.AddRange( + new QuoteStatusLookup { Id = 1, CompanyId = 1, StatusCode = "DRAFT", DisplayName = "Draft" }, + new QuoteStatusLookup { Id = 2, CompanyId = 1, StatusCode = "APPROVED", DisplayName = "Approved" }, + new QuoteStatusLookup { Id = 3, CompanyId = 1, StatusCode = "CONVERTED", DisplayName = "Converted" }); + + context.JobStatusLookups.Add(new JobStatusLookup + { + Id = 10, + CompanyId = 1, + StatusCode = "APPROVED", + DisplayName = "Approved" + }); + + context.JobPriorityLookups.AddRange( + new JobPriorityLookup { Id = 20, CompanyId = 1, PriorityCode = "NORMAL", DisplayName = "Normal" }, + new JobPriorityLookup { Id = 21, CompanyId = 1, PriorityCode = "RUSH", DisplayName = "Rush" }); + + context.PrepServices.Add(new PrepService + { + Id = 5, + CompanyId = 1, + ServiceName = "Sandblasting", + DisplayOrder = 1, + IsActive = true, + RequiresBlastSetup = true + }); + + context.Quotes.Add(new Quote + { + Id = 1, + CompanyId = 1, + QuoteNumber = "Q-1001", + CustomerId = 1, + QuoteStatusId = 1, + Total = 50m, + ShopSuppliesAmount = 2m, + ShopSuppliesPercent = 4m + }); + + context.QuoteItems.Add(new QuoteItem + { + Id = 100, + QuoteId = 1, + CompanyId = 1, + Description = "Merch mug", + Quantity = 2m, + SurfaceAreaSqFt = 10m, + IsSalesItem = true, + Sku = "MUG-01", + UnitPrice = 25m, + TotalPrice = 50m, + IncludePrepCost = false, + EstimatedMinutes = 12 + }); + + context.QuoteItemCoats.Add(new QuoteItemCoat + { + Id = 101, + QuoteItemId = 100, + CompanyId = 1, + CoatName = "Top Coat", + Sequence = 1, + InventoryItemId = 50, + ColorName = "Old Name", + ColorCode = "OLD", + Finish = "Old", + CoverageSqFtPerLb = 30m, + TransferEfficiency = 65m, + PowderToOrder = 1.5m + }); + + context.QuoteItemPrepServices.Add(new QuoteItemPrepService + { + Id = 102, + QuoteItemId = 100, + CompanyId = 1, + PrepServiceId = 5, + EstimatedMinutes = 9, + BlastSetupId = 77 + }); + } + + private static void SeedReworkData(ApplicationDbContext context) + { + context.Customers.Add(new Customer + { + Id = 1, + CompanyId = 1, + CompanyName = "Acme Fabrication" + }); + + context.JobStatusLookups.Add(new JobStatusLookup + { + Id = 1, + CompanyId = 1, + StatusCode = "PENDING", + DisplayName = "Pending" + }); + + context.JobPriorityLookups.Add(new JobPriorityLookup + { + Id = 2, + CompanyId = 1, + PriorityCode = "NORMAL", + DisplayName = "Normal" + }); + + context.Jobs.Add(new Job + { + Id = 1, + CompanyId = 1, + JobNumber = "JOB-2605-0001", + Description = "Original gate job", + CustomerId = 1, + JobStatusId = 1, + JobPriorityId = 2 + }); + + context.JobItems.Add(new JobItem + { + Id = 10, + JobId = 1, + CompanyId = 1, + Description = "Gate panel", + Quantity = 1m, + SurfaceArea = 22m, + SurfaceAreaSqFt = 22m, + CatalogItemId = 8, + IsSalesItem = true, + Sku = "GATE-BRZ", + UnitPrice = 140m, + TotalPrice = 140m, + LaborCost = 56m, + IncludePrepCost = false, + EstimatedMinutes = 90, + ColorName = "Bronze", + ColorCode = "BZ-22", + Finish = "Textured" + }); + + context.JobItemCoats.Add(new JobItemCoat + { + Id = 11, + JobItemId = 10, + CompanyId = 1, + CoatName = "Top Coat", + Sequence = 1, + CoverageSqFtPerLb = 26m, + TransferEfficiency = 70m, + PowderToOrder = 2.75m + }); + + context.JobItemPrepServices.Add(new JobItemPrepService + { + Id = 12, + JobItemId = 10, + CompanyId = 1, + PrepServiceId = 3, + EstimatedMinutes = 45, + BlastSetupId = 88 + }); + } + + private static Mock> CreateUserManager() + { + var store = new Mock>(); + return new Mock>( + store.Object, + null!, + null!, + null!, + null!, + null!, + null!, + null!, + null!); + } + + private static Mock CreateTenantContext() + { + var tenantContext = new Mock(); + tenantContext.Setup(x => x.GetCurrentCompanyId()).Returns(1); + tenantContext.Setup(x => x.IsSuperAdmin()).Returns(true); + tenantContext.Setup(x => x.IsPlatformAdmin()).Returns(true); + return tenantContext; + } + + private static ApplicationDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning)) + .Options; + + var identity = new ClaimsIdentity( + [new Claim(ClaimTypes.Role, "SuperAdmin")], + "Test"); + var principal = new ClaimsPrincipal(identity); + + byte[]? noBytes = null; + var sessionMock = new Mock(); + sessionMock.Setup(s => s.TryGetValue(It.IsAny(), out noBytes)).Returns(false); + + var httpContextMock = new Mock(); + httpContextMock.SetupGet(c => c.User).Returns(principal); + httpContextMock.SetupGet(c => c.Session).Returns(sessionMock.Object); + + var accessor = new Mock(); + accessor.SetupGet(a => a.HttpContext).Returns(httpContextMock.Object); + + return new ApplicationDbContext(options, accessor.Object, null!); + } +} diff --git a/tests/PowderCoating.UnitTests/QuotePhotoServiceTests.cs b/tests/PowderCoating.UnitTests/QuotePhotoServiceTests.cs index ffd9a86..a380038 100644 --- a/tests/PowderCoating.UnitTests/QuotePhotoServiceTests.cs +++ b/tests/PowderCoating.UnitTests/QuotePhotoServiceTests.cs @@ -42,7 +42,7 @@ public class QuotePhotoServiceTests var result = await service.SaveTempPhotoAsync(file, companyId: 1); Assert.False(result.Success); - Assert.Equal("File type '.bmp' is not allowed.", result.ErrorMessage); + Assert.Equal("File type not allowed. Allowed: .jpg, .jpeg, .png, .gif, .webp.", result.ErrorMessage); } [Fact] diff --git a/tests/PowderCoating.UnitTests/QuotePricingAssemblyServiceTests.cs b/tests/PowderCoating.UnitTests/QuotePricingAssemblyServiceTests.cs new file mode 100644 index 0000000..b95dae3 --- /dev/null +++ b/tests/PowderCoating.UnitTests/QuotePricingAssemblyServiceTests.cs @@ -0,0 +1,331 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Moq; +using PowderCoating.Application.DTOs.Quote; +using PowderCoating.Application.Interfaces; +using PowderCoating.Application.Services; +using PowderCoating.Core.Entities; +using PowderCoating.Infrastructure.Data; +using PowderCoating.Infrastructure.Repositories; + +namespace PowderCoating.UnitTests; + +public class QuotePricingAssemblyServiceTests +{ + [Fact] + public void ApplyPricingSnapshot_CopiesAllTotalsToQuote() + { + var service = CreateService(CreateContext(), Mock.Of()); + var quote = new Quote(); + var pricing = new QuotePricingResult + { + MaterialCosts = 10m, + LaborCosts = 20m, + EquipmentCosts = 30m, + ItemsSubtotal = 40m, + OvenBatchCost = 50m, + ShopSuppliesAmount = 60m, + ShopSuppliesPercent = 7m, + OverheadCosts = 80m, + OverheadPercent = 9m, + ProfitMargin = 100m, + ProfitPercent = 11m, + SubtotalBeforeDiscount = 120m, + DiscountPercent = 13m, + DiscountAmount = 14m, + RushFee = 15m, + TaxAmount = 16m, + Total = 17m + }; + + service.ApplyPricingSnapshot(quote, pricing); + + Assert.Equal(10m, quote.MaterialCosts); + Assert.Equal(20m, quote.LaborCosts); + Assert.Equal(30m, quote.EquipmentCosts); + Assert.Equal(40m, quote.ItemsSubtotal); + Assert.Equal(50m, quote.OvenBatchCost); + Assert.Equal(60m, quote.ShopSuppliesAmount); + Assert.Equal(7m, quote.ShopSuppliesPercent); + Assert.Equal(80m, quote.OverheadAmount); + Assert.Equal(9m, quote.OverheadPercent); + Assert.Equal(100m, quote.ProfitMargin); + Assert.Equal(11m, quote.ProfitPercent); + Assert.Equal(120m, quote.SubTotal); + Assert.Equal(13m, quote.DiscountPercent); + Assert.Equal(14m, quote.DiscountAmount); + Assert.Equal(15m, quote.RushFee); + Assert.Equal(16m, quote.TaxAmount); + Assert.Equal(17m, quote.Total); + } + + [Fact] + public async Task CreateQuoteItemsAsync_PreservesManualAndCalculatedPricingPaths() + { + await using var context = CreateContext(); + context.AiItemPredictions.Add(new AiItemPrediction + { + Id = 91, + CompanyId = 1, + PredictedSurfaceAreaSqFt = 4m, + PredictedUnitPrice = 100m, + PredictedMinutes = 15, + PredictedComplexity = "Moderate", + Confidence = "High" + }); + await context.SaveChangesAsync(); + + var pricingService = new Mock(); + pricingService + .Setup(x => x.CalculateQuoteItemPriceAsync( + It.Is(i => i.Description == "Custom frame"), + 1, + null)) + .ReturnsAsync(new QuoteItemPricingResult + { + UnitPrice = 77m, + TotalPrice = 154m, + MaterialCost = 22m, + LaborCost = 33m, + EquipmentCost = 11m + }); + pricingService + .Setup(x => x.CalculateCoatPriceAsync( + It.IsAny(), + 12m, + 2m, + 0, + 25, + 1)) + .ReturnsAsync(new QuoteItemCoatPricingResult + { + CoatMaterialCost = 5m, + CoatLaborCost = 6m, + CoatTotalCost = 11m + }); + + var service = CreateService(context, pricingService.Object); + + var items = await service.CreateQuoteItemsAsync( + [ + new CreateQuoteItemDto + { + Description = "AI wheel", + Quantity = 2m, + SurfaceAreaSqFt = 5m, + EstimatedMinutes = 20, + IsAiItem = true, + ManualUnitPrice = 123m, + AiPredictionId = 91 + }, + new CreateQuoteItemDto + { + Description = "Shop tumbler", + Quantity = 3m, + IsSalesItem = true, + Sku = "TMB-20", + ManualUnitPrice = 18m, + IncludePrepCost = false + }, + new CreateQuoteItemDto + { + Description = "Custom frame", + Quantity = 2m, + SurfaceAreaSqFt = 12m, + EstimatedMinutes = 25, + RequiresSandblasting = true, + Notes = "Calculated path", + Coats = + [ + new CreateQuoteItemCoatDto + { + CoatName = "Top Coat", + Sequence = 1, + ColorName = "Black", + CoverageSqFtPerLb = 30m, + TransferEfficiency = 65m + } + ], + PrepServices = + [ + new CreateQuoteItemPrepServiceDto + { + PrepServiceId = 7, + EstimatedMinutes = 15, + BlastSetupId = 44 + } + ] + } + ], + quoteId: 55, + companyId: 1, + ovenRateOverride: null, + createdAtUtc: new DateTime(2026, 5, 9, 15, 0, 0, DateTimeKind.Utc)); + + Assert.Equal(3, items.Count); + + var aiItem = items.Single(i => i.Description == "AI wheel"); + Assert.Equal(123m, aiItem.UnitPrice); + Assert.Equal(246m, aiItem.TotalPrice); + + var salesItem = items.Single(i => i.Description == "Shop tumbler"); + Assert.True(salesItem.IsSalesItem); + Assert.Equal("TMB-20", salesItem.Sku); + Assert.False(salesItem.IncludePrepCost); + Assert.Equal(18m, salesItem.UnitPrice); + Assert.Equal(54m, salesItem.TotalPrice); + + var customItem = items.Single(i => i.Description == "Custom frame"); + Assert.Equal(77m, customItem.UnitPrice); + Assert.Equal(154m, customItem.TotalPrice); + Assert.Equal(22m, customItem.ItemMaterialCost); + Assert.Equal(33m, customItem.ItemLaborCost); + Assert.Equal(11m, customItem.ItemEquipmentCost); + var customPrep = Assert.Single(customItem.PrepServices); + Assert.Equal(44, customPrep.BlastSetupId); + var customCoat = Assert.Single(customItem.Coats); + Assert.Equal(11m, customCoat.CoatTotalCost); + + var prediction = await context.AiItemPredictions.SingleAsync(); + Assert.True(prediction.UserOverrodeEstimate); + } + + [Fact] + public async Task CreateQuoteItemsAsync_CatalogItemWithoutCoats_UsesCatalogDefaultPrice() + { + await using var context = CreateContext(); + context.CatalogItems.Add(new CatalogItem + { + Id = 22, + CompanyId = 1, + Name = "Gate Hinge", + DefaultPrice = 42.5m + }); + await context.SaveChangesAsync(); + + var service = CreateService(context, Mock.Of()); + + var item = Assert.Single(await service.CreateQuoteItemsAsync( + [ + new CreateQuoteItemDto + { + Description = "Catalog hinge", + Quantity = 4m, + CatalogItemId = 22 + } + ], + quoteId: 1, + companyId: 1, + ovenRateOverride: null, + createdAtUtc: DateTime.UtcNow)); + + Assert.Equal(42.5m, item.UnitPrice); + Assert.Equal(170m, item.TotalPrice); + } + + [Fact] + public async Task CreateQuoteItemsAsync_AddAsIncoming_CreatesInventoryItemAndLinksCoat() + { + await using var context = CreateContext(); + context.Set().Add(new PowderCatalogItem + { + Id = 5, + VendorName = "Prismatic Powders", + Sku = "P-1001", + ColorName = "Candy Red", + UnitPrice = 19.5m, + CoverageSqFtPerLb = 85m, + TransferEfficiency = 70m + }); + await context.SaveChangesAsync(); + + var pricingService = new Mock(); + pricingService + .Setup(x => x.CalculateQuoteItemPriceAsync(It.IsAny(), 1, null)) + .ReturnsAsync(new QuoteItemPricingResult + { + UnitPrice = 50m, + TotalPrice = 50m, + MaterialCost = 10m, + LaborCost = 20m, + EquipmentCost = 5m + }); + pricingService + .Setup(x => x.CalculateCoatPriceAsync(It.IsAny(), 6m, 1m, 0, 10, 1)) + .ReturnsAsync(new QuoteItemCoatPricingResult + { + CoatMaterialCost = 3m, + CoatLaborCost = 4m, + CoatTotalCost = 7m + }); + + var service = CreateService(context, pricingService.Object); + var dto = new CreateQuoteItemDto + { + Description = "Incoming powder item", + Quantity = 1m, + SurfaceAreaSqFt = 6m, + EstimatedMinutes = 10, + Coats = + [ + new CreateQuoteItemCoatDto + { + CoatName = "Base", + Sequence = 1, + CatalogItemId = 5, + AddAsIncoming = true, + PowderCostPerLb = 22m + } + ] + }; + + var item = Assert.Single(await service.CreateQuoteItemsAsync( + [dto], + quoteId: 9, + companyId: 1, + ovenRateOverride: null, + createdAtUtc: DateTime.UtcNow)); + + var inventoryItem = await context.InventoryItems.SingleAsync(); + var coat = Assert.Single(item.Coats); + Assert.Equal(inventoryItem.Id, coat.InventoryItemId); + Assert.True(inventoryItem.IsIncoming); + Assert.Null(dto.Coats[0].PowderCostPerLb); + } + + private static QuotePricingAssemblyService CreateService(ApplicationDbContext context, IPricingCalculationService pricingService) + { + return new QuotePricingAssemblyService( + new UnitOfWork(context), + pricingService, + Mock.Of(), + Mock.Of>()); + } + + private static ApplicationDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + var identity = new ClaimsIdentity( + [new Claim(ClaimTypes.Role, "SuperAdmin")], + "Test"); + var principal = new ClaimsPrincipal(identity); + + byte[]? noBytes = null; + var sessionMock = new Mock(); + sessionMock.Setup(s => s.TryGetValue(It.IsAny(), out noBytes)).Returns(false); + + var httpContextMock = new Mock(); + httpContextMock.SetupGet(c => c.User).Returns(principal); + httpContextMock.SetupGet(c => c.Session).Returns(sessionMock.Object); + + var accessor = new Mock(); + accessor.SetupGet(a => a.HttpContext).Returns(httpContextMock.Object); + + return new ApplicationDbContext(options, accessor.Object, null!); + } +}