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

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