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, IsAiItem = source.IsAiItem, Sku = source.Sku, ManualUnitPrice = source.ManualUnitPrice, PowderCostOverride = source.PowderCostOverride, UnitPrice = pricing.UnitPrice, TotalPrice = pricing.TotalPrice, LaborCost = pricing.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(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, IsAiItem = source.IsAiItem, Sku = source.Sku, ManualUnitPrice = source.ManualUnitPrice, PowderCostOverride = source.PowderCostOverride, UnitPrice = source.UnitPrice, TotalPrice = source.TotalPrice, LaborCost = source.ItemLaborCost, 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, IsAiItem = source.IsAiItem, 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, IsAiItem = seed.IsAiItem, 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 bool IsAiItem { 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; } } }