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,61 @@
|
||||
using GeradoresService.Core;
|
||||
|
||||
namespace GeradoresService.Generators.Identificacao;
|
||||
|
||||
/// <summary>
|
||||
/// Número de documento do Cartão de Cidadão: NNNNNNNN C VV T
|
||||
/// - NNNNNNNN: número de identificação civil (NIC)
|
||||
/// - C: dígito de controlo do NIC (módulo 11)
|
||||
/// - VV: versão (duas letras/dígitos)
|
||||
/// - T: dígito de controlo do documento completo (Luhn com A=10..Z=35), conforme especificação da AMA.
|
||||
/// </summary>
|
||||
public sealed class CartaoCidadaoGenerator : GeneratorBase
|
||||
{
|
||||
public override string Id => "cc";
|
||||
public override string Name => "Cartão de Cidadão";
|
||||
public override string Category => Categories.Identificacao;
|
||||
public override string Description => "Número de documento do Cartão de Cidadão.";
|
||||
|
||||
public override IReadOnlyList<GeneratorOption> Options =>
|
||||
[
|
||||
Option("formato", "Formato", "espacos", ("espacos", "12345678 9 ZZ1"), ("compacto", "123456789ZZ1")),
|
||||
];
|
||||
|
||||
public override GeneratedItem Generate(GeneratorContext context)
|
||||
{
|
||||
var nic = context.Digits(8);
|
||||
var c = Checksums.Mod11(nic);
|
||||
// As versões começam em ZZ e vão decrementando.
|
||||
var version = "Z" + (char)('A' + context.Random.Next(26));
|
||||
var payload = $"{nic}{c}{version}";
|
||||
var t = Checksums.LuhnCheckDigit(payload);
|
||||
var compact = payload + t;
|
||||
return new GeneratedItem(context.Option(Options[0]) == "compacto" ? compact : Format(compact));
|
||||
}
|
||||
|
||||
private static string Format(string compact) => $"{compact[..8]} {compact[8]} {compact[9..]}";
|
||||
|
||||
public override ValidationResult Validate(string value)
|
||||
{
|
||||
var cc = Checksums.Clean(value).ToUpperInvariant();
|
||||
if (cc.Length != 12
|
||||
|| !Checksums.AllDigits(cc[..9])
|
||||
|| !cc[9..11].All(char.IsAsciiLetterOrDigit)
|
||||
|| !char.IsAsciiDigit(cc[11]))
|
||||
return ValidationResult.WrongFormat("Formato esperado: 8 dígitos, 1 dígito, 2 letras/dígitos de versão e 1 dígito (ex.: 00000000 0 ZZ4).");
|
||||
|
||||
var details = new List<string>
|
||||
{
|
||||
$"NIC: {cc[..8]}",
|
||||
$"Versão: {cc[9..11]}",
|
||||
"Controlo: algoritmo Luhn sobre os 12 caracteres (A=10 … Z=35)",
|
||||
};
|
||||
|
||||
if (!Checksums.LuhnValid(cc))
|
||||
{
|
||||
var expected = Checksums.LuhnCheckDigit(cc[..11]);
|
||||
return ValidationResult.BadChecksum($"Dígito de controlo inválido: esperado {expected}, encontrado {cc[11]}.", details);
|
||||
}
|
||||
return ValidationResult.Valid(Format(cc), "Número de documento do CC válido.", details);
|
||||
}
|
||||
}
|
||||
119
GeradoresService/Generators/Identificacao/NifGenerator.cs
Normal file
119
GeradoresService/Generators/Identificacao/NifGenerator.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using GeradoresService.Core;
|
||||
|
||||
namespace GeradoresService.Generators.Identificacao;
|
||||
|
||||
/// <summary>
|
||||
/// Número de Identificação Fiscal. 9 dígitos; o último é módulo 11 com pesos 9..2.
|
||||
/// </summary>
|
||||
public class NifGenerator : GeneratorBase
|
||||
{
|
||||
public sealed record NifPrefix(string Prefix, string Description, bool Colectiva);
|
||||
|
||||
public static readonly IReadOnlyList<NifPrefix> Prefixes =
|
||||
[
|
||||
new("1", "Pessoa singular", false),
|
||||
new("2", "Pessoa singular", false),
|
||||
new("3", "Pessoa singular", false),
|
||||
new("45", "Pessoa singular não residente", false),
|
||||
new("5", "Pessoa colectiva", true),
|
||||
new("6", "Administração pública", true),
|
||||
new("70", "Herança indivisa", false),
|
||||
new("71", "Pessoa colectiva não residente", true),
|
||||
new("72", "Fundo de investimento", true),
|
||||
new("74", "Herança indivisa", false),
|
||||
new("75", "Herança indivisa", false),
|
||||
new("77", "Atribuição oficiosa", false),
|
||||
new("78", "Não residente (reembolso de IVA)", false),
|
||||
new("79", "Regime excepcional (Expo 98)", false),
|
||||
new("8", "Empresário em nome individual (extinto)", false),
|
||||
new("90", "Condomínio / sociedade irregular", true),
|
||||
new("91", "Condomínio / sociedade irregular", true),
|
||||
new("98", "Não residente sem estabelecimento estável", true),
|
||||
new("99", "Sociedade civil sem personalidade jurídica", true),
|
||||
];
|
||||
|
||||
private const string AnySingular = "singular";
|
||||
private const string Any = "qualquer";
|
||||
|
||||
public override string Id => "nif";
|
||||
public override string Name => "NIF";
|
||||
public override string Category => Categories.Identificacao;
|
||||
public override string Description => "Número de Identificação Fiscal (contribuinte).";
|
||||
|
||||
protected virtual IEnumerable<NifPrefix> AllowedPrefixes => Prefixes;
|
||||
|
||||
public override IReadOnlyList<GeneratorOption> Options =>
|
||||
[
|
||||
Option("tipo", "Tipo", DefaultChoice,
|
||||
[.. ExtraChoices, .. AllowedPrefixes.Select(p => (p.Prefix, $"{p.Prefix} — {p.Description}"))]),
|
||||
];
|
||||
|
||||
protected virtual string DefaultChoice => AnySingular;
|
||||
|
||||
protected virtual (string, string)[] ExtraChoices =>
|
||||
[
|
||||
(AnySingular, "Pessoa singular (1, 2 ou 3)"),
|
||||
(Any, "Qualquer"),
|
||||
];
|
||||
|
||||
public override GeneratedItem Generate(GeneratorContext context)
|
||||
{
|
||||
var choice = context.Option(Options[0]);
|
||||
var prefix = choice switch
|
||||
{
|
||||
AnySingular => context.Pick(["1", "2", "3"]),
|
||||
Any => context.Pick(AllowedPrefixes.ToArray()).Prefix,
|
||||
_ => choice,
|
||||
};
|
||||
return new GeneratedItem(Build(prefix, context));
|
||||
}
|
||||
|
||||
public static string Build(string prefix, GeneratorContext context)
|
||||
{
|
||||
var body = prefix + context.Digits(8 - prefix.Length);
|
||||
return body + Checksums.Mod11(body);
|
||||
}
|
||||
|
||||
public override ValidationResult Validate(string value)
|
||||
{
|
||||
var nif = Checksums.Clean(value);
|
||||
if (nif.StartsWith("PT", StringComparison.OrdinalIgnoreCase))
|
||||
nif = nif[2..];
|
||||
|
||||
if (nif.Length != 9 || !Checksums.AllDigits(nif))
|
||||
return ValidationResult.WrongFormat("Um NIF tem 9 dígitos.");
|
||||
|
||||
var prefix = FindPrefix(nif);
|
||||
if (prefix is null || !AllowedPrefixes.Contains(prefix))
|
||||
return ValidationResult.WrongFormat($"Prefixo '{nif[..2]}' não corresponde a um {Name} válido.");
|
||||
|
||||
var expected = Checksums.Mod11(nif.AsSpan(0, 8));
|
||||
var details = new List<string>
|
||||
{
|
||||
$"Prefixo {prefix.Prefix}: {prefix.Description}",
|
||||
$"Dígito de controlo esperado: {expected} (módulo 11, pesos 9..2)",
|
||||
};
|
||||
|
||||
return expected == nif[8] - '0'
|
||||
? ValidationResult.Valid(nif, $"{Name} válido — {prefix.Description}.", details)
|
||||
: ValidationResult.BadChecksum($"Dígito de controlo inválido: esperado {expected}, encontrado {nif[8]}.", details);
|
||||
}
|
||||
|
||||
public static NifPrefix? FindPrefix(string nif) =>
|
||||
Prefixes.FirstOrDefault(p => p.Prefix.Length == 2 && nif.StartsWith(p.Prefix))
|
||||
?? Prefixes.FirstOrDefault(p => p.Prefix.Length == 1 && nif.StartsWith(p.Prefix));
|
||||
}
|
||||
|
||||
/// <summary>NIPC: NIF de pessoas colectivas. Mesmo algoritmo, prefixos restritos.</summary>
|
||||
public sealed class NipcGenerator : NifGenerator
|
||||
{
|
||||
public override string Id => "nipc";
|
||||
public override string Name => "NIPC";
|
||||
public override string Description => "Número de Identificação de Pessoa Colectiva (empresas e entidades).";
|
||||
|
||||
protected override IEnumerable<NifPrefix> AllowedPrefixes => Prefixes.Where(p => p.Colectiva);
|
||||
|
||||
protected override string DefaultChoice => "5";
|
||||
|
||||
protected override (string, string)[] ExtraChoices => [("qualquer", "Qualquer")];
|
||||
}
|
||||
57
GeradoresService/Generators/Identificacao/NissGenerator.cs
Normal file
57
GeradoresService/Generators/Identificacao/NissGenerator.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using GeradoresService.Core;
|
||||
|
||||
namespace GeradoresService.Generators.Identificacao;
|
||||
|
||||
/// <summary>
|
||||
/// Número de Identificação da Segurança Social. 11 dígitos, começa por 1 (singular) ou 2 (colectiva).
|
||||
/// Controlo: 9 - (Σ dígito × peso) mod 10, pesos 29, 23, 19, 17, 13, 11, 7, 5, 3, 2.
|
||||
/// </summary>
|
||||
public sealed class NissGenerator : GeneratorBase
|
||||
{
|
||||
private static readonly int[] Weights = [29, 23, 19, 17, 13, 11, 7, 5, 3, 2];
|
||||
|
||||
public override string Id => "niss";
|
||||
public override string Name => "NISS";
|
||||
public override string Category => Categories.Identificacao;
|
||||
public override string Description => "Número de Identificação da Segurança Social.";
|
||||
|
||||
public override IReadOnlyList<GeneratorOption> Options =>
|
||||
[
|
||||
Option("tipo", "Tipo", "1", ("1", "1 — Pessoa singular"), ("2", "2 — Pessoa colectiva")),
|
||||
];
|
||||
|
||||
public override GeneratedItem Generate(GeneratorContext context)
|
||||
{
|
||||
var body = context.Option(Options[0]) + context.Digits(9);
|
||||
return new GeneratedItem(body + CheckDigit(body));
|
||||
}
|
||||
|
||||
public static int CheckDigit(ReadOnlySpan<char> first10)
|
||||
{
|
||||
var sum = 0;
|
||||
for (var i = 0; i < 10; i++)
|
||||
sum += (first10[i] - '0') * Weights[i];
|
||||
return 9 - sum % 10;
|
||||
}
|
||||
|
||||
public override ValidationResult Validate(string value)
|
||||
{
|
||||
var niss = Checksums.Clean(value);
|
||||
if (niss.Length != 11 || !Checksums.AllDigits(niss))
|
||||
return ValidationResult.WrongFormat("Um NISS tem 11 dígitos.");
|
||||
if (niss[0] is not ('1' or '2'))
|
||||
return ValidationResult.WrongFormat("Um NISS começa por 1 (singular) ou 2 (colectiva).");
|
||||
|
||||
var expected = CheckDigit(niss);
|
||||
var tipo = niss[0] == '1' ? "pessoa singular" : "pessoa colectiva";
|
||||
var details = new List<string>
|
||||
{
|
||||
$"Tipo: {tipo}",
|
||||
$"Dígito de controlo esperado: {expected}",
|
||||
};
|
||||
|
||||
return expected == niss[10] - '0'
|
||||
? ValidationResult.Valid(niss, $"NISS válido — {tipo}.", details)
|
||||
: ValidationResult.BadChecksum($"Dígito de controlo inválido: esperado {expected}, encontrado {niss[10]}.", details);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using GeradoresService.Core;
|
||||
|
||||
namespace GeradoresService.Generators.Identificacao;
|
||||
|
||||
/// <summary>
|
||||
/// Número de passaporte português: uma letra seguida de 6 dígitos.
|
||||
/// Não existe dígito de controlo público no número em si; valida-se apenas o formato.
|
||||
/// </summary>
|
||||
public sealed partial class PassaporteGenerator : GeneratorBase
|
||||
{
|
||||
public override string Id => "passaporte";
|
||||
public override string Name => "Passaporte";
|
||||
public override string Category => Categories.Identificacao;
|
||||
public override string Description => "Número de passaporte português (letra + 6 dígitos). Apenas formato.";
|
||||
public override bool HasChecksum => false;
|
||||
|
||||
public override GeneratedItem Generate(GeneratorContext context) =>
|
||||
new($"{(char)('A' + context.Random.Next(26))}{context.Digits(6)}");
|
||||
|
||||
public override ValidationResult Validate(string value)
|
||||
{
|
||||
var p = Checksums.Clean(value).ToUpperInvariant();
|
||||
return Pattern().IsMatch(p)
|
||||
? ValidationResult.Valid(p, "Formato de passaporte português válido (sem dígito de controlo).")
|
||||
: ValidationResult.WrongFormat("Formato esperado: uma letra seguida de 6 dígitos (ex.: C123456).");
|
||||
}
|
||||
|
||||
[GeneratedRegex("^[A-Z][0-9]{6}$")]
|
||||
private static partial Regex Pattern();
|
||||
}
|
||||
Reference in New Issue
Block a user