@botler/1403-form-filler-module
v1.2.4
Published
Fills Alberta Form 1403 PDFs for canvassers. Fetches data from Neon PostgreSQL, fills PDF form fields, overlays cursive signatures, flattens to image-PDFs, merges per-batch, and uploads to S3.
Readme
@botler/1403-form-filler-module
Fills Alberta Form 1403 PDFs for canvassers. Fetches data from Neon PostgreSQL, fills AcroForm fields, overlays cursive signatures, flattens to image-PDFs, merges per-batch, generates a CSV manifest of canvasser data, and uploads both to AWS S3.
Usable as a library (import { FormFiller }) or as a CLI (npx fill-1403 --all).
Installation
npm install @botler/1403-form-filler-moduleRequires
NPM_TOKENfor the private@botlerscope (see.npmrc).
Library usage
ESM (.mjs / "type": "module")
import { FormFiller } from "@botler/1403-form-filler-module";
const filler = new FormFiller({
databaseUrl: process.env.WNC_TRACKING_DATABASE_URI,
});
// Single user
const result = await filler.fillForUser("user-abc-123");
console.log(result); // { success: true, userId: "user-abc-123", outputPath: "..." }
// All open batches (fill → flatten → merge → S3 upload)
const results = await filler.fillAll();
// Specific batch by ID (OPEN or FAILED batches only)
const batchResult = await filler.fillForBatch("clx1abc123");
console.log(batchResult); // { batchId, success, results, totalUsers, s3Key, ... }
// Cleanup (closes DB pool if we created it)
await filler.destroy();CJS (require)
const { FormFiller } = require("@botler/1403-form-filler-module");
const filler = new FormFiller({
databaseUrl: process.env.WNC_TRACKING_DATABASE_URI,
});Bring your own Pool
If your app already has a pg.Pool connected to the same database,
pass it in to avoid creating a second connection pool:
import { FormFiller } from "@botler/1403-form-filler-module";
import pg from "pg";
const pool = new pg.Pool({
connectionString: process.env.WNC_TRACKING_DATABASE_URI,
});
const filler = new FormFiller({ pool });
const result = await filler.fillForUser("user-abc-123");
// When pool is injected, destroy() does NOT close it — you manage its lifecycle
await filler.destroy();FormFillerConfig reference
All fields are optional — sensible defaults are applied.
| Field | Type | Default |
| ------------------------ | ----------------- | ------------------------------------------------- |
| pool | pg.Pool | — |
| databaseUrl | string | TEAM_DATABASE_URL → WNC_TRACKING_DATABASE_URI |
| templatePath | string | "s3://coal-forms/1403-template.pdf" |
| outputDirectory | string | "temp/filled-forms" |
| signatureConfig | SignatureConfig | Great Vibes cursive, 60px, black |
| proponentName | string | "Corby Lund" |
| proponentSignatureText | string | "Corby Lund" |
| petitionText | string | "No New Coal Mining in the Eastern Slopes…" |
| s3Bucket | string | "coal-forms" |
| s3KeyPrefix | string | "batches/1403" |
| batchSize | number | 10 |
| dryRun | boolean | false |
| skipExisting | boolean | true |
| keepTemp | boolean | false |
Public API
class FormFiller {
constructor(config?: FormFillerConfig);
/** Fill + flatten the form for a single user */
fillForUser(userId: string): Promise<ProcessingResult>;
/** Fill + flatten for multiple users */
fillForUsers(userIds: string[]): Promise<ProcessingResult[]>;
/** Process all open EA batches (fill → merge → S3) */
fillAll(): Promise<ProcessingResult[]>;
/** Process a specific batch by ID (OPEN or FAILED only) */
fillForBatch(batchId: string): Promise<BatchFillResult>;
/** Release DB pool (no-op if pool was injected) */
destroy(): Promise<void>;
}BatchFillResult
interface BatchFillResult {
batchId: string;
success: boolean;
results: ProcessingResult[];
totalUsers: number;
successfulCount: number;
failedCount: number;
s3Key?: string;
csvS3Key?: string;
error?: string;
}CLI usage
# Via npx (after install)
npx fill-1403 --all
npx fill-1403 --batch-id=clx1abc123
# Or during development
npx tsx src/cli.ts --id=<userId>
npx tsx src/cli.ts --ids=id1,id2,id3
npx tsx src/cli.ts --all --dry-run
npx tsx src/cli.ts --batch-id=<batchId>| Flag | Description |
| ------------------- | ------------------------------------------------------- |
| --id <userId> | Process a single user by ID |
| --ids <id1,id2,…> | Process multiple users (comma-separated) |
| --all | Process all users from open EA submission batches |
| --batch-id <ID> | Process a specific EA submission batch by ID |
| --dry-run | Preview what would be processed without generating PDFs |
| --skip-existing | Skip users whose PDF already exists (default: true) |
| --batch-size <N> | Records per processing batch (default: 10) |
| --keep-temp | Keep per-user PDF files after batch merge + S3 upload |
| --help | Show help |
Environment variables
| Variable | Required | Description |
| --------------------------- | -------- | ----------------------------------------------------- |
| TEAM_DATABASE_URL | | Neon PostgreSQL connection string (primary) |
| WNC_TRACKING_DATABASE_URI | | Fallback Neon connection string (same DB) |
| AWS_S3_ACCESS_KEY_ID | ✅ | AWS access key for S3 uploads |
| AWS_S3_SECRET_ACCESS_KEY | ✅ | AWS secret key for S3 uploads |
| AWS_S3_REGION | | AWS region (default: ca-central-1) |
| LOG_LEVEL | | debug, info, warn, or error (default: info) |
When used as a library, pass
databaseUrlorpoolinFormFillerConfiginstead.
Project structure
form-filler-module/
├── src/
│ ├── index.ts # Library barrel (public API exports)
│ ├── form-filler.ts # FormFiller class (library core)
│ ├── cli.ts # CLI entry point (dotenv, arg parsing)
│ ├── config.ts # CLI argument parsing & validation
│ ├── canvasser-data-mapper.ts # Maps DB profile → PDF filler data
│ ├── types/
│ │ ├── config.ts # FormFillerConfig, ScriptConfig, etc.
│ │ ├── db-tables.ts # Database table interfaces & enums
│ │ ├── canvasser-profile.ts # CanvasserProfile interface
│ │ └── index.ts
│ ├── shared/
│ │ └── logger.ts # Console logger with level filtering
│ ├── csv-exporter/ # CSV manifest generator
│ │ ├── exporter.ts # CsvExporter class (csv-stringify/sync)
│ │ ├── types.ts # CsvRow, CsvExportResult
│ │ └── index.ts
│ ├── pdf-filler/ # Vendored PDF form filler library
│ ├── pdf-converter/ # MuPDF WASM page rasteriser
│ ├── pdf-merger/ # pdf-lib page concatenation
│ ├── s3/ # S3 batch uploader (PDF + CSV)
│ └── db/ # Neon PostgreSQL pool & queries
│ ├── pool.ts # getPool / setPool / closePool
│ ├── queries.ts
│ └── index.ts
├── templates/
│ └── 1403-template.pdf # Form 1403 template (also on S3)
├── dist/ # Built output (published to npm)
├── package.json
├── tsconfig.json
├── .npmrc
└── .env.exampleCSV manifest export
When processing a batch (fillForBatch() or fillAll()), the module automatically generates a CSV manifest containing the canvasser data used to fill each PDF in the batch.
What's in the CSV?
Each row corresponds to one canvasser whose form was successfully filled:
| Column | Description |
| ------------------------ | ------------------------------------------------------ |
| Surname | Canvasser's last name |
| Given Name | Canvasser's first name |
| Middle Name | Middle name (may be empty) |
| Telephone Number | Formatted phone number |
| Email Address | Email (may be empty) |
| Physical Address | Residential street address |
| Physical Municipality | City/town of physical address |
| Physical Postal Code | Postal code of physical address |
| Mailing Address | Mailing street address (empty if same as physical) |
| Mailing Municipality | City/town of mailing address |
| Mailing Postal Code | Postal code of mailing address |
| Canvasser Signature Date | Date canvasser signed (ISO YYYY-MM-DD, may be empty) |
| Proponent Name | Proponent name on the form |
| Proponent Signature Date | Date proponent signed (ISO YYYY-MM-DD, may be empty) |
S3 storage
The CSV is uploaded to the same S3 prefix as the merged PDF, using the same batch ID but with a .csv extension:
s3://coal-forms/batches/1403/{batchId}.pdf ← merged batch PDF
s3://coal-forms/batches/1403/{batchId}.csv ← canvasser data manifestThe CSV S3 key is returned in BatchFillResult.csvS3Key.
Programmatic usage
The CsvExporter class can also be used standalone:
import { CsvExporter } from "@botler/1403-form-filler-module";
const exporter = new CsvExporter();
exporter.addRow(canvasserData);
const csvString = exporter.toCSV(); // in-memory string
await exporter.writeToFile("/tmp/manifest.csv"); // write to diskDevelopment
# Type-check
npm run typecheck
# Build to dist/
npm run build
# Run CLI in dev
npx tsx src/cli.ts --allPublishing
# Ensure NPM_TOKEN is set
export NPM_TOKEN=npm_...
# Build + publish
npm publish --access restrictedLicense
Private — internal use only.
