122 lines
5.0 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 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<ValidationResult> 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);
}
}
}