From 72bbbeea4a7e6e1bfc47cf2b1f44a536f94bb184 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Wed, 28 Jan 2026 10:54:15 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20valida=C3=A7=C3=B5es=20de=20tipo,=20rece?= =?UTF-8?q?ber=20na=20conta=20da=20empresa,=20og=20image=20e=20idioma.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controllers/PagamentoController.cs | 4 +- Program.cs | 1 - Views/Shared/_Layout.cshtml | 2 +- .../images/pix-og.png | Bin wwwroot/js/pix-generator.js | 188 +++++++++++++++--- 5 files changed, 163 insertions(+), 32 deletions(-) rename AoSelecionarPix.png => wwwroot/images/pix-og.png (100%) diff --git a/Controllers/PagamentoController.cs b/Controllers/PagamentoController.cs index 49220d1..13a4fa0 100644 --- a/Controllers/PagamentoController.cs +++ b/Controllers/PagamentoController.cs @@ -21,8 +21,8 @@ namespace QRRapidoApp.Controllers private readonly MongoDbContext _context; private readonly AdDisplayService _adDisplayService; private readonly StripeService _stripeService; // Injected StripeService - private readonly string _pixKey = "chave-pix-padrao@qrrapido.site"; - private readonly string _merchantName = "QR Rapido"; + private readonly string _pixKey = "12048391000101"; + private readonly string _merchantName = "RRCG Gerenciamento"; private readonly string _merchantCity = "SAO PAULO"; public PagamentoController( diff --git a/Program.cs b/Program.cs index 6f12c37..6374085 100644 --- a/Program.cs +++ b/Program.cs @@ -237,7 +237,6 @@ builder.Services.Configure(options => options.RequestCultureProviders.Clear(); options.RequestCultureProviders.Add(new CustomRouteDataRequestCultureProvider()); options.RequestCultureProviders.Add(new QueryStringRequestCultureProvider()); - options.RequestCultureProviders.Add(new CookieRequestCultureProvider()); }); // Custom Services diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index b77d644..591a12a 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -91,7 +91,7 @@ - + diff --git a/AoSelecionarPix.png b/wwwroot/images/pix-og.png similarity index 100% rename from AoSelecionarPix.png rename to wwwroot/images/pix-og.png diff --git a/wwwroot/js/pix-generator.js b/wwwroot/js/pix-generator.js index 99f8f5f..340a699 100644 --- a/wwwroot/js/pix-generator.js +++ b/wwwroot/js/pix-generator.js @@ -44,6 +44,15 @@ class PixQRGenerator { const previewElement = document.getElementById('pix-preview-text'); if (previewElement) { + // Validação de "Preguiça" (Laziness Validation) + // Checks if user selected one type but entered another + const validationWarning = this.validateKeyType(data.keyType, data.key); + + if (validationWarning) { + previewElement.innerHTML = ` ${validationWarning}`; + return; + } + if (!data.key || !data.name || !data.city) { previewElement.textContent = "Preencha os campos obrigatórios (Chave, Nome, Cidade) para ver o preview."; return; @@ -58,6 +67,157 @@ class PixQRGenerator { } } + validatePixData() { + const data = this.collectPixData(); + const errors = []; + + // Required fields + if (!data.key.trim()) errors.push('Chave PIX é obrigatória'); + if (!data.name.trim()) errors.push('Nome do beneficiário é obrigatório'); + if (!data.city.trim()) errors.push('Cidade do beneficiário é obrigatória'); + + // Length limits + if (data.name.length > 25) errors.push('Nome deve ter no máximo 25 caracteres'); + if (data.city.length > 15) errors.push('Cidade deve ter no máximo 15 caracteres (Regra do Banco Central)'); + + // Amount validation + if (data.amount) { + const amount = parseFloat(data.amount.replace(/\./g, '').replace(',', '.')); + if (isNaN(amount) || amount <= 0) { + errors.push('Valor deve ser um número maior que zero'); + } + } + + // Smart Key Validation + const keyWarning = this.validateKeyType(data.keyType, data.key); + if (keyWarning) { + errors.push(keyWarning); + } + + return errors; + } + + validateKeyType(type, key) { + if (!key) return null; + const cleanKey = key.replace(/\D/g, ''); // Numbers only + + // Case 1: Selected CPF/CNPJ + if (type === 'cpf') { + if (cleanKey.length === 11) { + if (!this.isValidCPF(cleanKey)) { + // Check if looks like phone (DDD + 9...) + if (cleanKey[2] === '9') { + return "Selecionado CPF, mas parece um Celular. Altere o tipo para 'Celular' ou corrija."; + } + return "CPF Inválido. Verifique os dígitos."; + } + } else if (cleanKey.length === 14) { + if (!this.isValidCNPJ(cleanKey)) { + return "CNPJ Inválido. Verifique os dígitos."; + } + } else { + if (cleanKey.length > 0) return `CPF/CNPJ deve ter 11 ou 14 números. (Atual: ${cleanKey.length})`; + } + } + + // Case 2: Selected Phone + if (type === 'phone') { + if (cleanKey.length > 0) { + if (cleanKey.length !== 10 && cleanKey.length !== 11) { + return `Telefone deve ter 10 ou 11 números (DDD + Número). (Atual: ${cleanKey.length})`; + } + + // Smart check: Typed a CPF in Phone field? + if (cleanKey.length === 11 && this.isValidCPF(cleanKey)) { + // Check if it's NOT a typical mobile number structure + // Mobile: DDD (11-99) + 9 + 8 digits. + const ddd = parseInt(cleanKey.substring(0, 2)); + const ninthDigit = cleanKey[2]; + + // If DDD is invalid or 9th digit is not 9, but it IS a valid CPF -> Warning + if (ddd < 11 || ddd > 99 || ninthDigit !== '9') { + return "Isso parece ser um CPF válido, mas o tipo selecionado é 'Celular'."; + } + } + } + } + + // Case 3: Email + if (type === 'email') { + if (key.length > 0 && !key.includes('@')) { + return "Email inválido. Deve conter '@'."; + } + } + + return null; + } + + isValidCPF(cpf) { + if (typeof cpf !== "string") return false; + cpf = cpf.replace(/[\s.-]*/igm, ''); + if (cpf.length !== 11 || !Array.from(cpf).filter(e => e !== cpf[0]).length) { + return false; + } + var sum = 0; + var remainder; + for (var i = 1; i <= 9; i++) + sum = sum + parseInt(cpf.substring(i-1, i)) * (11 - i); + remainder = (sum * 10) % 11; + if ((remainder == 10) || (remainder == 11)) remainder = 0; + if (remainder != parseInt(cpf.substring(9, 10)) ) return false; + sum = 0; + for (i = 1; i <= 10; i++) + sum = sum + parseInt(cpf.substring(i-1, i)) * (12 - i); + remainder = (sum * 10) % 11; + if ((remainder == 10) || (remainder == 11)) remainder = 0; + if (remainder != parseInt(cpf.substring(10, 11)) ) return false; + return true; + } + + isValidCNPJ(cnpj) { + if (!cnpj) return false; + cnpj = cnpj.replace(/[^\d]+/g,''); + if (cnpj.length != 14) return false; + // Elimina CNPJs invalidos conhecidos + if (cnpj == "00000000000000" || + cnpj == "11111111111111" || + cnpj == "22222222222222" || + cnpj == "33333333333333" || + cnpj == "44444444444444" || + cnpj == "55555555555555" || + cnpj == "66666666666666" || + cnpj == "77777777777777" || + cnpj == "88888888888888" || + cnpj == "99999999999999") + return false; + + // Valida DVs + let tamanho = cnpj.length - 2 + let numeros = cnpj.substring(0,tamanho); + let digitos = cnpj.substring(tamanho); + let soma = 0; + let pos = tamanho - 7; + for (let i = tamanho; i >= 1; i--) { + soma += numeros.charAt(tamanho - i) * pos--; + if (pos < 2) pos = 9; + } + let resultado = soma % 11 < 2 ? 0 : 11 - soma % 11; + if (resultado != digitos.charAt(0)) return false; + + tamanho = tamanho + 1; + numeros = cnpj.substring(0,tamanho); + soma = 0; + pos = tamanho - 7; + for (let i = tamanho; i >= 1; i--) { + soma += numeros.charAt(tamanho - i) * pos--; + if (pos < 2) pos = 9; + } + resultado = soma % 11 < 2 ? 0 : 11 - soma % 11; + if (resultado != digitos.charAt(1)) return false; + + return true; + } + collectPixData() { return { keyType: document.getElementById('pix-key-type')?.value || 'cpf', @@ -70,28 +230,6 @@ class PixQRGenerator { }; } - validatePixData() { - const data = this.collectPixData(); - const errors = []; - - if (!data.key.trim()) errors.push('Chave PIX é obrigatória'); - if (!data.name.trim()) errors.push('Nome do beneficiário é obrigatório'); - if (!data.city.trim()) errors.push('Cidade do beneficiário é obrigatória'); - - if (data.name.length > 25) errors.push('Nome deve ter no máximo 25 caracteres'); - if (data.city.length > 15) errors.push('Cidade deve ter no máximo 15 caracteres (Regra do Banco Central)'); - - if (data.amount) { - // Remove thousands separators (.) and replace decimal comma (,) with dot (.) - const amount = parseFloat(data.amount.replace(/\./g, '').replace(',', '.')); - if (isNaN(amount) || amount <= 0) { - errors.push('Valor deve ser um número maior que zero'); - } - } - - return errors; - } - generateCRC16(payload) { let crc = 0xFFFF; const polynomial = 0x1021; @@ -124,7 +262,6 @@ class PixQRGenerator { case 'phone': // Remove non-digits let nums = cleanKey.replace(/\D/g, ''); - // If it starts with 55 and is long enough, keep it // If 10 or 11 digits (DDD+Number), add +55 if ((nums.length === 10 || nums.length === 11) && !nums.startsWith('55')) { return `+55${nums}`; @@ -170,11 +307,6 @@ class PixQRGenerator { const gui = this.formatField('00', 'br.gov.bcb.pix'); const keyField = this.formatField('01', key); - // Field 26: Merchant Account Information - // Subfields: 00 (GUI), 01 (Key), 02 (Description - optional/standard varies) - // Standard EMVCo for PIX puts Description in field 02 of ID 26? - // Actually, BR Code standard puts "InfoAdicional" in Field 26, ID 02. - let merchantAccountInfo = gui + keyField; if (description) { merchantAccountInfo += this.formatField('02', description);