att
This commit is contained in:
parent
48c729e5b6
commit
e4be58d728
1
.gitignore
vendored
1
.gitignore
vendored
@ -361,3 +361,4 @@ MigrationBackup/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
.aider*
|
||||
|
||||
72
.history/Compliance/Program_20250131125538.cs
Normal file
72
.history/Compliance/Program_20250131125538.cs
Normal 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
18
.vscode/c_cpp_properties.json
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Win32",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**"
|
||||
],
|
||||
"defines": [
|
||||
"_DEBUG",
|
||||
"UNICODE",
|
||||
"_UNICODE"
|
||||
],
|
||||
"compilerPath": "cl.exe",
|
||||
"intelliSenseMode": "windows-msvc-x64"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||
@ -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>
|
||||
@ -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
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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; } = [];
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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; } = [];
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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; } = [];
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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; } = [];
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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; } = [];
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Compliance.DTOs;
|
||||
|
||||
namespace Compliance.Services
|
||||
{
|
||||
public interface IBillComplianceService
|
||||
{
|
||||
Task<BillComplianceResponse> ValidateBillAsync(Compliance.DTOs.BillComplianceRequest request);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
@ -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;
|
||||
}
|
||||
@ -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; }
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
@ -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
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user