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:
2026-10-11 23:51:39 +01:00
parent 0f764b1239
commit d7ea8a9b9e
119 changed files with 7764 additions and 2502 deletions

View File

@@ -1,81 +0,0 @@
using System;
namespace GeradoresService
{
public static class CartaoCidadao
{
// Função estática para gerar o número de documento
public static string Generate()
{
Random rand = new Random();
// Gerar o Número de Identificação Civil (8 dígitos)
string numeroIdentificacaoCivil = rand.Next(0, 100000000).ToString("D8");
// Gerar o Check Digit C
int checkDigitC = CalcularCheckDigit(numeroIdentificacaoCivil);
// Gerar a Versão (duas letras ou números aleatórios entre A-Z ou 0-9)
string versao = GerarVersao();
// Gerar o Check Digit T (Número aleatório entre 0-9)
int checkDigitT = rand.Next(0, 10); // T: Número aleatório por enquanto, conforme exemplo.
// Montar o número completo do documento com o formato desejado
string numeroDocumento = $"{numeroIdentificacaoCivil} {checkDigitC} {versao}{checkDigitT}";
return numeroDocumento;
}
// Função para calcular o Check Digit C com base no Número de Identificação Civil
private static int CalcularCheckDigit(string numeroIdentificacaoCivil)
{
int soma = 0;
bool segundoElemento = false;
// Percorrer o número da direita para a esquerda
for (int i = numeroIdentificacaoCivil.Length - 1; i >= 0; i--)
{
int valor = numeroIdentificacaoCivil[i] - '0'; // Converte o char para inteiro
// Multiplicar cada segundo número por 2
if (segundoElemento)
{
valor *= 2;
if (valor >= 10) valor -= 9; // Subtrair 9 se o valor for maior ou igual a 10
}
soma += valor;
segundoElemento = !segundoElemento;
}
// Retornar o check digit (resto da soma por 10)
return soma % 10;
}
// Função para gerar a versão (letras ou números)
private static string GerarVersao()
{
Random rand = new Random();
string versao = "";
for (int i = 0; i < 2; i++) // Gerar duas letras/números
{
int numeroAleatorio = rand.Next(0, 36); // 26 letras + 10 números
if (numeroAleatorio < 26)
{
// Gerar uma letra de A a Z
versao += (char)('A' + numeroAleatorio);
}
else
{
// Gerar um número de 0 a 9
versao += (char)('0' + (numeroAleatorio - 26));
}
}
return versao;
}
}
}

View File

@@ -0,0 +1,71 @@
using System.Numerics;
namespace GeradoresService.Core;
public static class Checksums
{
/// <summary>
/// Dígito de controlo módulo 11 com pesos decrescentes (n+1 .. 2), usado no NIF e no NIC do CC.
/// Resto 0 ou 1 dá dígito 0.
/// </summary>
public static int Mod11(ReadOnlySpan<char> digits)
{
var sum = 0;
for (var i = 0; i < digits.Length; i++)
sum += (digits[i] - '0') * (digits.Length + 1 - i);
var remainder = sum % 11;
return remainder < 2 ? 0 : 11 - remainder;
}
/// <summary>
/// Soma Luhn a partir da direita, duplicando posições alternadas.
/// Aceita letras (A=10 .. Z=35), como no número de documento do Cartão de Cidadão.
/// </summary>
public static int LuhnSum(ReadOnlySpan<char> value)
{
var sum = 0;
var doubleIt = false;
for (var i = value.Length - 1; i >= 0; i--)
{
var v = CharValue(value[i]);
if (doubleIt)
{
v *= 2;
if (v > 9) v -= 9;
}
sum += v;
doubleIt = !doubleIt;
}
return sum;
}
public static bool LuhnValid(ReadOnlySpan<char> value) => LuhnSum(value) % 10 == 0;
/// <summary>Calcula o dígito Luhn a acrescentar a <paramref name="payload"/>.</summary>
public static int LuhnCheckDigit(string payload)
{
var sum = LuhnSum(payload + "0");
return (10 - sum % 10) % 10;
}
/// <summary>ISO 7064 MOD 97-10 sobre uma string de dígitos.</summary>
public static int Mod97(string digits) => (int)(BigInteger.Parse(digits) % 97);
/// <summary>Converte letras em números (A=10 .. Z=35) para cálculo de IBAN.</summary>
public static string LettersToDigits(string value) =>
string.Concat(value.Select(c => char.IsLetter(c) ? (char.ToUpperInvariant(c) - 'A' + 10).ToString() : c.ToString()));
private static int CharValue(char c) => c switch
{
>= '0' and <= '9' => c - '0',
>= 'A' and <= 'Z' => c - 'A' + 10,
>= 'a' and <= 'z' => c - 'a' + 10,
_ => throw new ArgumentException($"Carácter inválido '{c}'."),
};
/// <summary>Remove espaços, hífens, pontos e barras.</summary>
public static string Clean(string value) =>
new(value.Where(c => !char.IsWhiteSpace(c) && c is not '-' and not '.' and not '/').ToArray());
public static bool AllDigits(string value) => value.Length > 0 && value.All(char.IsAsciiDigit);
}

View File

@@ -0,0 +1,18 @@
namespace GeradoresService.Core;
public abstract class GeneratorBase : IGenerator
{
public abstract string Id { get; }
public abstract string Name { get; }
public abstract string Category { get; }
public abstract string Description { get; }
public virtual IReadOnlyList<GeneratorOption> Options => [];
public virtual bool HasChecksum => true;
public virtual bool CanValidate => true;
public abstract GeneratedItem Generate(GeneratorContext context);
public abstract ValidationResult Validate(string value);
protected static GeneratorOption Option(string key, string label, string defaultValue, params (string Value, string Label)[] choices) =>
new(key, label, choices.Select(c => new OptionChoice(c.Value, c.Label)).ToArray(), defaultValue);
}

View File

@@ -0,0 +1,39 @@
namespace GeradoresService.Core;
/// <summary>
/// Um gerador/validador de um tipo de identificador.
/// Os metadados (Id, Name, Options, ...) são expostos pela API para o frontend construir a UI.
/// </summary>
public interface IGenerator
{
/// <summary>Identificador estável usado nos URLs (ex.: "nif").</summary>
string Id { get; }
string Name { get; }
string Category { get; }
string Description { get; }
IReadOnlyList<GeneratorOption> Options { get; }
/// <summary>
/// Indica se a validação verifica um dígito de controlo (true) ou apenas o formato (false).
/// </summary>
bool HasChecksum { get; }
/// <summary>False para geradores compostos que não têm validação (ex.: pessoa fictícia).</summary>
bool CanValidate { get; }
GeneratedItem Generate(GeneratorContext context);
ValidationResult Validate(string value);
}
public static class Categories
{
public const string Identificacao = "Identificação";
public const string Financeiro = "Financeiro";
public const string Contactos = "Contactos e Morada";
public const string Outros = "Outros";
}

View File

@@ -0,0 +1,72 @@
namespace GeradoresService.Core;
public sealed record GeneratorOption(
string Key,
string Label,
IReadOnlyList<OptionChoice> Choices,
string DefaultValue);
public sealed record OptionChoice(string Value, string Label);
/// <summary>
/// Valor gerado. <see cref="Fields"/> é usado por geradores compostos (ex.: pessoa fictícia).
/// </summary>
public sealed record GeneratedItem(string Value, IReadOnlyDictionary<string, string>? Fields = null);
/// <param name="IsValid">O valor é válido.</param>
/// <param name="FormatMatch">O valor tem o formato deste tipo (mesmo que o dígito de controlo falhe).</param>
/// <param name="Message">Resumo legível.</param>
/// <param name="Normalized">Valor normalizado/formatado, quando válido.</param>
/// <param name="Details">Passos do cálculo / informação extra.</param>
public sealed record ValidationResult(
bool IsValid,
bool FormatMatch,
string Message,
string? Normalized = null,
IReadOnlyList<string>? Details = null)
{
public static ValidationResult Valid(string normalized, string message, IReadOnlyList<string>? details = null) =>
new(true, true, message, normalized, details);
public static ValidationResult WrongFormat(string message) =>
new(false, false, message);
public static ValidationResult BadChecksum(string message, IReadOnlyList<string>? details = null) =>
new(false, true, message, null, details);
}
/// <summary>
/// Contexto de geração: fonte de aleatoriedade e opções escolhidas pelo utilizador.
/// </summary>
public sealed class GeneratorContext(Random random, IReadOnlyDictionary<string, string?> options)
{
public Random Random { get; } = random;
public static GeneratorContext Default(Random? random = null) =>
new(random ?? Random.Shared, new Dictionary<string, string?>());
/// <summary>
/// Devolve o valor da opção ou o default. O valor tem de ser uma das escolhas válidas.
/// </summary>
public string Option(GeneratorOption option)
{
if (options.TryGetValue(option.Key, out var value) && !string.IsNullOrWhiteSpace(value))
{
var match = option.Choices.FirstOrDefault(c => string.Equals(c.Value, value, StringComparison.OrdinalIgnoreCase));
if (match is null)
throw new ArgumentException($"Valor '{value}' inválido para a opção '{option.Key}'.");
return match.Value;
}
return option.DefaultValue;
}
public string Digits(int length)
{
Span<char> buffer = stackalloc char[length];
for (var i = 0; i < length; i++)
buffer[i] = (char)('0' + Random.Next(10));
return new string(buffer);
}
public T Pick<T>(IReadOnlyList<T> items) => items[Random.Next(items.Count)];
}

View File

@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
namespace GeradoresService.DAL;
public partial class Geradore
{
public int Id { get; set; }
public int? Tipo { get; set; }
public string? Valor { get; set; }
}

View File

@@ -1,34 +0,0 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
namespace GeradoresService.DAL;
public partial class GeradoresContext : DbContext
{
public GeradoresContext()
{
}
public GeradoresContext(DbContextOptions<GeradoresContext> options)
: base(options)
{
}
public virtual DbSet<Geradore> Geradores { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Geradore>(entity =>
{
entity.HasKey(e => e.Id).HasName("PK__Geradore__3214EC2710D5AD88");
entity.Property(e => e.Id).HasColumnName("ID");
entity.Property(e => e.Valor).HasMaxLength(50);
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

View File

@@ -0,0 +1,66 @@
namespace GeradoresService.Data;
/// <summary>Dados fictícios portugueses usados pelos geradores de pessoas, moradas e emails.</summary>
public static class PtData
{
public static readonly string[] MaleNames =
[
"João", "Francisco", "Santiago", "Afonso", "Tomás", "Duarte", "Rodrigo", "Martim", "Lourenço", "Gabriel",
"Miguel", "Pedro", "Tiago", "Diogo", "Rafael", "Gonçalo", "Guilherme", "Salvador", "Vicente", "Simão",
"António", "José", "Manuel", "Rui", "Paulo", "Ricardo", "Nuno", "Bruno", "André", "Luís",
];
public static readonly string[] FemaleNames =
[
"Maria", "Leonor", "Matilde", "Beatriz", "Carolina", "Mariana", "Ana", "Inês", "Sofia", "Margarida",
"Alice", "Benedita", "Francisca", "Constança", "Laura", "Clara", "Lara", "Joana", "Rita", "Catarina",
"Marta", "Sara", "Daniela", "Teresa", "Helena", "Isabel", "Patrícia", "Filipa", "Raquel", "Madalena",
];
public static readonly string[] Surnames =
[
"Silva", "Santos", "Ferreira", "Pereira", "Oliveira", "Costa", "Rodrigues", "Martins", "Jesus", "Sousa",
"Fernandes", "Gonçalves", "Gomes", "Lopes", "Marques", "Alves", "Almeida", "Ribeiro", "Pinto", "Carvalho",
"Teixeira", "Moreira", "Correia", "Mendes", "Nunes", "Soares", "Vieira", "Monteiro", "Cardoso", "Rocha",
"Raposo", "Neves", "Coelho", "Cruz", "Cunha", "Pires", "Ramos", "Reis", "Simões", "Antunes",
];
public static readonly string[] StreetTypes = ["Rua", "Avenida", "Travessa", "Largo", "Praceta", "Alameda", "Calçada"];
public static readonly string[] StreetNames =
[
"da Liberdade", "de Camões", "25 de Abril", "da República", "do Comércio", "das Flores", "dos Combatentes",
"Dom Afonso Henriques", "Almirante Reis", "da Boavista", "de Santa Catarina", "do Carmo", "da Estação",
"Gago Coutinho", "Infante D. Henrique", "Marquês de Pombal", "Miguel Bombarda", "da Igreja", "do Mercado",
"Bartolomeu Dias", "Vasco da Gama", "Fernando Pessoa", "Sá da Bandeira", "Cidade de Lyon", "dos Bombeiros",
];
/// <summary>Código postal (4 dígitos) e localidade de cada capital de distrito/região.</summary>
public static readonly (string Cp4, string Locality, string District)[] Localities =
[
("1000", "Lisboa", "Lisboa"),
("4000", "Porto", "Porto"),
("3000", "Coimbra", "Coimbra"),
("4700", "Braga", "Braga"),
("8000", "Faro", "Faro"),
("2400", "Leiria", "Leiria"),
("3800", "Aveiro", "Aveiro"),
("7000", "Évora", "Évora"),
("2900", "Setúbal", "Setúbal"),
("2000", "Santarém", "Santarém"),
("3500", "Viseu", "Viseu"),
("5000", "Vila Real", "Vila Real"),
("4900", "Viana do Castelo", "Viana do Castelo"),
("5300", "Bragança", "Bragança"),
("6300", "Guarda", "Guarda"),
("6000", "Castelo Branco", "Castelo Branco"),
("7300", "Portalegre", "Portalegre"),
("7800", "Beja", "Beja"),
("9000", "Funchal", "Madeira"),
("9500", "Ponta Delgada", "Açores"),
];
public static readonly string[] SafeEmailDomains = ["example.com", "example.org", "example.net"];
public static readonly string[] RealisticEmailDomains = ["gmail.com", "sapo.pt", "outlook.pt", "hotmail.com", "yahoo.com", "iol.pt"];
}

View File

@@ -0,0 +1,85 @@
using GeradoresService.Core;
using GeradoresService.Generators.Contactos;
using GeradoresService.Generators.Financeiro;
using GeradoresService.Generators.Identificacao;
using GeradoresService.Generators.Outros;
namespace GeradoresService;
public sealed record DetectionMatch(IGenerator Generator, ValidationResult Result);
public sealed class GeneratorRegistry
{
public const int MaxBatch = 1000;
public IReadOnlyList<IGenerator> All { get; }
public GeneratorRegistry() : this(
[
new NifGenerator(),
new NipcGenerator(),
new NissGenerator(),
new CartaoCidadaoGenerator(),
new PassaporteGenerator(),
new IbanGenerator(),
new NibGenerator(),
new CartaoCreditoGenerator(),
new ReferenciaMultibancoGenerator(),
new PessoaGenerator(),
new TelefoneGenerator(),
new CodigoPostalGenerator(),
new EmailGenerator(),
new MatriculaGenerator(),
new EanGenerator(),
new UuidGenerator(),
])
{ }
public GeneratorRegistry(IReadOnlyList<IGenerator> generators) => All = generators;
public IGenerator? Find(string id) =>
All.FirstOrDefault(g => string.Equals(g.Id, id, StringComparison.OrdinalIgnoreCase));
/// <summary>Gera <paramref name="count"/> valores. Um <paramref name="seed"/> torna o resultado reprodutível.</summary>
public IReadOnlyList<GeneratedItem> Generate(IGenerator generator, int count, IReadOnlyDictionary<string, string?> options, int? seed = null)
{
if (count is < 1 or > MaxBatch)
throw new ArgumentOutOfRangeException(nameof(count), $"A quantidade deve estar entre 1 e {MaxBatch}.");
var context = new GeneratorContext(seed is { } s ? new Random(s) : Random.Shared, options);
var items = new GeneratedItem[count];
for (var i = 0; i < count; i++)
items[i] = generator.Generate(context);
return items;
}
/// <summary>
/// Validador livre: tenta todos os tipos. Devolve primeiro os válidos, depois os que têm o formato
/// certo mas falham o controlo (ex.: "parece um NIF mas o dígito está errado").
/// </summary>
public IReadOnlyList<DetectionMatch> Detect(string value)
{
if (string.IsNullOrWhiteSpace(value))
return [];
return All
.Where(g => g.CanValidate)
.Select(g => new DetectionMatch(g, SafeValidate(g, value)))
.Where(m => m.Result.FormatMatch)
.OrderByDescending(m => m.Result.IsValid)
.ThenByDescending(m => m.Generator.HasChecksum)
.ToList();
}
private static ValidationResult SafeValidate(IGenerator generator, string value)
{
try
{
return generator.Validate(value);
}
catch (Exception ex) when (ex is FormatException or ArgumentException or OverflowException)
{
return ValidationResult.WrongFormat(ex.Message);
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Text.RegularExpressions;
using GeradoresService.Core;
using GeradoresService.Data;
namespace GeradoresService.Generators.Contactos;
/// <summary>Código postal português (NNNN-NNN). Apenas formato.</summary>
public sealed partial class CodigoPostalGenerator : GeneratorBase
{
public override string Id => "codigo-postal";
public override string Name => "Código postal";
public override string Category => Categories.Contactos;
public override string Description => "Código postal português (CP4-CP3) com localidade.";
public override bool HasChecksum => false;
public override GeneratedItem Generate(GeneratorContext context)
{
var (cp, locality, district) = Random(context);
return new GeneratedItem(cp, new Dictionary<string, string>
{
["Código postal"] = cp,
["Localidade"] = locality,
["Distrito"] = district,
});
}
public static (string Cp, string Locality, string District) Random(GeneratorContext context)
{
var l = context.Pick(PtData.Localities);
return ($"{l.Cp4}-{1 + context.Random.Next(999):D3}", l.Locality, l.District);
}
public override ValidationResult Validate(string value)
{
var m = Pattern().Match(value.Trim());
if (!m.Success)
return ValidationResult.WrongFormat("Formato esperado: NNNN-NNN (ex.: 1000-001).");
var cp = $"{m.Groups[1].Value}-{m.Groups[2].Value}";
return ValidationResult.Valid(cp, "Formato de código postal válido.");
}
[GeneratedRegex(@"^([1-9]\d{3})[-\s]?(\d{3})$")]
private static partial Regex Pattern();
}

View File

@@ -0,0 +1,56 @@
using System.Globalization;
using System.Net.Mail;
using System.Text;
using GeradoresService.Core;
using GeradoresService.Data;
namespace GeradoresService.Generators.Contactos;
public sealed class EmailGenerator : GeneratorBase
{
public override string Id => "email";
public override string Name => "Email";
public override string Category => Categories.Contactos;
public override string Description => "Endereço de email fictício. Por omissão usa domínios reservados para testes (example.com).";
public override bool HasChecksum => false;
public override IReadOnlyList<GeneratorOption> Options =>
[
Option("dominio", "Domínio", "teste",
("teste", "Reservado para testes (example.com)"),
("realista", "Realista (gmail.com, sapo.pt…)")),
];
public override GeneratedItem Generate(GeneratorContext context)
{
var first = context.Random.Next(2) == 0 ? context.Pick(PtData.MaleNames) : context.Pick(PtData.FemaleNames);
return new GeneratedItem(Build(first, context.Pick(PtData.Surnames), context, context.Option(Options[0]) == "realista"));
}
public static string Build(string firstName, string surname, GeneratorContext context, bool realistic = false)
{
var domains = realistic ? PtData.RealisticEmailDomains : PtData.SafeEmailDomains;
var separator = context.Pick([".", "_", ""]);
var suffix = context.Random.Next(3) == 0 ? context.Random.Next(1, 100).ToString() : "";
return $"{Slug(firstName)}{separator}{Slug(surname)}{suffix}@{context.Pick(domains)}";
}
private static string Slug(string s)
{
var sb = new StringBuilder();
foreach (var c in s.Normalize(NormalizationForm.FormD))
if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark && char.IsLetterOrDigit(c))
sb.Append(char.ToLowerInvariant(c));
return sb.ToString();
}
public override ValidationResult Validate(string value)
{
var v = value.Trim();
var at = v.LastIndexOf('@');
if (at <= 0 || at == v.Length - 1 || !v[(at + 1)..].Contains('.') || v.Any(char.IsWhiteSpace)
|| !MailAddress.TryCreate(v, out var address) || address.Address != v)
return ValidationResult.WrongFormat("Não é um endereço de email válido.");
return ValidationResult.Valid(v.ToLowerInvariant(), "Formato de email válido.");
}
}

View File

@@ -0,0 +1,69 @@
using GeradoresService.Core;
using GeradoresService.Data;
using GeradoresService.Generators.Financeiro;
using GeradoresService.Generators.Identificacao;
namespace GeradoresService.Generators.Contactos;
/// <summary>Pessoa fictícia completa, com todos os identificadores válidos.</summary>
public sealed class PessoaGenerator : GeneratorBase
{
public override string Id => "pessoa";
public override string Name => "Pessoa fictícia";
public override string Category => Categories.Contactos;
public override string Description => "Nome, data de nascimento, NIF, NISS, CC, contactos, morada e IBAN — tudo consistente e válido.";
public override bool CanValidate => false;
public override IReadOnlyList<GeneratorOption> Options =>
[
Option("genero", "Género", "qualquer", ("qualquer", "Qualquer"), ("f", "Feminino"), ("m", "Masculino")),
Option("idade", "Idade", "adulto", ("adulto", "Adulto (18–80)"), ("menor", "Menor (0–17)"), ("qualquer", "Qualquer (0–90)")),
];
public override ValidationResult Validate(string value) =>
ValidationResult.WrongFormat("Este gerador não tem validação.");
public override GeneratedItem Generate(GeneratorContext context)
{
var genero = context.Option(Options[0]);
if (genero == "qualquer")
genero = context.Random.Next(2) == 0 ? "f" : "m";
var names = genero == "f" ? PtData.FemaleNames : PtData.MaleNames;
var first = context.Pick(names);
var middle = context.Random.Next(3) == 0 ? " " + context.Pick(names.Where(n => n != first).ToArray()) : "";
var surname1 = context.Pick(PtData.Surnames);
var surname2 = context.Pick(PtData.Surnames.Where(s => s != surname1).ToArray());
var fullName = $"{first}{middle} {surname1} {surname2}";
var (minAge, maxAge) = context.Option(Options[1]) switch
{
"menor" => (0, 17),
"qualquer" => (0, 90),
_ => (18, 80),
};
var today = DateOnly.FromDateTime(DateTime.UtcNow);
var birth = today.AddYears(-maxAge - 1).AddDays(1 + context.Random.Next((maxAge - minAge + 1) * 365));
var (cp, locality, _) = CodigoPostalGenerator.Random(context);
var street = $"{context.Pick(PtData.StreetTypes)} {context.Pick(PtData.StreetNames)}, {1 + context.Random.Next(300)}";
var nibContext = new GeneratorContext(context.Random, new Dictionary<string, string?>());
var fields = new Dictionary<string, string>
{
["Nome"] = fullName,
["Género"] = genero == "f" ? "Feminino" : "Masculino",
["Data de nascimento"] = birth.ToString("yyyy-MM-dd"),
["NIF"] = NifGenerator.Build(context.Pick(["1", "2", "3"]), context),
["NISS"] = new NissGenerator().Generate(nibContext).Value,
["Cartão de Cidadão"] = new CartaoCidadaoGenerator().Generate(nibContext).Value,
["Telemóvel"] = new TelefoneGenerator().Generate(nibContext).Value,
["Email"] = EmailGenerator.Build(first, surname2, context),
["Morada"] = street,
["Código postal"] = cp,
["Localidade"] = locality,
["IBAN"] = new IbanGenerator().Generate(nibContext).Value,
};
return new GeneratedItem(fullName, fields);
}
}

View File

@@ -0,0 +1,67 @@
using GeradoresService.Core;
namespace GeradoresService.Generators.Contactos;
/// <summary>Números de telefone portugueses (Plano Nacional de Numeração). Apenas formato.</summary>
public sealed class TelefoneGenerator : GeneratorBase
{
private static readonly string[] MobilePrefixes = ["91", "92", "93", "96"];
public override string Id => "telefone";
public override string Name => "Telefone";
public override string Category => Categories.Contactos;
public override string Description => "Telemóvel ou telefone fixo português.";
public override bool HasChecksum => false;
public override IReadOnlyList<GeneratorOption> Options =>
[
Option("tipo", "Tipo", "movel", ("movel", "Telemóvel"), ("fixo", "Fixo"), ("qualquer", "Qualquer")),
Option("formato", "Formato", "nacional",
("nacional", "912 345 678"), ("internacional", "+351 912 345 678"), ("compacto", "912345678")),
];
public override GeneratedItem Generate(GeneratorContext context)
{
var tipo = context.Option(Options[0]);
if (tipo == "qualquer")
tipo = context.Random.Next(2) == 0 ? "movel" : "fixo";
var number = tipo == "movel"
? context.Pick(MobilePrefixes) + context.Digits(7)
: "2" + (1 + context.Random.Next(9)) + context.Digits(7);
return new GeneratedItem(Format(number, context.Option(Options[1])));
}
private static string Format(string n, string formato) => formato switch
{
"compacto" => n,
"internacional" => $"+351 {n[..3]} {n[3..6]} {n[6..]}",
_ => $"{n[..3]} {n[3..6]} {n[6..]}",
};
public override ValidationResult Validate(string value)
{
var n = Checksums.Clean(value).Replace("(", "").Replace(")", "");
if (n.StartsWith("+351")) n = n[4..];
else if (n.StartsWith("00351")) n = n[5..];
if (n.Length != 9 || !Checksums.AllDigits(n))
return ValidationResult.WrongFormat("Um número português tem 9 dígitos (opcionalmente com +351).");
var tipo = n switch
{
['9', '1' or '2' or '3' or '6', ..] => "telemóvel",
['2', ..] => "fixo",
['3', '0', ..] => "VoIP nómada",
['8', '0', '0', ..] => "número verde (gratuito)",
['8', '0', '8', ..] => "número azul (custo partilhado)",
['7', '0', '7', ..] or ['7', '6', '0', ..] => "número de valor acrescentado",
_ => null,
};
return tipo is null
? ValidationResult.WrongFormat("O prefixo não corresponde a um número português conhecido.")
: ValidationResult.Valid(Format(n, "internacional"), $"Número português válido ({tipo}).", [$"Tipo: {tipo}"]);
}
}

View File

@@ -0,0 +1,82 @@
using GeradoresService.Core;
namespace GeradoresService.Generators.Financeiro;
/// <summary>
/// Números de cartão de pagamento válidos pelo algoritmo de Luhn (apenas para testes).
/// </summary>
public sealed class CartaoCreditoGenerator : GeneratorBase
{
private sealed record Brand(string Key, string Name, int Length, Func<Random, string> Prefix, Func<string, bool> Matches);
private static readonly Brand[] Brands =
[
new("visa", "Visa", 16, _ => "4", n => n.StartsWith('4') && n.Length is 13 or 16 or 19),
new("mastercard", "Mastercard", 16,
r => r.Next(2) == 0 ? (51 + r.Next(5)).ToString() : (2221 + r.Next(500)).ToString(),
n => n.Length == 16 && (InRange(n, 2, 51, 55) || InRange(n, 4, 2221, 2720))),
new("amex", "American Express", 15, r => r.Next(2) == 0 ? "34" : "37",
n => n.Length == 15 && (n.StartsWith("34") || n.StartsWith("37"))),
];
public override string Id => "cartao-credito";
public override string Name => "Cartão de crédito";
public override string Category => Categories.Financeiro;
public override string Description => "Número de cartão válido pelo algoritmo de Luhn, com validade e CVV. Apenas para testes.";
public override IReadOnlyList<GeneratorOption> Options =>
[
Option("rede", "Rede", "visa", [.. Brands.Select(b => (b.Key, b.Name)), ("qualquer", "Qualquer")]),
Option("formato", "Formato", "espacos", ("espacos", "4111 1111 1111 1111"), ("compacto", "4111111111111111")),
];
public override GeneratedItem Generate(GeneratorContext context)
{
var key = context.Option(Options[0]);
var brand = key == "qualquer" ? context.Pick(Brands) : Brands.Single(b => b.Key == key);
var body = brand.Prefix(context.Random);
body += context.Digits(brand.Length - 1 - body.Length);
var number = body + Checksums.LuhnCheckDigit(body);
var expiry = DateTime.UtcNow.AddMonths(context.Random.Next(6, 60));
var cvv = context.Digits(brand.Key == "amex" ? 4 : 3);
var display = context.Option(Options[1]) == "compacto" ? number : Format(number);
return new GeneratedItem(display, new Dictionary<string, string>
{
["Número"] = display,
["Rede"] = brand.Name,
["Validade"] = expiry.ToString("MM/yy"),
["CVV"] = cvv,
});
}
private static string Format(string number) => number.Length == 15
? $"{number[..4]} {number[4..10]} {number[10..]}"
: string.Join(' ', number.Chunk(4).Select(c => new string(c)));
private static bool InRange(string n, int digits, int min, int max) =>
int.TryParse(n.AsSpan(0, digits), out var p) && p >= min && p <= max;
public override ValidationResult Validate(string value)
{
var number = Checksums.Clean(value);
if (number.Length is < 12 or > 19 || !Checksums.AllDigits(number))
return ValidationResult.WrongFormat("Um número de cartão tem entre 12 e 19 dígitos.");
var brand = Brands.FirstOrDefault(b => b.Matches(number));
var details = new List<string>
{
$"Rede: {brand?.Name ?? "desconhecida"}",
"Controlo: algoritmo de Luhn",
};
if (!Checksums.LuhnValid(number))
{
var expected = Checksums.LuhnCheckDigit(number[..^1]);
return ValidationResult.BadChecksum($"Falha no algoritmo de Luhn: último dígito devia ser {expected}.", details);
}
return ValidationResult.Valid(Format(number), $"Número de cartão válido{(brand is null ? "" : $" ({brand.Name})")}.", details);
}
}

View File

@@ -0,0 +1,75 @@
using GeradoresService.Core;
namespace GeradoresService.Generators.Financeiro;
/// <summary>
/// IBAN português: PT + 2 dígitos de controlo ISO 7064 (MOD 97-10) + NIB.
/// A validação aceita IBAN de qualquer país (verifica comprimento conhecido e MOD 97).
/// </summary>
public sealed class IbanGenerator : NibGenerator
{
private static readonly Dictionary<string, int> Lengths = new()
{
["PT"] = 25, ["ES"] = 24, ["FR"] = 27, ["DE"] = 22, ["IT"] = 27, ["GB"] = 22, ["IE"] = 22,
["NL"] = 18, ["BE"] = 16, ["LU"] = 20, ["CH"] = 21, ["AT"] = 20, ["DK"] = 18, ["FI"] = 18,
["SE"] = 24, ["NO"] = 15, ["PL"] = 28, ["CZ"] = 24, ["GR"] = 27, ["HR"] = 21, ["RO"] = 24,
["MT"] = 31, ["CY"] = 28, ["SK"] = 24, ["SI"] = 19, ["HU"] = 28, ["BG"] = 22, ["EE"] = 20,
["LT"] = 20, ["LV"] = 21, ["IS"] = 26, ["LI"] = 21, ["MC"] = 27, ["SM"] = 27, ["BR"] = 29,
};
public override string Id => "iban";
public override string Name => "IBAN";
public override string Description => "IBAN português (PT50 + NIB). Valida IBAN de qualquer país.";
public override IReadOnlyList<GeneratorOption> Options =>
[
.. base.Options,
Option("formato", "Formato", "espacos", ("espacos", "PT50 0000 0000 …"), ("compacto", "PT500000…")),
];
public override GeneratedItem Generate(GeneratorContext context)
{
var iban = FromBban("PT", BuildNib(context));
return new GeneratedItem(context.Option(Options[1]) == "compacto" ? iban : Format(iban));
}
public static string FromBban(string country, string bban)
{
var check = 98 - Checksums.Mod97(Checksums.LettersToDigits(bban + country) + "00");
return $"{country}{check:D2}{bban}";
}
public static string Format(string iban) =>
string.Join(' ', iban.Chunk(4).Select(c => new string(c)));
public override ValidationResult Validate(string value)
{
var iban = Checksums.Clean(value).ToUpperInvariant();
if (iban.Length < 15 || iban.Length > 34
|| !char.IsAsciiLetter(iban[0]) || !char.IsAsciiLetter(iban[1])
|| !char.IsAsciiDigit(iban[2]) || !char.IsAsciiDigit(iban[3])
|| !iban.All(char.IsAsciiLetterOrDigit))
return ValidationResult.WrongFormat("Um IBAN tem código de país (2 letras), 2 dígitos de controlo e o BBAN.");
var country = iban[..2];
if (Lengths.TryGetValue(country, out var expectedLength) && iban.Length != expectedLength)
return ValidationResult.WrongFormat($"Um IBAN {country} tem {expectedLength} caracteres (tem {iban.Length}).");
var rearranged = Checksums.LettersToDigits(iban[4..] + iban[..4]);
if (Checksums.Mod97(rearranged) != 1)
{
var expected = FromBban(country, iban[4..])[2..4];
return ValidationResult.BadChecksum(
$"Dígitos de controlo do IBAN inválidos: esperado {expected}, encontrado {iban[2..4]}.",
["Controlo: ISO 7064 MOD 97-10"]);
}
if (country == "PT")
{
var nibResult = ValidateNib(iban[4..], "IBAN");
return nibResult with { Normalized = nibResult.IsValid ? Format(iban) : null };
}
return ValidationResult.Valid(Format(iban), $"IBAN válido ({country}).", ["Controlo: ISO 7064 MOD 97-10"]);
}
}

View File

@@ -0,0 +1,75 @@
using GeradoresService.Core;
namespace GeradoresService.Generators.Financeiro;
/// <summary>
/// NIB: banco (4) + balcão (4) + conta (11) + controlo (2). Controlo = 98 - (NIB19 × 100 mod 97).
/// </summary>
public class NibGenerator : GeneratorBase
{
public static readonly IReadOnlyList<(string Code, string Name)> Banks =
[
("0007", "Novo Banco"),
("0010", "BPI"),
("0018", "Santander Totta"),
("0023", "Banco ActivoBank"),
("0033", "Millennium BCP"),
("0035", "Caixa Geral de Depósitos"),
("0036", "Banco Montepio"),
("0045", "Crédito Agrícola"),
("0079", "Banco BIC"),
("0193", "Banco CTT"),
("0269", "Bankinter"),
];
public override string Id => "nib";
public override string Name => "NIB";
public override string Category => Categories.Financeiro;
public override string Description => "Número de Identificação Bancária (21 dígitos).";
public override IReadOnlyList<GeneratorOption> Options =>
[
Option("banco", "Banco", "qualquer",
[("qualquer", "Qualquer"), .. Banks.Select(b => (b.Code, $"{b.Code} — {b.Name}"))]),
];
public override GeneratedItem Generate(GeneratorContext context) => new(BuildNib(context));
protected string BuildNib(GeneratorContext context)
{
var bank = context.Option(Options[0]);
if (bank == "qualquer")
bank = context.Pick(Banks).Code;
var body = bank + context.Digits(4) + context.Digits(11);
return body + NibCheck(body);
}
public static string NibCheck(string first19) =>
(98 - Checksums.Mod97(first19 + "00")).ToString("D2");
public static string? BankName(string nib) =>
Banks.FirstOrDefault(b => nib.StartsWith(b.Code)).Name;
public override ValidationResult Validate(string value)
{
var nib = Checksums.Clean(value);
if (nib.Length != 21 || !Checksums.AllDigits(nib))
return ValidationResult.WrongFormat("Um NIB tem 21 dígitos.");
return ValidateNib(nib, "NIB");
}
public static ValidationResult ValidateNib(string nib, string label)
{
var expected = NibCheck(nib[..19]);
var details = new List<string>
{
$"Banco: {nib[..4]}{(BankName(nib) is { } n ? $" ({n})" : "")}",
$"Balcão: {nib[4..8]}",
$"Conta: {nib[8..19]}",
$"Dígitos de controlo esperados: {expected} (módulo 97)",
};
return nib[19..] == expected
? ValidationResult.Valid(nib, $"{label} válido.", details)
: ValidationResult.BadChecksum($"Dígitos de controlo do NIB inválidos: esperado {expected}, encontrado {nib[19..]}.", details);
}
}

View File

@@ -0,0 +1,75 @@
using System.Globalization;
using System.Text.RegularExpressions;
using GeradoresService.Core;
namespace GeradoresService.Generators.Financeiro;
/// <summary>
/// Referência Multibanco: entidade (5) + referência (9) + montante.
/// Os 2 últimos dígitos da referência são controlo: 98 - (Σ dígito × peso mod 97),
/// calculado sobre entidade(5) + referência(7) + montante em cêntimos(8).
/// </summary>
public sealed partial class ReferenciaMultibancoGenerator : GeneratorBase
{
private static readonly int[] Weights = [51, 73, 17, 89, 38, 62, 45, 53, 15, 50, 5, 49, 34, 81, 76, 27, 90, 9, 30, 3];
public override string Id => "multibanco";
public override string Name => "Referência Multibanco";
public override string Category => Categories.Financeiro;
public override string Description => "Entidade, referência e montante com dígitos de controlo válidos.";
public override GeneratedItem Generate(GeneratorContext context)
{
var entity = (10000 + context.Random.Next(90000)).ToString();
var cents = 100 + context.Random.Next(50000);
var reference = context.Digits(7);
reference += CheckDigits(entity, reference, cents);
var amount = (cents / 100m).ToString("0.00", CultureInfo.InvariantCulture);
return new GeneratedItem($"{entity} {FormatReference(reference)} {amount}", new Dictionary<string, string>
{
["Entidade"] = entity,
["Referência"] = FormatReference(reference),
["Montante"] = $"{amount} €",
});
}
public static string CheckDigits(string entity, string reference7, long cents)
{
var chk = entity + reference7 + cents.ToString("D8");
var sum = 0;
for (var i = 0; i < 20; i++)
sum += (chk[i] - '0') * Weights[i];
return (98 - sum % 97).ToString("D2");
}
private static string FormatReference(string r) => $"{r[..3]} {r[3..6]} {r[6..]}";
public override ValidationResult Validate(string value)
{
var m = Pattern().Match(value.Trim());
if (!m.Success)
return ValidationResult.WrongFormat("Formato esperado: entidade (5 dígitos), referência (9 dígitos) e montante, ex.: 12345 123 456 789 10.00");
var entity = m.Groups["ent"].Value;
var reference = Checksums.Clean(m.Groups["ref"].Value);
var amount = decimal.Parse(m.Groups["val"].Value.Replace(',', '.'), CultureInfo.InvariantCulture);
var cents = (long)Math.Round(amount * 100);
if (cents > 99_999_999)
return ValidationResult.WrongFormat("Montante demasiado elevado.");
var expected = CheckDigits(entity, reference[..7], cents);
var details = new List<string>
{
$"Entidade: {entity}",
$"Montante: {amount.ToString("0.00", CultureInfo.InvariantCulture)} €",
$"Dígitos de controlo esperados: {expected}",
};
return reference[7..] == expected
? ValidationResult.Valid($"{entity} {FormatReference(reference)} {amount.ToString("0.00", CultureInfo.InvariantCulture)}", "Referência Multibanco válida.", details)
: ValidationResult.BadChecksum($"Dígitos de controlo inválidos: esperado {expected}, encontrado {reference[7..]}.", details);
}
[GeneratedRegex(@"^(?<ent>\d{5})[\s|;]+(?<ref>\d{3}\s?\d{3}\s?\d{3})[\s|;]+(?<val>\d{1,6}(?:[.,]\d{1,2})?)\s*€?$")]
private static partial Regex Pattern();
}

View File

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

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

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

View File

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

View File

@@ -0,0 +1,53 @@
using GeradoresService.Core;
namespace GeradoresService.Generators.Outros;
/// <summary>Códigos de barras EAN-13 / EAN-8 (GS1). Controlo: pesos 3,1,3,1… a partir da direita.</summary>
public sealed class EanGenerator : GeneratorBase
{
public override string Id => "ean";
public override string Name => "EAN / Código de barras";
public override string Category => Categories.Outros;
public override string Description => "Códigos EAN-13 e EAN-8 com dígito de controlo GS1.";
public override IReadOnlyList<GeneratorOption> Options =>
[
Option("tipo", "Tipo", "ean13", ("ean13", "EAN-13"), ("ean8", "EAN-8")),
Option("prefixo", "Prefixo GS1", "560", ("560", "560 — Portugal"), ("qualquer", "Qualquer")),
];
public override GeneratedItem Generate(GeneratorContext context)
{
var length = context.Option(Options[0]) == "ean8" ? 8 : 13;
var prefix = context.Option(Options[1]) == "560" ? "560" : context.Digits(3);
var body = prefix + context.Digits(length - 1 - prefix.Length);
return new GeneratedItem(body + CheckDigit(body));
}
public static int CheckDigit(string payload)
{
var sum = 0;
for (var i = 0; i < payload.Length; i++)
{
var weight = (payload.Length - i) % 2 == 1 ? 3 : 1;
sum += (payload[i] - '0') * weight;
}
return (10 - sum % 10) % 10;
}
public override ValidationResult Validate(string value)
{
var ean = Checksums.Clean(value);
if (ean.Length is not (8 or 13) || !Checksums.AllDigits(ean))
return ValidationResult.WrongFormat("Um EAN tem 8 ou 13 dígitos.");
var expected = CheckDigit(ean[..^1]);
var details = new List<string> { $"Tipo: EAN-{ean.Length}" };
if (ean.Length == 13 && ean.StartsWith("560"))
details.Add("Prefixo 560: Portugal");
return expected == ean[^1] - '0'
? ValidationResult.Valid(ean, $"EAN-{ean.Length} válido.", details)
: ValidationResult.BadChecksum($"Dígito de controlo inválido: esperado {expected}, encontrado {ean[^1]}.", details);
}
}

View File

@@ -0,0 +1,64 @@
using System.Text.RegularExpressions;
using GeradoresService.Core;
namespace GeradoresService.Generators.Outros;
/// <summary>Matrículas portuguesas em todos os formatos desde 1937. Apenas formato.</summary>
public sealed partial class MatriculaGenerator : GeneratorBase
{
// K, W e Y não eram usadas nas matrículas portuguesas.
private const string Letters = "ABCDEFGHIJLMNOPQRSTUVXZ";
private sealed record Format(string Key, string Label, string Mask, string Period);
private static readonly Format[] Formats =
[
new("2020", "AA-00-AA (desde 2020)", "LL-DD-LL", "2020 – actualidade"),
new("2005", "00-AA-00 (2005–2020)", "DD-LL-DD", "2005 – 2020"),
new("1992", "00-00-AA (1992–2005)", "DD-DD-LL", "1992 – 2005"),
new("1937", "AA-00-00 (1937–1992)", "LL-DD-DD", "1937 – 1992"),
];
public override string Id => "matricula";
public override string Name => "Matrícula";
public override string Category => Categories.Outros;
public override string Description => "Matrícula automóvel portuguesa em qualquer um dos formatos.";
public override bool HasChecksum => false;
public override IReadOnlyList<GeneratorOption> Options =>
[
Option("formato", "Formato", "2020", [.. Formats.Select(f => (f.Key, f.Label)), ("qualquer", "Qualquer")]),
];
public override GeneratedItem Generate(GeneratorContext context)
{
var key = context.Option(Options[0]);
var format = key == "qualquer" ? context.Pick(Formats) : Formats.Single(f => f.Key == key);
var plate = string.Concat(format.Mask.Select(c => c switch
{
'L' => Letters[context.Random.Next(Letters.Length)],
'D' => (char)('0' + context.Random.Next(10)),
_ => c,
}));
return new GeneratedItem(plate);
}
public override ValidationResult Validate(string value)
{
var plate = Checksums.Clean(value).ToUpperInvariant();
if (plate.Length != 6 || !plate.All(char.IsAsciiLetterOrDigit))
return ValidationResult.WrongFormat("Uma matrícula tem 6 caracteres em 3 pares (ex.: AA-00-AA).");
var mask = string.Concat(plate.Chunk(2).Select(p => p.All(char.IsAsciiDigit) ? "DD" : p.All(char.IsAsciiLetter) ? "LL" : "??"));
var masked = $"{mask[..2]}-{mask[2..4]}-{mask[4..]}";
var format = Formats.FirstOrDefault(f => f.Mask == masked);
if (format is null)
return ValidationResult.WrongFormat("Não corresponde a nenhum formato de matrícula portuguesa.");
var formatted = $"{plate[..2]}-{plate[2..4]}-{plate[4..]}";
if (plate.Any(c => c is 'K' or 'W' or 'Y'))
return ValidationResult.BadChecksum("As letras K, W e Y não são usadas em matrículas portuguesas.");
return ValidationResult.Valid(formatted, $"Matrícula válida (formato {format.Period}).", [$"Formato: {format.Label}"]);
}
}

View File

@@ -0,0 +1,50 @@
using GeradoresService.Core;
namespace GeradoresService.Generators.Outros;
public sealed class UuidGenerator : GeneratorBase
{
public override string Id => "uuid";
public override string Name => "UUID / GUID";
public override string Category => Categories.Outros;
public override string Description => "Identificadores únicos UUID v4 (aleatório) ou v7 (ordenado por tempo).";
public override bool HasChecksum => false;
public override IReadOnlyList<GeneratorOption> Options =>
[
Option("versao", "Versão", "v4", ("v4", "v4 — aleatório"), ("v7", "v7 — ordenado por tempo")),
Option("formato", "Formato", "minusculas",
("minusculas", "minúsculas com hífens"), ("maiusculas", "MAIÚSCULAS com hífens"), ("compacto", "sem hífens")),
];
public override GeneratedItem Generate(GeneratorContext context)
{
var guid = context.Option(Options[0]) == "v7" ? Guid.CreateVersion7() : NewV4(context.Random);
var text = context.Option(Options[1]) switch
{
"maiusculas" => guid.ToString("D").ToUpperInvariant(),
"compacto" => guid.ToString("N"),
_ => guid.ToString("D"),
};
return new GeneratedItem(text);
}
/// <summary>UUID v4 a partir de <paramref name="random"/> (permite resultados reprodutíveis com seed).</summary>
private static Guid NewV4(Random random)
{
Span<byte> bytes = stackalloc byte[16];
random.NextBytes(bytes);
// Versão 4 (nibble alto do byte 7 no layout do System.Guid) e variante RFC 9562.
bytes[7] = (byte)((bytes[7] & 0x0F) | 0x40);
bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80);
return new Guid(bytes);
}
public override ValidationResult Validate(string value)
{
var v = value.Trim().Trim('{', '}');
if (v.Length is not (32 or 36) || !Guid.TryParse(v, out var guid))
return ValidationResult.WrongFormat("Não é um UUID válido.");
return ValidationResult.Valid(guid.ToString("D"), $"UUID válido (versão {guid.Version}).", [$"Versão: {guid.Version}", $"Variante: {guid.Variant}"]);
}
}

View File

@@ -1,21 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="DAL\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
</ItemGroup>
</Project>

View File

@@ -1,97 +0,0 @@
using GeradoresService.DAL;
using System;
using System.ComponentModel;
using System.Linq;
using System.Text.Json.Serialization;
namespace GeradoresService
{
public class NIF
{
private readonly GeradoresContext _geradoresContext;
public NIF(GeradoresContext geradoresContext)
{
_geradoresContext = geradoresContext;
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum NIFType
{
[Description("Pessoa singular (1)")]
PessoaSingular1 = 1,
[Description("Pessoa singular (2)")]
PessoaSingular2 = 2,
[Description("Pessoa singular (3; novo em 2019)")]
PessoaSingular3 = 3,
[Description("Pessoa colectiva (5)")]
PessoaColectiva = 5,
[Description("Pessoa colectiva pública (6)")]
PessoaColectivaPublica = 6,
[Description("Empresário em nome individual (8)")]
EmpresarioIndividual = 8,
[Description("Pessoa colectiva irregular ou número provisório (9)")]
PessoaColectivaIrregular = 9
}
public string Generate(NIFType? type)
{
var nif = GenerateRandomNIF(type);
return nif;
}
public string GenerateRandomNIF(NIFType? nifType)
{
char firstDigit;
Random rnd = new Random();
if (nifType.HasValue)
{
firstDigit = ((int)nifType.Value).ToString()[0];
}
else
{
var validFirstDigits = Enum.GetValues(typeof(NIFType)).Cast<int>().Select(v => v.ToString()[0]).ToArray();
firstDigit = validFirstDigits[rnd.Next(validFirstDigits.Length)];
}
string nextDigits = string.Concat(Enumerable.Range(0, 7).Select(_ => rnd.Next(0, 10).ToString()));
int checkDigit = (firstDigit - '0') * 9;
for (int i = 0; i < 7; i++)
{
checkDigit += (nextDigits[i] - '0') * (8 - i);
}
checkDigit = 11 - (checkDigit % 11);
if (checkDigit >= 10)
checkDigit = 0;
return firstDigit + nextDigits + checkDigit;
}
public bool Validate(string nif)
{
if (nif.Length != 9 || !nif.All(char.IsDigit))
return false;
int[] digitos = nif.Select(c => c - '0').ToArray();
int soma = digitos.Take(8).Select((num, i) => num * (9 - i)).Sum();
int digitoControlo = (11 - (soma % 11)) % 10;
return digitoControlo == digitos[8];
}
public void SaveNIF(string NIF)
{
var ger = new Geradore() { Valor = NIF };
_geradoresContext.Geradores.Add(ger);
_geradoresContext.SaveChanges();
}
}
}

View File

@@ -1,90 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GeradoresService
{
public class NISS
{
private static readonly Random random = new Random();
public static string Generate() {
return GenerateNISS();
}
private static string GenerateRandomDigits(int length)
{
string digits = "1";
for (int i = 0; i < length; i++)
{
digits += random.Next(0, 10); // Gera um dígito aleatório entre 0 e 9
}
return digits;
}
public static string GenerateNISS()
{
// Gera 9 dígitos aleatórios
string niss = GenerateRandomDigits(9);
// Calcula o dígito de controlo
int soma = 0;
for (int i = 0; i < 10; i++)
{
int digito = int.Parse(niss[i].ToString());
soma += digito * (i + 1);
}
int resto = soma % 11;
int digitoControlo = 11 - resto;
// Verifica se o dígito de controlo está no intervalo [0, 9]
if (digitoControlo == 10)
{
niss += "0";
}
else if (digitoControlo == 11)
{
niss += "1";
}
else
{
niss += digitoControlo.ToString();
}
return niss;
}
public static bool Validate(string niss)
{
// Verifica se o NISS tem 11 dígitos
if (niss.Length != 11)
return false;
// Verifica se é um número
foreach (char c in niss)
{
if (!char.IsDigit(c))
return false;
}
// Calcula o dígito de controlo
int soma = 0;
for (int i = 0; i < 10; i++)
{
int digito = int.Parse(niss[i].ToString());
soma += digito * (i + 1);
}
int resto = soma % 11;
int digitoControlo = 11 - resto;
// Verifica o último dígito do NISS
int ultimoDigito = int.Parse(niss[10].ToString());
if (digitoControlo == 10 && ultimoDigito != 0)
return false;
if (digitoControlo == 11 && ultimoDigito != 0 && ultimoDigito != 1)
return false;
if (digitoControlo != ultimoDigito)
return false;
return true;
}
}
}

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
public static class EnumHelper
{
public static List<(T Value, string Description)> GetEnumValuesAndDescriptions<T>() where T : Enum
{
return typeof(T).GetFields()
.Select(field => (
Value: (T)field.GetValue(null),
Description: field.GetCustomAttribute<DescriptionAttribute>()?.Description ?? field.Name
))
.ToList();
}
}