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 ReactiveEnergyValidationRule(IDistributorRepository distributorRepository) : IValidationRule { private readonly IDistributorRepository _distributorRepository = distributorRepository; private const string RULE_NAME = "Reactive Energy Validation"; private const string PEAK_ERROR = "Peak reactive energy charge doesn't match the expected value"; private const string OFF_PEAK_ERROR = "Off-peak reactive energy charge doesn't match the expected value"; private const string POWER_FACTOR_ERROR = "Power factor is below minimum allowed"; private const decimal TOLERANCE = 0.01m; public int Priority => 3; // Run after demand validation public async Task ValidateAsync( BillComplianceRequest request, DistributorInformation distributorInfo, Client clientInfo) { if (!ShouldValidateReactiveEnergy(request.ConsumerGroup, request.Subgroup)) { return new ValidationResult { IsValid = true, RuleName = RULE_NAME, Message = string.Empty }; } var reactiveInfo = await _distributorRepository.GetReactiveEnergyInformationAsync( request.DistributorName, request.ConsumerGroup, request.Month) ?? throw new InvalidOperationException("Reactive energy information not found"); var isValid = true; var message = new StringBuilder(); // Validate power factor if (request.PowerFactor < reactiveInfo.MinimumPowerFactor) { isValid = false; message.AppendLine($"{POWER_FACTOR_ERROR}. Minimum: {reactiveInfo.MinimumPowerFactor:F2}, Found: {request.PowerFactor:F2}"); } // Calculate and validate peak period charges var expectedPeakCharge = CalculateReactiveCharge( request.PeakActiveEnergy, request.PeakReactiveEnergy, request.PeakDemandTariff, reactiveInfo.ReferenceRate, reactiveInfo.PeakAdjustmentFactor); if (Math.Abs(request.PeakReactiveCharge - expectedPeakCharge) > TOLERANCE) { isValid = false; message.AppendLine($"{PEAK_ERROR}. Expected: {expectedPeakCharge:F2}, Found: {request.PeakReactiveCharge:F2}"); } // Calculate and validate off-peak period charges var expectedOffPeakCharge = CalculateReactiveCharge( request.OffPeakActiveEnergy, request.OffPeakReactiveEnergy, request.OffPeakDemandTariff, reactiveInfo.ReferenceRate, reactiveInfo.OffPeakAdjustmentFactor); if (Math.Abs(request.OffPeakReactiveCharge - expectedOffPeakCharge) > TOLERANCE) { isValid = false; message.AppendLine($"{OFF_PEAK_ERROR}. Expected: {expectedOffPeakCharge:F2}, Found: {request.OffPeakReactiveCharge:F2}"); } return new ValidationResult { IsValid = isValid, RuleName = RULE_NAME, Message = message.ToString().TrimEnd() }; } private static bool ShouldValidateReactiveEnergy(string consumerGroup, string subgroup) { // According to ANEEL, reactive energy 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 CalculateReactiveCharge( decimal activeEnergy, decimal reactiveEnergy, decimal tariff, decimal referenceRate, decimal adjustmentFactor) { if (activeEnergy == 0) return 0; var powerFactor = activeEnergy / (decimal)Math.Sqrt((double)(activeEnergy * activeEnergy + reactiveEnergy * reactiveEnergy)); if (powerFactor >= 0.92m) return 0; var excessReactive = reactiveEnergy - (activeEnergy * 0.426m); // tan(arcos(0.92)) return excessReactive * tariff * referenceRate * adjustmentFactor; } } }