Add XML doc comments to pricing assembly services
Added comprehensive XML documentation to JobItemAssemblyService and QuotePricingAssemblyService — the most complex area of the codebase. Comments explain the three-overload pattern, seed class rationale, powder-to-order formula and industry default fallbacks, AI prediction override tracking, and the incoming inventory auto-creation workflow. PricingCalculationService was already well-documented; no changes needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,26 @@ using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Converts quote/job data into persisted <see cref="JobItem"/>, <see cref="JobItemCoat"/>,
|
||||
/// and <see cref="JobItemPrepService"/> entities.
|
||||
///
|
||||
/// Three source types are supported, each with a matching overload:
|
||||
/// 1. <see cref="CreateQuoteItemDto"/> — quote wizard (new job from form data + fresh pricing result)
|
||||
/// 2. <see cref="QuoteItem"/> — quote-to-job conversion (copies a saved quote line)
|
||||
/// 3. <see cref="JobItem"/> — job duplication / template instantiation (copies an existing job line)
|
||||
///
|
||||
/// The private <see cref="JobItemSeed"/> / <see cref="JobItemCoatSeed"/> / <see cref="JobItemPrepServiceSeed"/>
|
||||
/// intermediary classes exist solely to give all three overload paths a single <see cref="BuildJobItem"/>
|
||||
/// construction site — avoiding subtle copy-paste drift where one overload forgets to copy a new field.
|
||||
/// </summary>
|
||||
public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="JobItem"/> from a quote wizard DTO and a pre-calculated pricing result.
|
||||
/// Used when creating a job directly from the job form or from an approved quote via the wizard.
|
||||
/// Pricing is passed in separately because it was already computed upstream (CalculateQuoteItemPriceAsync).
|
||||
/// </summary>
|
||||
public JobItem CreateJobItem(CreateQuoteItemDto source, int jobId, int companyId, QuoteItemPricingResult pricing, DateTime createdAtUtc)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
@@ -42,6 +60,11 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
createdAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds <see cref="JobItemCoat"/> records from the coat DTOs in the quote wizard form.
|
||||
/// PowderToOrder is recalculated server-side here (not trusted from the form) using surface area,
|
||||
/// quantity, coverage, and transfer efficiency — the wizard's displayed value is for UI only.
|
||||
/// </summary>
|
||||
public IReadOnlyList<JobItemCoat> CreateJobItemCoats(CreateQuoteItemDto source, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
@@ -70,6 +93,11 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
.ToList() ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds <see cref="JobItemPrepService"/> records (sandblasting, masking, etc.) from the
|
||||
/// quote wizard DTO. These are per-item prep steps with individual time estimates that feed
|
||||
/// labor cost calculations and shop floor instructions.
|
||||
/// </summary>
|
||||
public IReadOnlyList<JobItemPrepService> CreateJobItemPrepServices(CreateQuoteItemDto source, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
@@ -85,6 +113,13 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
createdAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="JobItem"/> by copying a saved <see cref="QuoteItem"/> during quote-to-job conversion.
|
||||
/// Prices are taken directly from the quote snapshot — no repricing occurs — so the job starts with
|
||||
/// exactly the amounts that were approved by the customer.
|
||||
/// The first coat's color/finish is promoted to the job item's top-level fields for quick display
|
||||
/// (details remain in the coat records).
|
||||
/// </summary>
|
||||
public JobItem CreateJobItem(QuoteItem source, int jobId, int companyId, DateTime createdAtUtc)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
@@ -128,6 +163,12 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
createdAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds <see cref="JobItemCoat"/> records from a saved <see cref="QuoteItem"/> during quote-to-job conversion.
|
||||
/// Coat appearance (color name, code, finish) is resolved from the linked <see cref="InventoryItem"/> if available,
|
||||
/// because the inventory record is the canonical source of truth for a product's appearance —
|
||||
/// the values typed into the quote form may be incomplete or informal.
|
||||
/// </summary>
|
||||
public IReadOnlyList<JobItemCoat> CreateJobItemCoats(QuoteItem source, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
@@ -160,6 +201,9 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
.ToList() ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies prep service records from a <see cref="QuoteItem"/> to a new job item during quote-to-job conversion.
|
||||
/// </summary>
|
||||
public IReadOnlyList<JobItemPrepService> CreateJobItemPrepServices(QuoteItem source, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
@@ -175,6 +219,12 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
createdAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="JobItem"/> by cloning an existing one — used for job templates
|
||||
/// and rework duplication where an existing job line is reused on a new job.
|
||||
/// Prices are copied as-is from the source; the job controller is responsible for repricing
|
||||
/// if operating costs have changed since the original job was created.
|
||||
/// </summary>
|
||||
public JobItem CreateJobItem(JobItem source, int jobId, int companyId, DateTime createdAtUtc)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
@@ -214,6 +264,11 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
createdAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones coat records from an existing <see cref="JobItem"/> onto a new job item.
|
||||
/// PowderToOrder is copied verbatim (not recalculated) because the original job's powder
|
||||
/// quantities may have been manually adjusted after initial calculation.
|
||||
/// </summary>
|
||||
public IReadOnlyList<JobItemCoat> CreateJobItemCoats(JobItem source, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
@@ -242,6 +297,9 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
.ToList() ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones prep service records from an existing <see cref="JobItem"/> onto a new job item.
|
||||
/// </summary>
|
||||
public IReadOnlyList<JobItemPrepService> CreateJobItemPrepServices(JobItem source, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
@@ -257,6 +315,10 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
createdAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single construction point for all <see cref="JobItem"/> creation paths.
|
||||
/// Centralised here so that adding a new field only requires one code change, not three.
|
||||
/// </summary>
|
||||
private static JobItem BuildJobItem(JobItemSeed seed, int jobId, int companyId, DateTime createdAtUtc)
|
||||
{
|
||||
return new JobItem
|
||||
@@ -293,6 +355,9 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single construction point for all <see cref="JobItemCoat"/> creation paths.
|
||||
/// </summary>
|
||||
private static JobItemCoat BuildJobItemCoat(JobItemCoatSeed seed, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||
{
|
||||
return new JobItemCoat
|
||||
@@ -315,6 +380,11 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single construction point for all <see cref="JobItemPrepService"/> creation paths.
|
||||
/// Returns an empty list (not null) when <paramref name="seeds"/> is null so callers
|
||||
/// can safely iterate without a null check.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<JobItemPrepService> BuildJobItemPrepServices(IEnumerable<JobItemPrepServiceSeed>? seeds, int jobItemId, int companyId, DateTime createdAtUtc)
|
||||
{
|
||||
return seeds?
|
||||
@@ -330,6 +400,18 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
.ToList() ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the pounds of powder needed to coat a batch, preferring the pre-stored value
|
||||
/// (which the user may have manually adjusted in the wizard) over a fresh recalculation.
|
||||
///
|
||||
/// Formula: (surfaceAreaSqFt × quantity) ÷ (coverageSqFtPerLb × transferEfficiency)
|
||||
///
|
||||
/// Industry defaults are applied when catalog data is missing:
|
||||
/// - Coverage: 30 sqft/lb (typical for standard powder at 2–3 mil DFT)
|
||||
/// - Transfer efficiency: 65% (industry average for electrostatic spray)
|
||||
/// These are conservative defaults that slightly overestimate powder needed — intentional,
|
||||
/// so the shop doesn't run short on a job.
|
||||
/// </summary>
|
||||
private static decimal? CalculatePowderToOrder(decimal? storedPowderToOrder, decimal surfaceAreaSqFt, decimal quantity, decimal coverageSqFtPerLb, decimal transferEfficiency)
|
||||
{
|
||||
if (storedPowderToOrder.HasValue && storedPowderToOrder.Value > 0)
|
||||
@@ -343,6 +425,12 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
return Math.Round((surfaceAreaSqFt * quantity) / (coverage * efficiency), 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the display appearance (color name, code, finish) for a coat, preferring the linked
|
||||
/// <see cref="InventoryItem"/>'s values over whatever was typed into the quote form.
|
||||
/// The inventory record is the canonical source of truth — the form values are used as a fallback
|
||||
/// only when no inventory item is linked (e.g. custom/one-off powder).
|
||||
/// </summary>
|
||||
private static (string? ColorName, string? ColorCode, string? Finish) ResolveCoatAppearance(
|
||||
string? colorName,
|
||||
string? colorCode,
|
||||
@@ -355,6 +443,11 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
return (inventoryItem.Name, inventoryItem.ColorCode, inventoryItem.Finish);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intermediate value object that normalises the three different source types
|
||||
/// (DTO, QuoteItem, JobItem) into a single shape before the shared BuildJobItem factory method.
|
||||
/// Using a seed class prevents subtle bugs where an overload forgets to map a new field.
|
||||
/// </summary>
|
||||
private sealed class JobItemSeed
|
||||
{
|
||||
public string Description { get; init; } = string.Empty;
|
||||
@@ -385,6 +478,7 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
public int? AiPredictionId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Intermediate value object for coat creation — see <see cref="JobItemSeed"/> for rationale.</summary>
|
||||
private sealed class JobItemCoatSeed
|
||||
{
|
||||
public string CoatName { get; init; } = string.Empty;
|
||||
@@ -401,6 +495,7 @@ public class JobItemAssemblyService : IJobItemAssemblyService
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Intermediate value object for prep service creation — see <see cref="JobItemSeed"/> for rationale.</summary>
|
||||
private sealed class JobItemPrepServiceSeed
|
||||
{
|
||||
public int PrepServiceId { get; init; }
|
||||
|
||||
Reference in New Issue
Block a user