113 lines
4.6 KiB
C#
113 lines
4.6 KiB
C#
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<ValidationResult> 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;
|
|
}
|
|
}
|
|
} |