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

On this page