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>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
using GeradoresService.Core;
|
||||
|
||||
namespace GeradoresService.Generators.Financeiro;
|
||||
|
||||
/// <summary>
|
||||
/// Números de cartão de pagamento válidos pelo algoritmo de Luhn (apenas para testes).
|
||||
/// </summary>
|
||||
public sealed class CartaoCreditoGenerator : GeneratorBase
|
||||
{
|
||||
private sealed record Brand(string Key, string Name, int Length, Func<Random, string> Prefix, Func<string, bool> Matches);
|
||||
|
||||
private static readonly Brand[] Brands =
|
||||
[
|
||||
new("visa", "Visa", 16, _ => "4", n => n.StartsWith('4') && n.Length is 13 or 16 or 19),
|
||||
new("mastercard", "Mastercard", 16,
|
||||
r => r.Next(2) == 0 ? (51 + r.Next(5)).ToString() : (2221 + r.Next(500)).ToString(),
|
||||
n => n.Length == 16 && (InRange(n, 2, 51, 55) || InRange(n, 4, 2221, 2720))),
|
||||
new("amex", "American Express", 15, r => r.Next(2) == 0 ? "34" : "37",
|
||||
n => n.Length == 15 && (n.StartsWith("34") || n.StartsWith("37"))),
|
||||
];
|
||||
|
||||
public override string Id => "cartao-credito";
|
||||
public override string Name => "Cartão de crédito";
|
||||
public override string Category => Categories.Financeiro;
|
||||
public override string Description => "Número de cartão válido pelo algoritmo de Luhn, com validade e CVV. Apenas para testes.";
|
||||
|
||||
public override IReadOnlyList<GeneratorOption> Options =>
|
||||
[
|
||||
Option("rede", "Rede", "visa", [.. Brands.Select(b => (b.Key, b.Name)), ("qualquer", "Qualquer")]),
|
||||
Option("formato", "Formato", "espacos", ("espacos", "4111 1111 1111 1111"), ("compacto", "4111111111111111")),
|
||||
];
|
||||
|
||||
public override GeneratedItem Generate(GeneratorContext context)
|
||||
{
|
||||
var key = context.Option(Options[0]);
|
||||
var brand = key == "qualquer" ? context.Pick(Brands) : Brands.Single(b => b.Key == key);
|
||||
|
||||
var body = brand.Prefix(context.Random);
|
||||
body += context.Digits(brand.Length - 1 - body.Length);
|
||||
var number = body + Checksums.LuhnCheckDigit(body);
|
||||
|
||||
var expiry = DateTime.UtcNow.AddMonths(context.Random.Next(6, 60));
|
||||
var cvv = context.Digits(brand.Key == "amex" ? 4 : 3);
|
||||
var display = context.Option(Options[1]) == "compacto" ? number : Format(number);
|
||||
|
||||
return new GeneratedItem(display, new Dictionary<string, string>
|
||||
{
|
||||
["Número"] = display,
|
||||
["Rede"] = brand.Name,
|
||||
["Validade"] = expiry.ToString("MM/yy"),
|
||||
["CVV"] = cvv,
|
||||
});
|
||||
}
|
||||
|
||||
private static string Format(string number) => number.Length == 15
|
||||
? $"{number[..4]} {number[4..10]} {number[10..]}"
|
||||
: string.Join(' ', number.Chunk(4).Select(c => new string(c)));
|
||||
|
||||
private static bool InRange(string n, int digits, int min, int max) =>
|
||||
int.TryParse(n.AsSpan(0, digits), out var p) && p >= min && p <= max;
|
||||
|
||||
public override ValidationResult Validate(string value)
|
||||
{
|
||||
var number = Checksums.Clean(value);
|
||||
if (number.Length is < 12 or > 19 || !Checksums.AllDigits(number))
|
||||
return ValidationResult.WrongFormat("Um número de cartão tem entre 12 e 19 dígitos.");
|
||||
|
||||
var brand = Brands.FirstOrDefault(b => b.Matches(number));
|
||||
var details = new List<string>
|
||||
{
|
||||
$"Rede: {brand?.Name ?? "desconhecida"}",
|
||||
"Controlo: algoritmo de Luhn",
|
||||
};
|
||||
|
||||
if (!Checksums.LuhnValid(number))
|
||||
{
|
||||
var expected = Checksums.LuhnCheckDigit(number[..^1]);
|
||||
return ValidationResult.BadChecksum($"Falha no algoritmo de Luhn: último dígito devia ser {expected}.", details);
|
||||
}
|
||||
return ValidationResult.Valid(Format(number), $"Número de cartão válido{(brand is null ? "" : $" ({brand.Name})")}.", details);
|
||||
}
|
||||
}
|
||||
75
GeradoresService/Generators/Financeiro/IbanGenerator.cs
Normal file
75
GeradoresService/Generators/Financeiro/IbanGenerator.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
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"]);
|
||||
}
|
||||
}
|
||||
75
GeradoresService/Generators/Financeiro/NibGenerator.cs
Normal file
75
GeradoresService/Generators/Financeiro/NibGenerator.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using GeradoresService.Core;
|
||||
|
||||
namespace GeradoresService.Generators.Financeiro;
|
||||
|
||||
/// <summary>
|
||||
/// NIB: banco (4) + balcão (4) + conta (11) + controlo (2). Controlo = 98 - (NIB19 × 100 mod 97).
|
||||
/// </summary>
|
||||
public class NibGenerator : GeneratorBase
|
||||
{
|
||||
public static readonly IReadOnlyList<(string Code, string Name)> Banks =
|
||||
[
|
||||
("0007", "Novo Banco"),
|
||||
("0010", "BPI"),
|
||||
("0018", "Santander Totta"),
|
||||
("0023", "Banco ActivoBank"),
|
||||
("0033", "Millennium BCP"),
|
||||
("0035", "Caixa Geral de Depósitos"),
|
||||
("0036", "Banco Montepio"),
|
||||
("0045", "Crédito Agrícola"),
|
||||
("0079", "Banco BIC"),
|
||||
("0193", "Banco CTT"),
|
||||
("0269", "Bankinter"),
|
||||
];
|
||||
|
||||
public override string Id => "nib";
|
||||
public override string Name => "NIB";
|
||||
public override string Category => Categories.Financeiro;
|
||||
public override string Description => "Número de Identificação Bancária (21 dígitos).";
|
||||
|
||||
public override IReadOnlyList<GeneratorOption> Options =>
|
||||
[
|
||||
Option("banco", "Banco", "qualquer",
|
||||
[("qualquer", "Qualquer"), .. Banks.Select(b => (b.Code, $"{b.Code} — {b.Name}"))]),
|
||||
];
|
||||
|
||||
public override GeneratedItem Generate(GeneratorContext context) => new(BuildNib(context));
|
||||
|
||||
protected string BuildNib(GeneratorContext context)
|
||||
{
|
||||
var bank = context.Option(Options[0]);
|
||||
if (bank == "qualquer")
|
||||
bank = context.Pick(Banks).Code;
|
||||
var body = bank + context.Digits(4) + context.Digits(11);
|
||||
return body + NibCheck(body);
|
||||
}
|
||||
|
||||
public static string NibCheck(string first19) =>
|
||||
(98 - Checksums.Mod97(first19 + "00")).ToString("D2");
|
||||
|
||||
public static string? BankName(string nib) =>
|
||||
Banks.FirstOrDefault(b => nib.StartsWith(b.Code)).Name;
|
||||
|
||||
public override ValidationResult Validate(string value)
|
||||
{
|
||||
var nib = Checksums.Clean(value);
|
||||
if (nib.Length != 21 || !Checksums.AllDigits(nib))
|
||||
return ValidationResult.WrongFormat("Um NIB tem 21 dígitos.");
|
||||
return ValidateNib(nib, "NIB");
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateNib(string nib, string label)
|
||||
{
|
||||
var expected = NibCheck(nib[..19]);
|
||||
var details = new List<string>
|
||||
{
|
||||
$"Banco: {nib[..4]}{(BankName(nib) is { } n ? $" ({n})" : "")}",
|
||||
$"Balcão: {nib[4..8]}",
|
||||
$"Conta: {nib[8..19]}",
|
||||
$"Dígitos de controlo esperados: {expected} (módulo 97)",
|
||||
};
|
||||
return nib[19..] == expected
|
||||
? ValidationResult.Valid(nib, $"{label} válido.", details)
|
||||
: ValidationResult.BadChecksum($"Dígitos de controlo do NIB inválidos: esperado {expected}, encontrado {nib[19..]}.", details);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using GeradoresService.Core;
|
||||
|
||||
namespace GeradoresService.Generators.Financeiro;
|
||||
|
||||
/// <summary>
|
||||
/// Referência Multibanco: entidade (5) + referência (9) + montante.
|
||||
/// Os 2 últimos dígitos da referência são controlo: 98 - (Σ dígito × peso mod 97),
|
||||
/// calculado sobre entidade(5) + referência(7) + montante em cêntimos(8).
|
||||
/// </summary>
|
||||
public sealed partial class ReferenciaMultibancoGenerator : GeneratorBase
|
||||
{
|
||||
private static readonly int[] Weights = [51, 73, 17, 89, 38, 62, 45, 53, 15, 50, 5, 49, 34, 81, 76, 27, 90, 9, 30, 3];
|
||||
|
||||
public override string Id => "multibanco";
|
||||
public override string Name => "Referência Multibanco";
|
||||
public override string Category => Categories.Financeiro;
|
||||
public override string Description => "Entidade, referência e montante com dígitos de controlo válidos.";
|
||||
|
||||
public override GeneratedItem Generate(GeneratorContext context)
|
||||
{
|
||||
var entity = (10000 + context.Random.Next(90000)).ToString();
|
||||
var cents = 100 + context.Random.Next(50000);
|
||||
var reference = context.Digits(7);
|
||||
reference += CheckDigits(entity, reference, cents);
|
||||
|
||||
var amount = (cents / 100m).ToString("0.00", CultureInfo.InvariantCulture);
|
||||
return new GeneratedItem($"{entity} {FormatReference(reference)} {amount}", new Dictionary<string, string>
|
||||
{
|
||||
["Entidade"] = entity,
|
||||
["Referência"] = FormatReference(reference),
|
||||
["Montante"] = $"{amount} €",
|
||||
});
|
||||
}
|
||||
|
||||
public static string CheckDigits(string entity, string reference7, long cents)
|
||||
{
|
||||
var chk = entity + reference7 + cents.ToString("D8");
|
||||
var sum = 0;
|
||||
for (var i = 0; i < 20; i++)
|
||||
sum += (chk[i] - '0') * Weights[i];
|
||||
return (98 - sum % 97).ToString("D2");
|
||||
}
|
||||
|
||||
private static string FormatReference(string r) => $"{r[..3]} {r[3..6]} {r[6..]}";
|
||||
|
||||
public override ValidationResult Validate(string value)
|
||||
{
|
||||
var m = Pattern().Match(value.Trim());
|
||||
if (!m.Success)
|
||||
return ValidationResult.WrongFormat("Formato esperado: entidade (5 dígitos), referência (9 dígitos) e montante, ex.: 12345 123 456 789 10.00");
|
||||
|
||||
var entity = m.Groups["ent"].Value;
|
||||
var reference = Checksums.Clean(m.Groups["ref"].Value);
|
||||
var amount = decimal.Parse(m.Groups["val"].Value.Replace(',', '.'), CultureInfo.InvariantCulture);
|
||||
var cents = (long)Math.Round(amount * 100);
|
||||
if (cents > 99_999_999)
|
||||
return ValidationResult.WrongFormat("Montante demasiado elevado.");
|
||||
|
||||
var expected = CheckDigits(entity, reference[..7], cents);
|
||||
var details = new List<string>
|
||||
{
|
||||
$"Entidade: {entity}",
|
||||
$"Montante: {amount.ToString("0.00", CultureInfo.InvariantCulture)} €",
|
||||
$"Dígitos de controlo esperados: {expected}",
|
||||
};
|
||||
return reference[7..] == expected
|
||||
? ValidationResult.Valid($"{entity} {FormatReference(reference)} {amount.ToString("0.00", CultureInfo.InvariantCulture)}", "Referência Multibanco válida.", details)
|
||||
: ValidationResult.BadChecksum($"Dígitos de controlo inválidos: esperado {expected}, encontrado {reference[7..]}.", details);
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^(?<ent>\d{5})[\s|;]+(?<ref>\d{3}\s?\d{3}\s?\d{3})[\s|;]+(?<val>\d{1,6}(?:[.,]\d{1,2})?)\s*€?$")]
|
||||
private static partial Regex Pattern();
|
||||
}
|
||||
Reference in New Issue
Block a user