brazilian-boletos
Version:
Biblioteca para geração de boletos bancários brasileiros.
709 lines (650 loc) • 18.7 kB
JavaScript
const fs = require("node:fs");
const path = require("node:path");
const PDFKit = require("pdfkit");
const bwipjs = require("bwip-js");
const {
formatarData,
formatarMoeda,
formatarValorParaCodigo,
} = require("./utils/formatacao.js");
const {
gerarLinhaDigitavel,
gerarCampoLivre,
} = require("./utils/linhaDigitavel.js");
const { calcularFatorVencimento } = require("./utils/calculo.js");
const {
cnpjValidator,
cpfValidator,
carteiraValidator,
} = require("./utils/validator.js");
const { Banco, Carteiras } = require("./enums.js");
const { dvNossoNumero, calculaModulo11 } = require("./utils/modulos.js");
const pdfConfig = {
PDFKit: { size: "A4", margin: 20 },
x: 20,
y: 20,
maxWidth: 550,
linesHeight: 30,
textX: (x) => x + 20,
textY: (x) => x + 16,
};
let lastLineHeight = 0;
/**
* Classe para geração de boletos
*/
class BoletoGenerator {
/**
* @param {*} dados
*/
constructor(dados) {
try {
dados.banco.carteira = dados.banco.carteira.padStart(2, "0");
carteiraValidator(dados.banco.codigo, dados.banco.carteira);
if (dados.beneficiario.cpfCnpj.replace(/D/g, "").length === 11) {
cpfValidator(dados.beneficiario.cpfCnpj);
} else {
cnpjValidator(dados.beneficiario.cpfCnpj);
}
this.dados = dados;
this.linhaDigitavel =
dados.boleto.linhaDigitavel || gerarLinhaDigitavel(this.dados);
} catch (error) {
throw new Error(error.message);
}
}
/**
* Gera o código de barras como imagem PNG
* @returns {Promise<Buffer>}
*/
async gerarCodigoBarras() {
const banco = this.dados.banco.codigo.padStart(3, "0");
const moeda = "9";
const fator = calcularFatorVencimento(this.dados);
const valor = formatarValorParaCodigo(this.dados.boleto.valor);
const campoLivre = gerarCampoLivre(this.dados);
const dv = calculaModulo11(
`${banco}${moeda}${fator}${valor}${campoLivre}`,
this.dados.banco.codigo
);
const codigoBarras = `${banco}${moeda}${dv}${fator}${valor}${campoLivre}`;
return new Promise((resolve, reject) => {
bwipjs.toBuffer(
{
bcid: "interleaved2of5",
text: codigoBarras,
scale: 3,
height: 10,
includetext: false,
textxalign: "center",
},
(err, png) => {
if (err) {
reject(err);
} else {
resolve(png);
}
}
);
});
}
// Gera o PDF do boleto
/**
*
* @param {string | undefined} outputPath
* @returns {Promise<string>}
* @access public
*/
async gerarPDFFile(outputPath) {
// Define o caminho de saída se não for fornecido
if (!outputPath) {
outputPath = path.join(
process.cwd(),
"tmp",
"boletos",
`boleto-${this.dados.boleto.numeroDocumento.replace(/\//g, "-")}.pdf`
);
}
// Garante que o diretório exista
const diretorio = path.dirname(outputPath);
if (!fs.existsSync(diretorio)) {
fs.mkdirSync(diretorio, { recursive: true });
}
// Cria o PDF
const doc = new PDFKit({ size: "A4", margin: 20 });
const stream = fs.createWriteStream(outputPath);
doc.pipe(stream);
// Configurações gerais
this.configuracoesIniciais(doc);
// Parte superior (Recibo do Pagador)
this.desenharCabecalhoRecibo(doc);
this.desenharDadosRecibo(doc);
this.desenharLinhaRecortavel(doc, 295);
// Parte inferior (Ficha de Compensação)
this.desenharCabecalhoFicha(doc);
this.desenharDadosFicha(doc);
await this.desenharCodigoBarras(doc);
this.desenharDadosPagador(doc);
// Finaliza o PDF
doc.end();
// Retorna o caminho do arquivo quando o stream for fechado
return new Promise((resolve, reject) => {
stream.on("finish", () => resolve(outputPath));
stream.on("error", reject);
});
}
// Gera o PDF do boleto
/**
* @returns {Promise<Buffer>}
* @access public
*/
async gerarPDFBuffer() {
// Cria o PDF na memória
const doc = new PDFKit(pdfConfig.PDFKit);
const chunks = [];
//captura os chunks do PDF em um array
doc.on("data", (chunk) => chunks.push(chunk));
// Configurações gerais
this.configuracoesIniciais(doc);
// Parte superior (Recibo do Pagador)
this.desenharCabecalhoRecibo(doc);
this.desenharDadosRecibo(doc);
this.desenharLinhaRecortavel(doc);
// Parte inferior (Ficha de Compensação)
this.desenharCabecalhoFicha(doc);
this.desenharDadosFicha(doc);
await this.desenharCodigoBarras(doc);
this.desenharDadosPagador(doc);
// Finaliza o PDF
doc.end();
// Retorna o caminho do arquivo quando o stream for fechado
return new Promise((resolve, reject) => {
doc.on("end", () => resolve(Buffer.concat(chunks)));
doc.on("error", reject);
});
}
/**
* Define configurações iniciais do documento
* @param {PDFKit.PDFDocument} doc
* @returns {void}
* @access private
*/
configuracoesIniciais(doc) {
// Adiciona fonte padrão
doc.font("Helvetica");
doc.fontSize(10);
}
/**
* Desenha o cabeçalho do recibo
* @param {PDFKit.PDFDocument} doc
* @returns {void}
* @access private
*/
desenharCabecalhoRecibo(doc) {
const yAlign = 10;
// Título do recibo
doc
.fontSize(10)
.font("Helvetica-Bold")
.text(
"RECIBO DO PAGADOR",
pdfConfig.maxWidth - 100,
pdfConfig.y + yAlign,
{
align: "left",
}
);
// Caixa para o código do banco
doc.rect(pdfConfig.x, pdfConfig.y, 80, 30).stroke();
doc
.fontSize(16)
.font("Helvetica-Bold")
.text(
`${this.dados.banco.codigo}-${
this.dados.banco.codigo === Banco.BRADESCO ? "2" : "X"
}`,
pdfConfig.x + 20,
pdfConfig.y + yAlign
);
}
/**
* Desenha os dados do recibo do pagador
* @param {PDFKit.PDFDocument} doc
* @returns {void}
* @access private
*/
desenharDadosRecibo(doc) {
// Linha 1 - Beneficiário
const yLine1 = pdfConfig.y + pdfConfig.linesHeight + 10;
const texts = [
`${this.dados.beneficiario.nome} - CNPJ: ${this.dados.beneficiario.cpfCnpj}`,
`${this.dados.beneficiario.endereco}, ${this.dados.beneficiario.bairro}`,
`${this.dados.beneficiario.cidade}/${this.dados.beneficiario.uf} — ${this.dados.beneficiario.cep}`,
];
this.montarLinha(
doc,
[
{
title: "Beneficiário",
value: texts.join("\n"),
width: pdfConfig.maxWidth,
height: 60,
},
],
yLine1
);
// Linha 2 - Nome do Pagador, Data Vencimento, Valor Cobrado
const yLine2 = yLine1 + 60;
this.montarLinha(
doc,
[
{
title: "Nome do Pagador",
value: this.dados.pagador.nome,
width: 300,
},
{
title: "Data de Vencimento",
value: formatarData(this.dados.boleto.dataVencimento),
width: 125,
},
{ title: "Valor Cobrado", value: "", width: 125, fontSize: 9 },
],
yLine2
);
// Linha 3 - Carteira, Espécie Doc, Nº Documento, Valor Documento
const yLine3 = yLine2 + pdfConfig.linesHeight;
this.montarLinha(
doc,
[
{
title: "Carteira",
value: this.dados.banco.carteira,
width: 75,
},
{
title: "Espécie Doc.",
value: this.dados.boleto.especieDocumento,
width: 75,
},
{
title: "Nº do Documento",
value: this.dados.boleto.numeroDocumento,
width: 200,
},
{
title: "Valor do Documento",
value: formatarMoeda(this.dados.boleto.valor),
width: 200,
},
],
yLine3
);
// Linha 4 - Data Processamento, Agência/Código, Nosso Número
const yLine4 = yLine3 + pdfConfig.linesHeight;
this.montarLinha(
doc,
[
{
title: "Data Processamento",
value: formatarData(
this.dados.boleto.dataProcessamento || new Date()
),
},
{
title: "Agência / Código do Beneficiário",
value: `${this.dados.banco.agencia}/${this.dados.banco.conta}`,
},
{
title: "Nosso Número",
value: this.dados.boleto.nossoNumero,
},
],
yLine4
);
// Espaço para Autenticação Mecânica
doc
.fontSize(8)
.font("Helvetica")
.text(
"Autenticação Mecânica",
pdfConfig.maxWidth - 80,
yLine4 + pdfConfig.linesHeight + 20
);
// Linha Digitavel
doc
.fontSize(11)
.font("Helvetica-Bold")
.text(
this.linhaDigitavel,
pdfConfig.x + 20,
yLine4 + pdfConfig.linesHeight + 20
);
lastLineHeight = yLine4 + pdfConfig.linesHeight + 20;
}
/**
* Desenha linha recortável
* @param {PDFKit.PDFDocument} doc
* @param {number} y - Posição Y da linha
* @returns {void}
* @access private
*/
desenharLinhaRecortavel(doc, y) {
doc
.fontSize(8)
.text(
"- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -",
pdfConfig.x,
lastLineHeight + pdfConfig.linesHeight
);
lastLineHeight += pdfConfig.linesHeight;
}
/**
* Desenha o cabeçalho da ficha de compensação
* @param {PDFKit.PDFDocument} doc
* @returns {void}
* @access private
*/
desenharCabecalhoFicha(doc) {
const yLine1 = lastLineHeight + pdfConfig.linesHeight;
// Caixa para o código do banco
doc.rect(pdfConfig.x, yLine1, 80, 30).stroke();
doc
.fontSize(16)
.font("Helvetica-Bold")
.text(
`${this.dados.banco.codigo}-${
this.dados.banco.codigo === Banco.BRADESCO ? "2" : "X"
}`,
pdfConfig.x + 20,
yLine1 + 10
);
doc
.fontSize(11)
.font("Helvetica-Bold")
.text(this.linhaDigitavel, 140, yLine1 + 10);
lastLineHeight = yLine1;
}
/**
* Desenha os dados da ficha de compensação
* @param {PDFKit.PDFDocument} doc
* @returns {void}
* @access private
*/
desenharDadosFicha(doc) {
// Linha 1 - Local de Pagamento e Vencimento
const yLine1 = lastLineHeight + pdfConfig.linesHeight + 10;
this.montarLinha(
doc,
[
{
title: "Local do pagamento",
value: "PAGÁVEL EM QUALQUER BANCO ATÉ O VENCIMENTO",
width: 420,
},
{
title: "Vencimento",
value: formatarData(this.dados.boleto.dataVencimento),
width: 130,
},
],
yLine1
);
// Linha 2 - Beneficiário, Agência/Código
const yLine2 = yLine1 + pdfConfig.linesHeight;
this.montarLinha(
doc,
[
{
title: "Beneficiário",
value: this.dados.beneficiario.nome,
width: 420,
},
{
title: "Agência / Código do Beneficiário",
value: `${this.dados.banco.agencia}/${this.dados.banco.conta}`,
width: 130,
},
],
yLine2
);
// Linha 3 - Data Documento, Nº Documento, Espécie Doc, Aceite, Data Processamento
const yLine3 = yLine2 + pdfConfig.linesHeight;
const banco = this.dados.banco.codigo;
this.montarLinha(
doc,
[
{
title: "Data Documento",
value: formatarData(this.dados.boleto.dataDocumento || new Date()),
width: 100,
},
{
title: "Nº Documento",
value: this.dados.boleto.numeroDocumento,
width: 100,
},
{
title: "Espécie Doc.",
value: this.dados.boleto.especieDocumento,
width: 50,
},
{
title: "Aceite",
value: "N",
width: 50,
},
{
title: "Data Processamento",
value: formatarData(
this.dados.boleto.dataProcessamento || new Date()
),
width: 100,
},
{
title: "Nosso Número",
value:
banco !== Banco.BANCO_DO_BRASIL
? `${this.dados.banco.carteira}/${
this.dados.boleto.nossoNumero
}-${dvNossoNumero(this.dados.boleto.nossoNumero, banco)}`
: this.dados.boleto.nossoNumero,
width: 150,
},
],
yLine3
);
// Linha 4 - Uso do Banco, CIP, Carteira, Moeda, Quantidade, Valor
const yLine4 = yLine3 + pdfConfig.linesHeight;
this.montarLinha(
doc,
[
{
title: "Uso do Banco",
value: "",
width: 70,
},
{
title: "CIP",
value: "",
width: 100,
},
{
title: "Carteira",
value: this.dados.banco.carteira,
width: 70,
},
{
title: "Moeda",
value: "R$",
width: 70,
},
{
title: "Quantidade",
value: "",
width: 110,
},
{
title: "(=) Valor do Documento",
value: formatarMoeda(this.dados.boleto.valor),
width: 130,
},
],
yLine4
);
// Linha 5 - Valor do documento, Descontos, Abatimentos, Outras Deduções, Juros / Multa, Outros Acréscimos, Valor Cobrado
const yLine5 = yLine4 + pdfConfig.linesHeight;
this.montarLinha(
doc,
[
{
title: "(-) Desconto / Abatimento",
value: "",
},
{
title: "(-) Outras Deduções",
value: "",
},
{
title: "(+) Juros / Multa",
value: "",
},
{
title: "(+) Outros Acréscimos",
value: "",
},
{
title: "(=) Valor Cobrado",
value: "",
},
],
yLine5
);
// Linha 6 - Informações do beneficiário
const yLine6 = yLine5 + pdfConfig.linesHeight;
this.montarLinha(
doc,
[
{
title: "Informações de responsabilidade do beneficiário",
value: this.dados.boleto.instrucoes.join("\n"),
width: 550,
height: 100,
},
],
yLine6
);
lastLineHeight = yLine6 + 100;
}
/**
* Desenha o código de barras
* @param {PDFKit.PDFDocument} doc
* @returns {Promise<void>}
* @access private
*/
async desenharCodigoBarras(doc) {
const codigoBarras = await this.gerarCodigoBarras();
const y = lastLineHeight + 30; // Posição Y para o código de barras
//mudar para renderizar com font
doc.image(codigoBarras, pdfConfig.x, y, { width: 400, height: 50 });
lastLineHeight = y + 50;
}
/**
* Desenha os dados do pagador
* @param {PDFKit.PDFDocument} doc
* @returns {void}
* @access private
*/
desenharDadosPagador(doc) {
const y = lastLineHeight + 10;
const texts = [
`${this.dados.pagador.nome}`,
`${this.dados.pagador.endereco}, ${this.dados.pagador.bairro}`,
`${this.dados.pagador.cidade}/${this.dados.pagador.uf} — ${this.dados.pagador.cep}`,
];
const height = 70;
this.montarLinha(
doc,
[
{
title: "Pagador",
value: texts.join("\n"),
width: 550,
height,
},
],
y
);
doc.fontSize(8).text("Código de Baixa", pdfConfig.x + 20, y + height + 10);
doc
.fontSize(8)
.text(
"Autenticação mecânica - Ficha de Compensação",
350,
y + height + 10
);
}
/**
* Desenha um campo com cabeçalho e área para dados
* @param {PDFKit.PDFDocument} doc
* @param {string} titulo - Título do campo
* @param {number} x - Posição X do campo
* @param {number} y - Posição Y do campo
* @param {number} largura - Largura do campo
* @param {number} altura - Altura do campo
* @returns {void}
* @access private
*/
desenharCampoDadoCabecalho(doc, titulo, x, y, largura, altura) {
// Desenha o retângulo
doc.rect(x, y, largura, altura).stroke();
// Adiciona o título
doc
.fontSize(6)
.font("Helvetica-Bold")
.fillColor("#000000")
.text(titulo, x + 5, y + 5, { width: largura - 4 });
}
/**
* Gera os dados para a linha
* @param {PDFKit.PDFDocument} doc
* @param {{ title: string, value: any, width?: number, height?: number, fontSize: number }[]} arr
* @param {number} linePos - Posição Y da linha
* @access private
*/
montarLinha(doc, arr, linePos) {
const totalWidth = arr.reduce((acc, e) => (acc += e.width || 0), 0);
if (totalWidth > pdfConfig.maxWidth) {
throw new Error("Linha muito grande");
}
const widthDefault =
(pdfConfig.maxWidth - totalWidth) / arr.filter((e) => !e.width).length;
let lastLineWidth = 0;
arr.forEach((e) => {
this.desenharCampoDadoCabecalho(
doc,
e.title,
pdfConfig.x + lastLineWidth,
linePos,
e.width || widthDefault,
e.height || pdfConfig.linesHeight
);
if (e.value) {
doc
.fontSize(e.fontSize || 9)
.text(
e.value,
pdfConfig.textX(pdfConfig.x + lastLineWidth),
pdfConfig.textY(linePos),
{
lineBreak: true,
lineGap: 5,
}
);
}
lastLineWidth += e.width || widthDefault;
});
}
}
module.exports.BoletoGenerator = BoletoGenerator;
module.exports.Banco = Banco;
module.exports.Carteiras = Carteiras;