Skip to main content

Overview

Kontrak Backend uses Puppeteer (headless Chrome) to generate high-quality PDF contracts from HTML templates. The system combines Handlebars templating with precise PDF rendering to create professional, print-ready employment contracts.

Architecture

PDF Generator Service

The PDFGeneratorService is the core service responsible for contract PDF generation.
src/services/pdf-generator.service.ts
export class PDFGeneratorService {
  /**
   * Genera un contrato PDF basado en el tipo de contrato
   */
  async generateContract(
    employeeData: EmployeeData,
    contractType: ContractType,
    browser: Browser,
  ): Promise<{ buffer: Buffer; filename: string }> {
    // Validation
    if (!employeeData || !employeeData.dni) {
      throw new AppError(
        'Faltand datos del empleado para visualizar el contrato',
        400,
      );
    }

    let buffer: Buffer;

    // Generate based on contract type
    switch (contractType.toLowerCase()) {
      case 'planilla':
        buffer = await generatePlanillaContract(employeeData, browser);
        break;
      case 'subsidio':
        buffer = await generateSubsidioContract(employeeData, browser);
        break;
      case 'part time':
      case 'parttime':
        buffer = await generatePartTimeContract(employeeData, browser);
        break;
      default:
        throw new Error(`Tipo de contrato desconocido: ${contractType}`);
    }

    const filename = `${employeeData.dni}.pdf`;
    return { buffer, filename };
  }
}
PDFs are named using the employee’s DNI (e.g., 12345678.pdf) for easy identification and retrieval.

Template System

Kontrak Backend uses Handlebars templates for rendering contracts. Each contract type has its own template with specific fields and layout.

Template Structure

Templates are stored in src/template/templates.ts and include:
  • CONTRACT_FULL_TIME - PLANILLA contracts
  • CONTRACT_PART_TIME - PART TIME contracts
  • CONTRACT_SUBSIDIO - SUBSIDIO contracts
  • ANEXO - Addendum documents

Template Compilation Process

1

Compile Template

Handlebars compiles the HTML template string into a reusable function
const template = Handlebars.compile(CONTRACT_FULL_TIME);
2

Inject Data

Employee data is injected into the template placeholders
const finalHtml = template({
  fullName: `${data.name} ${data.lastNameFather} ${data.lastNameMother}`,
  dni: data.dni,
  salary: salaryFormatted,
  // ... more fields
});
3

Render HTML

The final HTML is loaded into a Puppeteer page
const page = await browser.newPage();
await page.setContent(finalHtml, { waitUntil: 'networkidle0' });
4

Generate PDF

Puppeteer renders the page as a PDF with specific settings
const pdfBuffer = await page.pdf({
  format: 'Letter',
  printBackground: true,
  preferCSSPageSize: true,
  margin: { top: '2.12cm', bottom: '0.49cm', ... }
});

Contract-Specific Generation

Full-Time Contract Generation

src/template/contracts.ts
export const generatePlanillaContract = async (
  data: EmployeeData,
  browser: Browser,
): Promise<Buffer> => {
  // Format salary with locale
  const salaryFormatted = Number(data.salary).toLocaleString('es-PE', {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  });
  
  const template = Handlebars.compile(CONTRACT_FULL_TIME);
  const finalHtml = template({
    // Employee data
    fullName: `${data.name} ${data.lastNameFather} ${data.lastNameMother}`,
    dni: data.dni,
    address: data.address,
    district: data.district,
    province: data.province,
    department: data.department,
    email: data.email,
    position: data.position,
    entryDate: data.entryDate,
    endDate: data.endDate,
    salary: salaryFormatted,
    salaryInWords: data.salaryInWords,
    probationaryPeriod: data.probationaryPeriod,
    subDivision: data.subDivisionOrParking,
    
    // Company signers
    signer1Name: FULL_NAME_PRIMARY_EMPLOYEE,
    signer1DNI: DNI_EMPLOYEE_PRIMARY,
    signer2Name: FULL_NAME_SECOND_EMPLOYEE,
    signer2DNI: DNI_EMPLOYEE_SECOND,
    signature1: SIGNATURE_EMPLOYEE,
    signature2: SIGNATURE_EMPLOYEE_TWO,
  });

  const page = await browser.newPage();
  await page.setContent(finalHtml, { waitUntil: 'networkidle0' });

  const pdfBuffer = await page.pdf({
    format: 'Letter',
    printBackground: true,
    preferCSSPageSize: true,
    margin: {
      top: '2.12cm',
      bottom: '0.49cm',
      left: '1.41cm',
      right: '2.26cm',
    },
  });

  await page.close();
  return Buffer.from(pdfBuffer);
};
Key Features:
  • Locale-formatted salary (Spanish/Peru format)
  • All standard employment fields
  • Probationary period clause
  • Dual company signatures
  • Letter format (8.5” x 11”)

PDF Configuration

Page Format

All contracts use Letter format (8.5” x 11” / 21.59cm x 27.94cm):
{
  format: 'Letter',
  printBackground: true,
  preferCSSPageSize: true
}

Margins by Contract Type

Different contract types use optimized margins:
margin: {
  top: '2.12cm',
  bottom: '0.49cm',
  left: '1.41cm',
  right: '2.26cm'
}

Browser Management

Puppeteer browser instances are managed at the application level and reused across requests for optimal performance.
The browser instance is:
  • Initialized once at application startup
  • Shared across all PDF generation requests
  • Properly closed on application shutdown
  • Headless Chrome instance for server environments
// Browser is passed to generation functions
await generateContract(employeeData, contractType, browser);

File Naming Convention

Generated PDFs follow a simple, consistent naming pattern:
const filename = `${employeeData.dni}.pdf`;
Examples:
  • Employee with DNI 1234567812345678.pdf
  • Employee with DNI 8765432187654321.pdf
DNI-based naming ensures uniqueness but means regenerating a contract for the same employee will overwrite the previous file.

Signatures

Contracts include digital signatures from two company representatives:
src/template/contracts.ts
import {
  SIGNATURE_EMPLOYEE,
  SIGNATURE_EMPLOYEE_TWO,
} from '../constants/signatures';

import {
  DNI_EMPLOYEE_PRIMARY,
  DNI_EMPLOYEE_SECOND,
  FULL_NAME_PRIMARY_EMPLOYEE,
  FULL_NAME_SECOND_EMPLOYEE,
} from './constants';
Signatures are embedded as base64 images or image URLs and positioned at the bottom of each contract.

Error Handling

src/services/pdf-generator.service.ts
try {
  let buffer: Buffer;
  // ... generation logic
  return { buffer, filename };
} catch (error) {
  if (error instanceof Error) {
    logger.error(
      {
        err: error,
        message: error.message,
        stack: error.stack,
        dni: employeeData.dni,
      },
      '❌ Error generando PDF',
    );
  }
  throw error;
}
Common Errors:
  • Missing employee data
  • Invalid contract type
  • Puppeteer page rendering failures
  • Template compilation errors
  • Font loading issues

Performance Considerations

Additional Documents

Personal Data Processing Document

The system also generates a personal data processing consent document using PDFKit:
src/template/contracts.ts
export const generateProcessingOfPresonalDataPDF = async (
  data: EmployeeData,
): Promise<Buffer> => {
  const doc = new PDFDocument({
    size: 'A4',
    margins: {
      top: cmToPt(2.12),
      bottom: cmToPt(2.4),
      left: cmToPt(2.7),
      right: cmToPt(2.8),
    },
  });
  
  // Custom fonts
  doc.registerFont('Arial Bold', arialBold);
  doc.registerFont('Arial Normal', arialNormal);
  
  // Document content generation...
  
  return buffer;
};
This document uses PDFKit instead of Puppeteer for more granular control over text formatting and positioning.

Contract Types

Learn about the three contract types and their requirements

Employee Data

Understand the data structure used in templates

API: Generate Contract

Use the API to generate contract PDFs

Batch Processing

Generate multiple contracts at once

Build docs developers (and LLMs) love