Files
PowderCoatingLogix/src/PowderCoating.Application/Mappings/QuoteProfile.cs
T
spouliot 9a52e7fae5 Ad-hoc quote email, accounting improvements, AI lookup fix, and misc service updates
- Quotes: ad-hoc email modal on Quote Details lets staff send to an address not on file;
  QuotesController passes overrideEmail through to NotificationService
- Quotes/Details view: SMS consent display, email/SMS send button state based on consent
- Accounting module: AccountingDisplayHelpers for consistent ledger formatting;
  AccountsController + Accounts views improvements; AccountingEnums additions
- Bills/Expenses: AI account categorization fixes in BillsController and ExpensesController
- InventoryAiLookupService: TDS cure fallback no longer fires on AiAugmentFromUrl path
  (LookupByUrlAsync already has it built in — was double-fetching)
- PdfService: quote/invoice PDF updates
- PricingCalculationService: minor pricing logic fix
- QuoteProfile: mapping updates for new quote fields
- ApplicationDbContextModelSnapshot: catches up to all 4 migrations in this branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 20:48:00 -04:00

323 lines
21 KiB
C#

using AutoMapper;
using PowderCoating.Application.DTOs.Quote;
using PowderCoating.Core.Entities;
namespace PowderCoating.Application.Mappings;
public class QuoteProfile : Profile
{
public QuoteProfile()
{
// ============================================================================
// Quote Entity <-> DTOs
// ============================================================================
// Quote -> QuoteListDto
CreateMap<Quote, QuoteListDto>()
.ForMember(dest => dest.CustomerOrProspectName, opt => opt.MapFrom(src =>
src.Customer != null
? (!string.IsNullOrWhiteSpace(src.Customer.CompanyName)
? src.Customer.CompanyName
: (src.Customer.ContactFirstName + " " + src.Customer.ContactLastName).Trim())
: src.ProspectCompanyName ?? src.ProspectContactName ?? "Unknown"))
.ForMember(dest => dest.IsProspect, opt => opt.MapFrom(src => !src.CustomerId.HasValue))
// Status lookup mappings
.ForMember(dest => dest.QuoteStatusId, opt => opt.MapFrom(src => src.QuoteStatusId))
.ForMember(dest => dest.StatusCode, opt => opt.MapFrom(src => src.QuoteStatus.StatusCode))
.ForMember(dest => dest.StatusDisplayName, opt => opt.MapFrom(src => src.QuoteStatus.DisplayName))
.ForMember(dest => dest.StatusColorClass, opt => opt.MapFrom(src => src.QuoteStatus.ColorClass))
.ForMember(dest => dest.StatusIsDraft, opt => opt.MapFrom(src => src.QuoteStatus.IsDraftStatus));
// Quote -> QuoteDto
CreateMap<Quote, QuoteDto>()
.ForMember(dest => dest.OvenLabel, opt => opt.MapFrom(src =>
src.OvenCost != null ? src.OvenCost.Label : null))
.ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src =>
src.Customer != null ? src.Customer.CompanyName : null))
.ForMember(dest => dest.CustomerEmail, opt => opt.MapFrom(src =>
src.Customer != null ? src.Customer.Email : src.ProspectEmail))
.ForMember(dest => dest.CustomerMobilePhone, opt => opt.MapFrom(src =>
src.Customer != null ? src.Customer.MobilePhone : null))
.ForMember(dest => dest.CustomerNotifyBySms, opt => opt.MapFrom(src =>
src.Customer != null && src.Customer.NotifyBySms))
.ForMember(dest => dest.CustomerNotifyByEmail, opt => opt.MapFrom(src =>
src.Customer == null || src.Customer.NotifyByEmail))
.ForMember(dest => dest.PreparedByName, opt => opt.MapFrom(src =>
src.PreparedBy != null ? $"{src.PreparedBy.FirstName} {src.PreparedBy.LastName}" : null))
// Discount enum to string conversion
.ForMember(dest => dest.DiscountType, opt => opt.MapFrom(src => src.DiscountType.ToString()))
// Status lookup mappings
.ForMember(dest => dest.QuoteStatusId, opt => opt.MapFrom(src => src.QuoteStatusId))
.ForMember(dest => dest.StatusCode, opt => opt.MapFrom(src => src.QuoteStatus.StatusCode))
.ForMember(dest => dest.StatusDisplayName, opt => opt.MapFrom(src => src.QuoteStatus.DisplayName))
.ForMember(dest => dest.StatusColorClass, opt => opt.MapFrom(src => src.QuoteStatus.ColorClass))
.ForMember(dest => dest.StatusIconClass, opt => opt.MapFrom(src => src.QuoteStatus.IconClass))
.ForMember(dest => dest.StatusIsApproved, opt => opt.MapFrom(src => src.QuoteStatus.IsApprovedStatus))
.ForMember(dest => dest.StatusIsConverted, opt => opt.MapFrom(src => src.QuoteStatus.IsConvertedStatus))
.ForMember(dest => dest.StatusIsDraft, opt => opt.MapFrom(src => src.QuoteStatus.IsDraftStatus))
// Prep Services mappings
.ForMember(dest => dest.PrepServices, opt => opt.MapFrom(src =>
src.QuotePrepServices.Select(qps => qps.PrepService).ToList()))
.ForMember(dest => dest.PrepServiceIds, opt => opt.MapFrom(src =>
src.QuotePrepServices.Select(qps => qps.PrepServiceId).ToList()))
.ReverseMap()
.ForMember(dest => dest.Customer, opt => opt.Ignore())
.ForMember(dest => dest.OvenCost, opt => opt.Ignore())
.ForMember(dest => dest.PreparedBy, opt => opt.Ignore())
.ForMember(dest => dest.QuoteStatus, opt => opt.Ignore()) // Will be set by FK
.ForMember(dest => dest.QuoteItems, opt => opt.Ignore())
.ForMember(dest => dest.QuotePrepServices, opt => opt.Ignore()) // Handled separately in controller
.ForMember(dest => dest.ConvertedToJob, opt => opt.Ignore())
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore());
// CreateQuoteDto -> Quote
CreateMap<CreateQuoteDto, Quote>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.ProspectSmsConsentedAt, opt => opt.Ignore()) // Set by controller on consent
.ForMember(dest => dest.QuoteNumber, opt => opt.Ignore()) // Generated by controller
.ForMember(dest => dest.QuoteStatus, opt => opt.Ignore()) // Will be set by FK to Draft status
.ForMember(dest => dest.OvenCost, opt => opt.Ignore())
.ForMember(dest => dest.SubTotal, opt => opt.Ignore()) // Calculated
// Discount: map type and value from DTO, calculated fields ignored
.ForMember(dest => dest.DiscountType, opt => opt.MapFrom(src =>
Enum.Parse<PowderCoating.Core.Enums.DiscountType>(src.DiscountType)))
.ForMember(dest => dest.DiscountValue, opt => opt.MapFrom(src => src.DiscountValue))
.ForMember(dest => dest.DiscountReason, opt => opt.MapFrom(src => src.DiscountReason))
.ForMember(dest => dest.DiscountPercent, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.DiscountAmount, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.TaxAmount, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.Total, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.QuoteItems, opt => opt.Ignore()) // Mapped separately
.ForMember(dest => dest.QuotePrepServices, opt => opt.Ignore()) // Handled separately in controller
.ForMember(dest => dest.Customer, opt => opt.Ignore())
.ForMember(dest => dest.PreparedBy, opt => opt.Ignore())
.ForMember(dest => dest.ConvertedToJob, opt => opt.Ignore())
.ForMember(dest => dest.PreparedById, opt => opt.Ignore())
.ForMember(dest => dest.ConvertedToJobId, opt => opt.Ignore())
.ForMember(dest => dest.ConvertedDate, opt => opt.Ignore())
.ForMember(dest => dest.SentDate, opt => opt.Ignore())
.ForMember(dest => dest.ApprovedDate, opt => opt.Ignore())
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore());
// UpdateQuoteDto -> Quote
CreateMap<UpdateQuoteDto, Quote>()
.ForMember(dest => dest.QuoteNumber, opt => opt.Ignore()) // Cannot change
.ForMember(dest => dest.ProspectSmsConsentedAt, opt => opt.Ignore()) // Managed by controller
.ForMember(dest => dest.CustomerId, opt => opt.Ignore()) // Cannot change after creation - preserved in controller
.ForMember(dest => dest.QuoteStatus, opt => opt.Ignore()) // Will be set by FK
.ForMember(dest => dest.OvenCost, opt => opt.Ignore())
.ForMember(dest => dest.SubTotal, opt => opt.Ignore()) // Calculated
// Discount: map type and value from DTO, calculated fields ignored
.ForMember(dest => dest.DiscountType, opt => opt.MapFrom(src =>
Enum.Parse<PowderCoating.Core.Enums.DiscountType>(src.DiscountType)))
.ForMember(dest => dest.DiscountValue, opt => opt.MapFrom(src => src.DiscountValue))
.ForMember(dest => dest.DiscountReason, opt => opt.MapFrom(src => src.DiscountReason))
.ForMember(dest => dest.DiscountPercent, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.DiscountAmount, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.TaxAmount, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.Total, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.QuoteItems, opt => opt.Ignore()) // Mapped separately
.ForMember(dest => dest.QuotePrepServices, opt => opt.Ignore()) // Handled separately in controller
.ForMember(dest => dest.Customer, opt => opt.Ignore())
.ForMember(dest => dest.PreparedBy, opt => opt.Ignore())
.ForMember(dest => dest.ConvertedToJob, opt => opt.Ignore())
.ForMember(dest => dest.PreparedById, opt => opt.Ignore())
.ForMember(dest => dest.ConvertedToJobId, opt => opt.Ignore())
.ForMember(dest => dest.ConvertedDate, opt => opt.Ignore())
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore());
// Quote -> UpdateQuoteDto
CreateMap<Quote, UpdateQuoteDto>()
.ForMember(dest => dest.IsForProspect, opt => opt.MapFrom(src => !src.CustomerId.HasValue))
.ForMember(dest => dest.QuoteStatusId, opt => opt.MapFrom(src => src.QuoteStatusId));
// ============================================================================
// QuoteItem Entity <-> DTOs
// ============================================================================
// QuoteItem -> QuoteItemDto
CreateMap<QuoteItem, QuoteItemDto>()
.ForMember(dest => dest.CatalogItemName, opt => opt.MapFrom(src =>
src.CatalogItem != null ? src.CatalogItem.Name : null))
.ForMember(dest => dest.Coats, opt => opt.MapFrom(src => src.Coats))
.ForMember(dest => dest.PrepServices, opt => opt.MapFrom(src => src.PrepServices))
.ReverseMap()
.ForMember(dest => dest.Quote, opt => opt.Ignore())
.ForMember(dest => dest.CatalogItem, opt => opt.Ignore())
.ForMember(dest => dest.Coats, opt => opt.Ignore())
.ForMember(dest => dest.PrepServices, opt => opt.Ignore())
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
.ForMember(dest => dest.DeletedAt, opt => opt.Ignore())
.ForMember(dest => dest.DeletedBy, opt => opt.Ignore())
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore());
// CreateQuoteItemDto -> QuoteItem
CreateMap<CreateQuoteItemDto, QuoteItem>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.QuoteId, opt => opt.Ignore())
.ForMember(dest => dest.UnitPrice, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.TotalPrice, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.Quote, opt => opt.Ignore())
.ForMember(dest => dest.Coats, opt => opt.Ignore()) // Mapped separately
.ForMember(dest => dest.PrepServices, opt => opt.Ignore()) // Mapped separately
.ForMember(dest => dest.CatalogItem, opt => opt.Ignore())
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
.ForMember(dest => dest.DeletedAt, opt => opt.Ignore())
.ForMember(dest => dest.DeletedBy, opt => opt.Ignore())
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore());
// QuoteItem -> CreateQuoteItemDto (for Edit view)
CreateMap<QuoteItem, CreateQuoteItemDto>()
.ForMember(dest => dest.PrepServices, opt => opt.MapFrom(src => src.PrepServices));
// ============================================================================
// QuoteItemCoat Entity <-> DTOs
// ============================================================================
// QuoteItemCoat -> QuoteItemCoatDto
CreateMap<QuoteItemCoat, QuoteItemCoatDto>()
.ForMember(dest => dest.InventoryItemName, opt => opt.MapFrom(src =>
src.InventoryItem != null ? src.InventoryItem.Name : null))
.ForMember(dest => dest.VendorName, opt => opt.MapFrom(src =>
src.Vendor != null ? src.Vendor.CompanyName : null))
.ReverseMap()
.ForMember(dest => dest.QuoteItem, opt => opt.Ignore())
.ForMember(dest => dest.InventoryItem, opt => opt.Ignore())
.ForMember(dest => dest.Vendor, opt => opt.Ignore())
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
.ForMember(dest => dest.DeletedAt, opt => opt.Ignore())
.ForMember(dest => dest.DeletedBy, opt => opt.Ignore())
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore());
// CreateQuoteItemCoatDto -> QuoteItemCoat
CreateMap<CreateQuoteItemCoatDto, QuoteItemCoat>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.QuoteItemId, opt => opt.Ignore())
.ForMember(dest => dest.CoatMaterialCost, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.CoatLaborCost, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.CoatTotalCost, opt => opt.Ignore()) // Calculated
.ForMember(dest => dest.QuoteItem, opt => opt.Ignore())
.ForMember(dest => dest.InventoryItem, opt => opt.Ignore())
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
.ForMember(dest => dest.DeletedAt, opt => opt.Ignore())
.ForMember(dest => dest.DeletedBy, opt => opt.Ignore())
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore());
// QuoteItemCoat -> CreateQuoteItemCoatDto (for Edit view)
CreateMap<QuoteItemCoat, CreateQuoteItemCoatDto>();
// ============================================================================
// QuoteItemPrepService Entity <-> DTOs
// ============================================================================
// CreateQuoteItemPrepServiceDto -> QuoteItemPrepService
CreateMap<CreateQuoteItemPrepServiceDto, QuoteItemPrepService>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.QuoteItemId, opt => opt.Ignore())
.ForMember(dest => dest.QuoteItem, opt => opt.Ignore())
.ForMember(dest => dest.PrepService, opt => opt.Ignore())
.ForMember(dest => dest.BlastSetup, opt => opt.Ignore())
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
.ForMember(dest => dest.DeletedAt, opt => opt.Ignore())
.ForMember(dest => dest.DeletedBy, opt => opt.Ignore())
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore());
// QuoteItemPrepService -> CreateQuoteItemPrepServiceDto (for Edit view)
CreateMap<QuoteItemPrepService, CreateQuoteItemPrepServiceDto>()
.ForMember(dest => dest.PrepServiceName, opt => opt.MapFrom(src =>
src.PrepService != null ? src.PrepService.ServiceName : null));
// ============================================================================
// Customer Conversion Mappings
// ============================================================================
// Quote -> ConvertQuoteToCustomerDto
CreateMap<Quote, ConvertQuoteToCustomerDto>()
.ForMember(dest => dest.QuoteId, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.CompanyName, opt => opt.MapFrom(src => src.ProspectCompanyName ?? ""))
.ForMember(dest => dest.ContactName, opt => opt.MapFrom(src => src.ProspectContactName ?? ""))
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.ProspectEmail))
.ForMember(dest => dest.Phone, opt => opt.MapFrom(src => src.ProspectPhone))
.ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.ProspectAddress))
.ForMember(dest => dest.City, opt => opt.MapFrom(src => src.ProspectCity))
.ForMember(dest => dest.State, opt => opt.MapFrom(src => src.ProspectState))
.ForMember(dest => dest.ZipCode, opt => opt.MapFrom(src => src.ProspectZipCode))
.ForMember(dest => dest.IsCommercial, opt => opt.MapFrom(src => src.IsCommercial))
.ForMember(dest => dest.CreditLimit, opt => opt.MapFrom(src => 0m))
.ForMember(dest => dest.SmsConsent, opt => opt.MapFrom(src => src.ProspectSmsConsent))
.ForMember(dest => dest.ProspectSmsConsentedAt, opt => opt.MapFrom(src => src.ProspectSmsConsentedAt))
.ForMember(dest => dest.PricingTierId, opt => opt.Ignore())
.ForMember(dest => dest.TaxId, opt => opt.Ignore())
.ForMember(dest => dest.PaymentTerms, opt => opt.Ignore())
.ForMember(dest => dest.Notes, opt => opt.Ignore());
// ConvertQuoteToCustomerDto -> Customer
CreateMap<ConvertQuoteToCustomerDto, Customer>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.ContactFirstName, opt => opt.MapFrom(src => src.ContactName))
.ForMember(dest => dest.ContactLastName, opt => opt.Ignore())
.ForMember(dest => dest.MobilePhone, opt => opt.Ignore())
.ForMember(dest => dest.Country, opt => opt.MapFrom(src => "USA"))
.ForMember(dest => dest.CurrentBalance, opt => opt.MapFrom(src => 0m))
.ForMember(dest => dest.IsActive, opt => opt.MapFrom(src => true))
.ForMember(dest => dest.GeneralNotes, opt => opt.MapFrom(src => src.Notes))
.ForMember(dest => dest.LastContactDate, opt => opt.Ignore())
.ForMember(dest => dest.PricingTier, opt => opt.Ignore())
.ForMember(dest => dest.Jobs, opt => opt.Ignore())
.ForMember(dest => dest.Quotes, opt => opt.Ignore())
.ForMember(dest => dest.CustomerNotes, opt => opt.Ignore())
.ForMember(dest => dest.CompanyId, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedBy, opt => opt.Ignore());
// ============================================================================
// QuoteChangeHistory Entity <-> DTOs
// ============================================================================
// QuoteChangeHistory -> QuoteChangeHistoryDto
CreateMap<QuoteChangeHistory, QuoteChangeHistoryDto>()
.ForMember(dest => dest.ChangedByName, opt => opt.MapFrom(src =>
src.ChangedBy != null ? $"{src.ChangedBy.FirstName} {src.ChangedBy.LastName}"
: src.ChangedByUserId == null ? "Customer"
: "Unknown"));
}
}