7e1676cfd7
- AP Aging report (GetApAgingAsync, controller actions, view, PDF export) mirrors AR Aging — groups open bills by vendor, buckets by days past due date - Trial Balance report (GetTrialBalanceAsync, view, PDF export) uses Account.CurrentBalance, groups by AccountType, validates debits == credits - Cash vs Accrual accounting method setting on Company entity switchable at any time — report-time only, no GL re-posting on change P&L cash: revenue = payments received; expenses = bills/expenses paid in period Balance Sheet cash: omits AR and AP lines (no receivables/payables concept) AccountingMethod badge shown on P&L and Balance Sheet views - Migration A (AddAccountingMethod) applied, default = Accrual for all existing companies - AP Aging and Trial Balance added to Reports Landing page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
369 lines
15 KiB
C#
369 lines
15 KiB
C#
using System.ComponentModel.DataAnnotations;
|
|
using PowderCoating.Application.DTOs.Notification;
|
|
using PowderCoating.Application.Services;
|
|
using PowderCoating.Core.Enums;
|
|
|
|
namespace PowderCoating.Application.DTOs.Company
|
|
{
|
|
/// <summary>
|
|
/// Read model for company settings including operating costs
|
|
/// </summary>
|
|
public class CompanySettingsDto
|
|
{
|
|
public int Id { get; set; }
|
|
public string CompanyName { get; set; } = string.Empty;
|
|
public string? CompanyCode { get; set; }
|
|
public string PrimaryContactName { get; set; } = string.Empty;
|
|
public string PrimaryContactEmail { get; set; } = string.Empty;
|
|
public string? Phone { get; set; }
|
|
public string? Address { get; set; }
|
|
public string? City { get; set; }
|
|
public string? State { get; set; }
|
|
public string? ZipCode { get; set; }
|
|
public string? TimeZone { get; set; }
|
|
public AccountingMethod AccountingMethod { get; set; } = AccountingMethod.Accrual;
|
|
public bool HasLogo { get; set; }
|
|
|
|
public CompanyOperatingCostsDto? OperatingCosts { get; set; }
|
|
public CompanyPreferencesDto? Preferences { get; set; }
|
|
public bool AiPhotoQuotesEnabled { get; set; }
|
|
public List<NotificationTemplateDto> NotificationTemplates { get; set; } = new();
|
|
|
|
// Stripe Connect / online payments
|
|
public StripeConnectStatus StripeConnectStatus { get; set; }
|
|
public string? StripeAccountId { get; set; }
|
|
public OnlinePaymentSurchargeType OnlinePaymentSurchargeType { get; set; }
|
|
public decimal OnlinePaymentSurchargeValue { get; set; }
|
|
public bool OnlineSurchargeAcknowledged { get; set; }
|
|
public bool AllowOnlinePayments { get; set; }
|
|
|
|
// SMS gating
|
|
public bool AllowSms { get; set; }
|
|
public bool SmsEnabled { get; set; }
|
|
public bool SmsDisabledByAdmin { get; set; }
|
|
/// <summary>True when the company has an accepted agreement for the current SmsTermsVersion.</summary>
|
|
public bool HasCurrentSmsAgreement { get; set; }
|
|
public string SmsTermsVersion { get; set; } = string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// DTO for the company admin SMS opt-in/out toggle.
|
|
/// When enabling for the first time (or after a terms version change), AgreedToTerms must
|
|
/// be true and TermsVersion must match <c>AppConstants.SmsTermsVersion</c>.
|
|
/// </summary>
|
|
public class UpdateSmsPreferencesDto
|
|
{
|
|
public bool SmsEnabled { get; set; }
|
|
public bool AgreedToTerms { get; set; }
|
|
public string? TermsVersion { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DTO for updating basic company information
|
|
/// </summary>
|
|
public class UpdateCompanySettingsDto
|
|
{
|
|
[Required(ErrorMessage = "Company name is required")]
|
|
[StringLength(200, ErrorMessage = "Company name cannot exceed 200 characters")]
|
|
public string CompanyName { get; set; } = string.Empty;
|
|
|
|
[StringLength(10, ErrorMessage = "Company code cannot exceed 10 characters")]
|
|
public string? CompanyCode { get; set; }
|
|
|
|
[Required(ErrorMessage = "Primary contact name is required")]
|
|
[StringLength(100, ErrorMessage = "Contact name cannot exceed 100 characters")]
|
|
public string PrimaryContactName { get; set; } = string.Empty;
|
|
|
|
[Required(ErrorMessage = "Primary contact email is required")]
|
|
[EmailAddress(ErrorMessage = "Invalid email format")]
|
|
[StringLength(100, ErrorMessage = "Email cannot exceed 100 characters")]
|
|
public string PrimaryContactEmail { get; set; } = string.Empty;
|
|
|
|
[Phone(ErrorMessage = "Invalid phone number format")]
|
|
[StringLength(20, ErrorMessage = "Phone number cannot exceed 20 characters")]
|
|
public string? Phone { get; set; }
|
|
|
|
[StringLength(200, ErrorMessage = "Address cannot exceed 200 characters")]
|
|
public string? Address { get; set; }
|
|
|
|
[StringLength(100, ErrorMessage = "City cannot exceed 100 characters")]
|
|
public string? City { get; set; }
|
|
|
|
[StringLength(2, ErrorMessage = "State must be 2 characters")]
|
|
public string? State { get; set; }
|
|
|
|
[StringLength(10, ErrorMessage = "Zip code cannot exceed 10 characters")]
|
|
public string? ZipCode { get; set; }
|
|
|
|
[StringLength(50, ErrorMessage = "Time zone cannot exceed 50 characters")]
|
|
public string? TimeZone { get; set; }
|
|
|
|
/// <summary>Cash or Accrual accounting method preference for financial reports.</summary>
|
|
public AccountingMethod AccountingMethod { get; set; } = AccountingMethod.Accrual;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read model for company operating costs
|
|
/// </summary>
|
|
public class CompanyOperatingCostsDto
|
|
{
|
|
public int Id { get; set; }
|
|
public int CompanyId { get; set; }
|
|
|
|
// Labor Rates
|
|
public decimal StandardLaborRate { get; set; }
|
|
public decimal AdditionalCoatLaborPercent { get; set; }
|
|
|
|
// Equipment Operating Costs
|
|
public decimal OvenOperatingCostPerHour { get; set; }
|
|
public decimal SandblasterCostPerHour { get; set; }
|
|
public decimal CoatingBoothCostPerHour { get; set; }
|
|
|
|
// Material Costs
|
|
public decimal PowderCoatingCostPerSqFt { get; set; }
|
|
|
|
// Tax
|
|
public decimal TaxPercent { get; set; }
|
|
|
|
// Shop Supplies Rate
|
|
public decimal ShopSuppliesRate { get; set; }
|
|
|
|
// Markup / Margin
|
|
public PowderCoating.Core.Enums.PricingMode PricingMode { get; set; } = PowderCoating.Core.Enums.PricingMode.MarkupOnMaterial;
|
|
public decimal GeneralMarkupPercentage { get; set; }
|
|
public decimal TargetMarginPercent { get; set; }
|
|
|
|
// Rush Charge
|
|
public string RushChargeType { get; set; } = "Percentage"; // "Percentage" or "FixedAmount"
|
|
public decimal RushChargePercentage { get; set; }
|
|
public decimal RushChargeFixedAmount { get; set; }
|
|
|
|
// Shop Minimum
|
|
public decimal ShopMinimumCharge { get; set; }
|
|
|
|
// Part Complexity Multipliers (%)
|
|
public decimal ComplexitySimplePercent { get; set; }
|
|
public decimal ComplexityModeratePercent { get; set; }
|
|
public decimal ComplexityComplexPercent { get; set; }
|
|
public decimal ComplexityExtremePercent { get; set; }
|
|
|
|
// AI Profile
|
|
public string? AiContextProfile { get; set; }
|
|
|
|
// Shop Capability
|
|
public ShopCapabilityTier ShopCapabilityTier { get; set; }
|
|
public BlastSetupType BlastSetupType { get; set; }
|
|
public decimal CompressorCfm { get; set; }
|
|
public int BlastNozzleSize { get; set; }
|
|
public BlastSubstrateType PrimaryBlastSubstrate { get; set; }
|
|
public decimal? BlastRateSqFtPerHourOverride { get; set; }
|
|
public CoatingGunType CoatingGunType { get; set; }
|
|
public decimal? CoatingRateSqFtPerHourOverride { get; set; }
|
|
|
|
/// <summary>Derived blast rate — shown to the user as a sanity-check value.</summary>
|
|
public decimal DerivedBlastRateSqFtPerHour { get; set; }
|
|
/// <summary>Derived coating rate — shown to the user as a sanity-check value.</summary>
|
|
public decimal DerivedCoatingRateSqFtPerHour { get; set; }
|
|
|
|
// Facility Overhead
|
|
public decimal MonthlyRent { get; set; }
|
|
public decimal MonthlyUtilities { get; set; }
|
|
public int MonthlyBillableHours { get; set; } = 160;
|
|
|
|
/// <summary>Derived facility overhead rate = (MonthlyRent + MonthlyUtilities) / MonthlyBillableHours.</summary>
|
|
public decimal FacilityOverheadRatePerHour { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DTO for updating company operating costs
|
|
/// </summary>
|
|
public class UpdateOperatingCostsDto
|
|
{
|
|
// Labor Rates (per hour)
|
|
[Required(ErrorMessage = "Standard labor rate is required")]
|
|
[Range(0, 10000, ErrorMessage = "Standard labor rate must be between 0 and 10,000")]
|
|
[Display(Name = "Standard Labor Rate ($/hr)")]
|
|
public decimal StandardLaborRate { get; set; }
|
|
|
|
[Range(0, 100, ErrorMessage = "Additional coat labor percent must be between 0 and 100")]
|
|
[Display(Name = "Additional Coat Labor (%)")]
|
|
public decimal AdditionalCoatLaborPercent { get; set; } = 30m;
|
|
|
|
// Equipment Operating Costs (per hour)
|
|
[Required(ErrorMessage = "Oven operating cost is required")]
|
|
[Range(0, 10000, ErrorMessage = "Oven operating cost must be between 0 and 10,000")]
|
|
[Display(Name = "Oven Operating Cost ($/hr)")]
|
|
public decimal OvenOperatingCostPerHour { get; set; }
|
|
|
|
[Range(0, 10000, ErrorMessage = "Sandblaster cost must be between 0 and 10,000")]
|
|
[Display(Name = "Sandblaster Cost ($/hr)")]
|
|
public decimal SandblasterCostPerHour { get; set; }
|
|
|
|
[Range(0, 10000, ErrorMessage = "Coating booth cost must be between 0 and 10,000")]
|
|
[Display(Name = "Coating Booth Cost ($/hr)")]
|
|
public decimal CoatingBoothCostPerHour { get; set; }
|
|
|
|
// Material Costs
|
|
[Range(0, 1000, ErrorMessage = "Powder coating cost must be between 0 and 1,000")]
|
|
[Display(Name = "Powder Coating Cost Per Sq Ft ($/sq ft)")]
|
|
public decimal PowderCoatingCostPerSqFt { get; set; }
|
|
|
|
// Tax
|
|
[Range(0, 100, ErrorMessage = "Tax percent must be between 0 and 100")]
|
|
[Display(Name = "Tax Percent (%)")]
|
|
public decimal TaxPercent { get; set; }
|
|
|
|
// Shop Supplies Rate
|
|
[Range(0, 100, ErrorMessage = "Shop supplies rate must be between 0 and 100")]
|
|
[Display(Name = "Shop Supplies Rate (%)")]
|
|
public decimal ShopSuppliesRate { get; set; }
|
|
|
|
// Markup / Margin Mode
|
|
[Display(Name = "Pricing Mode")]
|
|
public PowderCoating.Core.Enums.PricingMode PricingMode { get; set; } = PowderCoating.Core.Enums.PricingMode.MarkupOnMaterial;
|
|
|
|
[Range(0, 100, ErrorMessage = "General markup percentage must be between 0 and 100")]
|
|
[Display(Name = "General Markup (%)")]
|
|
public decimal GeneralMarkupPercentage { get; set; }
|
|
|
|
[Range(0, 99, ErrorMessage = "Target margin must be between 0 and 99")]
|
|
[Display(Name = "Target Margin (%)")]
|
|
public decimal TargetMarginPercent { get; set; }
|
|
|
|
// Rush Charge
|
|
[StringLength(20, ErrorMessage = "Rush charge type cannot exceed 20 characters")]
|
|
[Display(Name = "Rush Charge Type")]
|
|
public string RushChargeType { get; set; } = "Percentage"; // "Percentage" or "FixedAmount"
|
|
|
|
[Range(0, 100, ErrorMessage = "Rush charge percentage must be between 0 and 100")]
|
|
[Display(Name = "Rush Charge (%)")]
|
|
public decimal RushChargePercentage { get; set; }
|
|
|
|
[Range(0, 100000, ErrorMessage = "Rush charge fixed amount must be between 0 and 100,000")]
|
|
[Display(Name = "Rush Charge Fixed Amount ($)")]
|
|
public decimal RushChargeFixedAmount { get; set; }
|
|
|
|
// Shop Minimum
|
|
[Range(0, 100000, ErrorMessage = "Shop minimum charge must be between 0 and 100,000")]
|
|
[Display(Name = "Shop Minimum Charge ($)")]
|
|
public decimal ShopMinimumCharge { get; set; }
|
|
|
|
// Part Complexity Multipliers
|
|
[Range(0, 500)]
|
|
public decimal ComplexitySimplePercent { get; set; } = 0m;
|
|
|
|
[Range(0, 500)]
|
|
public decimal ComplexityModeratePercent { get; set; } = 5m;
|
|
|
|
[Range(0, 500)]
|
|
public decimal ComplexityComplexPercent { get; set; } = 15m;
|
|
|
|
[Range(0, 500)]
|
|
public decimal ComplexityExtremePercent { get; set; } = 25m;
|
|
|
|
// Facility Overhead
|
|
[Range(0, 1000000, ErrorMessage = "Monthly rent must be between 0 and 1,000,000")]
|
|
[Display(Name = "Monthly Rent ($)")]
|
|
public decimal MonthlyRent { get; set; } = 0m;
|
|
|
|
[Range(0, 1000000, ErrorMessage = "Monthly utilities must be between 0 and 1,000,000")]
|
|
[Display(Name = "Monthly Utilities ($)")]
|
|
public decimal MonthlyUtilities { get; set; } = 0m;
|
|
|
|
[Range(1, 10000, ErrorMessage = "Billable hours must be between 1 and 10,000")]
|
|
[Display(Name = "Billable Hours/Month")]
|
|
public int MonthlyBillableHours { get; set; } = 160;
|
|
}
|
|
|
|
/// <summary>DTO for updating the company AI profile text used for AI Photo Quote calibration.</summary>
|
|
public class UpdateAiProfileDto
|
|
{
|
|
[StringLength(2000, ErrorMessage = "AI profile cannot exceed 2000 characters")]
|
|
public string? AiContextProfile { get; set; }
|
|
}
|
|
|
|
/// <summary>DTO for saving the Quoting Calibration / Shop Capability Profile tab.</summary>
|
|
public class UpdateBlastProfileDto
|
|
{
|
|
public ShopCapabilityTier ShopCapabilityTier { get; set; }
|
|
public BlastSetupType BlastSetupType { get; set; }
|
|
|
|
[Range(0, 2000, ErrorMessage = "CFM must be between 0 and 2000")]
|
|
public decimal CompressorCfm { get; set; }
|
|
|
|
[Range(3, 8, ErrorMessage = "Nozzle size must be between #3 and #8")]
|
|
public int BlastNozzleSize { get; set; } = 4;
|
|
|
|
public BlastSubstrateType PrimaryBlastSubstrate { get; set; }
|
|
|
|
[Range(0, 5000, ErrorMessage = "Blast rate override must be between 0 and 5000")]
|
|
public decimal? BlastRateSqFtPerHourOverride { get; set; }
|
|
|
|
public CoatingGunType CoatingGunType { get; set; }
|
|
|
|
[Range(0, 5000, ErrorMessage = "Coating rate override must be between 0 and 5000")]
|
|
public decimal? CoatingRateSqFtPerHourOverride { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response returned after saving or recalculating the blast profile,
|
|
/// so the UI can display the freshly derived rates without a page reload.
|
|
/// </summary>
|
|
public class BlastProfileResultDto
|
|
{
|
|
public decimal DerivedBlastRate { get; set; }
|
|
public decimal DerivedCoatingRate { get; set; }
|
|
}
|
|
|
|
// ── Named blast setups ──────────────────────────────────────────────────────
|
|
|
|
public class BlastSetupDto
|
|
{
|
|
public int Id { get; set; }
|
|
public string Name { get; set; } = string.Empty;
|
|
public BlastSetupType SetupType { get; set; }
|
|
public decimal CompressorCfm { get; set; }
|
|
public int BlastNozzleSize { get; set; }
|
|
public BlastSubstrateType PrimarySubstrate { get; set; }
|
|
public decimal? BlastRateSqFtPerHourOverride { get; set; }
|
|
public bool IsDefault { get; set; }
|
|
public bool IsActive { get; set; }
|
|
public int DisplayOrder { get; set; }
|
|
public decimal DerivedRate { get; set; }
|
|
}
|
|
|
|
public class SaveBlastSetupDto
|
|
{
|
|
public int? Id { get; set; }
|
|
|
|
[Required]
|
|
[StringLength(100)]
|
|
public string Name { get; set; } = string.Empty;
|
|
|
|
public BlastSetupType SetupType { get; set; }
|
|
|
|
[Range(0, 9999)]
|
|
public decimal CompressorCfm { get; set; }
|
|
|
|
[Range(2, 8)]
|
|
public int BlastNozzleSize { get; set; } = 5;
|
|
|
|
public BlastSubstrateType PrimarySubstrate { get; set; }
|
|
|
|
[Range(0, 99999)]
|
|
public decimal? BlastRateSqFtPerHourOverride { get; set; }
|
|
|
|
public bool IsDefault { get; set; }
|
|
public bool IsActive { get; set; } = true;
|
|
}
|
|
|
|
/// <summary>Lightweight summary injected into wizard pages for the blast-setup dropdown.</summary>
|
|
public class BlastSetupSummaryDto
|
|
{
|
|
public int Id { get; set; }
|
|
public string Name { get; set; } = string.Empty;
|
|
public decimal DerivedRate { get; set; }
|
|
public bool IsDefault { get; set; }
|
|
}
|
|
}
|