PDF Module — Architecture Reference
Last Updated: 2026-03-24
Module: apps/backend/src/pdf/
Pattern: Hexagonal Architecture (Ports & Adapters)
Module Structure
pdf/
├── pdf.module.ts # NestJS module definition
├── application/
│ ├── dtos/
│ │ ├── pdf-controller.dto.ts # PDF generation DTOs
│ │ └── template.dto.ts # Template management DTOs
│ ├── ports/
│ │ ├── pdf-generator.port.ts # PDF generation contract
│ │ └── template-renderer.port.ts # Template rendering contract
│ ├── services/
│ │ └── template-resolver.service.ts # Template resolution chain
│ ├── transformers/
│ │ ├── sale-data.transformer.ts # (21 transformers, one per document type)
│ │ └── ...
│ └── use-cases/
│ ├── base-generate-pdf.use-case.ts # Abstract base for all PDF generation
│ ├── generate-sale-pdf.use-case.ts # (21 concrete use cases)
│ ├── upload-template.use-case.ts # Template CRUD use cases
│ ├── update-template.use-case.ts
│ ├── delete-template.use-case.ts
│ ├── get-templates.use-case.ts
│ ├── test-template.use-case.ts
│ ├── preview-pdf.use-case.ts
│ ├── generate-custom-pdf.use-case.ts
│ └── *-location-template-config.use-case.ts # Location config CRUD
├── domain/
│ ├── exceptions/
│ │ └── template.exceptions.ts # TemplateNotFoundError, etc.
│ ├── ports/
│ │ ├── template-repository.port.ts # Repository contract + types
│ │ ├── location-template-config-repository.port.ts
│ │ └── template-audit.port.ts # Audit logging contract
│ ├── services/
│ │ └── template-validator.service.ts # Pure domain validation (no deps)
│ └── value-objects/
│ └── pdf-options.vo.ts # Page size, margins, orientation
└── infrastructure/
├── adapters/
│ ├── puppeteer-pdf-generator.adapter.ts # Headless Chrome PDF gen (retry logic)
│ ├── handlebars-template-renderer.adapter.ts # Handlebars compilation
│ ├── file-system-pdf-storage.adapter.ts # File system storage
│ ├── template-audit.adapter.ts # Kysely-based audit logging
│ └── template-cache.adapter.ts # LRU cache for compiled templates
├── controllers/
│ ├── pdf.controller.ts # POST /pdf/generate, /pdf/preview
│ ├── template.controller.ts # CRUD /pdf/templates/*
│ └── location-template-config.controller.ts # CRUD /pdf/location-template-configs/*
├── queues/
│ └── preview-generation.processor.ts # BullMQ async preview generation
├── repositories/
│ ├── template.repository.ts # Kysely impl of TemplateRepositoryPort
│ └── location-template-config.repository.ts
└── templates/
└── *.hbs # 18 Handlebars file-based templates (legacy)
Dependency Flow
Controllers → Use Cases → Application Services → Domain Ports
↑
Infrastructure Adapters ──────────────────────────────┘
All cross-layer dependencies point inward toward the domain:
- Domain: Zero framework dependencies. Ports (interfaces), exceptions, value objects, pure validators.
- Application: Orchestration. Use cases depend on domain ports via injection. Contains DTOs, transformers, and the template resolver service.
- Infrastructure: Concrete implementations. Adapters implement domain ports. Controllers handle HTTP. Repositories use Kysely.
Key Design Decisions
Template Resolution Chain
TemplateResolverService resolves templates in priority order:
- Explicit template ID — caller passes a specific template UUID
- Location-specific —
location_template_configfor the location + document type - Business-level — default template for the business + document type
- System-level — global system template for the document type
- Fallback — file-based Handlebars template from
infrastructure/templates/
Base Use Case Pattern
All 21 PDF generation use cases extend BaseGeneratePdfUseCase<TEntity>:
- Subclasses implement:
getDocumentType(),getFallbackTemplateName(),getEntityBusinessId(),generateFilename() - Base class handles: template resolution, caching, PDF generation, error handling
- Each use case has a paired
DataTransformerthat maps the entity to template data
Port Bindings (pdf.module.ts)
| Injection Token | Concrete Implementation |
|---|---|
"PdfGeneratorPort" | PuppeteerPdfGeneratorAdapter |
"TemplateRendererPort" | HandlebarsTemplateRendererAdapter |
"PdfStoragePort" | FileSystemPdfStorageAdapter |
TEMPLATE_REPOSITORY_PORT | TemplateRepository |
LOCATION_TEMPLATE_CONFIG_REPOSITORY_PORT | LocationTemplateConfigRepository |
TEMPLATE_AUDIT_PORT | TemplateAuditAdapter |
Puppeteer Retry Logic
PuppeteerPdfGeneratorAdapter uses executeWithRetry() (max 3 attempts) with exponential backoff for both generatePdf and generatePdfFromUrl. Retryable errors: protocol errors, target closed, navigation timeouts.
Template Cache
TemplateCacheAdapter uses an LRU cache (100 entries) for compiled Handlebars templates. Cache key = template UUID. Invalidated on template update/delete.
API Endpoints
PDF Generation (PdfController — /pdf)
| Method | Path | Description |
|---|---|---|
| POST | /pdf/generate | Generate PDF from stored template + data |
| POST | /pdf/generate-from-html | Generate PDF from raw HTML |
| POST | /pdf/preview | Preview template as rendered HTML |
| GET | /pdf/template-files | List file-based templates (legacy) |
| POST | /pdf/templates/clear-cache | Clear file template cache |
| POST | /pdf/templates/:templateId/reload | Reload specific template |
| GET | /pdf/health | Service health check |
Template Management (TemplateController — /pdf/templates)
| Method | Path | Description |
|---|---|---|
| POST | /pdf/templates/upload | Upload new template (rate limited: 10/min) |
| GET | /pdf/templates | List templates with pagination + search |
| GET | /pdf/templates/available | All templates (business + system) |
| GET | /pdf/templates/health | Health check with cache stats (public) |
| GET | /pdf/templates/cache/stats | Cache statistics (public) |
| GET | /pdf/templates/variables/:documentType | Template variables for document type |
| GET | /pdf/templates/:id | Get template by ID |
| PATCH | /pdf/templates/:id | Update template |
| DELETE | /pdf/templates/:id | Soft-delete template |
| POST | /pdf/templates/:id/test | Test template with sample data → PDF |
| POST | /pdf/templates/test | Test template (alt endpoint) |
| POST | /pdf/templates/preview | Preview template rendering as HTML |
| GET | /pdf/templates/:id/audit | Audit history for template |
| POST | /pdf/templates/cache/clear | Clear all template caches |
Location Template Config (LocationTemplateConfigController — /pdf/location-template-configs)
| Method | Path | Description |
|---|---|---|
| POST | /pdf/location-template-configs | Create config (rate limited: 20/min) |
| GET | /pdf/location-template-configs | Get configs by location (query: locationId) |
| GET | /pdf/location-template-configs/by-document-type | Get by location + doc type |
| GET | /pdf/location-template-configs/:id | Get config by ID |
| PATCH | /pdf/location-template-configs/:id | Update config (rate limited: 30/min) |
| DELETE | /pdf/location-template-configs/:id | Delete config (rate limited: 20/min) |
Supported Document Types
All types defined in DocumentType enum (packages/global/enums/document-type.enums.ts):
sale, purchase, purchase_order, quote, cash_register_session, cash_register_z_report, accounts_receivable_receipt, accounts_payable_payment, service_booking, inventory_adjustment, transfer_request, transfer_dispatch_note, transfer_goods_receipt, inventory_transfer, contractor_assignment, goods_received_note, material_consumption, production_run, credit_note, debit_note, cancellation
Bruno API Collections
API collections for testing are at: api-client/flowpos/collections/pdf/
pdf/
├── opencollection.yml
├── Generate PDF from Template.yml
├── Preview HTML (No PDF Generation).yml
├── Generate PDF from HTML Content.yml
├── Check health.yml
├── ...
├── templates/
│ ├── folder.yml
│ ├── templates.yml (list)
│ ├── template - upload.yml
│ ├── template - get by id.yml
│ ├── template - update.yml
│ ├── template - delete.yml
│ ├── template - test.yml
│ ├── template - test (alt).yml
│ ├── template - preview.yml
│ ├── template - available.yml
│ ├── template - variables.yml
│ ├── template - audit.yml
│ ├── health.yml
│ ├── cache stats.yml
│ └── Clear cache.yml
└── location-template-configs/
├── folder.yml
├── create config.yml
├── get by location.yml
├── get by location and type.yml
├── get by id.yml
├── update config.yml
└── delete config.yml