- 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>
83 lines
3.6 KiB
C#
83 lines
3.6 KiB
C#
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);
|
|
}
|
|
}
|