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

62 lines
2.5 KiB
C#

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