This commit is contained in:
Adriano Serighelli 2025-06-03 10:23:41 -03:00
parent 48c729e5b6
commit e4be58d728
73 changed files with 92 additions and 35646 deletions

1
.gitignore vendored
View File

@ -361,3 +361,4 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
.aider*

View File

@ -0,0 +1,72 @@
// System
using System;
using System.Data;
using System.Data.OleDb;
using System.Text.Json;
// Microsoft
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
// Project
using Compliance.DTOs;
using Compliance.Infrastructure.Repositories;
using Compliance.Services;
using Compliance.Services.ValidationRules;
namespace Compliance
{
internal class Program
{
private static string GetConnectionString(IConfiguration configuration)
{
return configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string not found");
}
private static readonly JsonSerializerOptions s_writeOptions = new()
{
WriteIndented = true
};
public static async Task Main(string[] args)
{
//if (args.Length == 0) { return; }
var services = ConfigureServices();
var billComplianceService = services.GetRequiredService<IBillComplianceService>();
// Read JSON input
var jsonInput = await File.ReadAllTextAsync(args[0]);
var request = JsonSerializer.Deserialize<BillComplianceRequest>(jsonInput) ?? throw new InvalidOperationException("Failed to deserialize request");
var result = await billComplianceService.ValidateBillAsync(request);
// Output results
var jsonOutput = JsonSerializer.Serialize(result, s_writeOptions);
await File.WriteAllTextAsync("compliance_results.json", jsonOutput);
}
private static ServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
// Add configuration
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
services.AddSingleton<IConfiguration>(configuration);
var connectionString = GetConnectionString(configuration);
services.AddSingleton<IDistributorRepository>(new DistributorRepository(connectionString));
services.AddSingleton<IClientRepository>(new ClientRepository(connectionString));
// Register validation rule factory
services.AddSingleton<IValidationRuleFactory, ValidationRuleFactory>();
services.AddSingleton<IBillComplianceService, BillComplianceService>();
return services.BuildServiceProvider();
}
}
}

18
.vscode/c_cpp_properties.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"compilerPath": "cl.exe",
"intelliSenseMode": "windows-msvc-x64"
}
],
"version": 4
}

View File

@ -1 +0,0 @@
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)

View File

@ -1,27 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Data.OleDb" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -1,25 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Compliance", "Compliance.csproj", "{D1C5068C-00B9-458D-9659-BEB906BC331C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D1C5068C-00B9-458D-9659-BEB906BC331C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1C5068C-00B9-458D-9659-BEB906BC331C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1C5068C-00B9-458D-9659-BEB906BC331C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1C5068C-00B9-458D-9659-BEB906BC331C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {40E4BAE8-718C-4F97-956A-28C6BB170FDB}
EndGlobalSection
EndGlobal

View File

@ -1,124 +0,0 @@
namespace Compliance.DTOs
{
public class BillComplianceRequest
{
public required string SmartCode { get; set; }
public required string DistributorName { get; set; }
public string Month { get; set; } = string.Empty;
public required string ConsumerGroup { get; set; }
public required string Subgroup { get; set; }
public required string SupplyVoltage { get; set; }
public required DateTime PreviousReadingDate { get; set; }
public required DateTime CurrentReadingDate { get; set; }
public required decimal PreviousReading { get; set; }
public required decimal CurrentReading { get; set; }
public decimal ConsumptionAmount { get; set; }
public decimal PeakConsumption { get; set; }
public decimal OffPeakConsumption { get; set; }
public decimal PowerFactor { get; set; }
public decimal TUSDAmount { get; set; }
public decimal TEAmount { get; set; }
public decimal TotalAmount { get; set; }
public decimal PowerFactorAdjustment { get; set; }
public decimal PublicLightingAmount { get; set; }
public decimal FlagAmount { get; set; }
public decimal PISAmount { get; set; }
public decimal COFINSAmount { get; set; }
public decimal ICMSAmount { get; set; }
public decimal ICMSBase { get; set; }
public decimal MeasuredPeakDemand { get; set; }
public decimal MeasuredOffPeakDemand { get; set; }
public decimal PeakDemandTariff { get; set; }
public decimal OffPeakDemandTariff { get; set; }
public decimal PeakDemandCharge { get; set; }
public decimal OffPeakDemandCharge { get; set; }
public decimal PeakDemandExcessCharge { get; set; }
public decimal OffPeakDemandExcessCharge { get; set; }
public decimal PeakActiveEnergy { get; set; }
public decimal OffPeakActiveEnergy { get; set; }
public decimal PeakReactiveEnergy { get; set; }
public decimal OffPeakReactiveEnergy { get; set; }
public decimal PeakReactiveCharge { get; set; }
public decimal OffPeakReactiveCharge { get; set; }
public string Municipality { get; set; } = string.Empty;
public decimal MunicipalTaxBase { get; set; }
public decimal MunicipalTaxAmount { get; set; }
public string AppliedSeason { get; set; } = string.Empty;
public decimal SeasonalTUSDAmount { get; set; }
public decimal SeasonalTEAmount { get; set; }
public string AppliedFlag { get; set; } = "GREEN";
public int BillingDays { get; set; }
public decimal DiscountAmount { get; set; }
public string DiscountType { get; set; } = string.Empty;
public List<AdditionalCharge> AdditionalCharges { get; set; } = [];
public DateTime IssueDate { get; set; }
public DateTime DueDate { get; set; }
public DateTime PaymentDate { get; set; }
public decimal LatePaymentFee { get; set; }
public decimal InterestAmount { get; set; }
public bool IsPartialPayment { get; set; }
public decimal BillTotalBeforeLateCharges =>
TUSDAmount +
TEAmount +
PowerFactorAdjustment +
FlagAmount +
PublicLightingAmount +
ICMSAmount +
MunicipalTaxAmount +
AdditionalCharges.Sum(c => c.Amount) -
DiscountAmount;
// Emergency Situation Properties (Art. 350-354)
public bool IsEmergencySituation { get; set; }
public string? EmergencyDeclarationNumber { get; set; }
public int EmergencyPeriodDays { get; set; }
public decimal EmergencySupplyPercentage { get; set; }
public decimal NormalLoadBillEstimate { get; set; }
// Meter Reading Properties
public bool IsEstimatedReading { get; set; }
public string? EstimationJustification { get; set; }
public bool IsMeterReset { get; set; }
public string? MeterResetJustification { get; set; }
public DateTime? MeterResetDate { get; set; }
// Measurement Properties
public string MeterNumber { get; set; } = string.Empty;
public string MeterType { get; set; } = string.Empty;
public DateTime LastCalibrationDate { get; set; }
public bool IsSmartMeter { get; set; }
// Reading Access Properties
public bool HasReadingImpediment { get; set; }
public string? ImpedimentDescription { get; set; }
public int ConsecutiveEstimations { get; set; }
// Group-Specific Properties (Art. 297-299)
public List<string> ApplicableActivities { get; set; } = []; // Required for activity discount validation
public decimal ActivityDiscountAmount { get; set; } // Required for activity discount validation
public string BillingMonth { get; set; } = string.Empty; // Required for seasonal rules
// Billing Properties
public decimal BillTotalBeforeTaxes { get; set; } // Required for discount calculations
public decimal BillTotalAfterTaxes { get; set; } // Required for final amount validation
// Demand Properties
public decimal ContractedPeakDemand { get; set; } // Required for demand validation
public decimal ContractedOffPeakDemand { get; set; } // Required for demand validation
// Emergency Properties (already present but needs clarification)
public string? EmergencyType { get; set; } // Type of emergency situation
public List<string> EmergencyDocumentation { get; set; } = []; // Supporting documentation
// Subsidy Properties
public List<string> SubsidyTypes { get; set; } = []; // Types of subsidies applied
public Dictionary<string, decimal> SubsidyAmounts { get; set; } = []; // Amount per subsidy type
}
public class AdditionalCharge
{
public string Type { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string Justification { get; set; } = string.Empty;
}
}

View File

@ -1,11 +0,0 @@
using Compliance.Services.ValidationRules;
using Compliance.Domain.Models;
namespace Compliance.DTOs
{
public class BillComplianceResponse
{
public bool IsValid { get; set; }
public IEnumerable<ValidationResult> ValidationResults { get; set; } = [];
}
}

View File

@ -1,11 +0,0 @@
namespace Compliance.Domain.Models
{
public class AdditionalChargeInformation
{
public Dictionary<string, decimal> ChargeRates { get; set; } = []; // Rates for different charge types
public HashSet<string> MandatoryCharges { get; set; } = []; // Charges that must be applied
public HashSet<string> ConsumerGroupExemptions { get; set; } = []; // Groups exempt from specific charges
public bool RequireJustification { get; set; } = true; // Whether charges require justification
public decimal MaximumTotalPercentage { get; set; } = 5m; // Maximum total as percentage of bill
}
}

View File

@ -1,14 +0,0 @@
namespace Compliance.Domain.Models
{
public class Client
{
public string SmartCode { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Management { get; set; } = string.Empty;
public string Unit { get; set; } = string.Empty;
public string ConsumerGroup { get; set; } = string.Empty;
public string Subgroup { get; set; } = string.Empty;
public string SupplyVoltage { get; set; } = string.Empty;
public bool IsGovernmentEntity { get; set; }
}
}

View File

@ -1,11 +0,0 @@
namespace Compliance.Domain.Models
{
public class DemandInformation
{
public decimal ContractedPeakDemand { get; set; }
public decimal ContractedOffPeakDemand { get; set; }
public decimal TolerancePercentage { get; set; } = 5m; // Default tolerance
public decimal MinimumDemandPercentage { get; set; } = 10m; // Minimum billing percentage
public decimal UltrapassagemRate { get; set; } = 2m; // Penalty rate for exceeding contracted demand
}
}

View File

@ -1,11 +0,0 @@
namespace Compliance.Domain.Models
{
public class DistributedGenerationInfo
{
public int codigo { get; set; }
public string? GenerationType { get; set; }
public decimal InstalledCapacity { get; set; }
public string? CompensationMode { get; set; }
public string? CompensationType { get; set; }
}
}

View File

@ -1,19 +0,0 @@
namespace Compliance.Domain.Models
{
public class DistributorInformation
{
public int DistributorId { get; set; }
public string? Name { get; set; }
public string? Address { get; set; }
public string? CNPJ { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? State { get; set; }
public string? Region { get; set; }
public decimal PISPercentage { get; set; }
public decimal COFINSPercentage { get; set; }
public decimal ICMSPercentage { get; set; }
public string FlagColor { get; set; } = string.Empty;
public decimal FlagValue { get; set; }
}
}

View File

@ -1,14 +0,0 @@
namespace Compliance.Domain.Models
{
public class ElectricBill
{
public required string SmartCode { get; set; }
public required string Distributor { get; set; }
public required string Month { get; set; }
public decimal ConsumptionAmount { get; set; }
public decimal TUSDAmount { get; set; }
public decimal TEAmount { get; set; }
public decimal TotalAmount { get; set; }
public DateTime ReferenceDate { get; set; }
}
}

View File

@ -1,11 +0,0 @@
namespace Compliance.Domain.Models
{
public class FlagTariffInformation
{
public string FlagColor { get; set; } = "GREEN";
public decimal FlagValue { get; set; }
public HashSet<string> ExemptGroups { get; set; } = [];
public bool ApplyPartialMonth { get; set; }
public int ValidFromDay { get; set; } = 1;
}
}

View File

@ -1,18 +0,0 @@
namespace Compliance.Domain.Models
{
public class GroupSpecificRulesInfo
{
public string ConsumerGroup { get; set; } = string.Empty;
public string Subgroup { get; set; } = string.Empty;
public bool IsRural { get; set; }
public bool IsIrrigation { get; set; }
public string? IrrigationSchedule { get; set; }
public decimal? IrrigationDiscount { get; set; }
public string Season { get; set; } = string.Empty;
public DateTime SeasonStartDate { get; set; }
public DateTime SeasonEndDate { get; set; }
public decimal SeasonalMultiplier { get; set; }
public List<string> SpecialConditions { get; set; } = [];
public Dictionary<string, decimal> ActivityDiscounts { get; set; } = [];
}
}

View File

@ -1,10 +0,0 @@
namespace Compliance.Domain.Models
{
public class ICMSInformation
{
public decimal BaseRate { get; set; } // Base ICMS rate for the state
public Dictionary<string, decimal> GroupRates { get; set; } = []; // Special rates by consumer group
public HashSet<string> ExemptGroups { get; set; } = []; // Consumer groups exempt from ICMS
public bool IncludesTaxInBase { get; set; } // If true, ICMS is calculated "por dentro"
}
}

View File

@ -1,18 +0,0 @@
namespace Compliance.Domain.Models
{
public class MeasurementSystemInfo
{
public string MeterNumber { get; set; } = string.Empty;
public string MeterType { get; set; } = string.Empty;
public string AccuracyClass { get; set; } = string.Empty;
public DateTime LastCalibrationDate { get; set; }
public DateTime InstallationDate { get; set; }
public bool IsSmartMeter { get; set; }
public decimal MaximumError { get; set; }
public string Location { get; set; } = string.Empty;
public bool HasSeal { get; set; }
public string? SealNumber { get; set; }
public List<string> CommunicationProtocols { get; set; } = [];
public Dictionary<string, DateTime> ComponentCalibrations { get; set; } = [];
}
}

View File

@ -1,10 +0,0 @@
namespace Compliance.Domain.Models
{
public class MinimumBillingInformation
{
public int DistributorId { get; set; }
public string? ConsumerGroup { get; set; }
public decimal MinimumValue { get; set; }
public decimal MinimumConsumption { get; set; }
}
}

View File

@ -1,11 +0,0 @@
namespace Compliance.Domain.Models
{
public class MunicipalTaxInformation
{
public decimal BaseRate { get; set; } // Base municipal tax rate
public bool ApplyToICMS { get; set; } // Whether ICMS is part of the tax base
public bool ApplyToPublicLighting { get; set; } // Whether public lighting is part of the tax base
public HashSet<string> ExemptGroups { get; set; } = []; // Consumer groups exempt from municipal tax
public Dictionary<string, decimal> SpecialRates { get; set; } = []; // Special rates for specific groups
}
}

View File

@ -1,12 +0,0 @@
namespace Compliance.Domain.Models
{
public class PaymentTermsInformation
{
public int MinimumDueDays { get; set; } = 5; // Minimum days between bill issue and due date
public decimal LatePaymentFeePercentage { get; set; } = 2m; // Default late payment fee
public decimal MonthlyInterestRate { get; set; } = 1m; // Monthly interest rate for late payments
public bool AllowPartialPayments { get; set; } // Whether partial payments are accepted
public Dictionary<string, int> GroupSpecificDueDays { get; set; } = []; // Special due days for specific groups
public HashSet<string> ExemptFromLateCharges { get; set; } = []; // Groups exempt from late payment charges
}
}

View File

@ -1,15 +0,0 @@
namespace Compliance.Domain.Models
{
public class PublicLightingInformation
{
public decimal BaseRate { get; set; } // Base rate for public lighting
public HashSet<string> ExemptGroups { get; set; } = []; // Consumer groups exempt from public lighting charge
public Dictionary<string, decimal> ConsumptionRanges { get; set; } = []; // Different rates based on consumption ranges
public bool IsFixedCharge { get; set; } // Whether the charge is fixed or consumption-based
public decimal FixedAmount { get; set; } // Fixed amount when applicable
public bool HasValidMunicipalLaw { get; set; }
public string? CalculationType { get; set; }
public decimal Percentage { get; set; }
public IEnumerable<SubsidyInformation.ConsumptionRange> Ranges { get; set; } = [];
}
}

View File

@ -1,11 +0,0 @@
namespace Compliance.Domain.Models
{
public class ReactiveEnergyInformation
{
public decimal ReferenceRate { get; set; } // Reference rate for reactive energy charges
public decimal MinimumPowerFactor { get; set; } = 0.92m; // ANEEL standard
public bool ApplyPeakOffPeakDifferentiation { get; set; }
public decimal PeakAdjustmentFactor { get; set; } = 1.0m;
public decimal OffPeakAdjustmentFactor { get; set; } = 1.0m;
}
}

View File

@ -1,16 +0,0 @@
namespace Compliance.Domain.Models
{
public class ReadingImpedimentInfo
{
public string ImpedimentCode { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public DateTime ReportDate { get; set; }
public string ReportedBy { get; set; } = string.Empty;
public int ConsecutiveOccurrences { get; set; }
public string? ResolutionAttempts { get; set; }
public bool RequiresCustomerAction { get; set; }
public DateTime? CustomerNotificationDate { get; set; }
public string? AlternativeReadingMethod { get; set; }
public List<string> PhotoEvidence { get; set; } = [];
}
}

View File

@ -1,11 +0,0 @@
namespace Compliance.Domain.Models
{
public class ReadingPeriodInfo
{
public int MinimumDays { get; set; }
public int MaximumDays { get; set; }
public int StandardDays { get; set; }
public string[] ExemptConsumerGroups { get; set; } = [];
public bool AllowWeekendReadings { get; set; }
}
}

View File

@ -1,11 +0,0 @@
namespace Compliance.Domain.Models
{
public class ReadingPeriodRules
{
public int RuleId { get; set; }
public string? Description { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public string? ApplicableConsumerGroup { get; set; }
}
}

View File

@ -1,11 +0,0 @@
namespace Compliance.Domain.Models
{
public class SeasonalTariffInformation
{
public decimal DrySeasonMultiplier { get; set; } = 1.2m; // Default multiplier for dry season
public decimal WetSeasonMultiplier { get; set; } = 0.8m; // Default multiplier for wet season
public HashSet<string> ApplicableGroups { get; set; } = []; // Consumer groups eligible for seasonal rates
public DateOnly DrySeasonStart { get; set; } // Usually May
public DateOnly DrySeasonEnd { get; set; } // Usually November
}
}

View File

@ -1,18 +0,0 @@
namespace Compliance.Domain.Models
{
public class SubsidyInformation
{
public decimal BaseDiscountPercentage { get; set; } // Base discount rate
public Dictionary<string, decimal> GroupDiscounts { get; set; } = []; // Special discounts by consumer group
public bool ApplyToTUSD { get; set; } = true; // Whether discount applies to TUSD
public bool ApplyToTE { get; set; } = true; // Whether discount applies to TE
public bool ApplyToFlags { get; set; } = false; // Whether discount applies to flag charges
public ConsumptionRange Consumption { get; set; } = new();
public class ConsumptionRange
{
public decimal MinConsumption { get; set; }
public decimal? MaxConsumption { get; set; }
public decimal Amount { get; set; }
}
}
}

View File

@ -1,13 +0,0 @@
namespace Compliance.Domain.Models
{
public class TariffInformation
{
public int DistributorId { get; set; }
public string? Month { get; set; }
public string? ConsumerGroup { get; set; }
public string? Subgroup { get; set; }
public decimal TUSDValue { get; set; }
public decimal TEValue { get; set; }
public string? TariffModality { get; set; }
}
}

View File

@ -1,25 +0,0 @@
namespace Compliance.Domain.Models
{
public class TaxInformation
{
public int DistributorId { get; set; }
public string? Month { get; set; }
public decimal PISValue { get; set; }
public decimal COFINSValue { get; set; }
public decimal ICMSValue { get; set; }
public decimal IRRFValue { get; set; }
public decimal INSSValue { get; set; }
public decimal ISSValue { get; set; }
public decimal CSLLValue { get; set; }
public decimal IRPJValue { get; set; }
public decimal PISRetencaoValue { get; set; }
public decimal COFINSRetencaoValue { get; set; }
public decimal ICMSRetencaoValue { get; set; }
public decimal IRRFRetencaoValue { get; set; }
public string? TaxType { get; set; }
public string? State { get; set; }
public string? Municipality { get; set; }
public decimal Rate { get; set; }
public string? Description { get; set; }
}
}

View File

@ -1,39 +0,0 @@
namespace Compliance.Domain.Models
{
public class ValidationResult
{
public bool IsValid { get; set; }
public required string RuleName { get; set; }
public required string Message { get; set; }
public static ValidationResult Success(string ruleName)
{
return new ValidationResult
{
IsValid = true,
RuleName = ruleName,
Message = string.Empty
};
}
public static ValidationResult Failure(string ruleName, string error)
{
return new ValidationResult
{
IsValid = false,
RuleName = ruleName,
Message = error
};
}
public static ValidationResult Failure(string ruleName, IEnumerable<string> errors)
{
return new ValidationResult
{
IsValid = false,
RuleName = ruleName,
Message = string.Join("; ", errors)
};
}
}
}

View File

@ -1,55 +0,0 @@
using System.Data.OleDb;
using System.Threading.Tasks;
using Compliance.Domain.Models;
namespace Compliance.Infrastructure.Repositories
{
public class ClientRepository(string connectionString) : IClientRepository, IDisposable
{
private readonly string _connectionString = connectionString;
private bool _disposed;
public async Task<Client?> GetClientInformationAsync(string smartCode)
{
using var connection = new OleDbConnection(_connectionString);
await connection.OpenAsync();
var query = @"SELECT * FROM Dados_cadastrais WHERE Cod_Smart_unidade = @smartCode";
using var command = new OleDbCommand(query, connection);
command.Parameters.AddWithValue("@smartCode", smartCode);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new Client
{
SmartCode = reader["Cod_Smart_unidade"].ToString()!,
Name = reader["Cliente"].ToString()!,
Management = reader["Gestao"].ToString()!,
Unit = reader["Unidade"].ToString()!,
ConsumerGroup = reader["Grupo"].ToString()!,
Subgroup = reader["Subgrupo"].ToString()!,
SupplyVoltage = reader["Tensao"].ToString()!
};
}
return null;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
}
}
}
}

View File

@ -1,235 +0,0 @@
using System;
using System.Data.OleDb;
using System.Linq;
using System.Threading.Tasks;
using Compliance.Domain.Models;
using Microsoft.Extensions.Configuration;
using System.Net.Http;
using System.Data.Common;
using System.Collections.Generic;
namespace Compliance.Infrastructure.Repositories
{
public class DistributorRepository : IDistributorRepository
{
private readonly string _connectionString;
private const string BASE_QUERY = "SELECT * FROM [{0}] WHERE Distribuidora = @dist";
private const string GROUP_QUERY = BASE_QUERY + " AND Grupo = @grupo";
private const string MONTH_QUERY = BASE_QUERY + " AND Mes = @mes";
private const string GROUP_MONTH_QUERY = GROUP_QUERY + " AND Mes = @mes";
public DistributorRepository(string connectionString)
{
_connectionString = connectionString ??
throw new ArgumentNullException(nameof(connectionString));
}
public async Task<T?> QuerySingleAsync<T>(string tableName, object parameters)
where T : class, new()
{
using var connection = new OleDbConnection(_connectionString);
await connection.OpenAsync();
using var command = CreateCommand(connection, tableName, parameters);
using var reader = await command.ExecuteReaderAsync();
return await reader.ReadAsync()
? MapReaderToType<T>(reader)
: null;
}
private static OleDbCommand CreateCommand(
OleDbConnection connection,
string tableName,
object parameters)
{
var command = connection.CreateCommand();
var query = string.Format(BASE_QUERY, tableName);
foreach (var prop in parameters.GetType().GetProperties())
{
command.Parameters.AddWithValue($"@{prop.Name.ToLower()}",
prop.GetValue(parameters) ?? DBNull.Value);
}
command.CommandText = query;
return command;
}
private static T MapReaderToType<T>(DbDataReader reader) where T : class, new()
{
var result = new T();
var properties = typeof(T).GetProperties();
foreach (var prop in properties)
{
var columnName = prop.Name;
var ordinal = reader.GetOrdinal(columnName);
if (!reader.IsDBNull(ordinal))
{
var value = reader.GetValue(ordinal);
prop.SetValue(result, Convert.ChangeType(value, prop.PropertyType));
}
}
return result;
}
public async Task<DistributorInformation?> GetDistributorInformationAsync(string distributorName, string month)
{
return await QuerySingleAsync<DistributorInformation>("Distribuidoras",
new { dist = distributorName, mes = month });
}
public async Task<TariffInformation?> GetTariffInformationAsync(string distributorName, string month)
{
return await QuerySingleAsync<TariffInformation>("Tarifas",
new { dist = distributorName, mes = month });
}
public async Task<TaxInformation?> GetTaxInformationAsync(string distributorName, string month)
{
return await QuerySingleAsync<TaxInformation>("Impostos",
new { dist = distributorName, mes = month });
}
public async Task<FlagTariffInformation?> GetFlagTariffInformationAsync(string distributorName, string month)
{
return await QuerySingleAsync<FlagTariffInformation>("BandeiraTarifaria",
new { dist = distributorName, mes = month });
}
public async Task<PublicLightingInformation?> GetPublicLightingInformationAsync(
string distributorName,
string consumerGroup,
string month)
{
return await QuerySingleAsync<PublicLightingInformation>("IluminacaoPublica",
new { dist = distributorName, grupo = consumerGroup, mes = month });
}
public async Task<ICMSInformation?> GetICMSInformationAsync(
string distributorName,
string consumerGroup,
string month)
{
return await QuerySingleAsync<ICMSInformation>("ICMS",
new { dist = distributorName, grupo = consumerGroup, mes = month });
}
public async Task<DemandInformation?> GetDemandInformationAsync(
string distributorName,
string consumerGroup,
string month)
{
return await QuerySingleAsync<DemandInformation>("Demanda",
new { dist = distributorName, grupo = consumerGroup, mes = month });
}
public async Task<ReactiveEnergyInformation?> GetReactiveEnergyInformationAsync(
string distributorName,
string consumerGroup,
string month)
{
return await QuerySingleAsync<ReactiveEnergyInformation>("EnergiaReativa",
new { dist = distributorName, grupo = consumerGroup, mes = month });
}
public async Task<MunicipalTaxInformation?> GetMunicipalTaxInformationAsync(
string distributorName,
string municipality,
string month)
{
return await QuerySingleAsync<MunicipalTaxInformation>("TaxaMunicipal",
new { dist = distributorName, municipio = municipality, mes = month });
}
public async Task<SeasonalTariffInformation?> GetSeasonalTariffInformationAsync(
string distributorName,
string consumerGroup,
string month)
{
return await QuerySingleAsync<SeasonalTariffInformation>("TarifaSazonal",
new { dist = distributorName, grupo = consumerGroup, mes = month });
}
public async Task<PaymentTermsInformation?> GetPaymentTermsInformationAsync(
string distributorName,
string consumerGroup,
string month)
{
return await QuerySingleAsync<PaymentTermsInformation>("CondicoesPagamento",
new { dist = distributorName, grupo = consumerGroup, mes = month });
}
public async Task<MinimumBillingInformation?> GetMinimumBillingAsync(string distributorName)
{
return await QuerySingleAsync<MinimumBillingInformation>("FaturamentoMinimo",
new { dist = distributorName });
}
public async Task<ReadingPeriodInfo> GetReadingPeriodRulesAsync()
{
return await QuerySingleAsync<ReadingPeriodInfo>("RegrasLeitura", new { })
?? throw new InvalidOperationException("Reading period rules not found");
}
public async Task<DistributedGenerationInfo> GetDistributedGenerationInfoAsync(string smartCode)
{
return await QuerySingleAsync<DistributedGenerationInfo>("GeracaoDistribuida",
new { codigo = smartCode })
?? throw new InvalidOperationException("Distributed generation info not found");
}
public async Task<AdditionalChargeInformation?> GetAdditionalChargeInformationAsync(
string distributorName,
string month)
{
return await QuerySingleAsync<AdditionalChargeInformation>("CobrancasAdicionais",
new { dist = distributorName, mes = month });
}
public async Task<SubsidyInformation?> GetSubsidyInformationAsync(
string distributorName,
string consumerGroup,
string month)
{
return await QuerySingleAsync<SubsidyInformation>("Subsidios",
new { dist = distributorName, grupo = consumerGroup, mes = month });
}
public async Task<MinimumBillingInformation> GetMinimumBillingInformationAsync(
string distributorName,
string consumerGroup,
string month)
{
return await QuerySingleAsync<MinimumBillingInformation>("FaturamentoMinimo",
new { dist = distributorName, grupo = consumerGroup, mes = month })
?? throw new InvalidOperationException("Minimum billing information not found");
}
public async Task<MeasurementSystemInfo?> GetMeasurementSystemInfoAsync(string meterNumber)
{
return await QuerySingleAsync<MeasurementSystemInfo>("MeasurementSystems",
new { medidor = meterNumber });
}
public async Task<ReadingImpedimentInfo?> GetReadingImpedimentInfoAsync(
string smartCode,
DateTime readingDate)
{
return await QuerySingleAsync<ReadingImpedimentInfo>("ReadingImpediments",
new { codigo = smartCode, data = readingDate });
}
public async Task<GroupSpecificRulesInfo?> GetGroupSpecificRulesInfoAsync(
string distributorName,
string consumerGroup,
string month)
{
return await QuerySingleAsync<GroupSpecificRulesInfo>("RegrasGrupo",
new { dist = distributorName, grupo = consumerGroup, mes = month });
}
}
}

View File

@ -1,11 +0,0 @@
using System;
using System.Threading.Tasks;
using Compliance.Domain.Models;
namespace Compliance.Infrastructure.Repositories
{
public interface IClientRepository : IDisposable
{
Task<Client?> GetClientInformationAsync(string smartCode);
}
}

View File

@ -1,51 +0,0 @@
using System;
using System.Threading.Tasks;
using Compliance.Domain.Models;
namespace Compliance.Infrastructure.Repositories
{
public interface IDistributorRepository
{
Task<DistributorInformation?> GetDistributorInformationAsync(string distributorName, string month);
Task<TariffInformation?> GetTariffInformationAsync(string distributorName, string month);
Task<TaxInformation?> GetTaxInformationAsync(string distributorName, string month);
Task<FlagTariffInformation?> GetFlagTariffInformationAsync(string distributorName, string month);
Task<PublicLightingInformation?> GetPublicLightingInformationAsync(
string distributorName,
string consumerGroup,
string month);
Task<ICMSInformation?> GetICMSInformationAsync(string distributorName, string consumerGroup, string month);
Task<DemandInformation?> GetDemandInformationAsync(string distributorName, string consumerGroup, string month);
Task<ReactiveEnergyInformation?> GetReactiveEnergyInformationAsync(
string distributorName,
string consumerGroup,
string month);
Task<MunicipalTaxInformation?> GetMunicipalTaxInformationAsync(
string distributorName,
string municipality,
string month);
Task<SeasonalTariffInformation?> GetSeasonalTariffInformationAsync(
string distributorName,
string consumerGroup,
string month);
Task<PaymentTermsInformation?> GetPaymentTermsInformationAsync(
string distributorName,
string consumerGroup,
string month);
Task<MinimumBillingInformation?> GetMinimumBillingAsync(string distributorName);
Task<ReadingPeriodInfo> GetReadingPeriodRulesAsync();
Task<DistributedGenerationInfo> GetDistributedGenerationInfoAsync(string smartCode);
Task<AdditionalChargeInformation?> GetAdditionalChargeInformationAsync(string distributorName, string month);
Task<SubsidyInformation?> GetSubsidyInformationAsync(
string distributorName,
string consumerGroup,
string month);
Task<MinimumBillingInformation> GetMinimumBillingInformationAsync(
string distributorName,
string consumerGroup,
string month);
Task<MeasurementSystemInfo?> GetMeasurementSystemInfoAsync(string meterNumber);
Task<ReadingImpedimentInfo?> GetReadingImpedimentInfoAsync(string smartCode, DateTime readingDate);
Task<GroupSpecificRulesInfo?> GetGroupSpecificRulesInfoAsync(string consumerGroup, string subgroup, string billingMonth);
}
}

View File

@ -1,44 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using Compliance.DTOs;
using Compliance.Domain.Models;
using Compliance.Infrastructure.Repositories;
using Compliance.Services.ValidationRules;
namespace Compliance.Services
{
public class BillComplianceService(
IDistributorRepository distributorRepository,
IClientRepository clientRepository,
IValidationRuleFactory validationRuleFactory) : IBillComplianceService
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private readonly IClientRepository _clientRepository = clientRepository;
private readonly IValidationRuleFactory _validationRuleFactory = validationRuleFactory;
public async Task<BillComplianceResponse> ValidateBillAsync(BillComplianceRequest request)
{
var distributorInfo = await _distributorRepository.GetDistributorInformationAsync(
request.DistributorName,
request.Month) ?? throw new InvalidOperationException("Distributor information not found");
var clientInfo = await _clientRepository.GetClientInformationAsync(
request.SmartCode) ?? throw new InvalidOperationException("Client information not found");
var rules = _validationRuleFactory.CreateRules();
var validationResults = await Task.WhenAll(
rules.Select(rule => rule.ValidateAsync(request, distributorInfo, clientInfo)));
return new BillComplianceResponse
{
IsValid = validationResults.All(r => r.IsValid),
ValidationResults = validationResults.Select(r => new ValidationResult
{
IsValid = r.IsValid,
RuleName = r.RuleName,
Message = r.Message
})
};
}
}
}

View File

@ -1,10 +0,0 @@
using System.Threading.Tasks;
using Compliance.DTOs;
namespace Compliance.Services
{
public interface IBillComplianceService
{
Task<BillComplianceResponse> ValidateBillAsync(Compliance.DTOs.BillComplianceRequest request);
}
}

View File

@ -1,115 +0,0 @@
using System;
using System.Threading.Tasks;
using System.Text;
using System.Linq;
using Compliance.Domain.Models;
using Compliance.DTOs;
using Compliance.Infrastructure.Repositories;
namespace Compliance.Services.ValidationRules
{
public class AdditionalChargeValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Additional Charge Validation";
private const string AMOUNT_ERROR = "Additional charge amount doesn't match the expected value";
private const string MISSING_CHARGE_ERROR = "Mandatory charge not applied";
private const string INVALID_CHARGE_ERROR = "Invalid charge type applied";
private const string JUSTIFICATION_ERROR = "Missing justification for additional charge";
private const string TOTAL_ERROR = "Total additional charges exceed maximum allowed percentage";
private const decimal TOLERANCE = 0.01m;
public int Priority => 8; // Run after subsidy validation
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var chargeInfo = await _distributorRepository.GetAdditionalChargeInformationAsync(
request.DistributorName,
request.Month) ?? throw new InvalidOperationException("Additional charge information not found");
var isValid = true;
var message = new StringBuilder();
// Validate mandatory charges
foreach (var mandatoryCharge in chargeInfo.MandatoryCharges)
{
if (!request.AdditionalCharges.Any(c => c.Type.Equals(mandatoryCharge, StringComparison.OrdinalIgnoreCase)))
{
isValid = false;
message.AppendLine($"{MISSING_CHARGE_ERROR}: {mandatoryCharge}");
}
}
// Calculate bill base for percentage validation
var billBase = CalculateBillBase(request);
var totalCharges = 0m;
// Validate each charge
foreach (var charge in request.AdditionalCharges)
{
// Validate charge type
if (!chargeInfo.ChargeRates.ContainsKey(charge.Type.ToLower()))
{
isValid = false;
message.AppendLine($"{INVALID_CHARGE_ERROR}: {charge.Type}");
continue;
}
// Check for exemptions
if (chargeInfo.ConsumerGroupExemptions.Contains($"{charge.Type.ToLower()}:{request.ConsumerGroup.ToLower()}"))
{
if (charge.Amount != 0)
{
isValid = false;
message.AppendLine($"Consumer group exempt from charge: {charge.Type}");
}
continue;
}
// Validate justification
if (chargeInfo.RequireJustification && string.IsNullOrWhiteSpace(charge.Justification))
{
isValid = false;
message.AppendLine($"{JUSTIFICATION_ERROR}: {charge.Type}");
}
// Validate amount
var expectedAmount = billBase * (chargeInfo.ChargeRates[charge.Type.ToLower()] / 100m);
if (Math.Abs(charge.Amount - expectedAmount) > TOLERANCE)
{
isValid = false;
message.AppendLine($"{AMOUNT_ERROR} for {charge.Type}. Expected: {expectedAmount:F2}, Found: {charge.Amount:F2}");
}
totalCharges += charge.Amount;
}
// Validate total charges percentage
var maxAllowed = billBase * (chargeInfo.MaximumTotalPercentage / 100m);
if (totalCharges > maxAllowed)
{
isValid = false;
message.AppendLine($"{TOTAL_ERROR}. Maximum: {maxAllowed:F2}, Total: {totalCharges:F2}");
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
private static decimal CalculateBillBase(BillComplianceRequest request)
{
return request.TUSDAmount +
request.TEAmount +
request.PowerFactorAdjustment +
request.FlagAmount +
request.ICMSAmount +
request.MunicipalTaxAmount;
}
}
}

View File

@ -1,68 +0,0 @@
using System;
using System.Threading.Tasks;
using System.Text;
using Compliance.Domain.Models;
using Compliance.DTOs;
namespace Compliance.Services.ValidationRules
{
public class BasicInformationValidationRule : IValidationRule
{
private const string RULE_NAME = "Basic Information Validation";
public int Priority => 1; // Should run first
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var isValid = true;
var message = new StringBuilder();
// Validate Consumer SmartCode match
if (request.SmartCode != clientInfo.SmartCode)
{
isValid = false;
message.AppendLine("SmartCode in bill doesn't match client registration.");
}
// Validate Distributor match
if (request.DistributorName != distributorInfo.Name)
{
isValid = false;
message.AppendLine("Distributor name in bill doesn't match distributor information.");
}
// Validate Consumer Classification
if (!string.IsNullOrEmpty(clientInfo.ConsumerGroup))
{
if (clientInfo.ConsumerGroup != request.ConsumerGroup)
{
isValid = false;
message.AppendLine("Consumer group in bill doesn't match client registration.");
}
if (clientInfo.Subgroup != request.Subgroup)
{
isValid = false;
message.AppendLine("Consumer subgroup in bill doesn't match client registration.");
}
}
// Validate Supply Voltage
if (!string.IsNullOrEmpty(clientInfo.SupplyVoltage) &&
clientInfo.SupplyVoltage != request.SupplyVoltage)
{
isValid = false;
message.AppendLine("Supply voltage in bill doesn't match client registration.");
}
return await Task.FromResult(new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
});
}
}
}

View File

@ -1,31 +0,0 @@
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 ConsecutiveEstimationValidationRule : IValidationRule
{
private const string RULE_NAME = "Consecutive Estimation Validation";
private const int MAX_CONSECUTIVE_ESTIMATIONS = 3;
public int Priority => 1;
public Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
if (request.ConsecutiveEstimations > MAX_CONSECUTIVE_ESTIMATIONS)
{
return Task.FromResult(ValidationResult.Failure(RULE_NAME,
$"Maximum consecutive estimated readings ({MAX_CONSECUTIVE_ESTIMATIONS}) exceeded"));
}
return Task.FromResult(ValidationResult.Success(RULE_NAME));
}
}
}

View File

@ -1,69 +0,0 @@
using System;
using System.Threading.Tasks;
using System.Text;
using Compliance.Domain.Models;
using Compliance.DTOs;
namespace Compliance.Services.ValidationRules
{
public class ConsumptionValidationRule : IValidationRule
{
private const string RULE_NAME = "Consumption Validation";
private const string TOTAL_ERROR = "Total amount doesn't match the sum of TUSD and TE amounts";
private const string READING_PERIOD_ERROR = "Invalid reading period";
private const string CONSUMPTION_ERROR = "Consumption amount doesn't match readings difference";
private const string PEAK_OFFPEAK_ERROR = "Total consumption doesn't match sum of peak and off-peak consumption";
private const decimal TOLERANCE = 0.01m;
private const int MAX_BILLING_DAYS = 33; // According to ANEEL Resolution
public int Priority => 2; // Run after basic validation
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var isValid = true;
var message = new StringBuilder();
// Validate reading period
var billingDays = (request.CurrentReadingDate - request.PreviousReadingDate).Days;
if (billingDays <= 0 || billingDays > MAX_BILLING_DAYS)
{
isValid = false;
message.AppendLine(READING_PERIOD_ERROR);
}
// Validate consumption calculation
var calculatedConsumption = request.CurrentReading - request.PreviousReading;
if (Math.Abs(calculatedConsumption - request.ConsumptionAmount) > TOLERANCE)
{
isValid = false;
message.AppendLine(CONSUMPTION_ERROR);
}
// Validate peak/off-peak total
if (Math.Abs(request.ConsumptionAmount - (request.PeakConsumption + request.OffPeakConsumption)) > TOLERANCE)
{
isValid = false;
message.AppendLine(PEAK_OFFPEAK_ERROR);
}
// Validate total amount matches sum of components
var expectedTotal = request.TUSDAmount + request.TEAmount +
request.PowerFactorAdjustment + request.PublicLightingAmount +
request.FlagAmount;
if (Math.Abs(request.TotalAmount - expectedTotal) > TOLERANCE)
{
isValid = false;
message.AppendLine(TOTAL_ERROR);
}
return await Task.FromResult(new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
});
}
}
}

View File

@ -1,122 +0,0 @@
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);
}
}
}

View File

@ -1,75 +0,0 @@
using System;
using System.Threading.Tasks;
using Compliance.DTOs;
using Compliance.Domain.Models;
namespace Compliance.Services.ValidationRules
{
public class EmergencyValidationRule : IValidationRule
{
private const string RULE_NAME = "Emergency Situation Validation";
private const int MAX_EMERGENCY_PERIOD_DAYS = 180;
private const decimal MIN_EMERGENCY_SUPPLY_PERCENTAGE = 10m;
private const string MISSING_DECLARATION_ERROR = "Emergency declaration number is required for emergency situations";
private const string PERIOD_EXCEEDED_ERROR = "Emergency period cannot exceed 180 days";
private const string SUPPLY_BELOW_MINIMUM_ERROR = "Emergency supply must be at least 10% of normal load";
private const string BILLING_EXCEEDED_ERROR = "Emergency billing cannot exceed normal load billing estimate";
public int Priority => 1; // High priority for emergency situations
public Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
if (!request.IsEmergencySituation)
return Task.FromResult(ValidationResult.Success(RULE_NAME));
var errors = ValidateEmergencyRequirements(request);
return Task.FromResult(errors.Count == 0
? ValidationResult.Success(RULE_NAME)
: ValidationResult.Failure(RULE_NAME, errors));
}
private static List<string> ValidateEmergencyRequirements(BillComplianceRequest request)
{
var errors = new List<string>();
// Art. 350 - Emergency Declaration Validation
ValidateDeclaration(request, errors);
// Art. 351 - Emergency Period Validation
ValidatePeriod(request, errors);
// Art. 352 - Emergency Supply Percentage Validation
ValidateSupplyPercentage(request, errors);
// Art. 353 - Emergency Billing Amount Validation
ValidateBillingAmount(request, errors);
return errors;
}
private static void ValidateDeclaration(BillComplianceRequest request, List<string> errors)
{
if (string.IsNullOrEmpty(request.EmergencyDeclarationNumber))
errors.Add(MISSING_DECLARATION_ERROR);
}
private static void ValidatePeriod(BillComplianceRequest request, List<string> errors)
{
if (request.EmergencyPeriodDays > MAX_EMERGENCY_PERIOD_DAYS)
errors.Add(PERIOD_EXCEEDED_ERROR);
}
private static void ValidateSupplyPercentage(BillComplianceRequest request, List<string> errors)
{
if (request.EmergencySupplyPercentage < MIN_EMERGENCY_SUPPLY_PERCENTAGE)
errors.Add(SUPPLY_BELOW_MINIMUM_ERROR);
}
private static void ValidateBillingAmount(BillComplianceRequest request, List<string> errors)
{
if (request.BillTotalBeforeLateCharges > request.NormalLoadBillEstimate)
errors.Add(BILLING_EXCEEDED_ERROR);
}
}
}

View File

@ -1,117 +0,0 @@
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 FlagTariffValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Flag Tariff Validation";
private const string FLAG_ERROR = "Flag tariff amount doesn't match the expected value for the current flag color";
private const string EXEMPT_ERROR = "Flag tariff charged to exempt consumer";
private const string COLOR_ERROR = "Invalid flag color applied";
private const decimal TOLERANCE = 0.01m;
private const decimal KWH_BLOCK = 100m; // Flag tariff is applied per 100 kWh
public int Priority => 4; // Run after tariff component validation
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var flagInfo = await _distributorRepository.GetFlagTariffInformationAsync(
request.DistributorName,
request.Month) ?? throw new InvalidOperationException("Flag tariff information not found");
var isValid = true;
var message = new StringBuilder();
// Check if consumer is exempt
if (flagInfo.ExemptGroups.Contains(request.ConsumerGroup.ToLower()))
{
if (request.FlagAmount != 0)
{
isValid = false;
message.AppendLine(EXEMPT_ERROR);
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
// Validate flag color
if (!IsValidFlagColor(request.AppliedFlag))
{
isValid = false;
message.AppendLine($"{COLOR_ERROR}. Found: {request.AppliedFlag}");
}
// Calculate expected flag amount
var expectedFlagAmount = CalculateFlagAmount(
request.ConsumptionAmount,
flagInfo.FlagColor,
flagInfo.FlagValue,
request.BillingDays,
flagInfo.ApplyPartialMonth,
flagInfo.ValidFromDay);
if (Math.Abs(request.FlagAmount - expectedFlagAmount) > TOLERANCE)
{
isValid = false;
message.AppendLine($"{FLAG_ERROR}. Expected: {expectedFlagAmount:F2}, Found: {request.FlagAmount:F2}");
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
private static bool IsValidFlagColor(string flagColor)
{
return flagColor.ToUpper() switch
{
"GREEN" => true,
"YELLOW" => true,
"RED1" => true,
"RED2" => true,
_ => false
};
}
private static decimal CalculateFlagAmount(
decimal consumption,
string flagColor,
decimal flagValue,
int billingDays,
bool applyPartialMonth,
int validFromDay)
{
if (flagColor.Equals("GREEN", StringComparison.CurrentCultureIgnoreCase)) return 0;
// Calculate number of 100 kWh blocks
var blocks = Math.Ceiling(consumption / KWH_BLOCK);
// If partial month application is enabled, prorate the flag value
if (applyPartialMonth)
{
var applicableDays = billingDays - (validFromDay - 1);
if (applicableDays < billingDays)
{
return blocks * flagValue * (applicableDays / (decimal)billingDays);
}
}
return blocks * flagValue;
}
}
}

View File

@ -1,152 +0,0 @@
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 GroupSpecificRulesValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Group-Specific Rules Validation";
// Art. 297-299 - Rural and Irrigation Requirements
private const decimal MIN_IRRIGATION_DISCOUNT = 0.10m;
private const decimal MAX_IRRIGATION_DISCOUNT = 0.90m;
private const string SCHEDULE_ERROR = "Irrigation schedule must be documented for irrigation consumers";
private const string SEASON_ERROR = "Invalid seasonal period configuration";
private const string DISCOUNT_ERROR = "Invalid irrigation discount";
private const string ACTIVITY_ERROR = "Activity-specific discount not properly documented";
public int Priority => 3; // Run after basic and consumption validation
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var groupRules = await _distributorRepository.GetGroupSpecificRulesInfoAsync(
request.ConsumerGroup,
request.Subgroup,
request.Month) ?? throw new InvalidOperationException("Group rules not found");
var errors = new List<string>();
ValidateRuralClassification(request, groupRules, errors);
ValidateIrrigationRules(request, groupRules, errors);
ValidateSeasonalRules(request, groupRules, errors);
ValidateActivityDiscounts(request, groupRules, errors);
return errors.Count == 0
? ValidationResult.Success(RULE_NAME)
: ValidationResult.Failure(RULE_NAME, errors);
}
private static void ValidateRuralClassification(
BillComplianceRequest request,
GroupSpecificRulesInfo rules,
List<string> errors)
{
if (rules.IsRural)
{
// Art. 297 - Rural consumer validation
if (!rules.SpecialConditions.Any())
{
errors.Add("Rural classification requires documented special conditions");
}
// Validate activity-specific requirements
foreach (var condition in rules.SpecialConditions)
{
if (!rules.ActivityDiscounts.ContainsKey(condition))
{
errors.Add($"Missing discount configuration for rural activity: {condition}");
}
}
}
}
private static void ValidateIrrigationRules(
BillComplianceRequest request,
GroupSpecificRulesInfo rules,
List<string> errors)
{
if (rules.IsIrrigation)
{
// Art. 298 - Irrigation requirements
if (string.IsNullOrEmpty(rules.IrrigationSchedule))
{
errors.Add(SCHEDULE_ERROR);
}
if (!rules.IrrigationDiscount.HasValue)
{
errors.Add(DISCOUNT_ERROR);
}
else if (rules.IrrigationDiscount < MIN_IRRIGATION_DISCOUNT ||
rules.IrrigationDiscount > MAX_IRRIGATION_DISCOUNT)
{
errors.Add($"Irrigation discount ({rules.IrrigationDiscount:P}) outside allowed range " +
$"({MIN_IRRIGATION_DISCOUNT:P}-{MAX_IRRIGATION_DISCOUNT:P})");
}
}
}
private static void ValidateSeasonalRules(
BillComplianceRequest request,
GroupSpecificRulesInfo rules,
List<string> errors)
{
// Art. 299 - Seasonal variations
if (!string.IsNullOrEmpty(rules.Season))
{
if (rules.SeasonStartDate == default || rules.SeasonEndDate == default)
{
errors.Add(SEASON_ERROR);
}
var currentDate = request.CurrentReadingDate;
if (currentDate >= rules.SeasonStartDate && currentDate <= rules.SeasonEndDate)
{
if (rules.SeasonalMultiplier <= 0)
{
errors.Add("Invalid seasonal multiplier");
}
// Validate if seasonal multiplier was correctly applied
var expectedAmount = request.ConsumptionAmount * rules.SeasonalMultiplier;
if (Math.Abs(expectedAmount - request.BillTotalBeforeTaxes) > 0.01m)
{
errors.Add("Seasonal multiplier not correctly applied to consumption amount");
}
}
}
}
private static void ValidateActivityDiscounts(
BillComplianceRequest request,
GroupSpecificRulesInfo rules,
List<string> errors)
{
foreach (var (activity, discount) in rules.ActivityDiscounts)
{
if (discount <= 0 || discount > 1)
{
errors.Add($"{ACTIVITY_ERROR}: Invalid discount percentage for {activity}");
}
// If this activity applies to the current bill
if (request.ApplicableActivities.Contains(activity))
{
var expectedDiscount = request.BillTotalBeforeTaxes * discount;
if (Math.Abs(expectedDiscount - request.ActivityDiscountAmount) > 0.01m)
{
errors.Add($"Activity discount for {activity} not correctly applied");
}
}
}
}
}
}

View File

@ -1,101 +0,0 @@
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 ICMSValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "ICMS Validation";
private const string AMOUNT_ERROR = "ICMS amount doesn't match the expected value";
private const string EXEMPT_ERROR = "ICMS charged to exempt consumer";
private const string BASE_ERROR = "ICMS base calculation is incorrect";
private const decimal TOLERANCE = 0.01m;
public int Priority => 6; // Run after public lighting validation
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var icmsInfo = await _distributorRepository.GetICMSInformationAsync(
request.DistributorName,
request.ConsumerGroup,
request.Month) ?? throw new InvalidOperationException("ICMS information not found");
var isValid = true;
var message = new StringBuilder();
// Check if consumer is exempt from ICMS
if (icmsInfo.ExemptGroups.Contains(request.ConsumerGroup.ToLower()))
{
if (request.ICMSAmount != 0)
{
isValid = false;
message.AppendLine(EXEMPT_ERROR);
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
// Get applicable rate
var rate = icmsInfo.GroupRates.TryGetValue(request.ConsumerGroup.ToLower(), out var groupRate)
? groupRate
: icmsInfo.BaseRate;
// Calculate base and expected ICMS
var baseAmount = CalculateICMSBase(request, icmsInfo.IncludesTaxInBase);
var expectedICMS = CalculateICMSAmount(baseAmount, rate, icmsInfo.IncludesTaxInBase);
if (Math.Abs(request.ICMSBase - baseAmount) > TOLERANCE)
{
isValid = false;
message.AppendLine($"{BASE_ERROR}. Expected: {baseAmount:F2}, Found: {request.ICMSBase:F2}");
}
if (Math.Abs(request.ICMSAmount - expectedICMS) > TOLERANCE)
{
isValid = false;
message.AppendLine($"{AMOUNT_ERROR}. Expected: {expectedICMS:F2}, Found: {request.ICMSAmount:F2}");
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
private static decimal CalculateICMSBase(BillComplianceRequest request, bool includesTaxInBase)
{
// Sum all components that are part of ICMS base
return request.TUSDAmount +
request.TEAmount +
request.PowerFactorAdjustment +
request.FlagAmount;
}
private static decimal CalculateICMSAmount(decimal baseAmount, decimal rate, bool includesTaxInBase)
{
rate /= 100m; // Convert percentage to decimal
if (!includesTaxInBase)
{
// Regular calculation ("por fora")
return baseAmount * rate;
}
// "Por dentro" calculation where ICMS is included in its own base
return baseAmount * rate / (1 - rate);
}
}
}

View File

@ -1,15 +0,0 @@
using System.Threading.Tasks;
using Compliance.Domain.Models;
using Compliance.DTOs;
namespace Compliance.Services.ValidationRules
{
public interface IValidationRule
{
int Priority { get; }
Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo);
}
}

View File

@ -1,123 +0,0 @@
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 MeasurementSystemValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Measurement System Validation";
// Art. 238 - Maximum calibration periods
private const int MAX_CALIBRATION_YEARS_REGULAR = 10;
private const int MAX_CALIBRATION_YEARS_ELECTRONIC = 5;
private const int MAX_CALIBRATION_YEARS_SMART = 3;
// Art. 239 - Accuracy requirements
private const decimal MAX_ERROR_PERCENTAGE = 2.5m;
private const decimal SMART_METER_ERROR_PERCENTAGE = 1.0m;
public int Priority => 1; // High priority as measurements affect other validations
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var measurementInfo = await _distributorRepository.GetMeasurementSystemInfoAsync(
request.MeterNumber) ?? throw new InvalidOperationException("Measurement system info not found");
var errors = new List<string>();
ValidateCalibration(measurementInfo, errors);
ValidateAccuracy(measurementInfo, errors);
ValidateSmartMeterRequirements(measurementInfo, errors);
ValidateSeals(measurementInfo, errors);
ValidateLocation(measurementInfo, errors);
return errors.Count == 0
? ValidationResult.Success(RULE_NAME)
: ValidationResult.Failure(RULE_NAME, errors);
}
private static void ValidateCalibration(MeasurementSystemInfo info, List<string> errors)
{
var maxYears = info.IsSmartMeter ? MAX_CALIBRATION_YEARS_SMART :
info.MeterType.Contains("Electronic", StringComparison.OrdinalIgnoreCase)
? MAX_CALIBRATION_YEARS_ELECTRONIC
: MAX_CALIBRATION_YEARS_REGULAR;
var calibrationAge = (DateTime.Now - info.LastCalibrationDate).TotalDays / 365.25;
if (calibrationAge > maxYears)
{
errors.Add($"Meter calibration expired. Last calibration: {info.LastCalibrationDate:d}. " +
$"Maximum period for {info.MeterType}: {maxYears} years");
}
// Validate component calibrations
foreach (var component in info.ComponentCalibrations)
{
var componentAge = (DateTime.Now - component.Value).TotalDays / 365.25;
if (componentAge > maxYears)
{
errors.Add($"Component {component.Key} calibration expired. " +
$"Last calibration: {component.Value:d}");
}
}
}
private static void ValidateAccuracy(MeasurementSystemInfo info, List<string> errors)
{
var maxError = info.IsSmartMeter ? SMART_METER_ERROR_PERCENTAGE : MAX_ERROR_PERCENTAGE;
if (info.MaximumError > maxError)
{
errors.Add($"Meter accuracy ({info.MaximumError}%) exceeds maximum allowed error " +
$"({maxError}%) for {(info.IsSmartMeter ? "smart" : "regular")} meters");
}
}
private static void ValidateSmartMeterRequirements(MeasurementSystemInfo info, List<string> errors)
{
if (!info.IsSmartMeter) return;
var requiredProtocols = new[] { "DLMS/COSEM", "ANSI C12.19" };
foreach (var protocol in requiredProtocols)
{
if (!info.CommunicationProtocols.Contains(protocol))
{
errors.Add($"Smart meter missing required protocol: {protocol}");
}
}
}
private static void ValidateSeals(MeasurementSystemInfo info, List<string> errors)
{
if (!info.HasSeal)
{
errors.Add("Meter missing required security seal");
}
else if (string.IsNullOrEmpty(info.SealNumber))
{
errors.Add("Security seal number not documented");
}
}
private static void ValidateLocation(MeasurementSystemInfo info, List<string> errors)
{
if (string.IsNullOrEmpty(info.Location))
{
errors.Add("Meter location not documented");
}
else if (!info.Location.Contains("accessible", StringComparison.OrdinalIgnoreCase))
{
errors.Add("Meter must be installed in an accessible location");
}
}
}
}

View File

@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Compliance.Domain.Models;
using Compliance.DTOs;
using Compliance.Infrastructure.Repositories;
namespace Compliance.Services.ValidationRules
{
public class MeterReadingValidationRule : IValidationRule
{
private const string RULE_NAME = "Meter Reading Validation";
private const string READING_ERROR = "Invalid meter reading value";
private const string SEQUENCE_ERROR = "Current reading must be greater than previous reading";
private const string RESET_ERROR = "Meter reset not properly documented";
private const string ESTIMATION_ERROR = "Estimated reading without proper justification";
public int Priority => 1; // High priority as other validations depend on readings
public Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var errors = new List<string>();
ValidateReadingValues(request, errors);
ValidateReadingSequence(request, errors);
ValidateEstimatedReadings(request, errors);
ValidateMeterReset(request, errors);
return Task.FromResult(errors.Count == 0
? ValidationResult.Success(RULE_NAME)
: ValidationResult.Failure(RULE_NAME, errors));
}
private static void ValidateReadingValues(BillComplianceRequest request, List<string> errors)
{
if (request.CurrentReading < 0 || request.PreviousReading < 0)
errors.Add($"{READING_ERROR}: Negative readings are not allowed");
}
private static void ValidateReadingSequence(BillComplianceRequest request, List<string> errors)
{
if (!request.IsMeterReset && request.CurrentReading < request.PreviousReading)
errors.Add(SEQUENCE_ERROR);
}
private static void ValidateEstimatedReadings(BillComplianceRequest request, List<string> errors)
{
if (request.IsEstimatedReading && string.IsNullOrEmpty(request.EstimationJustification))
errors.Add(ESTIMATION_ERROR);
}
private static void ValidateMeterReset(BillComplianceRequest request, List<string> errors)
{
if (request.IsMeterReset &&
(string.IsNullOrEmpty(request.MeterResetJustification) ||
request.MeterResetDate == default))
{
errors.Add(RESET_ERROR);
}
}
}
}

View File

@ -1,55 +0,0 @@
using System;
using System.Threading.Tasks;
using System.Text;
using Compliance.Domain.Models;
using Compliance.DTOs;
using Compliance.Infrastructure.Repositories;
using Compliance.Services.ValidationRules;
namespace Compliance.Services.ValidationRules;
public class MinimumBillingValidationRule : IValidationRule
{
private readonly IDistributorRepository _distributorRepository;
private const string RULE_NAME = "Minimum Billing Validation";
private const decimal TOLERANCE = 0.01m;
public int Priority => 7;
public MinimumBillingValidationRule(IDistributorRepository distributorRepository)
{
_distributorRepository = distributorRepository;
}
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var minBillingInfo = await _distributorRepository.GetMinimumBillingInformationAsync(
request.DistributorName,
request.ConsumerGroup,
request.Month);
var isValid = true;
var message = new StringBuilder();
// Check if consumption is below minimum
if (request.ConsumptionAmount < minBillingInfo.MinimumConsumption)
{
var expectedMinCharge = minBillingInfo.MinimumValue;
var actualCharge = request.TotalAmount;
if (Math.Abs(actualCharge - expectedMinCharge) > TOLERANCE)
{
isValid = false;
message.AppendLine($"Incorrect minimum billing charge. Expected: {expectedMinCharge:F2}, Found: {actualCharge:F2}");
}
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
}

View File

@ -1,98 +0,0 @@
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 MunicipalTaxValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Municipal Tax Validation";
private const string AMOUNT_ERROR = "Municipal tax amount doesn't match the expected value";
private const string BASE_ERROR = "Municipal tax base calculation is incorrect";
private const string EXEMPT_ERROR = "Municipal tax charged to exempt consumer";
private const decimal TOLERANCE = 0.01m;
public int Priority => 7; // Run after ICMS validation
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var taxInfo = await _distributorRepository.GetMunicipalTaxInformationAsync(
request.DistributorName,
request.Municipality,
request.Month) ?? throw new InvalidOperationException("Municipal tax information not found");
var isValid = true;
var message = new StringBuilder();
// Check if consumer is exempt
if (taxInfo.ExemptGroups.Contains(request.ConsumerGroup.ToLower()))
{
if (request.MunicipalTaxAmount != 0)
{
isValid = false;
message.AppendLine(EXEMPT_ERROR);
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
// Get applicable rate
var rate = taxInfo.SpecialRates.TryGetValue(request.ConsumerGroup.ToLower(), out var specialRate)
? specialRate
: taxInfo.BaseRate;
// Calculate base and expected tax
var baseAmount = CalculateTaxBase(request, taxInfo);
var expectedTax = baseAmount * (rate / 100m);
if (Math.Abs(request.MunicipalTaxBase - baseAmount) > TOLERANCE)
{
isValid = false;
message.AppendLine($"{BASE_ERROR}. Expected: {baseAmount:F2}, Found: {request.MunicipalTaxBase:F2}");
}
if (Math.Abs(request.MunicipalTaxAmount - expectedTax) > TOLERANCE)
{
isValid = false;
message.AppendLine($"{AMOUNT_ERROR}. Expected: {expectedTax:F2}, Found: {request.MunicipalTaxAmount:F2}");
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
private static decimal CalculateTaxBase(BillComplianceRequest request, MunicipalTaxInformation taxInfo)
{
var baseAmount = request.TUSDAmount +
request.TEAmount +
request.PowerFactorAdjustment +
request.FlagAmount;
if (taxInfo.ApplyToICMS)
{
baseAmount += request.ICMSAmount;
}
if (taxInfo.ApplyToPublicLighting)
{
baseAmount += request.PublicLightingAmount;
}
return baseAmount;
}
}
}

View File

@ -1,107 +0,0 @@
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 PaymentTermsValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Payment Terms Validation";
private const string DUE_DATE_ERROR = "Due date doesn't meet minimum required days";
private const string LATE_FEE_ERROR = "Late payment fee calculation is incorrect";
private const string INTEREST_ERROR = "Interest calculation is incorrect";
private const string PARTIAL_ERROR = "Partial payment not allowed for this consumer";
private const decimal TOLERANCE = 0.01m;
public int Priority => 9; // Run after additional charges validation
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var termsInfo = await _distributorRepository.GetPaymentTermsInformationAsync(
request.DistributorName,
request.ConsumerGroup,
request.Month) ?? throw new InvalidOperationException("Payment terms information not found");
var isValid = true;
var message = new StringBuilder();
// Get applicable minimum due days
var minimumDueDays = termsInfo.GroupSpecificDueDays.TryGetValue(
request.ConsumerGroup.ToLower(),
out var specialDays) ? specialDays : termsInfo.MinimumDueDays;
// Validate due date
var daysUntilDue = (request.DueDate - request.IssueDate).Days;
if (daysUntilDue < minimumDueDays)
{
isValid = false;
message.AppendLine($"{DUE_DATE_ERROR}. Minimum: {minimumDueDays}, Found: {daysUntilDue}");
}
// Validate late payment charges if applicable
if (request.PaymentDate > request.DueDate &&
!termsInfo.ExemptFromLateCharges.Contains(request.ConsumerGroup.ToLower()))
{
// Validate late payment fee
var expectedLateFee = CalculateLateFee(request.BillTotalBeforeLateCharges, termsInfo.LatePaymentFeePercentage);
if (Math.Abs(request.LatePaymentFee - expectedLateFee) > TOLERANCE)
{
isValid = false;
message.AppendLine($"{LATE_FEE_ERROR}. Expected: {expectedLateFee:F2}, Found: {request.LatePaymentFee:F2}");
}
// Validate interest
var expectedInterest = CalculateInterest(
request.BillTotalBeforeLateCharges,
request.DueDate,
request.PaymentDate,
termsInfo.MonthlyInterestRate);
if (Math.Abs(request.InterestAmount - expectedInterest) > TOLERANCE)
{
isValid = false;
message.AppendLine($"{INTEREST_ERROR}. Expected: {expectedInterest:F2}, Found: {request.InterestAmount:F2}");
}
}
// Validate partial payment if applicable
if (request.IsPartialPayment && !termsInfo.AllowPartialPayments)
{
isValid = false;
message.AppendLine(PARTIAL_ERROR);
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
private static decimal CalculateLateFee(decimal amount, decimal feePercentage)
{
return amount * (feePercentage / 100m);
}
private static decimal CalculateInterest(
decimal amount,
DateTime dueDate,
DateTime paymentDate,
decimal monthlyRate)
{
var monthsLate = ((paymentDate.Year - dueDate.Year) * 12) +
paymentDate.Month - dueDate.Month +
(paymentDate.Day >= dueDate.Day ? 0 : -1);
if (monthsLate <= 0) return 0;
return amount * (monthlyRate / 100m) * monthsLate;
}
}
}

View File

@ -1,82 +0,0 @@
using System;
using System.Threading.Tasks;
using System.Text;
using Compliance.Domain.Models;
using Compliance.DTOs;
using Compliance.Infrastructure.Repositories;
using System.Linq;
using System.Collections.Generic;
namespace Compliance.Services.ValidationRules
{
public class PublicLightingValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Public Lighting Validation";
private const string AMOUNT_ERROR = "Public lighting charge doesn't match the expected value";
private const string EXEMPT_ERROR = "Public lighting charge applied to exempt consumer";
private const decimal TOLERANCE = 0.01m;
public int Priority => 8;
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var lightingInfo = await _distributorRepository.GetPublicLightingInformationAsync(
request.DistributorName,
request.Municipality,
request.Month) ?? throw new InvalidOperationException("Public lighting information not found");
var isValid = true;
var message = new StringBuilder();
// Check municipal legislation compliance
if (!lightingInfo.HasValidMunicipalLaw)
{
if (request.PublicLightingAmount > 0)
{
isValid = false;
message.AppendLine(EXEMPT_ERROR);
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString()
};
}
// Validate charge calculation method
var expectedCharge = lightingInfo.CalculationType switch
{
"Fixed" => lightingInfo.FixedAmount,
"Percentage" => request.ConsumptionAmount * (lightingInfo.Percentage / 100m),
"ConsumptionRange" => CalculateRangeBasedCharge(request.ConsumptionAmount, lightingInfo.Ranges),
_ => 0m
};
if (Math.Abs(request.PublicLightingAmount - expectedCharge) > TOLERANCE)
{
isValid = false;
message.AppendLine($"{AMOUNT_ERROR}. Expected: {expectedCharge:F2}, Found: {request.PublicLightingAmount:F2}");
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
private decimal CalculateRangeBasedCharge(decimal consumption, IEnumerable<SubsidyInformation.ConsumptionRange> ranges)
{
var applicableRange = ranges.FirstOrDefault(r =>
consumption >= r.MinConsumption &&
consumption <= (r.MaxConsumption ?? decimal.MaxValue));
return applicableRange?.Amount ?? 0m;
}
}
}

View File

@ -1,113 +0,0 @@
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;
}
}
}

View File

@ -1,126 +0,0 @@
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 ReadingImpedimentValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Reading Impediment Validation";
// Art. 231 - Impediment documentation requirements
private const int MAX_DAYS_WITHOUT_NOTIFICATION = 15;
private const int MAX_CONSECUTIVE_IMPEDIMENTS = 3;
private const string NOTIFICATION_ERROR = "Customer must be notified within 15 days of impediment";
private const string CONSECUTIVE_ERROR = "Maximum consecutive impediments exceeded";
private const string DOCUMENTATION_ERROR = "Insufficient impediment documentation";
private const string RESOLUTION_ERROR = "No resolution attempts documented";
private const string EVIDENCE_ERROR = "Photo evidence required for access impediment";
public int Priority => 1; // High priority as it affects reading validation
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
if (!request.HasReadingImpediment)
return ValidationResult.Success(RULE_NAME);
var impedimentInfo = await _distributorRepository.GetReadingImpedimentInfoAsync(
request.SmartCode,
request.CurrentReadingDate) ?? throw new InvalidOperationException("Impediment information not found");
var errors = new List<string>();
ValidateImpedimentDocumentation(impedimentInfo, errors);
ValidateCustomerNotification(impedimentInfo, errors);
ValidateConsecutiveImpediments(impedimentInfo, errors);
ValidateResolutionAttempts(impedimentInfo, errors);
ValidateAlternativeReading(request, impedimentInfo, errors);
return errors.Count == 0
? ValidationResult.Success(RULE_NAME)
: ValidationResult.Failure(RULE_NAME, errors);
}
private static void ValidateImpedimentDocumentation(ReadingImpedimentInfo info, List<string> errors)
{
if (string.IsNullOrEmpty(info.ImpedimentCode) ||
string.IsNullOrEmpty(info.Description) ||
info.ReportDate == default ||
string.IsNullOrEmpty(info.ReportedBy))
{
errors.Add(DOCUMENTATION_ERROR);
}
// Art. 231 §1 - Photo evidence requirement
if (info.ImpedimentCode.Contains("ACCESS", StringComparison.OrdinalIgnoreCase) &&
info.PhotoEvidence.Count == 0)
{
errors.Add(EVIDENCE_ERROR);
}
}
private static void ValidateCustomerNotification(ReadingImpedimentInfo info, List<string> errors)
{
if (info.RequiresCustomerAction)
{
if (!info.CustomerNotificationDate.HasValue)
{
errors.Add(NOTIFICATION_ERROR);
}
else
{
var notificationDelay = (info.CustomerNotificationDate.Value - info.ReportDate).Days;
if (notificationDelay > MAX_DAYS_WITHOUT_NOTIFICATION)
{
errors.Add($"{NOTIFICATION_ERROR}. Actual delay: {notificationDelay} days");
}
}
}
}
private static void ValidateConsecutiveImpediments(ReadingImpedimentInfo info, List<string> errors)
{
if (info.ConsecutiveOccurrences > MAX_CONSECUTIVE_IMPEDIMENTS)
{
errors.Add($"{CONSECUTIVE_ERROR}. Current consecutive: {info.ConsecutiveOccurrences}");
}
}
private static void ValidateResolutionAttempts(ReadingImpedimentInfo info, List<string> errors)
{
if (info.ConsecutiveOccurrences > 1 &&
string.IsNullOrEmpty(info.ResolutionAttempts))
{
errors.Add(RESOLUTION_ERROR);
}
}
private static void ValidateAlternativeReading(
BillComplianceRequest request,
ReadingImpedimentInfo info,
List<string> errors)
{
// Art. 232 - Alternative reading methods
if (string.IsNullOrEmpty(info.AlternativeReadingMethod))
{
errors.Add("Alternative reading method must be documented");
}
else if (info.AlternativeReadingMethod.Equals("ESTIMATION",
StringComparison.OrdinalIgnoreCase))
{
// Ensure estimation is properly justified
if (string.IsNullOrEmpty(request.EstimationJustification))
{
errors.Add("Estimation justification required for impediment-based reading");
}
}
}
}
}

View File

@ -1,41 +0,0 @@
using System;
using System.Threading.Tasks;
using Compliance.DTOs;
using Compliance.Domain.Models;
namespace Compliance.Services.ValidationRules
{
public class ReadingPeriodValidationRule : IValidationRule
{
private const string RULE_NAME = "Reading Period Validation";
private const int MIN_READING_DAYS = 27;
private const int MAX_READING_DAYS = 33;
private const string PERIOD_ERROR_FORMAT =
"Reading period of {0} days is outside allowed range ({1}-{2} days)";
public int Priority => 1;
public Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var readingPeriod = CalculateReadingPeriod(request);
var isValid = IsValidReadingPeriod(readingPeriod);
return Task.FromResult(isValid
? ValidationResult.Success(RULE_NAME)
: ValidationResult.Failure(RULE_NAME,
FormatErrorMessage(readingPeriod)));
}
private static int CalculateReadingPeriod(BillComplianceRequest request) =>
(request.CurrentReadingDate - request.PreviousReadingDate).Days;
private static bool IsValidReadingPeriod(int days) =>
days >= MIN_READING_DAYS && days <= MAX_READING_DAYS;
private static string FormatErrorMessage(int days) =>
string.Format(PERIOD_ERROR_FORMAT, days, MIN_READING_DAYS, MAX_READING_DAYS);
}
}

View File

@ -1,108 +0,0 @@
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 SeasonalTariffValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Seasonal Tariff Validation";
private const string TUSD_ERROR = "TUSD amount with seasonal adjustment doesn't match the expected value";
private const string TE_ERROR = "TE amount with seasonal adjustment doesn't match the expected value";
private const string SEASON_ERROR = "Incorrect season applied for the billing period";
private const decimal TOLERANCE = 0.01m;
public int Priority => 4; // Run after reactive energy validation
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
if (!ShouldApplySeasonalTariff(request))
{
return new ValidationResult
{
IsValid = true,
RuleName = RULE_NAME,
Message = string.Empty
};
}
var seasonalInfo = await _distributorRepository.GetSeasonalTariffInformationAsync(
request.DistributorName,
request.ConsumerGroup,
request.Month) ?? throw new InvalidOperationException("Seasonal tariff information not found");
var tariffInfo = await _distributorRepository.GetTariffInformationAsync(
request.DistributorName,
request.Month) ?? throw new InvalidOperationException("Tariff information not found");
var isValid = true;
var message = new StringBuilder();
var billingPeriod = DateOnly.ParseExact(request.Month, "MM/yyyy");
var isDrySeason = IsInDrySeason(billingPeriod, seasonalInfo);
var seasonalMultiplier = isDrySeason ?
seasonalInfo.DrySeasonMultiplier :
seasonalInfo.WetSeasonMultiplier;
if (request.AppliedSeason != (isDrySeason ? "DRY" : "WET"))
{
isValid = false;
message.AppendLine($"{SEASON_ERROR}. Expected: {(isDrySeason ? "DRY" : "WET")}, Found: {request.AppliedSeason}");
}
// Validate TUSD with seasonal adjustment
var expectedTUSD = request.ConsumptionAmount * tariffInfo.TUSDValue * seasonalMultiplier;
if (Math.Abs(request.SeasonalTUSDAmount - expectedTUSD) > TOLERANCE)
{
isValid = false;
message.AppendLine($"{TUSD_ERROR}. Expected: {expectedTUSD:F2}, Found: {request.SeasonalTUSDAmount:F2}");
}
// Validate TE with seasonal adjustment
var expectedTE = request.ConsumptionAmount * tariffInfo.TEValue * seasonalMultiplier;
if (Math.Abs(request.SeasonalTEAmount - expectedTE) > TOLERANCE)
{
isValid = false;
message.AppendLine($"{TE_ERROR}. Expected: {expectedTE:F2}, Found: {request.SeasonalTEAmount:F2}");
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
private static bool ShouldApplySeasonalTariff(BillComplianceRequest request)
{
// According to ANEEL, seasonal tariffs typically apply to irrigation/rural consumers
return request.ConsumerGroup.ToUpper() switch
{
"RURAL" => true,
"IRRIGATION" => true,
"AQUACULTURE" => true,
_ => false
};
}
private static bool IsInDrySeason(DateOnly date, SeasonalTariffInformation seasonalInfo)
{
// Handle year wrap-around case
if (seasonalInfo.DrySeasonStart.Month > seasonalInfo.DrySeasonEnd.Month)
{
return date.Month >= seasonalInfo.DrySeasonStart.Month ||
date.Month <= seasonalInfo.DrySeasonEnd.Month;
}
return date.Month >= seasonalInfo.DrySeasonStart.Month &&
date.Month <= seasonalInfo.DrySeasonEnd.Month;
}
}
}

View File

@ -1,97 +0,0 @@
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 SubsidyValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Subsidy and Discount Validation";
private const string AMOUNT_ERROR = "Discount amount doesn't match the expected value";
private const string ELIGIBILITY_ERROR = "Consumer not eligible for applied discount";
private const string CONSUMPTION_ERROR = "Consumption outside eligible range for discount";
private const decimal TOLERANCE = 0.01m;
public int Priority => 6; // Run after flag tariff validation
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var subsidyInfo = await _distributorRepository.GetSubsidyInformationAsync(
request.DistributorName,
request.ConsumerGroup,
request.Month) ?? throw new InvalidOperationException("Subsidy information not found");
var isValid = true;
var message = new StringBuilder();
// Check consumption eligibility
if (request.ConsumptionAmount < subsidyInfo.Consumption.MinConsumption ||
(subsidyInfo.Consumption.MaxConsumption > 0 && request.ConsumptionAmount > subsidyInfo.Consumption.MaxConsumption))
{
if (request.DiscountAmount != 0)
{
isValid = false;
message.AppendLine($"{CONSUMPTION_ERROR}. Range: {subsidyInfo.Consumption.MinConsumption}-{subsidyInfo.Consumption.MaxConsumption} kWh");
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
// Get applicable discount rate
var discountRate = subsidyInfo.GroupDiscounts.TryGetValue(request.ConsumerGroup.ToLower(), out var specialRate)
? specialRate
: subsidyInfo.BaseDiscountPercentage;
// Calculate expected discount
var expectedDiscount = CalculateDiscount(request, subsidyInfo, discountRate);
if (Math.Abs(request.DiscountAmount - expectedDiscount) > TOLERANCE)
{
isValid = false;
message.AppendLine($"{AMOUNT_ERROR}. Expected: {expectedDiscount:F2}, Found: {request.DiscountAmount:F2}");
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
private static decimal CalculateDiscount(
BillComplianceRequest request,
SubsidyInformation subsidyInfo,
decimal discountRate)
{
var baseAmount = 0m;
if (subsidyInfo.ApplyToTUSD)
{
baseAmount += request.TUSDAmount;
}
if (subsidyInfo.ApplyToTE)
{
baseAmount += request.TEAmount;
}
if (subsidyInfo.ApplyToFlags)
{
baseAmount += request.FlagAmount;
}
return baseAmount * (discountRate / 100m);
}
}
}

View File

@ -1,89 +0,0 @@
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 TariffComponentValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Tariff Component Validation";
private const string TUSD_ERROR = "TUSD amount doesn't match the expected value";
private const string TE_ERROR = "TE amount doesn't match the expected value";
private const string POWER_FACTOR_ERROR = "Power factor adjustment is incorrect";
private const decimal TOLERANCE = 0.01m;
private const decimal MIN_POWER_FACTOR = 0.92m;
public int Priority => 3; // Run after consumption validation
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var tariffInfo = await _distributorRepository.GetTariffInformationAsync(
request.DistributorName,
request.Month) ?? throw new InvalidOperationException("Tariff information not found");
var isValid = true;
var message = new StringBuilder();
// Validate TUSD calculation
var expectedTUSD = request.ConsumptionAmount * tariffInfo.TUSDValue;
if (Math.Abs(request.TUSDAmount - expectedTUSD) > TOLERANCE)
{
isValid = false;
message.AppendLine(TUSD_ERROR);
}
// Validate TE calculation
var expectedTE = request.ConsumptionAmount * tariffInfo.TEValue;
if (Math.Abs(request.TEAmount - expectedTE) > TOLERANCE)
{
isValid = false;
message.AppendLine(TE_ERROR);
}
// Validate power factor adjustment if applicable
if (request.PowerFactor < MIN_POWER_FACTOR)
{
var powerFactorAdjustment = CalculatePowerFactorAdjustment(
request.ConsumptionAmount,
request.PowerFactor,
tariffInfo);
if (Math.Abs(request.PowerFactorAdjustment - powerFactorAdjustment) > TOLERANCE)
{
isValid = false;
message.AppendLine(POWER_FACTOR_ERROR);
}
}
else if (request.PowerFactorAdjustment != 0)
{
isValid = false;
message.AppendLine("Power factor adjustment should be zero when power factor is above minimum");
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString().TrimEnd()
};
}
private static decimal CalculatePowerFactorAdjustment(
decimal consumption,
decimal powerFactor,
TariffInformation tariffInfo)
{
if (powerFactor >= MIN_POWER_FACTOR)
return 0;
var adjustment = (MIN_POWER_FACTOR / powerFactor - 1) * 100;
return consumption * (tariffInfo.TUSDValue + tariffInfo.TEValue) * adjustment / 100;
}
}
}

View File

@ -1,42 +0,0 @@
using System;
using System.Threading.Tasks;
using Compliance.Domain.Models;
using Compliance.DTOs;
using Compliance.Infrastructure.Repositories;
using System.Text;
namespace Compliance.Services.ValidationRules
{
public class TariffValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Tariff Validation";
private const string TUSD_ERROR = "TUSD amount doesn't match the expected value based on consumption and tariff";
public int Priority => 1;
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var tariffInfo = await _distributorRepository.GetTariffInformationAsync(
request.DistributorName,
request.Month) ?? throw new InvalidOperationException("Tariff information not found");
var isValid = true;
var message = new StringBuilder();
if (request.TUSDAmount != request.ConsumptionAmount * tariffInfo.TUSDValue)
{
isValid = false;
message.Append(TUSD_ERROR);
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString()
};
}
}
}

View File

@ -1,54 +0,0 @@
using System;
using System.Threading.Tasks;
using Compliance.Domain.Models;
using Compliance.DTOs;
using Compliance.Infrastructure.Repositories;
using System.Text;
namespace Compliance.Services.ValidationRules
{
public class TaxValidationRule(IDistributorRepository distributorRepository) : IValidationRule
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private const string RULE_NAME = "Tax Validation";
private const string PIS_ERROR = "PIS amount doesn't match the expected value. ";
private const string COFINS_ERROR = "COFINS amount doesn't match the expected value.";
private const decimal TOLERANCE = 0.01m;
public int Priority => 2;
public async Task<ValidationResult> ValidateAsync(
BillComplianceRequest request,
DistributorInformation distributorInfo,
Client clientInfo)
{
var taxInfo = await _distributorRepository.GetTaxInformationAsync(
request.DistributorName,
request.Month) ?? throw new InvalidOperationException("Tax information not found");
var isValid = true;
var message = new StringBuilder();
var totalBeforeTaxes = request.TUSDAmount + request.TEAmount;
var expectedPIS = totalBeforeTaxes * (distributorInfo.PISPercentage / 100);
var expectedCOFINS = totalBeforeTaxes * (distributorInfo.COFINSPercentage / 100);
if (Math.Abs(request.PISAmount - expectedPIS) > TOLERANCE)
{
isValid = false;
message.Append(PIS_ERROR);
}
if (request.COFINSAmount != expectedCOFINS)
{
isValid = false;
message.Append(COFINS_ERROR);
}
return new ValidationResult
{
IsValid = isValid,
RuleName = RULE_NAME,
Message = message.ToString()
};
}
}
}

View File

@ -1,47 +0,0 @@
using System.Collections.Generic;
using Compliance.Infrastructure.Repositories;
using Compliance.Services.ValidationRules;
namespace Compliance.Services.ValidationRules
{
public interface IValidationRuleFactory
{
IEnumerable<IValidationRule> CreateRules();
}
public class ValidationRuleFactory(IDistributorRepository distributorRepository) : IValidationRuleFactory
{
private readonly IDistributorRepository _distributorRepository = distributorRepository;
private IEnumerable<IValidationRule>? _cachedRules;
private static class RuleTypes
{
public const string Tariff = nameof(TariffValidationRule);
public const string Tax = nameof(TaxValidationRule);
public const string TariffComponent = nameof(TariffComponentValidationRule);
public const string FlagTariff = nameof(FlagTariffValidationRule);
public const string PublicLighting = nameof(PublicLightingValidationRule);
public const string ICMS = nameof(ICMSValidationRule);
}
public IEnumerable<IValidationRule> CreateRules()
{
return _cachedRules ??= CreateRulesInternal().OrderBy(r => r.Priority);
}
private Dictionary<string, IValidationRule>.ValueCollection CreateRulesInternal()
{
var rules = new Dictionary<string, IValidationRule>
{
[nameof(BasicInformationValidationRule)] = new BasicInformationValidationRule(),
[nameof(ConsumptionValidationRule)] = new ConsumptionValidationRule(),
[RuleTypes.TariffComponent] = new TariffComponentValidationRule(_distributorRepository),
[RuleTypes.FlagTariff] = new FlagTariffValidationRule(_distributorRepository),
[RuleTypes.PublicLighting] = new PublicLightingValidationRule(_distributorRepository),
[RuleTypes.ICMS] = new ICMSValidationRule(_distributorRepository),
[RuleTypes.Tax] = new TaxValidationRule(_distributorRepository)
};
return rules.Values;
}
}
}

View File

@ -1,5 +0,0 @@
{
"ConnectionStrings": {
"DefaultConnection": "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=\"X:/Middle/Informativo Setorial/Modelo Word/BD1_dados cadastrais e faturas.accdb\";Jet OLEDB:Database Password=gds21"
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,494 +0,0 @@
body{
margin: 0;
}
body{
display: grid;
}
body{
display: grid;
grid-template-columns: 300px auto;
}
div.before_header{
grid-column-start: 1;
}
header{
grid-column-start: 2;
display: grid;
grid-template-columns: 200px 200px auto;
}
header nav.left{
grid-column-start: 1;
font-size: 150%;
}
header nav.center{
grid-column-start: 2;
text-align: left;
}
header nav.right{
grid-column-start: 3;
text-align: right;
}
nav.menu{
grid-column-start: 2;
}
main{
grid-column-start: 2;
}
sidebar{
grid-column-start: 1;
margin-left: 5%;
}
button{
border: solid 1px rgb(28, 21, 91);
}
sidebar{
margin-top: 10px;
}
main{
margin-top: 0px;
}
h1{
margin-bottom: 10px;
margin-top: 10px;
}
table th{
text-align: left;
font-weight: bold;
}
table th, table td{
padding-right: 1em;
}
table.compact th, table.compact td{
padding-right: 0.3em;
}
a{
color: rgb(0, 116, 232);
text-decoration: none;
}
header, div.before_header{
background-color: #39d4f6;
}
header.admin, div.before_header.admin{
background-color: #f6393c;
}
header a{
color: white;
}
header nav.left{
font-size: 150%;
}
header nav.center{
font-size: 150%;
}
header nav.right{
}
nav ul{
display: inline-block;
}
nav ul li{
display: inline-block;
margin-right: 20px;
font-weight: bold;
}
nav.menu ul{
padding: 0;
margin-bottom: 0;
}
nav.menu li:before{
content: "» ";
}
pre .nl{
color: rgb(0, 116, 232);
}
pre .s2{
color: rgb(221, 0, 169);
}
pre .mf{
color: rgb(5, 139, 0)
}
pre{
white-space: pre-wrap;
}
label{
display: block;
font-weight: bold;
}
label.inline{
display: inline;
}
#columns label{
display: inline;
}
td.md5 div{
text-overflow: ellipsis;
overflow: hidden;
width: 2em;
white-space: nowrap;
}
td.json_hide div{
text-overflow: ellipsis;
overflow: hidden;
width: 20em;
white-space: nowrap;
}
table.details td{
vertical-align: top;
}
table.details p{
margin: 0;
}
table.details ul{
margin-top: 0;
}
table.details tr td:first-child{
font-weight: bold;
}
table.details td{
padding-top: 5px;
}
#filter li, #columns li{
display: block;
padding: 0;
margin: 0;
margin-top: 10px;
}
#filter, #columns{
padding: 0;
margin: 0;
}
h1.problem{
color: red;
}
.limcol{
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
width: 8em;
white-space: nowrap;
vertical-align: text-bottom;
}
.limcol2{
text-overflow: ellipsis;
overflow: hidden;
width: 14em;
white-space: nowrap;
}
tr.alert td{
background-color: #ffcccc;
}
div#sidebars{
grid-column-start: 1;
margin-left: 5%;
}
div#sidebars div.sidebar#1{
display: block;
}
div#sidebars div.sidebar#2{
display: block;
}
span.cell_value{
}
span.time_info{
padding-right: 1em;
font-size: 80%;
font-family: monospace;
}
div.locations tr.status_undefined td{
background-color: #998877;
}
div.locations tr.auto_deactivated td{
text-decoration: line-through;
}
div.locations tr.status_success td{
background-color: #ffffff;
}
div.locations tr.status_alert_client td{
background-color: #ffcccc;
}
div.locations tr.status_error td{
background-color: #ff0000;
color: #ffffff;
}
div.locations tr.status_too_long td{
background-color: #ff007f;
color: #ffffff;
}
div.locations tr.status_stale td, div.invoices tr.status_stale td{
background-color: #aa0000;
color: #ffffff;
}
tr.status_stale a, tr.status_error a{
color: #ffffff;
}
div.locations tr.status_in_progress td{
background-color: #ffcc44;
}
div.locations tr.status_never td{
background-color: #aa55ff;
color: #ffffff;
}
div.locations tr.status_locked td, div.accounts tr.status_locked td, tr.status_locked td div{
text-decoration: underline;
text-decoration-color: red;
}
span.website_error_text{
font-weight: bold;
}
span.aida_error_text{
font-family: monospace;
}
span.permanent_status, span.blame{
font-family: monospace;
padding-left: 3px;
padding-right: 3px;
}
span.screenshot_link{
}
span.screenshot_link.disabled{
color: #ccc;
}
div.message{
display: inline-block;
vertical-align: bottom;
}
span.message_source{
font-weight: bold;
padding-left: 3px;
padding-right: 3px;
}
td.status_error{
background-color: red;
color: white;
}
td.status_updating{
background-color: #ffcc44;
color: black;
}
td.status_success{
background-color: white;
color: black;
}
td.status_none{
background-color: #aa55ff;
color: #ffffff;
}
td.account_locked{
text-decoration: underline;
text-decoration-color: red;
}
td.account_error, td.invoice_error{
background-color: #ffcaca;
color: white;
}
span.backtrace_click, span.url_click{
cursor: pointer;
}
td.error_count_bad{
background-color: #ffcaca;
}
td.error_count_very_bad{
background-color: red;
}
td.error_count_very_very_bad{
background-color: red;
font-size: 150%;
}
tr.job_success td{
background-color: #c1ffc8;
}
tr.job_failure td{
background-color: #ffc1c8;
}
tr.job_failure_user td{
background-color: #ffa1f8;
}
tr.job_failure_provider td{
background-color: #ffa0a0;
}
tr.job_failure_system td{
background-color: #ff6060;
color: #ffffff;
}
tr.job_failure_system td a{
background-color: #ff6060;
color: #aff;
}
tr.job_tryagain td{
background-color: #fff1f8;
}
tr.job_started td{
background-color: #ffcc44;
}
tr.error_permanent_system td{
background-color: #ffc1c8;
}
tr.divider td{
border-top: solid 1px black;
}
table.cal_month td, table.cal_month th{
text-align: center;
}
td.cal_weekend{
color: red;
}
td.cal_working_day{
color: black;
}
td.cal_holiday{
color: orange;
font-weight: bold;
}
td.actions{
}
td.has_actions{
padding-right: 0px;
}
p.agree_hide{
display: none;
}
p#message.ok{
color: blue;
font-weight: bold;
}
p#message.error{
color: red;
font-weight: bold;
}
table:not(.colorful) td:not(.colorful) {
background-color: #dbeceb;
}
table:not(.colorful) tr:nth-child(even) td:not(.colorful){
background-color: white;
}
.invisible{
background-color: #ebb8ff;
}
.invalid:after{
content: "*";
}
span.checked{
background-color: #aaff7f;
}
td.taking_too_long_to_start{
color: red;
}
td.taking_too_long_to_finish{
color: red;
}
td.task_failed{
color: red;
}
table#main_table thead{
position: sticky;
top: 0;
background-color: #1f7586;
color: white;
}
table#main_table thead th{
padding-left: 2px;
padding-right: 2px;
}
span.blacklisted{
background-color: black;
color: white;
padding-left: 0.2em;
padding-right: 0.2em;
}
span.graylisted{
background-color: #888;
color: white;
padding-left: 0.2em;
padding-right: 0.2em;
}
span.whitelisted{
color: #555;
}
span.enum{
border: dotted 1px #777;
padding-left: 5px;
padding-right: 5px;
}
span.job_status_ready, span.attempt_status_ready{
background-color: #f5dcff;
}
span.job_status_success, span.attempt_status_success, span.operation_status_success{
background-color: #a7d9a3;
}
span.job_status_failure, span.attempt_status_failure, span.operation_status_failure{
background-color: #d97c7e;
}
span.job_status_scheduled{
background-color: #c5c4d9;
}
span.job_status_queued, span.attempt_status_queued, span.task_status_queued, span.operation_status_waiting{
background-color: #d9d9bf;
}
span.job_status_running, span.attempt_status_running, span.task_status_running{
background-color: #a8cad9;
}
span.task_status_finished{
background-color: #ffd9f4;
}
span.failure_blame_system{
background-color: #ffd3d3;
}
span.failure_blame_user{
background-color: #ffd0fc;
}
span.failure_blame_provider{
background-color: #c1fffd;
}
span.attempt_severity_temporary{
background-color: #d3e0ff;
}
span.attempt_severity_permanent{
background-color: #ffcdce;
}
ul.settings.disabled li{
color: #ccc;
}
div.form_result{
padding: 2em;
margin-top: 2em;
display: none;
}
div.form_result.failure{
background-color: #ffcdce;
display: inline-block;
}
div.form_result.success{
background-color: #a7d9a3;
display: inline-block;
}
tr.duplicate_bug td{
text-decoration: line-through;
color: white;
background-color: red !important;
}

View File

@ -1,183 +0,0 @@
.title { text-align: center;
margin-bottom: .2em; }
.subtitle { text-align: center;
font-size: medium;
font-weight: bold;
margin-top:0; }
.todo { font-family: monospace; color: red; }
.done { font-family: monospace; color: green; }
.priority { font-family: monospace; color: orange; }
.tag { background-color: #eee; font-family: monospace;
padding: 2px; font-size: 80%; font-weight: normal; }
.timestamp { color: #bebebe; }
.timestamp-kwd { color: #5f9ea0; }
.org-right { margin-left: auto; margin-right: 0px; text-align: right; }
.org-left { margin-left: 0px; margin-right: auto; text-align: left; }
.org-center { margin-left: auto; margin-right: auto; text-align: center; }
.underline { text-decoration: underline; }
#postamble p, #preamble p { font-size: 90%; margin: .2em; }
p.verse { margin-left: 3%; }
pre {
border: 1px solid #ccc;
box-shadow: 3px 3px 3px #eee;
padding: 8pt;
font-family: monospace;
overflow: auto;
margin: 1.2em;
}
pre.src {
position: relative;
overflow: visible;
padding-top: 1.2em;
}
pre.src:before {
display: none;
position: absolute;
background-color: white;
top: -10px;
right: 10px;
padding: 3px;
border: 1px solid black;
}
pre.src:hover:before { display: inline;}
/* Languages per Org manual */
pre.src-asymptote:before { content: 'Asymptote'; }
pre.src-awk:before { content: 'Awk'; }
pre.src-C:before { content: 'C'; }
/* pre.src-C++ doesn't work in CSS */
pre.src-clojure:before { content: 'Clojure'; }
pre.src-css:before { content: 'CSS'; }
pre.src-D:before { content: 'D'; }
pre.src-ditaa:before { content: 'ditaa'; }
pre.src-dot:before { content: 'Graphviz'; }
pre.src-calc:before { content: 'Emacs Calc'; }
pre.src-emacs-lisp:before { content: 'Emacs Lisp'; }
pre.src-fortran:before { content: 'Fortran'; }
pre.src-gnuplot:before { content: 'gnuplot'; }
pre.src-haskell:before { content: 'Haskell'; }
pre.src-hledger:before { content: 'hledger'; }
pre.src-java:before { content: 'Java'; }
pre.src-js:before { content: 'Javascript'; }
pre.src-latex:before { content: 'LaTeX'; }
pre.src-ledger:before { content: 'Ledger'; }
pre.src-lisp:before { content: 'Lisp'; }
pre.src-lilypond:before { content: 'Lilypond'; }
pre.src-lua:before { content: 'Lua'; }
pre.src-matlab:before { content: 'MATLAB'; }
pre.src-mscgen:before { content: 'Mscgen'; }
pre.src-ocaml:before { content: 'Objective Caml'; }
pre.src-octave:before { content: 'Octave'; }
pre.src-org:before { content: 'Org mode'; }
pre.src-oz:before { content: 'OZ'; }
pre.src-plantuml:before { content: 'Plantuml'; }
pre.src-processing:before { content: 'Processing.js'; }
pre.src-python:before { content: 'Python'; }
pre.src-R:before { content: 'R'; }
pre.src-ruby:before { content: 'Ruby'; }
pre.src-sass:before { content: 'Sass'; }
pre.src-scheme:before { content: 'Scheme'; }
pre.src-screen:before { content: 'Gnu Screen'; }
pre.src-sed:before { content: 'Sed'; }
pre.src-sh:before { content: 'shell'; }
pre.src-sql:before { content: 'SQL'; }
pre.src-sqlite:before { content: 'SQLite'; }
/* additional languages in org.el's org-babel-load-languages alist */
pre.src-forth:before { content: 'Forth'; }
pre.src-io:before { content: 'IO'; }
pre.src-J:before { content: 'J'; }
pre.src-makefile:before { content: 'Makefile'; }
pre.src-maxima:before { content: 'Maxima'; }
pre.src-perl:before { content: 'Perl'; }
pre.src-picolisp:before { content: 'Pico Lisp'; }
pre.src-scala:before { content: 'Scala'; }
pre.src-shell:before { content: 'Shell Script'; }
pre.src-ebnf2ps:before { content: 'ebfn2ps'; }
/* additional language identifiers per "defun org-babel-execute"
in ob-*.el */
pre.src-cpp:before { content: 'C++'; }
pre.src-abc:before { content: 'ABC'; }
pre.src-coq:before { content: 'Coq'; }
pre.src-groovy:before { content: 'Groovy'; }
/* additional language identifiers from org-babel-shell-names in
ob-shell.el: ob-shell is the only babel language using a lambda to put
the execution function name together. */
pre.src-bash:before { content: 'bash'; }
pre.src-csh:before { content: 'csh'; }
pre.src-ash:before { content: 'ash'; }
pre.src-dash:before { content: 'dash'; }
pre.src-ksh:before { content: 'ksh'; }
pre.src-mksh:before { content: 'mksh'; }
pre.src-posh:before { content: 'posh'; }
/* Additional Emacs modes also supported by the LaTeX listings package */
pre.src-ada:before { content: 'Ada'; }
pre.src-asm:before { content: 'Assembler'; }
pre.src-caml:before { content: 'Caml'; }
pre.src-delphi:before { content: 'Delphi'; }
pre.src-html:before { content: 'HTML'; }
pre.src-idl:before { content: 'IDL'; }
pre.src-mercury:before { content: 'Mercury'; }
pre.src-metapost:before { content: 'MetaPost'; }
pre.src-modula-2:before { content: 'Modula-2'; }
pre.src-pascal:before { content: 'Pascal'; }
pre.src-ps:before { content: 'PostScript'; }
pre.src-prolog:before { content: 'Prolog'; }
pre.src-simula:before { content: 'Simula'; }
pre.src-tcl:before { content: 'tcl'; }
pre.src-tex:before { content: 'TeX'; }
pre.src-plain-tex:before { content: 'Plain TeX'; }
pre.src-verilog:before { content: 'Verilog'; }
pre.src-vhdl:before { content: 'VHDL'; }
pre.src-xml:before { content: 'XML'; }
pre.src-nxml:before { content: 'XML'; }
/* add a generic configuration mode; LaTeX export needs an additional
(add-to-list 'org-latex-listings-langs '(conf " ")) in .emacs */
pre.src-conf:before { content: 'Configuration File'; }
table { border-collapse:collapse; }
caption.t-above { caption-side: top; }
caption.t-bottom { caption-side: bottom; }
td, th { vertical-align:top; }
th.org-right { text-align: center; }
th.org-left { text-align: center; }
th.org-center { text-align: center; }
td.org-right { text-align: right; }
td.org-left { text-align: left; }
td.org-center { text-align: center; }
dt { font-weight: bold; }
.footpara { display: inline; }
.footdef { margin-bottom: 1em; }
.figure { padding: 1em; }
.figure p { text-align: center; }
.equation-container {
display: table;
text-align: center;
width: 100%;
}
.equation {
vertical-align: middle;
}
.equation-label {
display: table-cell;
text-align: right;
vertical-align: middle;
}
.inlinetask {
padding: 10px;
border: 2px solid gray;
margin: 10px;
background: #ffffcc;
}
#org-div-home-and-up
{ text-align: right; font-size: 70%; white-space: nowrap; }
textarea { overflow-x: auto; }
.linenr { font-size: smaller }
.code-highlighted { background-color: #ffff00; }
.org-info-js_info-navigation { border-style: none; }
#org-info-js_console-label
{ font-size: 10px; font-weight: bold; white-space: nowrap; }
.org-info-js_search-highlight
{ background-color: #ffff00; color: #000000; font-weight: bold; }
.org-svg { width: 90%; }
div.warning p{ border: solid 1px red; padding: 1em; box-shadow: 5px 5px 5px #f00; }

View File

@ -1,65 +0,0 @@
#popup{
position: fixed;
display: none;
background-color: #e5fbff;
top: 10px;
left: 10px;
border: solid 5px black;
padding: 20px;
z-index: 10000;
}
#popup.visible{
display: block;
}
#quick_jump_input{
display: none;
margin-top: 1em;
}
#quick_jump_input.visible{
display: inline;
}
.quick_jump_help{
display: none;
}
.quick_jump_help.visible{
display: block;
}
ul.quick_jump{
display: none;
}
ul.quick_jump.visible{
display: block;
}
ul.quick_jump div.menu{
display: none;
}
ul.quick_jump div.menu.visible{
display: block;
}
ul.quick_jump div.action{
display: none;
}
ul.quick_jump div.action.visible{
display: block;
}
ul.quick_jump li{
display: none;
}
ul.quick_jump li.visible{
display: block;
}
ul.quick_jump span.input{
background-color: #fbffb5;
}
div#quick_jump_start ul{
padding-left: 1em;
padding-top: 0;
padding-bottom: 0;
margin: 0;
}
ul.quick_jump div.selected{
color: red;
}
ul.quick_jump{
padding: 0;
}

View File

@ -1,154 +0,0 @@
var quick_jump_state = null;
var quick_jump_inputs = [];
var quick_jump_prevent_key = false;
function quick_jump_update(key){
// let dbg = "DEBUG: ";
// if (key)
// dbg += "K: " + key;
// for (let x of quick_jump_inputs){
// dbg += "[" + x + "]";
// }
// document.getElementById("qjstatus").textContent = dbg;
for(let el of document.getElementsByClassName("quick_jump_help"))
el.classList.remove("visible");
if (quick_jump_state != null){
quick_jump_state.classList.add("visible");
let ul = quick_jump_state.getElementsByTagName("ul");
if (ul.length >= 1 && !document.getElementById("quick_jump_input").classList.contains("visible")){
ul = ul[0];
ul.classList.add("visible");
for (let li of ul.getElementsByTagName("li")){
let menu = li.getElementsByClassName("menu")[0];
let action = li.getElementsByClassName("action")[0];
li.classList.add("visible");
menu.classList.add("visible");
menu.classList.remove("selected");
action.classList.remove("visible");
}
}
}
}
function quick_jump_show(show){
let popup = document.getElementById("popup");
if (show){
popup.classList.add("visible");
quick_jump_state = document.getElementById("quick_jump_start");
quick_jump_inputs = [];
document.getElementById("quick_jump_input").value = "";
document.getElementById("quick_jump_input").classList.remove("visible");
quick_jump_update("-");
}
else{
popup.classList.remove("visible");
quick_jump_state = null;
return;
}
}
function quick_jump_toggle(){
quick_jump_show(!document.getElementById("popup").classList.contains("visible"));
}
function quick_jump_go(){
let text = document.getElementById("quick_jump_input").value;
var customer_id;
if (typeof(CUSTOMER_ID) == "undefined")
customer_id = "1";
else
customer_id = CUSTOMER_ID;
let url = "/quick_jump?customer_id=" + customer_id + "&action=" + quick_jump_state.dataset.action;
let n = 1;
for (let x of quick_jump_inputs)
{
url += "&input" + n + "=" + x;
n += 1;
}
window.location.href = url;
}
document.getElementById("quick_jump_input").addEventListener("keypress", function(e){
if (e.key == "Enter")
{
let value = document.getElementById("quick_jump_input").value;
quick_jump_inputs.push(value);
document.getElementById("quick_jump_input").classList.remove("visible");
document.getElementById("quick_jump_input").value = "";
if (quick_jump_state.dataset.go == "wait-input")
quick_jump_go();
else{
quick_jump_state.querySelector("span.input").appendChild(document.createTextNode(value));
}
quick_jump_prevent_key = true;
quick_jump_update();
}
});
function quick_jump_key(key){
if (document.getElementById("quick_jump_input").classList.contains("visible"))
return;
if (quick_jump_prevent_key)
return;
for (let li of quick_jump_state.getElementsByTagName("li")){
let action = li.getElementsByClassName("action")[0];
if (action.dataset.key == key)
{
for (let hide_li of quick_jump_state.getElementsByTagName("li"))
hide_li.classList.remove("visible");
li.classList.add("visible");
li.getElementsByClassName("menu")[0].classList.add("selected");
quick_jump_state = action;
if (action.dataset.input == "yes")
{
document.getElementById("quick_jump_input").classList.add("visible");
document.getElementById("quick_jump_input").focus();
}
let span_input = quick_jump_state.querySelector("span.input");
if (span_input)
span_input.innerHTML = "";
if (action.dataset.go == "yes")
quick_jump_go();
break;
}
}
quick_jump_update(key);
return;
}
document.addEventListener("keydown", function(e){
quick_jump_prevent_key = false;
if (document.getElementById("popup").classList.contains("visible"))
e.stopPropagation();
});
document.addEventListener("keyup", function(e){
if (e.code == "F8")
{
quick_jump_toggle();
e.stopPropagation();
}
else if (document.getElementById("popup").classList.contains("visible"))
{
if (e.code == "Escape")
{
quick_jump_show(false);
}
else if (quick_jump_state != null)
{
quick_jump_key(e.key);
}
e.stopPropagation();
}
});

View File

@ -1,148 +0,0 @@
# Electricity Bill Compliance Validator
## Overview
This application validates electricity bills according to ANEEL Resolution 1000/2021 (REN 1000/2021). It ensures that bills issued by electricity distributors comply with all regulatory requirements, tariff calculations, and consumer rights.
## Regulatory Compliance
This validator implements the rules defined in [ANEEL Resolution 1000/2021](https://www2.aneel.gov.br/cedoc/ren20211000.html), which establishes the General Conditions for Electricity Supply. The validation covers all aspects of billing as defined in Chapter VIII of the resolution.
## Features
- ✅ Basic Validation
- ✅ Consumption Calculation
- ✅ Demand Charges
- ✅ Reactive Energy
- ✅ Seasonal Tariffs
- ✅ Public Lighting
- ✅ ICMS Tax
- ✅ Municipal Taxes
- ✅ Flag System
- ✅ Subsidies and Discounts
- ✅ Additional Charges
- ✅ Payment Terms
- ✅ Emergency Situations (Art. 350-354)
## Input
The system accepts a `BillComplianceRequest` containing:
- Bill identification (SmartCode)
- Distributor information
- Consumer group and subgroup
- Reading dates and values
- Consumption details
- Tariff components (TUSD, TE)
- Charges and adjustments
- Tax amounts
- Payment information
Example:
```csharp
var request = new BillComplianceRequest
{
SmartCode = "123456789",
DistributorName = "ENERGY_DIST",
ConsumerGroup = "B1",
Subgroup = "RESIDENTIAL",
ConsumptionAmount = 150.5m,
TUSDAmount = 100.25m,
TEAmount = 80.15m,
// ... other properties
};
```
## Output
The system returns a `ValidationResult` containing:
- Validation status (IsValid)
- Rule name that was validated
- Detailed message explaining any violations
Example:
```csharp
{
"IsValid": false,
"RuleName": "Consumption Validation",
"Message": "Consumption amount doesn't match readings difference"
}
```
## Validation Rules
Each validation rule implements the `IValidationRule` interface and follows a specific priority order:
1. **Basic Validation** (Priority: 1)
- Validates fundamental bill information
- Checks data consistency
2. **Consumption Validation** (Priority: 2)
- Verifies reading periods
- Validates consumption calculations
- Checks peak/off-peak totals
3. **Tariff Component Validation** (Priority: 3)
- Validates TUSD and TE calculations
- Checks power factor adjustments
4. **Seasonal Tariff Validation** (Priority: 4)
- Applies to rural/irrigation consumers
- Validates dry/wet season multipliers
5. **Flag System Validation** (Priority: 5)
- Validates flag colors and values
- Checks exemptions
6. **Subsidy Validation** (Priority: 6)
- Validates discount eligibility
- Checks consumption ranges
- Calculates correct discount amounts
7. **Additional Charges Validation** (Priority: 8)
- Validates mandatory charges
- Checks justifications
- Enforces maximum limits
8. **Payment Terms Validation** (Priority: 9)
- Validates due dates
- Calculates late fees
- Checks partial payment eligibility
## Dependencies
The system relies on an `IDistributorRepository` to fetch:
- Tariff information
- Subsidy rules
- Additional charge configurations
- Payment terms
- Seasonal tariff parameters
## Error Handling
- All validation errors are clearly documented
- Tolerance of 0.01 for decimal comparisons
- Detailed error messages for troubleshooting
## Usage Example
```csharp
var validator = new BillComplianceValidator(distributorRepository);
var result = await validator.ValidateAsync(billRequest);
if (!result.IsValid)
{
Console.WriteLine($"Validation failed: {result.Message}");
}
```
## Best Practices
- All monetary values are handled as decimal
- Date comparisons account for time zones
- Consumer groups are case-insensitive
- Validation rules are independent and maintainable
## Contributing
When adding new validation rules:
1. Implement IValidationRule interface
2. Define appropriate priority
3. Add corresponding model classes
4. Update repository interfaces
## Regulatory Updates
This validator is based on ANEEL Resolution 1000/2021. When regulatory updates occur:
1. Review affected validation rules
2. Update calculation parameters
3. Modify validation logic as needed
4. Update documentation
```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -14,8 +14,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compliance", "Compliance\Compliance.csproj", "{C964A170-4A1E-4492-A52F-258BF831BDAA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -34,10 +32,6 @@ Global
{75EFF4FA-14FE-4540-B872-F84224CDECAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75EFF4FA-14FE-4540-B872-F84224CDECAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75EFF4FA-14FE-4540-B872-F84224CDECAF}.Release|Any CPU.Build.0 = Release|Any CPU
{C964A170-4A1E-4492-A52F-258BF831BDAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C964A170-4A1E-4492-A52F-258BF831BDAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C964A170-4A1E-4492-A52F-258BF831BDAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C964A170-4A1E-4492-A52F-258BF831BDAA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE