using System; using System.Threading.Tasks; using System.Text; using Compliance.Domain.Models; using Compliance.DTOs; using Compliance.Infrastructure.Repositories; namespace Compliance.Services.ValidationRules { public class DemandValidationRule(IDistributorRepository distributorRepository) : IValidationRule { private readonly IDistributorRepository _distributorRepository = distributorRepository; private const string RULE_NAME = "Demand Validation"; private const string PEAK_DEMAND_ERROR = "Peak demand charge doesn't match the expected value"; private const string OFF_PEAK_DEMAND_ERROR = "Off-peak demand charge doesn't match the expected value"; private const string ULTRAPASSAGEM_ERROR = "Demand excess charge is incorrect"; private const decimal TOLERANCE = 0.01m; public int Priority => 2; // High priority, run right after basic validation public async Task ValidateAsync( BillComplianceRequest request, DistributorInformation distributorInfo, Client clientInfo) { // Only validate demand for applicable consumer groups if (!IsApplicable(request.ConsumerGroup, request.Subgroup)) { return new ValidationResult { IsValid = true, RuleName = RULE_NAME, Message = string.Empty }; } var demandInfo = await _distributorRepository.GetDemandInformationAsync( request.DistributorName, request.ConsumerGroup, request.Month) ?? throw new InvalidOperationException("Demand information not found"); var isValid = true; var message = new StringBuilder(); // Validate peak demand charges var (expectedPeakCharge, expectedPeakExcess) = CalculateDemandCharges( request.MeasuredPeakDemand, demandInfo.ContractedPeakDemand, request.PeakDemandTariff, demandInfo); if (Math.Abs(request.PeakDemandCharge - expectedPeakCharge) > TOLERANCE) { isValid = false; message.AppendLine($"{PEAK_DEMAND_ERROR}. Expected: {expectedPeakCharge:F2}, Found: {request.PeakDemandCharge:F2}"); } if (Math.Abs(request.PeakDemandExcessCharge - expectedPeakExcess) > TOLERANCE) { isValid = false; message.AppendLine($"{ULTRAPASSAGEM_ERROR} (Peak). Expected: {expectedPeakExcess:F2}, Found: {request.PeakDemandExcessCharge:F2}"); } // Validate off-peak demand charges var (expectedOffPeakCharge, expectedOffPeakExcess) = CalculateDemandCharges( request.MeasuredOffPeakDemand, demandInfo.ContractedOffPeakDemand, request.OffPeakDemandTariff, demandInfo); if (Math.Abs(request.OffPeakDemandCharge - expectedOffPeakCharge) > TOLERANCE) { isValid = false; message.AppendLine($"{OFF_PEAK_DEMAND_ERROR}. Expected: {expectedOffPeakCharge:F2}, Found: {request.OffPeakDemandCharge:F2}"); } if (Math.Abs(request.OffPeakDemandExcessCharge - expectedOffPeakExcess) > TOLERANCE) { isValid = false; message.AppendLine($"{ULTRAPASSAGEM_ERROR} (Off-Peak). Expected: {expectedOffPeakExcess:F2}, Found: {request.OffPeakDemandExcessCharge:F2}"); } return new ValidationResult { IsValid = isValid, RuleName = RULE_NAME, Message = message.ToString().TrimEnd() }; } private static bool IsApplicable(string consumerGroup, string subgroup) { // According to ANEEL, demand charges apply to these groups return consumerGroup.ToUpper() switch { "A" => true, // All A group consumers "B" when subgroup == "B3" => true, // Commercial/industrial B3 consumers _ => false }; } private static (decimal charge, decimal excess) CalculateDemandCharges( decimal measuredDemand, decimal contractedDemand, decimal tariff, DemandInformation info) { var minDemand = contractedDemand * (info.MinimumDemandPercentage / 100); var maxDemand = contractedDemand * (1 + info.TolerancePercentage / 100); // Calculate base charge var billingDemand = Math.Max(measuredDemand, minDemand); var baseCharge = Math.Min(billingDemand, contractedDemand) * tariff; // Calculate excess charge if applicable var excess = measuredDemand > maxDemand ? (measuredDemand - contractedDemand) * tariff * info.UltrapassagemRate : 0; return (baseCharge, excess); } } }