Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,832 @@
using System.ComponentModel.DataAnnotations;
using PowderCoating.Application.DTOs.PrepService;
namespace PowderCoating.Application.DTOs.Quote;
// ============================================================================
// LIST DTO - For Index page listing
// ============================================================================
public class QuoteListDto
{
public int Id { get; set; }
public string QuoteNumber { get; set; } = string.Empty;
public string CustomerOrProspectName { get; set; } = string.Empty;
// Quote Status (from lookup table)
public int QuoteStatusId { get; set; }
public string StatusCode { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = "secondary";
public bool StatusIsDraft { get; set; }
public DateTime QuoteDate { get; set; }
public DateTime? ExpirationDate { get; set; }
public decimal Total { get; set; }
public string? Notes { get; set; }
public string? Description { get; set; }
public string? Tags { get; set; }
public bool IsExpired => ExpirationDate.HasValue && ExpirationDate.Value < DateTime.UtcNow && StatusIsDraft;
public bool IsProspect { get; set; }
}
// ============================================================================
// FULL QUOTE DTO - For Details page
// ============================================================================
public class QuoteDto
{
public int Id { get; set; }
public string QuoteNumber { get; set; } = string.Empty;
// Customer or Prospect Info
public int? CustomerId { get; set; }
public string? CustomerName { get; set; }
public string? CustomerCompanyName { get; set; }
public string? CustomerContactFirstName { get; set; }
public string? CustomerContactLastName { get; set; }
public string? CustomerEmail { get; set; }
public string? CustomerPhone { get; set; }
public string? ProspectCompanyName { get; set; }
public string? ProspectContactFirstName { get; set; }
public string? ProspectContactLastName { get; set; }
public string? ProspectContactName { get; set; }
public string? ProspectEmail { get; set; }
public string? ProspectPhone { get; set; }
public string? ProspectAddress { get; set; }
public string? ProspectCity { get; set; }
public string? ProspectState { get; set; }
public string? ProspectZipCode { get; set; }
public string? PreparedById { get; set; }
public string? PreparedByName { get; set; }
public string? PreparedByEmail { get; set; }
// Quote Status (from lookup table)
public int QuoteStatusId { get; set; }
public string StatusCode { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = "secondary";
public string? StatusIconClass { get; set; }
public bool StatusIsApproved { get; set; }
public bool StatusIsConverted { get; set; }
public bool StatusIsDraft { get; set; }
public bool IsCommercial { get; set; }
// Dates
public DateTime QuoteDate { get; set; }
public DateTime? ExpirationDate { get; set; }
public DateTime? SentDate { get; set; }
public DateTime? ApprovedDate { get; set; }
// Pricing
public decimal SubTotal { get; set; }
// Discount Information
public string DiscountType { get; set; } = "None"; // None, Percentage, FixedAmount
public decimal DiscountValue { get; set; }
public decimal DiscountPercent { get; set; }
public decimal DiscountAmount { get; set; }
public string? DiscountReason { get; set; }
public bool HideDiscountFromCustomer { get; set; }
public decimal TaxPercent { get; set; }
public decimal TaxAmount { get; set; }
public decimal RushFee { get; set; }
public decimal Total { get; set; }
public bool IsRushJob { get; set; }
// Additional Information
public string? Description { get; set; }
public string? Terms { get; set; }
public string? Notes { get; set; }
public string? CustomerPO { get; set; }
public string? Tags { get; set; }
// Items
public List<QuoteItemDto> QuoteItems { get; set; } = new();
// Prep Services
public List<PrepServiceDto> PrepServices { get; set; } = new();
public List<int> PrepServiceIds { get; set; } = new();
// Pricing Breakdown
public QuotePricingBreakdownDto? PricingBreakdown { get; set; }
// Oven selection
public int? OvenCostId { get; set; }
public string? OvenLabel { get; set; }
// Oven batch pricing
public int OvenBatches { get; set; } = 1;
public int? OvenCycleMinutes { get; set; }
// Conversion Tracking
public int? ConvertedToJobId { get; set; }
// Customer Approval Tracking
public string? ApprovalToken { get; set; }
public DateTime? ApprovalTokenExpiresAt { get; set; }
public DateTime? ApprovalTokenUsedAt { get; set; }
public string? DeclineReason { get; set; }
public bool IsProspect => !CustomerId.HasValue;
public bool IsExpired => ExpirationDate.HasValue && ExpirationDate.Value < DateTime.UtcNow && StatusIsDraft;
public string CustomerOrProspectName => CustomerName ?? ProspectCompanyName ?? ProspectContactName ?? "Unknown";
}
// ============================================================================
// CREATE QUOTE DTO - For creating new quotes
// ============================================================================
public class CreateQuoteDto
{
// Customer or Prospect Selection
public bool IsForProspect { get; set; }
[Display(Name = "Customer")]
public int? CustomerId { get; set; }
// Prospect Contact Information
[Display(Name = "Company Name")]
[StringLength(100)]
public string? ProspectCompanyName { get; set; }
[Display(Name = "Contact Name")]
[StringLength(100)]
public string? ProspectContactName { get; set; }
[Display(Name = "Email")]
[EmailAddress]
[StringLength(100)]
public string? ProspectEmail { get; set; }
[Display(Name = "Phone")]
[Phone]
[StringLength(20)]
public string? ProspectPhone { get; set; }
[Display(Name = "Address")]
[StringLength(200)]
public string? ProspectAddress { get; set; }
[Display(Name = "City")]
[StringLength(100)]
public string? ProspectCity { get; set; }
[Display(Name = "State")]
[StringLength(2)]
public string? ProspectState { get; set; }
[Display(Name = "Zip Code")]
[StringLength(10)]
public string? ProspectZipCode { get; set; }
// Oven Selection
[Display(Name = "Oven")]
public int? OvenCostId { get; set; }
// Oven Batch Pricing
[Display(Name = "Oven Batches")]
[Range(1, 9999)]
public int OvenBatches { get; set; } = 1;
[Display(Name = "Oven Cycle Time (min)")]
[Range(1, 1440)]
public int? OvenCycleMinutes { get; set; }
// Quote Information
[Display(Name = "Commercial Quote")]
public bool IsCommercial { get; set; }
[Display(Name = "Rush Job")]
public bool IsRushJob { get; set; }
[Display(Name = "Quote Date")]
[DataType(DataType.Date)]
public DateTime QuoteDate { get; set; } = DateTime.Today;
[Display(Name = "Expiration Date")]
[DataType(DataType.Date)]
public DateTime? ExpirationDate { get; set; }
[Display(Name = "Description")]
[StringLength(500)]
public string? Description { get; set; }
[Display(Name = "Terms & Conditions")]
[DataType(DataType.MultilineText)]
public string? Terms { get; set; }
[Display(Name = "Internal Notes")]
[DataType(DataType.MultilineText)]
public string? Notes { get; set; }
[Display(Name = "Customer PO Number")]
[StringLength(50)]
public string? CustomerPO { get; set; }
[Display(Name = "Tags")]
[StringLength(500)]
public string? Tags { get; set; }
// Pricing
[Display(Name = "Tax Percent (%)")]
[Range(0, 100)]
public decimal TaxPercent { get; set; }
// Discount
[Display(Name = "Discount Type")]
public string DiscountType { get; set; } = "None"; // None, Percentage, FixedAmount
[Display(Name = "Discount Value")]
[Range(0, 999999)]
public decimal DiscountValue { get; set; }
[Display(Name = "Discount Reason")]
[StringLength(200)]
public string? DiscountReason { get; set; }
[Display(Name = "Hide discount from customer")]
public bool HideDiscountFromCustomer { get; set; } = false;
// Items
[Required]
// Note: MinLength validation removed to prevent false positives on initial page load
// JavaScript validation handles this check on form submission
public List<CreateQuoteItemDto> QuoteItems { get; set; } = new();
// Prep Services
[Display(Name = "Preparation Services")]
public List<int> PrepServiceIds { get; set; } = new();
// Email Options
[Display(Name = "Send quote via email")]
public bool SendEmailToCustomer { get; set; } = false;
// AI Photo TempIds — list of uploaded temp photo IDs to promote on save
public List<string> AiPhotoTempIds { get; set; } = new();
// Quote Photo TempIds — general (non-AI) photos staged on the Create page
public List<string> QuotePhotoTempIds { get; set; } = new();
public List<string> QuotePhotoFileNames { get; set; } = new();
}
// ============================================================================
// UPDATE QUOTE DTO - For editing existing quotes
// ============================================================================
public class UpdateQuoteDto
{
public int Id { get; set; }
// Customer or Prospect Info (read-only after creation, but included for completeness)
public int? CustomerId { get; set; }
public bool IsForProspect { get; set; }
// Prospect Contact Information
[Display(Name = "Company Name")]
[StringLength(100)]
public string? ProspectCompanyName { get; set; }
[Display(Name = "Contact Name")]
[StringLength(100)]
public string? ProspectContactName { get; set; }
[Display(Name = "Email")]
[EmailAddress]
[StringLength(100)]
public string? ProspectEmail { get; set; }
[Display(Name = "Phone")]
[Phone]
[StringLength(20)]
public string? ProspectPhone { get; set; }
[Display(Name = "Address")]
[StringLength(200)]
public string? ProspectAddress { get; set; }
[Display(Name = "City")]
[StringLength(100)]
public string? ProspectCity { get; set; }
[Display(Name = "State")]
[StringLength(2)]
public string? ProspectState { get; set; }
[Display(Name = "Zip Code")]
[StringLength(10)]
public string? ProspectZipCode { get; set; }
// Oven Selection
[Display(Name = "Oven")]
public int? OvenCostId { get; set; }
// Oven Batch Pricing
[Display(Name = "Oven Batches")]
[Range(1, 9999)]
public int OvenBatches { get; set; } = 1;
[Display(Name = "Oven Cycle Time (min)")]
[Range(1, 1440)]
public int? OvenCycleMinutes { get; set; }
// Quote Information
[Display(Name = "Status")]
public int QuoteStatusId { get; set; } // FK to lookup table
[Display(Name = "Commercial Quote")]
public bool IsCommercial { get; set; }
[Display(Name = "Rush Job")]
public bool IsRushJob { get; set; }
[Display(Name = "Quote Date")]
[DataType(DataType.Date)]
public DateTime QuoteDate { get; set; }
[Display(Name = "Expiration Date")]
[DataType(DataType.Date)]
public DateTime? ExpirationDate { get; set; }
[Display(Name = "Description")]
[StringLength(500)]
public string? Description { get; set; }
[Display(Name = "Terms & Conditions")]
[DataType(DataType.MultilineText)]
public string? Terms { get; set; }
[Display(Name = "Internal Notes")]
[DataType(DataType.MultilineText)]
public string? Notes { get; set; }
[Display(Name = "Customer PO Number")]
[StringLength(50)]
public string? CustomerPO { get; set; }
[Display(Name = "Tags")]
[StringLength(500)]
public string? Tags { get; set; }
// Pricing
[Display(Name = "Tax Percent (%)")]
[Range(0, 100)]
public decimal TaxPercent { get; set; }
// Discount
[Display(Name = "Discount Type")]
public string DiscountType { get; set; } = "None"; // None, Percentage, FixedAmount
[Display(Name = "Discount Value")]
[Range(0, 999999)]
public decimal DiscountValue { get; set; }
[Display(Name = "Discount Reason")]
[StringLength(200)]
public string? DiscountReason { get; set; }
[Display(Name = "Hide discount from customer")]
public bool HideDiscountFromCustomer { get; set; } = false;
// Items
[Required]
// Note: MinLength validation removed to prevent false positives on initial page load
// JavaScript validation handles this check on form submission
public List<CreateQuoteItemDto> QuoteItems { get; set; } = new();
// Prep Services
[Display(Name = "Preparation Services")]
public List<int> PrepServiceIds { get; set; } = new();
// AI Photo TempIds — list of uploaded temp photo IDs to promote on save
public List<string> AiPhotoTempIds { get; set; } = new();
// Quote Photo TempIds — general (non-AI) photos staged on the Edit page (not used by direct-upload Edit path,
// kept for symmetry / future use)
public List<string> QuotePhotoTempIds { get; set; } = new();
public List<string> QuotePhotoFileNames { get; set; } = new();
}
// ============================================================================
// QUOTE ITEM DTO - For displaying quote items
// ============================================================================
public class QuoteItemDto
{
public int Id { get; set; }
public int QuoteId { get; set; }
[Display(Name = "Description")]
public string Description { get; set; } = string.Empty;
[Display(Name = "Quantity")]
public decimal Quantity { get; set; }
[Display(Name = "Surface Area (sq ft)")]
public decimal SurfaceAreaSqFt { get; set; }
[Display(Name = "Estimated Minutes")]
public int EstimatedMinutes { get; set; }
[Display(Name = "Requires Sandblasting")]
public bool RequiresSandblasting { get; set; }
[Display(Name = "Requires Masking")]
public bool RequiresMasking { get; set; }
[Display(Name = "Catalog Item")]
public int? CatalogItemId { get; set; }
public string? CatalogItemName { get; set; }
[Display(Name = "Unit Price")]
public decimal UnitPrice { get; set; }
[Display(Name = "Total Price")]
public decimal TotalPrice { get; set; }
public string? Notes { 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; }
// Coating layers
public List<QuoteItemCoatDto> Coats { get; set; } = new();
// Preparation services
public List<CreateQuoteItemPrepServiceDto> PrepServices { get; set; } = new();
// Part complexity level for calculated items
public string? Complexity { get; set; }
public bool IsAiItem { get; set; }
// Cost breakdown snapshot
public decimal ItemMaterialCost { get; set; }
public decimal ItemLaborCost { get; set; }
public decimal ItemEquipmentCost { get; set; }
}
// ============================================================================
// CREATE QUOTE ITEM DTO - For creating quote items
// ============================================================================
public class CreateQuoteItemDto
{
// Description is required for calculated items but auto-populated for catalog items
[Display(Name = "Description")]
[StringLength(200)]
public string Description { get; set; } = string.Empty;
[Required]
[Display(Name = "Quantity")]
[Range(0.01, 10000)]
public decimal Quantity { get; set; } = 1;
[Required]
[Display(Name = "Surface Area (sq ft)")]
[Range(0, 100000)] // 0 is valid for generic items
public decimal SurfaceAreaSqFt { get; set; }
[Required]
[Display(Name = "Estimated Minutes")]
[Range(0, 10000, ErrorMessage = "Estimated minutes must be between 0 and 10,000 (0 for catalog items)")]
public int EstimatedMinutes { get; set; }
[Display(Name = "Requires Sandblasting")]
public bool RequiresSandblasting { get; set; }
[Display(Name = "Requires Masking")]
public bool RequiresMasking { get; set; }
[Display(Name = "Catalog Item")]
public int? CatalogItemId { get; set; }
[Display(Name = "Notes")]
[StringLength(500)]
public string? Notes { get; set; }
// Generic item — price entered manually, bypasses calculation engine
public bool IsGenericItem { get; set; }
[Range(0, 1000000)]
public decimal? ManualUnitPrice { get; set; }
// Labor item — Quantity = hours; priced using StandardLaborRate × markup, no coats
public bool IsLaborItem { get; set; }
// Sales item — off-the-shelf merchandise (T-shirts, tumblers, etc.)
public bool IsSalesItem { get; set; }
public string? Sku { get; set; }
// Coating layers
public List<CreateQuoteItemCoatDto> Coats { get; set; } = new();
// Prep services (each has its own time estimate)
// For catalog items, prep cost is only included when IncludePrepCost = true
public List<CreateQuoteItemPrepServiceDto> PrepServices { get; set; } = new();
// Controls whether prep service labor cost is added to the item price.
// Always true for calculated items; catalog items default to false (costs baked in)
// but can be opted in via the wizard toggle.
public bool IncludePrepCost { get; set; } = true;
// Part complexity — drives a price multiplier on calculated items only
// Values: null | "Simple" | "Moderate" | "Complex" | "Extreme"
public string? Complexity { get; set; }
// Optional unit price override for catalog items — overrides the catalog default price
public decimal? PowderCostOverride { get; set; }
// True when this item was generated via AI photo analysis
public bool IsAiItem { get; set; }
// AI-generated standardized tags (comma-separated, e.g. "automotive,tubular")
public string? AiTags { get; set; }
// ID of the AiItemPrediction record captured at analysis time (null for non-AI items)
public int? AiPredictionId { get; set; }
}
// ============================================================================
// QUOTE ITEM PREP SERVICE DTO
// ============================================================================
public class CreateQuoteItemPrepServiceDto
{
[Required]
public int PrepServiceId { get; set; }
[Required]
[Range(0, 10000)]
public int EstimatedMinutes { get; set; }
/// <summary>Blast setup selected in wizard for this sandblasting prep service.</summary>
public int? BlastSetupId { get; set; }
// Populated when mapping from entity (for display purposes)
public string? PrepServiceName { get; set; }
}
// ============================================================================
// PRICING BREAKDOWN DTO - For transparent cost breakdown
// ============================================================================
public class QuotePricingBreakdownDto
{
// Per-Item Costs
public decimal MaterialCosts { get; set; }
public decimal LaborCosts { get; set; }
public decimal EquipmentCosts { get; set; }
public decimal ItemSubtotal { get; set; }
// Quote-Level Costs
public decimal ItemsSubtotal { get; set; }
public decimal ShopSuppliesAmount { get; set; }
public decimal ShopSuppliesPercent { get; set; }
public decimal OverheadCosts { get; set; }
public decimal OverheadPercent { get; set; }
public decimal ProfitMargin { get; set; }
public decimal ProfitPercent { get; set; }
public decimal SubtotalBeforeDiscount { get; set; }
public decimal DiscountAmount { get; set; }
public decimal DiscountPercent { get; set; }
public decimal RushFee { get; set; }
public decimal SubtotalAfterDiscount { get; set; }
public decimal TaxAmount { get; set; }
public decimal TaxPercent { get; set; }
public decimal OvenBatchCost { get; set; }
public int OvenBatches { get; set; }
public int OvenCycleMinutes { get; set; }
public decimal Total { get; set; }
// Cost Breakdown Details
public string CostBreakdownDetails { get; set; } = string.Empty;
}
// ============================================================================
// CONVERT TO CUSTOMER DTO - For prospect-to-customer conversion
// ============================================================================
public class ConvertQuoteToCustomerDto
{
public int QuoteId { get; set; }
public string QuoteNumber { get; set; } = string.Empty;
// Pre-filled from prospect data
[Display(Name = "Company Name")]
[StringLength(100)]
public string? CompanyName { get; set; }
[Required]
[Display(Name = "Contact Name")]
[StringLength(100)]
public string ContactName { get; set; } = string.Empty;
[Display(Name = "Email")]
[EmailAddress]
[StringLength(100)]
public string? Email { get; set; }
[Display(Name = "Phone")]
[Phone]
[StringLength(20)]
public string? Phone { get; set; }
[Display(Name = "Address")]
[StringLength(200)]
public string? Address { get; set; }
[Display(Name = "City")]
[StringLength(100)]
public string? City { get; set; }
[Display(Name = "State")]
[StringLength(2)]
public string? State { get; set; }
[Display(Name = "Zip Code")]
[StringLength(10)]
public string? ZipCode { get; set; }
// Additional customer fields
[Display(Name = "Tax ID / EIN")]
[StringLength(50)]
public string? TaxId { get; set; }
[Display(Name = "Credit Limit")]
[Range(0, 1000000)]
public decimal CreditLimit { get; set; }
[Display(Name = "Pricing Tier")]
public int? PricingTierId { get; set; }
[Display(Name = "Customer Type")]
public bool IsCommercial { get; set; } = true;
[Display(Name = "Payment Terms")]
[StringLength(100)]
public string? PaymentTerms { get; set; }
[Display(Name = "Notes")]
[DataType(DataType.MultilineText)]
public string? Notes { get; set; }
}
// ============================================================================
// QUOTE ITEM COAT DTOs - For multi-coat support
// ============================================================================
/// <summary>
/// DTO for creating/editing a coating layer on a quote item
/// </summary>
public class CreateQuoteItemCoatDto
{
[Required]
[StringLength(100)]
[Display(Name = "Coat Name")]
public string CoatName { get; set; } = "Base Coat";
public int Sequence { get; set; } = 1;
[Display(Name = "Inventory Powder")]
public int? InventoryItemId { get; set; }
[StringLength(100)]
[Display(Name = "Color Name")]
public string? ColorName { get; set; }
[Display(Name = "Vendor")]
public int? VendorId { get; set; }
[StringLength(50)]
[Display(Name = "Color Code")]
public string? ColorCode { get; set; }
[StringLength(50)]
[Display(Name = "Finish")]
public string? Finish { get; set; }
[Range(1, 500)]
[Display(Name = "Coverage (sq ft/lb)")]
public decimal CoverageSqFtPerLb { get; set; } = 30m;
[Range(1, 100)]
[Display(Name = "Transfer Efficiency (%)")]
public decimal TransferEfficiency { get; set; } = 65m;
[Range(0, 1000)]
[Display(Name = "Powder Cost ($/lb)")]
public decimal? PowderCostPerLb { get; set; }
[Range(0, 10000)]
[Display(Name = "Powder to Order (lbs)")]
public decimal? PowderToOrder { get; set; }
[StringLength(500)]
[Display(Name = "Notes")]
public string? Notes { get; set; }
/// <summary>
/// When true, the additional layer labor charge is not applied even if this is not the first coat.
/// </summary>
public bool NoExtraLayerCharge { get; set; }
}
/// <summary>
/// DTO for displaying a coating layer on a quote item
/// </summary>
public class QuoteItemCoatDto
{
public int Id { get; set; }
public string CoatName { get; set; } = string.Empty;
public int Sequence { get; set; }
public int? InventoryItemId { get; set; }
public string? InventoryItemName { get; set; }
public int? VendorId { get; set; }
public string? VendorName { get; set; }
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public string? Finish { get; set; }
public decimal CoverageSqFtPerLb { get; set; }
public decimal TransferEfficiency { get; set; }
public decimal? PowderToOrder { get; set; }
public decimal CoatMaterialCost { get; set; }
public decimal CoatLaborCost { get; set; }
public decimal CoatTotalCost { get; set; }
public string? Notes { get; set; }
}
// ============================================================================
// PRICING CALCULATION RESULT - Internal use for pricing service
// ============================================================================
/// <summary>
/// Result of calculating costs for a single coating layer
/// </summary>
public class QuoteItemCoatPricingResult
{
public decimal CoatMaterialCost { get; set; }
public decimal CoatLaborCost { get; set; }
public decimal CoatTotalCost { get; set; }
}
public class QuoteItemPricingResult
{
public decimal MaterialCost { get; set; }
public decimal LaborCost { get; set; }
public decimal EquipmentCost { get; set; }
public decimal ItemSubtotal { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
}
public class QuotePricingResult
{
public decimal ItemsSubtotal { get; set; }
public decimal ShopSuppliesAmount { get; set; }
public decimal ShopSuppliesPercent { get; set; }
public decimal OverheadCosts { get; set; }
public decimal OverheadPercent { get; set; }
public decimal ProfitMargin { get; set; }
public decimal ProfitPercent { get; set; }
public decimal SubtotalBeforeDiscount { get; set; }
// Pricing Tier Discount (from customer's pricing tier)
public decimal PricingTierDiscountAmount { get; set; }
public decimal PricingTierDiscountPercent { get; set; }
// Quote-Level Discount (applied on the quote)
public decimal QuoteDiscountAmount { get; set; }
public decimal QuoteDiscountPercent { get; set; }
// Combined Discount (for backward compatibility)
public decimal DiscountAmount { get; set; }
public decimal DiscountPercent { get; set; }
public decimal SubtotalAfterDiscount { get; set; }
public decimal RushFee { get; set; }
public decimal TaxAmount { get; set; }
public decimal TaxPercent { get; set; }
public decimal Total { get; set; }
// Oven batch cost (quote-level, separate from per-item calculations)
public decimal OvenBatchCost { get; set; }
public int OvenBatches { get; set; }
public int OvenCycleMinutes { get; set; }
// Detailed breakdown for transparency
public decimal MaterialCosts { get; set; }
public decimal LaborCosts { get; set; }
public decimal EquipmentCosts { get; set; }
// Per-item results (same order as input items)
public List<QuoteItemPricingResult> ItemResults { get; set; } = new();
}