using GeradoresService.Core;
namespace GeradoresService.Generators.Financeiro;
///
/// Números de cartão de pagamento válidos pelo algoritmo de Luhn (apenas para testes).
///
public sealed class CartaoCreditoGenerator : GeneratorBase
{
private sealed record Brand(string Key, string Name, int Length, Func Prefix, Func 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 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
{
["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
{
$"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);
}
}