UNPKG

thunderpix

Version:

Biblioteca javascript de padronização de gateways de pagamentos PIX

376 lines (333 loc) 12.6 kB
import qrcode from 'qrcode'; import { cpf, cnpj } from 'cpf-cnpj-validator'; import ProviderInterface from '../../interfaces/ProviderInterface'; import pix from '../../utils/Bacem/pix'; import { randomUUID } from '../../utils/all/index'; interface ProviderConstruct { pixkey: string; } interface pixTypeOutput { key: string; type: string; } interface pixGenerateCopyAndPastOutput { qrcode: string; metadata: string; } interface PixPayloadOutput { format: string; // Indicador do formato do payload method?: string; // Método de iniciação (se presente) chave: string; // Chave Pix do recebedor valor?: string; // Valor da transação, se especificado moeda: string; // Código da moeda pais: string; // Código do país nomeRecebedor: string; // Nome do recebedor cidadeRecebedor: string; // Cidade do recebedor cep?: string; // Código postal, se especificado crc: string; // Código CRC16 para validação additionalInfo?: string; // Informações adicionais, se presentes } export default class PixProvider implements ProviderInterface { private pixkey: string; public providerInfo = { name: 'Pix', description: 'Provedor padrão de qrcode-pix', documentation: 'https://bacen.github.io/pix-api', isOnline: true, vendor: { name: 'Banco Central do Brasil', shotname: 'bacem', url: 'https://www.bcb.gov.br', api: 'https://pix.bcb.gov.br/api', versions: [ { name: 'br.gov.bacem-pix-api-v1', version: '1.0.0', path: '/v1', }, { name: 'br.gov.bacem-pix-api-v1', version: '2.0.0', path: '/v2', }, ], }, }; public constructor(configs: ProviderConstruct) { this.pixkey = configs.pixkey; } // Função que gera o payload do Pix (copia e cola) public generatePixPayload( valor: number, chave: string | null = null, descricao: string | null = null, nomeRecebedor: string | null = null, cidadeRecebedor: string | null = null, ): string { // Se não houver chave passada, usa a chave padrão da classe if (!chave) { chave = this.pixkey; } // Se o valor não for inteiro, converte para centavos if (!Number.isInteger(valor)) { valor = Math.round(valor * 100); // Converte para centavos, arredondando } // Definindo os valores padrão, se não forem passados if (!nomeRecebedor) { nomeRecebedor = 'Recebedor'; } if (!descricao) { descricao = 'Pague antes do vencimento'; } if (!cidadeRecebedor) { cidadeRecebedor = 'Sao Paulo'; } // Validação da chave Pix if (!this.validateChavePix(chave)) { throw new Error('Chave Pix inválida'); } // Criação do payload Pix usando a biblioteca `pix-payload` const payload = pix({ key: chave, // Chave Pix name: nomeRecebedor, // Nome do Recebedor city: cidadeRecebedor, // Cidade do Recebedor transactionId: '***', // ID da transação (pode ser substituído ou gerado dinamicamente) // message: descricao, // Descrição da transação amount: valor / 100, // Valor formatado corretamente para Pix (exemplo: 19.99) }); // Retorna o payload gerado return payload; } public async generateCopyAndPastQrCode( code: string, options?: qrcode.QRCodeToDataURLOptions, ): Promise<pixGenerateCopyAndPastOutput> { const qrCodeDataURL = await qrcode.toDataURL(code, options); return { qrcode: qrCodeDataURL, metadata: code, }; } // Função para gerar o QR Code Pix public async generatePixQRCode( chave: string, valor: number, descricao: string, nomeRecebedor: string, cidadeRecebedor: string, ): Promise<string> { const payload = this.generatePixPayload( valor, chave, descricao, nomeRecebedor, cidadeRecebedor, ); const qrCodeDataUrl = await qrcode.toDataURL(payload); return qrCodeDataUrl; // Retorna a URL do QRCode gerado } private CpfOrCnpjKey(key: string): boolean { if (cpf.isValid(key)) { return true; } if (cnpj.isValid(key)) { return true; } return false; } // Função que valida a chave Pix private validateChavePix(chave: string): boolean { const isEmail = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(chave); const isTelefone = /^\+\d{1,3}\d{9,13}$/.test(chave); // Exemplo: +5511999999999 const isAleatoria = /^[a-zA-Z0-9]{32}$/.test(chave); // Chave aleatória tem 32 caracteres const isUuid = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( chave, ); // Formato UUID const isCpfCnpj = this.CpfOrCnpjKey(chave); // Validação de CPF ou CNPJ return isEmail || isTelefone || isAleatoria || isUuid || isCpfCnpj; } public determinePixType(chave?: any): pixTypeOutput { if (!chave) { chave = this.pixkey; } const isEmail = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(chave); const isTelefone = /^\+\d{1,3}\d{9,13}$/.test(chave); // Exemplo: +5511999999999 const isAleatoria = /^[a-zA-Z0-9]{32}$/.test(chave); // Chave aleatória tem 32 caracteres const isUuid = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( chave, ); // Formato UUID const isCpfCnpj = this.CpfOrCnpjKey(chave); // Validação de CPF ou CNPJ return { key: chave, type: isEmail ? 'email' : isTelefone ? 'phone' : isAleatoria ? 'token' : isUuid ? 'random' : isCpfCnpj ? 'cpf' : 'cnpj', }; } // Função que gera o checksum CRC16 (necessário para o payload Pix) public generateCRC16(payload: string): string { let crc = 0xffff; for (let i = 0; i < payload.length; i++) { crc ^= payload.charCodeAt(i) << 8; for (let j = 0; j < 8; j++) { if ((crc & 0x8000) !== 0) { crc = (crc << 1) ^ 0x1021; } else { crc = crc << 1; } } } crc &= 0xffff; return crc.toString(16).toUpperCase().padStart(4, '0'); } public extractPixPayload(evmpix: string): PixPayloadOutput { const data: any = {}; const mappings: { [key: string]: string } = { '00': 'payloadFormatIndicator', '01': 'pointOfInitiationMethod', '26': 'merchantAccountInfo', '52': 'merchantCategoryCode', '53': 'transactionCurrency', '54': 'transactionAmount', '58': 'countryCode', '59': 'merchantName', '60': 'merchantCity', '61': 'postalCode', '62': 'additionalDataFieldTemplate', '63': 'crc', }; function processField( evmpix: string, offset: number, ): { id: string; length: number; value: string; nextOffset: number } { const id = evmpix.slice(offset, offset + 2); const length = parseInt(evmpix.slice(offset + 2, offset + 4), 10); const value = evmpix.slice(offset + 4, offset + 4 + length); return { id, length, value, nextOffset: offset + 4 + length }; } let offset = 0; while (offset < evmpix.length) { const { id, value, nextOffset } = processField(evmpix, offset); offset = nextOffset; const fieldName = mappings[id as keyof typeof mappings] || `unknownField_${id}`; // Tratamento especial para merchantAccountInfo (ID 26) if (id === '26') { const subfields: any = {}; let subOffset = 0; while (subOffset < value.length) { const { id: subId, value: subValue, nextOffset: subNextOffset, } = processField(value, subOffset); subOffset = subNextOffset; if (subId === '01') subfields.pixKey = subValue; // Identificador da chave Pix } data[fieldName] = subfields.pixKey || ''; } else { data[fieldName] = value; } } // Validação CRC const crcIndex = evmpix.indexOf('6304'); if (crcIndex !== -1) { const crcPayload = evmpix.substring(0, crcIndex + 4); const generatedCRC = this.generateCRC16(crcPayload); if (generatedCRC !== evmpix.slice(crcIndex + 4, crcIndex + 8)) { throw new Error('CRC16 mismatch - invalid EVM Pix code'); } } return { format: data.payloadFormatIndicator || '', method: data.pointOfInitiationMethod, chave: data.merchantAccountInfo || '', valor: data.transactionAmount, moeda: data.transactionCurrency, pais: data.countryCode, nomeRecebedor: data.merchantName, cidadeRecebedor: data.merchantCity, cep: data.postalCode, crc: data.crc, additionalInfo: data.additionalDataFieldTemplate, }; } async generatingPixBilling( body: PixGeneratingPixBillingInterface, ): Promise<Object> { try { body.pixkey = this.pixkey ?? body.pixkey; var valueCents: number = Number.isInteger(body.valueCents) ? body.valueCents : Math.round(body.valueCents * 100); var pixkey = this.generatePixPayload( body.valueCents, body.pixkey, body.description, body.name, body.city, ); var qrcode = await this.generatePixQRCode( body.pixkey, body.valueCents, body.description, body.name, body.city, ); var expireTimestamp = Math.round( new Date().getTime() / 1000 + (body.expires ?? 3600), ); return { qrcode: qrcode, pixkey: pixkey, value: { original: body.valueCents, cents: valueCents, fixed: (valueCents / 100).toFixed(2), float: valueCents / 100, }, expires: { timestamp: expireTimestamp, dateTime: new Date(expireTimestamp * 1000).toLocaleString( 'pt-BR', ), iso: new Date(expireTimestamp * 1000).toISOString(), }, code: randomUUID(), }; } catch (error: any) { throw new Error(`Fail or error: ${error.message}`); } } listingPixBilling(body?: object): Promise<Object> { throw new Error('Method not implemented.'); } searchPixBilling(body?: object): Promise<Object> { throw new Error('Method not implemented.'); } generateProviderWidthdraw(body?: object): Promise<Object> { throw new Error('Method not implemented.'); } listProviderWidthdraw(body?: object): Promise<Object> { throw new Error('Method not implemented.'); } async getBalance(): Promise<BalanceOutput> { return { valueCents: 0, valueFloat: 0.0, }; } searchProviderWidthdraw(body?: object): Promise<Object> { throw new Error('Method not implemented.'); } }