- 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>
120 lines
4.7 KiB
C#
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")];
|
|
}
|