Files
PowderCoatingLogix/tests/PowderCoating.UnitTests/PricingCalculationServiceTests.cs
T
spouliot 71caa93461 Fix unit test build failures after logo service and pricing changes
DepositsController and GiftCertificatesController gained a required
ICompanyLogoService constructor parameter in the PDF logo fix; their
test factories were not updated and failed to compile on Jenkins.
Added Mock.Of<ICompanyLogoService>() to both factory methods and the
missing using directive to DepositsControllerTests.

PricingCalculationService now only charges oven cost for items that
have explicit coating layers (Coats collection non-empty), because
sandblast/prep-only and labor items do not go in the oven. Two tests
that tested the old "all items count toward oven fraction" logic were
updated to include a single coat entry on each item, which restores
the expected oven fraction math without changing the tested behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 13:38:18 -04:00

682 lines
24 KiB
C#

using Microsoft.Extensions.Logging;
using Moq;
using PowderCoating.Application.DTOs.Quote;
using PowderCoating.Application.Services;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Interfaces;
using PowderCoating.Core.Interfaces.Repositories;
using Xunit;
namespace PowderCoating.UnitTests;
public class PricingCalculationServiceTests
{
[Fact]
public async Task CalculateCoatPriceAsync_CustomPowder_ChargesFullOrderQuantity()
{
var costs = CreateOperatingCosts();
var unitOfWork = CreateUnitOfWorkMock(costs);
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var coat = new CreateQuoteItemCoatDto
{
CoatName = "Custom Red",
PowderCostPerLb = 10m,
PowderToOrder = 3m,
CoverageSqFtPerLb = 30m,
TransferEfficiency = 65m
};
var result = await service.CalculateCoatPriceAsync(
coat,
itemSurfaceAreaSqFt: 5m,
quantity: 2m,
coatIndex: 0,
estimatedMinutesBase: 15,
companyId: 1);
Assert.Equal(30m, result.CoatMaterialCost);
Assert.Equal(30m, result.CoatLaborCost);
Assert.Equal(60m, result.CoatTotalCost);
}
[Fact]
public async Task CalculateQuoteItemPriceAsync_LaborItem_UsesStandardLaborRate()
{
var costs = CreateOperatingCosts();
costs.StandardLaborRate = 80m;
var unitOfWork = CreateUnitOfWorkMock(costs);
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var item = new CreateQuoteItemDto
{
Description = "Shop labor",
IsLaborItem = true,
Quantity = 2.5m
};
var result = await service.CalculateQuoteItemPriceAsync(item, companyId: 1);
Assert.Equal(0m, result.MaterialCost);
Assert.Equal(200m, result.LaborCost);
Assert.Equal(80m, result.UnitPrice);
Assert.Equal(200m, result.TotalPrice);
}
[Fact]
public async Task CalculateQuoteItemPriceAsync_AiItem_UsesManualUnitPriceWithoutAdditionalCosts()
{
var unitOfWork = CreateUnitOfWorkMock(CreateOperatingCosts());
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var item = new CreateQuoteItemDto
{
Description = "AI wheel estimate",
IsAiItem = true,
ManualUnitPrice = 123m,
Quantity = 2m
};
var result = await service.CalculateQuoteItemPriceAsync(item, companyId: 1);
Assert.Equal(0m, result.MaterialCost);
Assert.Equal(0m, result.LaborCost);
Assert.Equal(123m, result.UnitPrice);
Assert.Equal(246m, result.TotalPrice);
}
[Fact]
public async Task CalculateCoatPriceAsync_InventoryPowder_UsesCalculatedUsageOnly()
{
var unitOfWork = CreateUnitOfWorkMock(
CreateOperatingCosts(),
inventoryItem: new InventoryItem
{
Id = 12,
CompanyId = 1,
Name = "Gloss Black",
UnitCost = 12m
});
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var coat = new CreateQuoteItemCoatDto
{
CoatName = "Gloss Black",
InventoryItemId = 12,
CoverageSqFtPerLb = 24m,
TransferEfficiency = 50m
};
var result = await service.CalculateCoatPriceAsync(
coat,
itemSurfaceAreaSqFt: 10m,
quantity: 2m,
coatIndex: 0,
estimatedMinutesBase: 30,
companyId: 1);
Assert.Equal(20m, result.CoatMaterialCost, 2);
Assert.Equal(60m, result.CoatLaborCost);
Assert.Equal(80m, result.CoatTotalCost, 2);
}
[Fact]
public async Task CalculateCoatPriceAsync_MetricTenant_ConvertsSurfaceAreaBeforePricing()
{
var unitOfWork = CreateUnitOfWorkMock(CreateOperatingCosts());
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(true);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var coat = new CreateQuoteItemCoatDto
{
CoatName = "Metric Blue",
PowderCostPerLb = 5m,
CoverageSqFtPerLb = 10m,
TransferEfficiency = 100m
};
var result = await service.CalculateCoatPriceAsync(
coat,
itemSurfaceAreaSqFt: 1m,
quantity: 1m,
coatIndex: 0,
estimatedMinutesBase: 0,
companyId: 1);
Assert.Equal(5.38m, result.CoatMaterialCost);
Assert.Equal(0m, result.CoatLaborCost);
Assert.Equal(5.38m, result.CoatTotalCost);
}
[Fact]
public async Task CalculateCoatPriceAsync_AdditionalCoatWithNoExtraLayerCharge_SkipsLabor()
{
var unitOfWork = CreateUnitOfWorkMock(CreateOperatingCosts());
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var coat = new CreateQuoteItemCoatDto
{
CoatName = "Clear Coat",
PowderCostPerLb = 4m,
PowderToOrder = 1m,
CoverageSqFtPerLb = 20m,
TransferEfficiency = 100m,
NoExtraLayerCharge = true
};
var result = await service.CalculateCoatPriceAsync(
coat,
itemSurfaceAreaSqFt: 0m,
quantity: 2m,
coatIndex: 1,
estimatedMinutesBase: 45,
companyId: 1);
Assert.Equal(4m, result.CoatMaterialCost);
Assert.Equal(0m, result.CoatLaborCost);
Assert.Equal(4m, result.CoatTotalCost);
}
[Fact]
public async Task CalculateCoatPriceAsync_WhenOperatingCostsMissing_ReturnsZeros()
{
var unitOfWork = CreateUnitOfWorkMock(costs: null);
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var result = await service.CalculateCoatPriceAsync(
new CreateQuoteItemCoatDto { CoatName = "Unpriced" },
itemSurfaceAreaSqFt: 10m,
quantity: 1m,
coatIndex: 0,
estimatedMinutesBase: 30,
companyId: 1);
Assert.Equal(0m, result.CoatMaterialCost);
Assert.Equal(0m, result.CoatLaborCost);
Assert.Equal(0m, result.CoatTotalCost);
}
[Fact]
public async Task CalculateQuoteItemPriceAsync_CatalogItem_UsesPowderCostOverrideAsBasePrice()
{
var unitOfWork = CreateUnitOfWorkMock(
CreateOperatingCosts(),
catalogItem: new CatalogItem
{
Id = 50,
CompanyId = 1,
Name = "Wheel",
DefaultPrice = 10m
});
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var item = new CreateQuoteItemDto
{
Description = "Override catalog item",
CatalogItemId = 50,
PowderCostOverride = 77m,
Quantity = 3m
};
var result = await service.CalculateQuoteItemPriceAsync(item, companyId: 1);
Assert.Equal(0m, result.MaterialCost);
Assert.Equal(0m, result.LaborCost);
Assert.Equal(77m, result.UnitPrice);
Assert.Equal(231m, result.TotalPrice);
}
[Fact]
public async Task CalculateQuoteItemPriceAsync_CatalogItem_AddsPrepCostAndCustomPowder()
{
var unitOfWork = CreateUnitOfWorkMock(
CreateOperatingCosts(),
catalogItem: new CatalogItem
{
Id = 51,
CompanyId = 1,
Name = "Bracket",
DefaultPrice = 50m
});
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var item = new CreateQuoteItemDto
{
Description = "Bracket with prep",
CatalogItemId = 51,
Quantity = 2m,
IncludePrepCost = true,
PrepServices = new List<CreateQuoteItemPrepServiceDto>
{
new() { PrepServiceId = 1, EstimatedMinutes = 30 }
},
Coats = new List<CreateQuoteItemCoatDto>
{
new()
{
CoatName = "Custom Green",
PowderCostPerLb = 5m,
PowderToOrder = 2m
}
}
};
var result = await service.CalculateQuoteItemPriceAsync(item, companyId: 1);
Assert.Equal(10m, result.MaterialCost);
Assert.Equal(30m, result.LaborCost);
Assert.Equal(0m, result.EquipmentCost);
Assert.Equal(70m, result.UnitPrice);
Assert.Equal(140m, result.TotalPrice);
}
[Fact]
public async Task CalculateQuoteItemPriceAsync_MarginMode_AppliesAdditionalCoatAndComplexity()
{
var costs = CreateOperatingCosts();
costs.PricingMode = PowderCoating.Core.Enums.PricingMode.MarginOnTotalCost;
costs.TargetMarginPercent = 50m;
costs.CoatingBoothCostPerHour = 0m;
costs.ComplexityModeratePercent = 5m;
var unitOfWork = CreateUnitOfWorkMock(costs);
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var item = new CreateQuoteItemDto
{
Description = "Complex fabricated part",
Quantity = 1m,
SurfaceAreaSqFt = 10m,
EstimatedMinutes = 60,
Complexity = "Moderate",
Coats = new List<CreateQuoteItemCoatDto>
{
new()
{
CoatName = "Base",
PowderCostPerLb = 10m,
CoverageSqFtPerLb = 10m,
TransferEfficiency = 100m
},
new()
{
CoatName = "Top",
PowderCostPerLb = 10m,
CoverageSqFtPerLb = 10m,
TransferEfficiency = 100m
}
}
};
var result = await service.CalculateQuoteItemPriceAsync(item, companyId: 1);
Assert.Equal(10.5m, result.MaterialCost);
Assert.Equal(60m, result.LaborCost);
Assert.Equal(0m, result.EquipmentCost);
Assert.Equal(222.075m, result.ItemSubtotal);
Assert.Equal(222.075m, result.TotalPrice);
}
[Fact]
public async Task CalculateQuoteTotalsAsync_MixedAiAndManualItems_ScalesOvenCostBySurfaceAreaAndUsesManualTax()
{
var costs = CreateOperatingCosts();
costs.OvenOperatingCostPerHour = 30m;
costs.DefaultOvenCycleMinutes = 60;
costs.ShopSuppliesRate = 0m;
costs.TaxPercent = 5m;
costs.MonthlyRent = 0m;
costs.MonthlyUtilities = 0m;
var unitOfWork = CreateUnitOfWorkMock(costs);
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var items = new List<CreateQuoteItemDto>
{
new()
{
Description = "AI estimate",
IsAiItem = true,
ManualUnitPrice = 200m,
Quantity = 1m,
SurfaceAreaSqFt = 50m,
Coats = new List<CreateQuoteItemCoatDto> { new() { CoatName = "Base Coat", Sequence = 1 } }
},
new()
{
Description = "Labor item",
IsLaborItem = true,
Quantity = 1m,
SurfaceAreaSqFt = 50m,
EstimatedMinutes = 60,
Coats = new List<CreateQuoteItemCoatDto> { new() { CoatName = "Base Coat", Sequence = 1 } }
}
};
var result = await service.CalculateQuoteTotalsAsync(
items,
companyId: 1,
manualTaxPercent: 8m);
Assert.Equal(260m, result.ItemsSubtotal);
Assert.Equal(15m, result.OvenBatchCost);
Assert.Equal(275m, result.SubtotalBeforeDiscount);
Assert.Equal(8m, result.TaxPercent);
Assert.Equal(22m, result.TaxAmount);
Assert.Equal(297m, result.Total);
}
[Fact]
public async Task CalculateQuoteTotalsAsync_ZeroSurfaceAreaFallback_UsesItemCountForOvenFraction()
{
var costs = CreateOperatingCosts();
costs.OvenOperatingCostPerHour = 20m;
costs.DefaultOvenCycleMinutes = 60;
costs.ShopSuppliesRate = 0m;
costs.TaxPercent = 0m;
costs.MonthlyRent = 0m;
costs.MonthlyUtilities = 0m;
var unitOfWork = CreateUnitOfWorkMock(costs);
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var items = new List<CreateQuoteItemDto>
{
new()
{
Description = "AI item",
IsAiItem = true,
ManualUnitPrice = 100m,
Quantity = 1m,
Coats = new List<CreateQuoteItemCoatDto> { new() { CoatName = "Base Coat", Sequence = 1 } }
},
new()
{
Description = "Shelf item",
IsSalesItem = true,
ManualUnitPrice = 40m,
Quantity = 1m,
Coats = new List<CreateQuoteItemCoatDto> { new() { CoatName = "Base Coat", Sequence = 1 } }
}
};
var result = await service.CalculateQuoteTotalsAsync(items, companyId: 1);
Assert.Equal(140m, result.ItemsSubtotal);
Assert.Equal(10m, result.OvenBatchCost);
Assert.Equal(150m, result.Total);
}
[Fact]
public async Task CalculateQuoteTotalsAsync_FixedRushAndFacilityOverhead_AreAppliedBeforeTotal()
{
var costs = CreateOperatingCosts();
costs.OvenOperatingCostPerHour = 0m;
costs.MonthlyRent = 1600m;
costs.MonthlyUtilities = 0m;
costs.MonthlyBillableHours = 160;
costs.ShopSuppliesRate = 10m;
costs.RushChargeType = "FixedAmount";
costs.RushChargeFixedAmount = 25m;
costs.TaxPercent = 0m;
var unitOfWork = CreateUnitOfWorkMock(costs);
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var items = new List<CreateQuoteItemDto>
{
new()
{
Description = "Labor item",
IsLaborItem = true,
Quantity = 2m,
EstimatedMinutes = 60
}
};
var result = await service.CalculateQuoteTotalsAsync(
items,
companyId: 1,
isRushJob: true);
Assert.Equal(120m, result.ItemsSubtotal);
Assert.Equal(10m, result.FacilityOverheadRatePerHour);
Assert.Equal(20m, result.FacilityOverheadCost);
Assert.Equal(12m, result.ShopSuppliesAmount);
Assert.Equal(152m, result.SubtotalBeforeDiscount);
Assert.Equal(25m, result.RushFee);
Assert.Equal(177m, result.Total);
}
[Fact]
public async Task CalculateQuoteTotalsAsync_AppliesTierDiscount_QuoteDiscount_RushFee_AndTax()
{
var costs = CreateOperatingCosts();
costs.StandardLaborRate = 100m;
costs.ShopSuppliesRate = 10m;
costs.RushChargeType = "Percentage";
costs.RushChargePercentage = 20m;
costs.TaxPercent = 5m;
costs.OvenOperatingCostPerHour = 0m;
costs.MonthlyRent = 0m;
costs.MonthlyUtilities = 0m;
var customerRepo = new Mock<ICustomerRepository>();
customerRepo
.Setup(x => x.FindAsync(It.IsAny<System.Linq.Expressions.Expression<Func<Customer, bool>>>(), false, It.IsAny<System.Linq.Expressions.Expression<Func<Customer, object>>[]>()))
.ReturnsAsync(new[]
{
new Customer { Id = 1, CompanyId = 1, PricingTierId = 10 }
});
var pricingTierRepo = new Mock<IRepository<PricingTier>>();
pricingTierRepo
.Setup(x => x.GetByIdAsync(10, false, It.IsAny<System.Linq.Expressions.Expression<Func<PricingTier, object>>[]>()))
.ReturnsAsync(new PricingTier { Id = 10, CompanyId = 1, DiscountPercent = 10m });
var unitOfWork = CreateUnitOfWorkMock(costs);
unitOfWork.SetupGet(x => x.Customers).Returns(customerRepo.Object);
unitOfWork.SetupGet(x => x.PricingTiers).Returns(pricingTierRepo.Object);
var tenantContext = new Mock<ITenantContext>();
tenantContext.Setup(x => x.UseMetricSystemAsync()).ReturnsAsync(false);
var service = new PricingCalculationService(
unitOfWork.Object,
Mock.Of<ILogger<PricingCalculationService>>(),
new MeasurementConversionService(),
tenantContext.Object);
var items = new List<CreateQuoteItemDto>
{
new()
{
Description = "Labor item",
IsLaborItem = true,
Quantity = 2m
}
};
var result = await service.CalculateQuoteTotalsAsync(
items,
companyId: 1,
customerId: 1,
discountType: "FixedAmount",
discountValue: 5m,
isRushJob: true);
Assert.Equal(200m, result.ItemsSubtotal);
Assert.Equal(20m, result.ShopSuppliesAmount);
Assert.Equal(220m, result.SubtotalBeforeDiscount);
Assert.Equal(22m, result.PricingTierDiscountAmount);
Assert.Equal(5m, result.QuoteDiscountAmount);
Assert.Equal(193m, result.SubtotalAfterDiscount);
Assert.Equal(38.6m, result.RushFee);
Assert.Equal(11.58m, result.TaxAmount);
Assert.Equal(243.18m, result.Total);
}
private static Mock<IUnitOfWork> CreateUnitOfWorkMock(
CompanyOperatingCosts? costs,
InventoryItem? inventoryItem = null,
CatalogItem? catalogItem = null)
{
var unitOfWork = new Mock<IUnitOfWork>();
var companyOperatingCostsRepo = new Mock<IRepository<CompanyOperatingCosts>>();
companyOperatingCostsRepo
.Setup(x => x.FindAsync(It.IsAny<System.Linq.Expressions.Expression<Func<CompanyOperatingCosts, bool>>>(), false, It.IsAny<System.Linq.Expressions.Expression<Func<CompanyOperatingCosts, object>>[]>()))
.ReturnsAsync(costs != null ? new[] { costs } : Array.Empty<CompanyOperatingCosts>());
var inventoryRepo = new Mock<IRepository<InventoryItem>>();
inventoryRepo
.Setup(x => x.GetByIdAsync(It.IsAny<int>(), false, It.IsAny<System.Linq.Expressions.Expression<Func<InventoryItem, object>>[]>()))
.ReturnsAsync(inventoryItem);
var catalogRepo = new Mock<IRepository<CatalogItem>>();
catalogRepo
.Setup(x => x.GetByIdAsync(It.IsAny<int>(), false, It.IsAny<System.Linq.Expressions.Expression<Func<CatalogItem, object>>[]>()))
.ReturnsAsync(catalogItem);
var customerRepo = new Mock<ICustomerRepository>();
customerRepo
.Setup(x => x.FindAsync(It.IsAny<System.Linq.Expressions.Expression<Func<Customer, bool>>>(), false, It.IsAny<System.Linq.Expressions.Expression<Func<Customer, object>>[]>()))
.ReturnsAsync(Array.Empty<Customer>());
var pricingTierRepo = new Mock<IRepository<PricingTier>>();
pricingTierRepo
.Setup(x => x.GetByIdAsync(It.IsAny<int>(), false, It.IsAny<System.Linq.Expressions.Expression<Func<PricingTier, object>>[]>()))
.ReturnsAsync((PricingTier?)null);
unitOfWork.SetupGet(x => x.CompanyOperatingCosts).Returns(companyOperatingCostsRepo.Object);
unitOfWork.SetupGet(x => x.InventoryItems).Returns(inventoryRepo.Object);
unitOfWork.SetupGet(x => x.CatalogItems).Returns(catalogRepo.Object);
unitOfWork.SetupGet(x => x.Customers).Returns(customerRepo.Object);
unitOfWork.SetupGet(x => x.PricingTiers).Returns(pricingTierRepo.Object);
return unitOfWork;
}
private static CompanyOperatingCosts CreateOperatingCosts()
{
return new CompanyOperatingCosts
{
Id = 1,
CompanyId = 1,
StandardLaborRate = 60m,
AdditionalCoatLaborPercent = 50m,
OvenOperatingCostPerHour = 25m,
SandblasterCostPerHour = 20m,
CoatingBoothCostPerHour = 10m,
PowderCoatingCostPerSqFt = 1m,
PricingMode = PowderCoating.Core.Enums.PricingMode.MarkupOnMaterial,
GeneralMarkupPercentage = 20m,
TargetMarginPercent = 40m,
TaxPercent = 5m,
ShopSuppliesRate = 10m,
DefaultOvenCycleMinutes = 60,
RushChargeType = "Percentage",
RushChargePercentage = 15m,
RushChargeFixedAmount = 50m,
ShopMinimumCharge = 0m,
ComplexitySimplePercent = 0m,
ComplexityModeratePercent = 5m,
ComplexityComplexPercent = 15m,
ComplexityExtremePercent = 25m,
MonthlyBillableHours = 160
};
}
}