Skip to content

PDF Builder Templates

Date: 2026-05-29

Problem / motivation

The visual PDF builder on the frontend needs a place to persist the document layouts a tenant designs. Today pdf_templates stores which header/footer metadata to render per document type (an array of DocumentMetadata IDs), but it cannot hold the full builder layout — the drag-and-drop canvas state, block configuration, styling, and bindings the builder produces. That layout is a free form JSON blob, it is versioned (the builder schema evolves), and it is scoped to a document entity type (QUOTE, ORDER, INVOICE, …).

This feature adds a dedicated PDF Builder Templates module that stores one versioned JSON layout per builder template, tagged by entity type.

In scope

  • New top-level resource pdf_builder_templates.
  • Fields: name (str), value (JSONB layout payload), version (str), entity_type (reuses PdfSourceType: INVOICE, ORDER, QUOTE, …), is_default (bool — at most one default per entity type).
  • Full module chain: model → repository → service → GraphQL (input, response, queries, mutations) → schema auto-mount → migration → tests → docs.

Out of scope

  • Actually rendering a PDF from the stored layout (the builder/renderer consumes value separately — this module only persists it).
  • RBAC Path/Resource members. Mirrors the sibling pdf_templates module, which is settings-level configuration and does not own a dedicated RBAC path.
  • Migrating any data from pdf_templates; the two coexist.

Data model changes

New table pdf_builder_templates:

column type notes
id uuid PK gen_random_uuid()
name varchar(255) not null human-readable template name
entity_type smallint not null, index PdfSourceType enum (QUOTE, ORDER, …)
value jsonb not null builder layout payload
version varchar(50) not null builder schema/template version string
is_default boolean not null default template for its entity type
created_at timestamptz not null now()

A partial unique index (entity_type WHERE is_default) enforces at most one default template per entity type.

Alembic revisions:

  • add_pdf_builder_templates_table (down_revision add_sales_safety_tier_1) — creates the table.
  • add_is_default_to_pdf_builder_templates (down_revision add_pdf_builder_templates_table) — adds is_default and the one-default-per-entity-type partial unique index.

GraphQL surface

  • Query pdfBuilderTemplates(includeEntityType: PdfSourceType): [PdfBuilderTemplate!]!
  • Query pdfBuilderTemplate(id: UUID!): PdfBuilderTemplate
  • Query defaultPdfBuilderTemplate(entityType: PdfSourceType!): PdfBuilderTemplate — the current default for an entity type
  • Query pdfBuilderTemplatesByEntityType(entityType: PdfSourceType!): [PdfBuilderTemplate!]!
  • Mutation createPdfBuilderTemplate(template: PdfBuilderTemplateInput!): PdfBuilderTemplate!
  • Mutation updatePdfBuilderTemplate(templateId: UUID!, template: PdfBuilderTemplateInput!): PdfBuilderTemplate!
  • Mutation deletePdfBuilderTemplate(templateId: UUID!): Boolean!

Types: PdfBuilderTemplate (response), PdfBuilderTemplateInput (input).

RBAC

None. Mirrors pdf_templates (no dedicated Path/Resource). No backfill script required.

Background tasks / cron

None.

Frontend contract

  • The builder serializes its canvas to a JSON object and sends it as value.
  • version is the builder's template-schema version string (e.g. "1"), so the renderer can branch on layout shape over time.
  • entityType is one of the PdfSourceType enum values.
  • List/create/update/delete + fetch-by-entity-type are all available, so the builder can show "templates for Quotes", load one to edit, save a new version, and delete.
  • Set isDefault: true on create/update to mark a template the default for its entity type; the backend automatically demotes any previous default for that type (one default per entity type). Read the current default with defaultPdfBuilderTemplate(entityType: …) — this is what the renderer should resolve when no specific template is chosen.

What's being implemented

  • Model: pdf_builder_template.py
  • Repository: app/graphql/pdf_builder_templates/repositories/pdf_builder_template_repository.py
  • Service: app/graphql/pdf_builder_templates/services/pdf_builder_template_service.py
  • Input/Response: app/graphql/pdf_builder_templates/strawberry/
  • Queries/Mutations: app/graphql/pdf_builder_templates/queries/, .../mutations/
  • Model registration in load_models.py
  • Migration alembic/versions/20260529_add_pdf_builder_templates_table.py
  • Tests under tests/graphql/pdf_builder_templates/

What shipped

Everything in scope above. The module is fully wired and discoverable through the auto-discovery in query.py / mutation.py and the DI provider discovery.

Future additions

  • RBAC path — if PDF builder access needs to be gated independently of general settings, add a Path/Resource pair and a backfill. Deferred because the sibling pdf_templates module has no dedicated path and the builder is settings-level today.
  • Renderer integration — consume value in the PDF generation pipeline. Deferred: out of scope; the renderer team owns that contract.