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