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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
71
GeradoresService/Core/Checksums.cs
Normal file
71
GeradoresService/Core/Checksums.cs
Normal 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);
|
||||
}
|
||||
18
GeradoresService/Core/GeneratorBase.cs
Normal file
18
GeradoresService/Core/GeneratorBase.cs
Normal 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);
|
||||
}
|
||||
39
GeradoresService/Core/IGenerator.cs
Normal file
39
GeradoresService/Core/IGenerator.cs
Normal 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";
|
||||
}
|
||||
72
GeradoresService/Core/Models.cs
Normal file
72
GeradoresService/Core/Models.cs
Normal 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)];
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
66
GeradoresService/Data/PtData.cs
Normal file
66
GeradoresService/Data/PtData.cs
Normal 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"];
|
||||
}
|
||||
85
GeradoresService/GeneratorRegistry.cs
Normal file
85
GeradoresService/GeneratorRegistry.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
56
GeradoresService/Generators/Contactos/EmailGenerator.cs
Normal file
56
GeradoresService/Generators/Contactos/EmailGenerator.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
69
GeradoresService/Generators/Contactos/PessoaGenerator.cs
Normal file
69
GeradoresService/Generators/Contactos/PessoaGenerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
67
GeradoresService/Generators/Contactos/TelefoneGenerator.cs
Normal file
67
GeradoresService/Generators/Contactos/TelefoneGenerator.cs
Normal 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}"]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
75
GeradoresService/Generators/Financeiro/IbanGenerator.cs
Normal file
75
GeradoresService/Generators/Financeiro/IbanGenerator.cs
Normal 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"]);
|
||||
}
|
||||
}
|
||||
75
GeradoresService/Generators/Financeiro/NibGenerator.cs
Normal file
75
GeradoresService/Generators/Financeiro/NibGenerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
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")];
|
||||
}
|
||||
57
GeradoresService/Generators/Identificacao/NissGenerator.cs
Normal file
57
GeradoresService/Generators/Identificacao/NissGenerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
53
GeradoresService/Generators/Outros/EanGenerator.cs
Normal file
53
GeradoresService/Generators/Outros/EanGenerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
64
GeradoresService/Generators/Outros/MatriculaGenerator.cs
Normal file
64
GeradoresService/Generators/Outros/MatriculaGenerator.cs
Normal 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}"]);
|
||||
}
|
||||
}
|
||||
50
GeradoresService/Generators/Outros/UuidGenerator.cs
Normal file
50
GeradoresService/Generators/Outros/UuidGenerator.cs
Normal 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}"]);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user