Files
PowderCoatingLogix/tests/PowderCoating.UnitTests/JobItemAssemblyServiceTests.cs
T
spouliot edd7389d7d Refactor: extract shared helpers, fix field drift, add assembly services
- IJobItemAssemblyService / IQuotePricingAssemblyService: centralize job item
  and quote pricing construction that was duplicated across create, rework copy,
  and quote-to-job conversion paths
- BlobFileHelper: single ValidateUpload/GetContentType/SanitizeFileName used by
  6 blob services (JobPhoto, QuotePhoto, ProfilePhoto, CompanyLogo, Equipment,
  Catalog) and BillsController + ExpensesController, removing 8 private copies
- PagedResult<T>.From(): static factory eliminates 6-line boilerplate in 11
  controllers (Appointments, Customers, Equipment, Inventory, Invoices, Jobs,
  Maintenance, CompanyUsers, PlatformUsers, Quotes, Vendors)
- AccountingDropdownHelper: single LoadAsync() call replaces duplicate
  vendor/account/job queries in BillsController and ExpensesController
- JobTemplateItem: add IsSalesItem + Sku fields with migration; propagate
  through JobTemplatesController snapshot copy and GetTemplatesJson projection,
  and JobsController template-application path
- Test assertions updated for standardized BlobFileHelper error messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:12:33 -04:00

278 lines
9.6 KiB
C#

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);
}
}