Files
Geradores/GeradoresService/Generators/Identificacao/NifGenerator.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

120 lines
4.7 KiB
C#

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")];
}