122 lines
5.0 KiB
C#
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);
|
|
}
|
|
}
|
|
} |