thunderpix
Version:
Biblioteca javascript de padronização de gateways de pagamentos PIX
376 lines (333 loc) • 12.6 kB
text/typescript
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.');
}
}