Use cases
Faturas e Notas Fiscais
Gere faturas profissionais automaticamente com dados do seu sistema
Este guia mostra como implementar geração automática de faturas e notas fiscais usando RenderHub.
Visão Geral
Faturas são um dos casos de uso mais comuns para geração de PDFs. Com RenderHub, você pode:
- ✅ Gerar faturas automaticamente após vendas
- ✅ Enviar por email para clientes
- ✅ Armazenar em cloud storage (S3, etc)
- ✅ Integrar com sistemas de contabilidade
- ✅ Personalizar layout por marca ou cliente
Estrutura de Dados
Modelo de Dados
interface Invoice {
// Identificação
id: string;
number: string; // INV-2024-001
status: 'draft' | 'sent' | 'paid' | 'overdue' | 'cancelled';
// Datas
issueDate: Date;
dueDate: Date;
paidAt?: Date;
// Empresa
company: {
name: string;
logo?: string;
address: string;
city: string;
state: string;
zipCode: string;
taxId: string; // CNPJ
email: string;
phone: string;
website?: string;
};
// Cliente
customer: {
id: string;
name: string;
email: string;
taxId?: string; // CPF/CNPJ
address: string;
city: string;
state: string;
zipCode: string;
};
// Itens
items: Array<{
id: string;
description: string;
quantity: number;
unitPrice: number;
discount?: number;
tax?: number;
total: number;
}>;
// Totais
subtotal: number;
discountTotal: number;
taxTotal: number;
total: number;
// Informações adicionais
notes?: string;
terms?: string;
paymentMethod?: string;
paymentDetails?: {
bankName?: string;
accountNumber?: string;
pixKey?: string;
};
}
Template HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: #333;
line-height: 1.6;
padding: 40px;
}
.header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 3px solid #2563eb;
}
.company-info {
flex: 1;
}
.logo {
max-width: 200px;
margin-bottom: 15px;
}
.company-details {
font-size: 12px;
color: #666;
}
.invoice-info {
text-align: right;
}
.invoice-title {
font-size: 32px;
font-weight: bold;
color: #2563eb;
margin-bottom: 10px;
}
.invoice-meta {
font-size: 14px;
}
.invoice-meta strong {
color: #333;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
margin-top: 8px;
}
.status-paid {
background: #dcfce7;
color: #166534;
}
.status-sent {
background: #dbeafe;
color: #1e40af;
}
.status-overdue {
background: #fee2e2;
color: #991b1b;
}
.parties {
display: flex;
gap: 40px;
margin-bottom: 40px;
}
.party {
flex: 1;
background: #f9fafb;
padding: 20px;
border-radius: 8px;
}
.party-title {
font-size: 12px;
text-transform: uppercase;
color: #6b7280;
margin-bottom: 10px;
font-weight: 600;
}
.party-name {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
}
.party-details {
font-size: 13px;
color: #4b5563;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}
.items-table th {
background: #f3f4f6;
padding: 12px;
text-align: left;
font-size: 12px;
text-transform: uppercase;
color: #6b7280;
font-weight: 600;
border-bottom: 2px solid #e5e7eb;
}
.items-table td {
padding: 12px;
border-bottom: 1px solid #e5e7eb;
font-size: 14px;
}
.items-table tr:last-child td {
border-bottom: none;
}
.text-right {
text-align: right;
}
.totals {
margin-left: auto;
width: 300px;
}
.totals-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 14px;
}
.totals-row.subtotal {
color: #6b7280;
}
.totals-row.total {
border-top: 2px solid #2563eb;
margin-top: 8px;
padding-top: 12px;
font-size: 18px;
font-weight: bold;
color: #2563eb;
}
.payment-info {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
}
.payment-title {
font-weight: 600;
margin-bottom: 10px;
color: #0c4a6e;
}
.payment-details {
font-size: 13px;
color: #075985;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
font-size: 12px;
color: #6b7280;
}
.notes {
margin-bottom: 15px;
}
.terms {
font-size: 11px;
color: #9ca3af;
font-style: italic;
}
</style>
</head>
<body>
<div class="header">
<div class="company-info">
{{#if company.logo}}
<img src="{{company.logo}}" alt="{{company.name}}" class="logo">
{{/if}}
<div class="company-details">
<strong>{{company.name}}</strong><br>
CNPJ: {{company.taxId}}<br>
{{company.address}}, {{company.city}} - {{company.state}}<br>
CEP: {{company.zipCode}}<br>
{{company.email}} | {{company.phone}}
{{#if company.website}}<br>{{company.website}}{{/if}}
</div>
</div>
<div class="invoice-info">
<div class="invoice-title">FATURA</div>
<div class="invoice-meta">
<strong>#{{number}}</strong><br>
Data de Emissão: {{formatDate issueDate}}<br>
Vencimento: {{formatDate dueDate}}
</div>
{{#if status}}
<span class="status-badge status-{{status}}">{{status}}</span>
{{/if}}
</div>
</div>
<div class="parties">
<div class="party">
<div class="party-title">Faturado Para</div>
<div class="party-name">{{customer.name}}</div>
<div class="party-details">
{{#if customer.taxId}}{{customer.taxId}}<br>{{/if}}
{{customer.address}}<br>
{{customer.city}} - {{customer.state}}<br>
CEP: {{customer.zipCode}}<br>
{{customer.email}}
</div>
</div>
</div>
<table class="items-table">
<thead>
<tr>
<th>Descrição</th>
<th class="text-right">Qtd</th>
<th class="text-right">Preço Unit.</th>
{{#if items.0.discount}}
<th class="text-right">Desconto</th>
{{/if}}
{{#if items.0.tax}}
<th class="text-right">Impostos</th>
{{/if}}
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{description}}</td>
<td class="text-right">{{quantity}}</td>
<td class="text-right">{{formatCurrency unitPrice}}</td>
{{#if discount}}
<td class="text-right">{{formatCurrency discount}}</td>
{{/if}}
{{#if tax}}
<td class="text-right">{{formatCurrency tax}}</td>
{{/if}}
<td class="text-right"><strong>{{formatCurrency total}}</strong></td>
</tr>
{{/each}}
</tbody>
</table>
<div class="totals">
<div class="totals-row subtotal">
<span>Subtotal:</span>
<span>{{formatCurrency subtotal}}</span>
</div>
{{#if discountTotal}}
<div class="totals-row subtotal">
<span>Desconto:</span>
<span>-{{formatCurrency discountTotal}}</span>
</div>
{{/if}}
{{#if taxTotal}}
<div class="totals-row subtotal">
<span>Impostos:</span>
<span>{{formatCurrency taxTotal}}</span>
</div>
{{/if}}
<div class="totals-row total">
<span>TOTAL:</span>
<span>{{formatCurrency total}}</span>
</div>
</div>
{{#if paymentDetails}}
<div class="payment-info">
<div class="payment-title">Informações de Pagamento</div>
<div class="payment-details">
{{#if paymentMethod}}
<strong>Método:</strong> {{paymentMethod}}<br>
{{/if}}
{{#if paymentDetails.pixKey}}
<strong>Chave PIX:</strong> {{paymentDetails.pixKey}}<br>
{{/if}}
{{#if paymentDetails.bankName}}
<strong>Banco:</strong> {{paymentDetails.bankName}}<br>
<strong>Conta:</strong> {{paymentDetails.accountNumber}}
{{/if}}
</div>
</div>
{{/if}}
<div class="footer">
{{#if notes}}
<div class="notes">
<strong>Observações:</strong><br>
{{notes}}
</div>
{{/if}}
{{#if terms}}
<div class="terms">
{{terms}}
</div>
{{/if}}
</div>
</body>
</html>
Implementação Completa
1. Mapper de Dados
// src/mappers/invoice.mapper.ts
import { Invoice } from '../models/Invoice';
export class InvoiceMapper {
static toTemplateData(invoice: Invoice): any {
return {
// Identificação
id: invoice.id,
number: invoice.number,
status: invoice.status,
// Datas
issueDate: invoice.issueDate.toISOString().split('T')[0],
dueDate: invoice.dueDate.toISOString().split('T')[0],
// Empresa
company: {
name: invoice.company.name,
logo: invoice.company.logo,
address: invoice.company.address,
city: invoice.company.city,
state: invoice.company.state,
zipCode: this.formatZipCode(invoice.company.zipCode),
taxId: this.formatCNPJ(invoice.company.taxId),
email: invoice.company.email,
phone: this.formatPhone(invoice.company.phone),
website: invoice.company.website,
},
// Cliente
customer: {
id: invoice.customer.id,
name: invoice.customer.name,
email: invoice.customer.email,
taxId: invoice.customer.taxId
? this.formatTaxId(invoice.customer.taxId)
: null,
address: invoice.customer.address,
city: invoice.customer.city,
state: invoice.customer.state,
zipCode: this.formatZipCode(invoice.customer.zipCode),
},
// Itens
items: invoice.items.map(item => ({
id: item.id,
description: item.description,
quantity: item.quantity,
unitPrice: item.unitPrice,
discount: item.discount || 0,
tax: item.tax || 0,
total: item.total,
})),
// Totais
subtotal: invoice.subtotal,
discountTotal: invoice.discountTotal || 0,
taxTotal: invoice.taxTotal || 0,
total: invoice.total,
// Informações adicionais
notes: invoice.notes,
terms: invoice.terms || 'Pagamento deve ser efetuado até a data de vencimento.',
paymentMethod: invoice.paymentMethod,
paymentDetails: invoice.paymentDetails,
};
}
private static formatCNPJ(cnpj: string): string {
return cnpj.replace(/^(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})$/, '$1.$2.$3/$4-$5');
}
private static formatTaxId(taxId: string): string {
// CPF: 000.000.000-00 ou CNPJ: 00.000.000/0000-00
if (taxId.length === 11) {
return taxId.replace(/^(\d{3})(\d{3})(\d{3})(\d{2})$/, '$1.$2.$3-$4');
} else {
return this.formatCNPJ(taxId);
}
}
private static formatZipCode(zipCode: string): string {
return zipCode.replace(/^(\d{5})(\d{3})$/, '$1-$2');
}
private static formatPhone(phone: string): string {
// (00) 0000-0000 ou (00) 00000-0000
if (phone.length === 10) {
return phone.replace(/^(\d{2})(\d{4})(\d{4})$/, '($1) $2-$3');
} else {
return phone.replace(/^(\d{2})(\d{5})(\d{4})$/, '($1) $2-$3');
}
}
}
2. Serviço de Geração
// src/services/invoice-pdf.service.ts
import { RenderHubClient } from '@renderhub/sdk';
import { InvoiceMapper } from '../mappers/invoice.mapper';
import { Invoice } from '../models/Invoice';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { sendEmail } from './email.service';
export class InvoicePDFService {
private renderHub: RenderHubClient;
private s3: S3Client;
private templateId = 'invoice-v1';
constructor() {
this.renderHub = new RenderHubClient({
apiKey: process.env.RENDERHUB_API_KEY!,
});
this.s3 = new S3Client({ region: 'us-east-1' });
}
async generateAndSend(invoiceId: string): Promise<void> {
// 1. Buscar dados da fatura
const invoice = await this.getInvoice(invoiceId);
// 2. Mapear dados para template
const templateData = InvoiceMapper.toTemplateData(invoice);
// 3. Gerar PDF
const pdf = await this.renderHub.generatePDF(
this.templateId,
templateData
);
// 4. Upload para S3
const s3Key = `invoices/${invoice.id}/${invoice.number}.pdf`;
await this.uploadToS3(pdf, s3Key);
// 5. Enviar email para cliente
await this.sendInvoiceEmail(invoice, pdf);
// 6. Atualizar status
await this.updateInvoiceStatus(invoiceId, 'sent');
}
private async uploadToS3(pdf: Buffer, key: string): Promise<void> {
await this.s3.send(
new PutObjectCommand({
Bucket: 'my-invoices-bucket',
Key: key,
Body: pdf,
ContentType: 'application/pdf',
ServerSideEncryption: 'AES256',
})
);
}
private async sendInvoiceEmail(
invoice: Invoice,
pdfBuffer: Buffer
): Promise<void> {
await sendEmail({
to: invoice.customer.email,
subject: `Fatura ${invoice.number} - ${invoice.company.name}`,
html: `
<h2>Olá ${invoice.customer.name},</h2>
<p>Segue em anexo a fatura <strong>${invoice.number}</strong>.</p>
<p><strong>Valor:</strong> R$ ${invoice.total.toFixed(2)}</p>
<p><strong>Vencimento:</strong> ${invoice.dueDate.toLocaleDateString('pt-BR')}</p>
<p>Obrigado!</p>
<p>${invoice.company.name}</p>
`,
attachments: [
{
filename: `${invoice.number}.pdf`,
content: pdfBuffer,
contentType: 'application/pdf',
},
],
});
}
private async getInvoice(id: string): Promise<Invoice> {
// Implementar busca do banco de dados
throw new Error('Not implemented');
}
private async updateInvoiceStatus(
id: string,
status: Invoice['status']
): Promise<void> {
// Implementar atualização no banco
throw new Error('Not implemented');
}
}
3. Endpoint da API
// src/routes/invoices.routes.ts
import { Router } from 'express';
import { InvoicePDFService } from '../services/invoice-pdf.service';
import { requireAuth } from '../middleware/auth';
const router = Router();
const pdfService = new InvoicePDFService();
// Gerar e enviar fatura
router.post(
'/invoices/:id/send',
requireAuth,
async (req, res) => {
try {
const invoiceId = req.params.id;
await pdfService.generateAndSend(invoiceId);
res.json({
success: true,
message: 'Fatura enviada com sucesso',
});
} catch (error) {
console.error('Error sending invoice:', error);
res.status(500).json({
success: false,
error: 'Erro ao enviar fatura',
});
}
}
);
// Download de fatura
router.get(
'/invoices/:id/pdf',
requireAuth,
async (req, res) => {
try {
const invoiceId = req.params.id;
const invoice = await getInvoice(invoiceId);
const templateData = InvoiceMapper.toTemplateData(invoice);
const pdf = await renderHub.generatePDF('invoice-v1', templateData);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader(
'Content-Disposition',
`attachment; filename="${invoice.number}.pdf"`
);
res.send(pdf);
} catch (error) {
console.error('Error generating invoice PDF:', error);
res.status(500).json({ error: 'Erro ao gerar PDF' });
}
}
);
export default router;
Automação com Webhooks
Gerar Fatura Após Venda
// src/webhooks/order.webhook.ts
import { InvoicePDFService } from '../services/invoice-pdf.service';
export async function handleOrderCompleted(order: Order) {
// 1. Criar registro de fatura no banco
const invoice = await createInvoiceFromOrder(order);
// 2. Gerar e enviar PDF
const pdfService = new InvoicePDFService();
await pdfService.generateAndSend(invoice.id);
console.log(`Invoice ${invoice.number} sent for order ${order.id}`);
}
Integração com Sistemas de Contabilidade
// src/integrations/accounting.service.ts
export class AccountingIntegration {
async syncInvoice(invoiceId: string, pdfUrl: string): Promise<void> {
const invoice = await getInvoice(invoiceId);
// Exemplo: integração com ContaAzul
await fetch('https://api.contaazul.com/v1/invoices', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.CONTA_AZUL_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
number: invoice.number,
customer: invoice.customer.name,
total: invoice.total,
dueDate: invoice.dueDate,
pdfUrl: pdfUrl,
}),
});
}
}
Próximos Passos
- Veja como gerar Certificados
- Confira exemplos de Relatórios
- Aprenda sobre Contratos