Loan Agreement Generation

Overview

The LOC Service generates loan agreement PDFs using LaTeX templates. The agreements are generated when a loan application is approved and include all required disclosures, loan terms, and installment schedules.

Architecture

Loan Application Approved
         │
         ▼
┌─────────────────────────┐
│     API Lambda          │
│  (generates event)      │
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐      ┌─────────────────────────┐
│      SQS Queue          │  OR  │    Direct Invoke        │
│  (async generation)     │      │  (sync generation)      │
└───────────┬─────────────┘      └───────────┬─────────────┘
            │                                │
            └────────────┬───────────────────┘
                         │
                         ▼
            ┌─────────────────────────┐
            │  Agreements Generator   │
            │       Lambda            │
            └───────────┬─────────────┘
                        │
            ┌───────────┼───────────┐
            │           │           │
            ▼           ▼           ▼
    ┌────────────┐ ┌────────┐ ┌────────────┐
    │   LaTeX    │ │Tectonic│ │    S3      │
    │  Template  │ │ Render │ │  Upload    │
    └────────────┘ └────────┘ └────────────┘

Components

Agreements Generator Lambda (cmd/agreements-generator)

The Lambda function handles both SQS and direct invocation:

  • SQS Mode: Processes batch messages from the queue

  • Direct Invoke Mode: Returns the S3 key immediately for synchronous workflows

LaTeX Renderer (pkg/latex/latex.go)

The core rendering engine that:

  1. Copies embedded LaTeX templates to a temp directory

  2. Injects loan data into the template using Go’s text/template

  3. Executes Tectonic to render the PDF

  4. Returns the generated PDF bytes

Worker (pkg/latex/worker.go)

Orchestrates the generation process:

  1. Receives the generation event

  2. Calls the renderer to generate the PDF

  3. Uploads the PDF to S3

  4. Updates the loan application with the S3 key

Template System

Template Files

The templates are embedded in the binary using Go’s embed package:

File Purpose

loan_agreement.tex

Main LaTeX template with placeholders

Onest-Regular.ttf

Primary font

Onest-SemiBold.ttf

Bold font variant

floatme_logo.png

Company logo for header

tectonic-render

Tectonic binary for PDF rendering

Template Delimiters

The template uses custom delimiters to avoid conflicts with LaTeX syntax:

|@ .FieldName @|

Example in the template:

\newcommand{\BorrowerName}{|@ .Name @|}
\newcommand{\LoanNumber}{|@ .LoanNumber @|}
\newcommand{\PrincipalAmount}{\$|@ .LoanAmount @|}

Template Data Structure

The AgreementGenerateEvent struct defines all available template fields:

type AgreementGenerateEvent struct {
    UserID            string `json:"user_id"`
    LoanApplicationID string `json:"loan_application_id"`

    // Borrower Information
    Name         string `json:"name"`
    Address      string `json:"address"`
    CityStateZip string `json:"city_state_zip"`
    PhoneNumber  string `json:"phone_number"`

    // Loan Identification
    LoanNumber   string `json:"loan_number"`
    LoanDate     string `json:"loan_date"`

    // Loan Amounts
    LoanAmount      string `json:"loan_amount"`      // Principal
    TotalAmount     string `json:"total_amount"`     // Total to repay
    AmountDisbursed string `json:"amount_disbursed"` // Net to borrower

    // Fee Information
    APR                   string `json:"apr"`
    AdminFee              string `json:"admin_fee"`
    OriginationFee        string `json:"origination_fee"`
    OriginationFeePercent string `json:"origination_fee_percent"`

    // Installment Information
    NumInstallments    int                `json:"num_installments"`
    InstallmentSummary string             `json:"installment_summary"`
    InstallmentStart   string             `json:"installment_start"`
    InstallmentEnd     string             `json:"installment_end"`
    Installments       []LoanInstallments `json:"installments"`
}

type LoanInstallments struct {
    Amount string `json:"amount"`
    Due    string `json:"due"`
}

LaTeX Escaping

User-provided data must be escaped to prevent LaTeX injection and rendering errors. The Escape function handles special characters:

var replacements = []string{
    "&", `\&`,
    "%", `\%`,
    "$", `\$`,
    "#", `\#`,
    "_", `\_`,
    "{", `\{`,
    "}", `\}`,
    "~", `\textasciitilde`,
    "^", `\textasciicircum`,
    `\`, `\textbackslash`,
}

All string fields in the event are automatically escaped before template evaluation.

Rendering Process

Step 1: Prepare Temp Directory

tmp, err := os.MkdirTemp("", "*-loan-agreement")
defer os.RemoveAll(tmp)

Step 2: Copy Embedded Files

All template files are copied from the embedded filesystem to the temp directory:

  • LaTeX source files

  • Font files

  • Logo image

  • Tectonic binary

Step 3: Inject Data

The template is evaluated with the escaped data:

tmpl, err := template.New(fileName).Delims("|@", "@|").Parse(string(templateData))
err = tmpl.Execute(writer, data)

Step 4: Render PDF

Tectonic is executed to compile the LaTeX to PDF:

cmd := exec.CommandContext(ctx,
    filepath.Join(tmp, "templates/tectonic-render"),
    "--only-cached",
    filepath.Join(tmp, agreementTemplateFile))

The --only-cached flag ensures Tectonic doesn’t try to download packages at runtime.

Step 5: Upload to S3

The generated PDF is uploaded to S3 with a structured key:

loan-agreements/<user_id>/<loan_application_id>.pdf

S3 Storage

Bucket Configuration

Environment Bucket

test

test-loc-agreements

prod

prod-loc-agreements

Object Key Format

loan-agreements/{user_id}/{loan_application_id}.pdf

Error Handling

Rendering Errors

If Tectonic fails to render the PDF, a RenderingError is returned with:

  • Standard error output from Tectonic

  • Standard output (compilation logs)

  • The underlying error

This helps diagnose template issues.

SQS Batch Failures

When processing SQS batches, individual message failures are reported:

failures = append(failures, sqs.NewSQSBatchItemFailure(msg.MessageId))

Failed messages will be retried or sent to a dead-letter queue.

Updating the Template

To modify the loan agreement template:

  1. Edit pkg/latex/templates/loan_agreement.tex

  2. Un-skip the TestRenderLoanAgreement unit test and run the unit test

  3. Ensure all placeholders use |@ .FieldName @| format

  4. Verify special characters are properly escaped

Adding New Fields

  1. Add the field to AgreementGenerateEvent struct

  2. Add the placeholder to loan_agreement.tex

  3. Populate the field when creating the event

  4. Add escaping if the field contains user-provided data

Testing

Local Testing

Generate a test PDF locally:

renderer := latex.NewRenderer()
pdf, err := renderer.RenderLoanAgreement(ctx, &latex.AgreementGenerateEvent{
    Name:         "John Doe",
    LoanNumber:   "FM-000001",
    LoanAmount:   "500.00",
    // ... other fields
})

Unit Tests

The pkg/latex/latex_test.go file contains tests for:

  • Template rendering with valid data

  • LaTeX character escaping

  • Error handling for invalid templates

Dependencies

Tectonic

Tectonic is a modernized TeX/LaTeX engine that:

  • Self-contained (no external TeX installation needed)

  • Caches packages locally

  • Produces consistent output

The binary is embedded in the Lambda deployment package.

Fonts

The template uses the Onest font family:

  • Onest-Regular.ttf - Body text

  • Onest-SemiBold.ttf - Headers and emphasis

Code Location

Component Path

Lambda Handler

cmd/agreements-generator/main.go

Renderer

pkg/latex/latex.go

Worker

pkg/latex/worker.go

Escaping

pkg/latex/escape.go

LaTeX Template

pkg/latex/templates/loan_agreement.tex

Fonts

pkg/latex/templates/*.ttf

Logo

pkg/latex/templates/floatme_logo.png