Files
Geradores/GeradoresService/Generators/Financeiro/IbanGenerator.cs
Marco Santos d7ea8a9b9e Reescrita dos geradores: biblioteca IGenerator, minimal API e novo frontend
- GeradoresService: cada tipo passa a ser um IGenerator (NIF, NIPC, NISS, CC,
  passaporte, IBAN, NIB, cartões, referência MB, pessoa, telefone, código postal,
  email, matrícula, EAN, UUID) registado em GeneratorRegistry; remove DAL/EF
- GeradoresWS: minimal APIs em /api/v1 com OpenAPI + Scalar e health check
- UnitTest: testes de ida e volta, valores conhecidos, detecção e API
- geradoresfe: UI construída a partir dos metadados da API, validador livre,
  exportação CSV/JSON/TXT, nginx com API_UPSTREAM configurável
- Docker, docker-compose e Jenkinsfile actualizados

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:51:39 +01:00

76 lines
3.3 KiB
C#

using GeradoresService.Core;
namespace GeradoresService.Generators.Financeiro;
/// <summary>
/// IBAN português: PT + 2 dígitos de controlo ISO 7064 (MOD 97-10) + NIB.
/// A validação aceita IBAN de qualquer país (verifica comprimento conhecido e MOD 97).
/// </summary>
public sealed class IbanGenerator : NibGenerator
{
private static readonly Dictionary<string, int> Lengths = new()
{
["PT"] = 25, ["ES"] = 24, ["FR"] = 27, ["DE"] = 22, ["IT"] = 27, ["GB"] = 22, ["IE"] = 22,
["NL"] = 18, ["BE"] = 16, ["LU"] = 20, ["CH"] = 21, ["AT"] = 20, ["DK"] = 18, ["FI"] = 18,
["SE"] = 24, ["NO"] = 15, ["PL"] = 28, ["CZ"] = 24, ["GR"] = 27, ["HR"] = 21, ["RO"] = 24,
["MT"] = 31, ["CY"] = 28, ["SK"] = 24, ["SI"] = 19, ["HU"] = 28, ["BG"] = 22, ["EE"] = 20,
["LT"] = 20, ["LV"] = 21, ["IS"] = 26, ["LI"] = 21, ["MC"] = 27, ["SM"] = 27, ["BR"] = 29,
};
public override string Id => "iban";
public override string Name => "IBAN";
public override string Description => "IBAN português (PT50 + NIB). Valida IBAN de qualquer país.";
public override IReadOnlyList<GeneratorOption> Options =>
[
.. base.Options,
Option("formato", "Formato", "espacos", ("espacos", "PT50 0000 0000 …"), ("compacto", "PT500000…")),
];
public override GeneratedItem Generate(GeneratorContext context)
{
var iban = FromBban("PT", BuildNib(context));
return new GeneratedItem(context.Option(Options[1]) == "compacto" ? iban : Format(iban));
}
public static string FromBban(string country, string bban)
{
var check = 98 - Checksums.Mod97(Checksums.LettersToDigits(bban + country) + "00");
return $"{country}{check:D2}{bban}";
}
public static string Format(string iban) =>
string.Join(' ', iban.Chunk(4).Select(c => new string(c)));
public override ValidationResult Validate(string value)
{
var iban = Checksums.Clean(value).ToUpperInvariant();
if (iban.Length < 15 || iban.Length > 34
|| !char.IsAsciiLetter(iban[0]) || !char.IsAsciiLetter(iban[1])
|| !char.IsAsciiDigit(iban[2]) || !char.IsAsciiDigit(iban[3])
|| !iban.All(char.IsAsciiLetterOrDigit))
return ValidationResult.WrongFormat("Um IBAN tem código de país (2 letras), 2 dígitos de controlo e o BBAN.");
var country = iban[..2];
if (Lengths.TryGetValue(country, out var expectedLength) && iban.Length != expectedLength)
return ValidationResult.WrongFormat($"Um IBAN {country} tem {expectedLength} caracteres (tem {iban.Length}).");
var rearranged = Checksums.LettersToDigits(iban[4..] + iban[..4]);
if (Checksums.Mod97(rearranged) != 1)
{
var expected = FromBban(country, iban[4..])[2..4];
return ValidationResult.BadChecksum(
$"Dígitos de controlo do IBAN inválidos: esperado {expected}, encontrado {iban[2..4]}.",
["Controlo: ISO 7064 MOD 97-10"]);
}
if (country == "PT")
{
var nibResult = ValidateNib(iban[4..], "IBAN");
return nibResult with { Normalized = nibResult.IsValid ? Format(iban) : null };
}
return ValidationResult.Valid(Format(iban), $"IBAN válido ({country}).", ["Controlo: ISO 7064 MOD 97-10"]);
}
}