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:
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")];
|
||||
}
|
||||
Reference in New Issue
Block a user