From 1e3ad8b94760a5d57dc9ff9f0e58d4a02163ebce Mon Sep 17 00:00:00 2001 From: FlopBut Date: Sun, 30 Aug 2026 23:30:41 +0200 Subject: [PATCH 1/5] docs: cover the PDF module where the docs already look for it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PDF export shipped with its own guide, a homepage card and a README bullet, but the three pages a reader consults *about* the library rather than about the feature never learned it existed: the CLI reference listed every `ksef invoice` subcommand except `pdf`, the architecture page walked every layer except `src/pdf/`, and the test inventory stopped at spec 34 while carrying unit counts from several releases ago. - CLI: `ksef invoice pdf` gets the section its siblings have — what it does offline, the flag table, and the optional peer it needs to render at all. - Architecture: the PDF layer alongside the others, said plainly to be the one layer `KSeFClient` does not reach and the package root does not export, with the entry-point split that follows from `pdfmake` being optional — including what that split means for catching its errors. - Tests: specs 35 and 36 in the suite table, a note on why they are the two that authenticate against nothing and leave a reviewable set of pages behind, the `pdf` row in the unit coverage table, and counts that match what the suite actually runs today. Every flag, default, template name and count here was read off the current code or a run of it, not off the feature guide. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M7sLete4AKPr5oHz2cMFqM --- packages/ksef-client-ts/docs/architecture.md | 31 ++++++++++++++++++ packages/ksef-client-ts/docs/cli.md | 34 ++++++++++++++++++++ packages/ksef-client-ts/docs/tests.md | 13 ++++++-- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/ksef-client-ts/docs/architecture.md b/packages/ksef-client-ts/docs/architecture.md index 3e9fc4e7..3435b67e 100644 --- a/packages/ksef-client-ts/docs/architecture.md +++ b/packages/ksef-client-ts/docs/architecture.md @@ -70,6 +70,7 @@ src/ ├── qr/ # QR code + verification link generation ├── offline/ # Offline invoice mode (types, deadlines, storage) ├── xml/ # Invoice XML layer (UPO parser, field extractor, serialization) +├── pdf/ # PDF rendering layer, its own entry point (see below) ├── errors/ # Error hierarchy (see below) ├── validation/ # Regex patterns, checksum validators, constraints ├── workflows/ # High-level orchestration (auth, sessions, export, polling) @@ -215,6 +216,36 @@ See [Offline Mode](./offline-mode.md) for usage. --- +## PDF Layer (`src/pdf/`) + +Renders an invoice or a UPO receipt to a print-ready PDF. It is the one layer that is **not** reachable from `KSeFClient` and not exported from the package root: it ships as its own entry point, `ksef-client-ts/pdf`, because `pdfmake` is an optional peer. Importing the subpath without it does not throw — only a render call does, with an install hint — and nothing in the public type surface depends on `@types/pdfmake`, so consumers who never render still type-check. + +Nothing here calls the API. A render reads a local document and produces bytes. + +| File | Purpose | +|------|---------| +| `index.ts` | The public surface: `renderInvoicePdf`, `renderInvoicePdfFromTemplate`, `renderInvoicePdfFromFile`, `renderUpoPdf`, `getBuiltinTemplate`, `builtinTemplateNames`, plus the error classes this entry point throws | +| `parse.ts` | Reads the XML into a plain tree and detects which document it is — `FA(2)`/`FA(3)` or `UPO(4.2)`/`UPO(4.3)` — from the namespace the root element is bound to | +| `template/dsl.ts` | The template DSL: block/field types and the zod schema that validates an untrusted template, throwing `KSeFValidationError` | +| `template/interpret.ts` | Walks a validated template against the document, resolving bindings, `when` conditions and repeaters into pdfmake content | +| `template/blocks/*.ts` | One renderer per block kind — `header`, `parties`, `lines`, `totals`, `payment`, `annotations`, `notes`, `qr`, `table`, `each`, `image`, `footer` | +| `template/builtin/` | The five shipped layouts as JSON: `fa2-default`, `fa3-default`, `upo-4_2`, `upo-4_3`, and `fa3-showcase` (which exists to exercise the DSL, not to be used on a real invoice) | +| `document-flags.ts` | Facts a template gates on rather than computes: which of `P_15`'s readings this document supports, how much of it has been paid, what kind of invoice it is | +| `accessor.ts` | Dot-path reads over the parsed tree, with the strict/lenient distinction a template's `optional` marks rely on | +| `format.ts` | Value formatters (`money`, `date`, `number`, `nip`, `paymentForm`) and decimal-safe summing | +| `i18n/` | Label bundles for Polish, English and Ukrainian, plus the resolver that pairs two of them for a bilingual render | +| `qr.ts` | Derives the Code I verification URL from the document, hashing the *original* bytes so the digest matches what KSeF registered | +| `fonts.ts` | Lazily loads `pdfmake` and its font VFS, and turns a document definition into bytes | +| `errors.ts` | `KSeFPdfError` — a missing or incompatible `pdfmake`, a template/document version mismatch, a nesting overflow | + +**Templates are data, not code.** The DSL has bindings, conditions, repeaters and formatters, and deliberately no scripting: a template can only name paths the document already carries. + +**Error identity across entry points.** Each entry point is bundled separately, so `/pdf` carries its own copies of the error classes. `KSeFError` recognises its own kind by a registered symbol and therefore matches across them, which keeps one catch-all working; a *subclass* check is per entry point, so import `KSeFValidationError` and `KSeFPdfError` from `ksef-client-ts/pdf` when telling one failure apart from another. See [Error Handling](./error-handling.md). + +See [PDF Export](./pdf-export.md) for the usage guide and the template reference, and [CLI](./cli.md#render-a-pdf) for `ksef invoice pdf`. + +--- + ## Models (`src/models/`) TypeScript interfaces organized by API domain. No runtime code — types only. diff --git a/packages/ksef-client-ts/docs/cli.md b/packages/ksef-client-ts/docs/cli.md index 0e7e5679..0dc79a2a 100644 --- a/packages/ksef-client-ts/docs/cli.md +++ b/packages/ksef-client-ts/docs/cli.md @@ -224,6 +224,40 @@ ksef invoice export-status # Check export status | `--page ` | Page offset (0-based) | | `--size ` | Page size | +### Render a PDF + +Turns an invoice or a UPO receipt into a print-ready PDF, entirely offline — nothing here calls KSeF. The document kind is detected from the file, so a receipt does not need `--upo`. + +```bash +ksef invoice pdf faktura.xml # → faktura.pdf, Polish labels +ksef invoice pdf faktura.xml --locale en+pl --out invoice.pdf # bilingual, explicit output path +ksef invoice pdf faktura.xml --qr --ksef-number # with the KSeF Code I verification QR +ksef invoice pdf upo.xml --out upo.pdf # a receipt, detected as such +``` + +Rendering needs the optional `pdfmake` peer (`npm i "pdfmake@^0.2.20"`); without it the command exits with an install hint instead of writing a file. + +| Flag | Description | +|------|-------------| +| `--template ` | Built-in template: `fa2-default`, `fa3-default`, `fa3-showcase`, `upo-4_2`, `upo-4_3`. Default: matched to the document's schema version | +| `--template-file ` | A custom JSON template instead of a built-in. Mutually exclusive with `--template` | +| `--locale ` | `pl` (default), `en`, `uk`, or any two joined by `+` (`pl+uk`, `en+pl`) for side-by-side labels | +| `--out ` | Output path. Default: alongside the source, with `.pdf` | +| `--totals ` | Tax breakdown above the amount due: `none`, `buckets` (as recorded, default), `summary` (computed), `both` | +| `--qr` | Embed the KSeF Code I QR, derived from the document | +| `--qr-url ` | Use this Code I URL verbatim instead of deriving one | +| `--qr-cert-url ` | Code II (offline certificate) URL — build it with `ksef qr certificate` | +| `--qr-links` | Print a clickable link under each QR code | +| `--ksef-number ` | KSeF number to print. Without it the page is marked OFFLINE | +| `--env ` | Environment the QR verification host comes from: `test`, `demo`, `prod` | +| `--logo ` | Logo image for the header (PNG or JPEG — pdfmake draws no other format) | +| `--accent ` | Accent colour for the title and headings (`#5AB595` or `#b04`) | +| `--notes ` | JSON file of extra sections: `[{ "head": "…", "body": "…" }, …]`; either half may be omitted | +| `--upo` | Treat the input as a UPO receipt instead of auto-detecting | +| `--json` | Report the result as JSON | + +See [PDF Export](./pdf-export.md) for the template DSL, the built-in layouts, and the library API behind this command. + ## Permissions ### Grant Permissions diff --git a/packages/ksef-client-ts/docs/tests.md b/packages/ksef-client-ts/docs/tests.md index 58ce0076..b5082b78 100644 --- a/packages/ksef-client-ts/docs/tests.md +++ b/packages/ksef-client-ts/docs/tests.md @@ -22,13 +22,13 @@ yarn vitest run tests/unit/services/auth.test.ts # Single file ## Unit Tests -126 test files, 2165 passed / 9 skipped (2174 total). Located in `tests/unit/`. All service/HTTP calls are mocked — no network access, fast execution. +157 test files, 2905 tests. Located in `tests/unit/`. All service/HTTP calls are mocked — no network access, fast execution. ### Coverage by Area | Area | Files | What is tested | |------|-------|----------------| -| **cli** | 32 | All 17 command groups, client factory, config/session store, error handler, output formatting | +| **cli** | 34 | All 17 command groups, client factory, config/session store, error handler, output formatting | | **services** | 14 | All 14 API services — request construction, response parsing, error propagation | | **http** | 10 | RestClient, RetryPolicy (backoff, jitter), RateLimitPolicy (token bucket), PresignedUrlPolicy, AuthManager (401 refresh), transport, RestRequest builder, KSeF feature constants, circuit breaker | | **workflows** | 10 | Auth workflow, online/batch session, invoice export, incremental export, HWM coordinator, polling utility | @@ -36,6 +36,7 @@ yarn vitest run tests/unit/services/auth.test.ts # Single file | **crypto** | 8 | CryptographyService (AES, RSA, ECDH, CSR), SignatureService (XAdES), CertificateService (self-signed), CertificateFetcher, PKCS#12 loader, auth XML builder | | **validation** | 10 | Regex patterns + checksum validators (NIP, PESEL, KSeF number CRC-8), constraints, char validity, XSD validation helpers | | **xml** | 9 | FA2/FA3/PEF builders, invoice serializer, XSD validation, property ordering | +| **pdf** | 29 | Template DSL validation and interpretation, every block renderer, the built-in layouts against fixtures, document flags, formatters, i18n bundles, QR derivation and sizing, font loading, strict mode | | **utils** | 7 | Concurrency helpers, filesystem utilities, hashing, date/time helpers | | **builders** | 5 | AuthTokenRequest, AuthKsefTokenRequest, InvoiceQueryFilter, Permissions (person/entity/authorization), batch file | | **offline** | 4 | Offline invoice deadlines, file storage, holiday calendar, workflow orchestration | @@ -108,6 +109,14 @@ This means tests can run on any machine, any CI, without configuring credentials | 32 | `32-offline-invoice.test.ts` | Offline invoice lifecycle | Cert + Crypto | 300s | | 33 | `33-xml-serialization.test.ts` | Invoice XML serialization round-trip | None | 60s | | 34 | `34-collective-identifiers.test.ts` | Collective identifier lifecycle | Cert + Crypto | 180s | +| 35 | `35-invoice-pdf-cli.test.ts` | `ksef invoice pdf` through the built CLI: the whole preview set, plus the inputs it refuses | None | 120s | +| 36 | `36-invoice-pdf-library.test.ts` | The same rendering through `ksef-client-ts/pdf`: template objects, supplied QR URLs, every render option | None | 120s | + +### The PDF Specs (35, 36) + +Both are exceptions to everything above: they authenticate against nothing and reach the network at no point — a render reads a local document and writes bytes. They need `yarn build` first, because 35 drives the built CLI (`dist/cli.js`) and 36 imports the built package. + +They also leave something behind on purpose. Each writes a numbered set of PDFs into `.pdf-preview/` (override with `KSEF_PDF_OUT`), distinguished by a `cli-`/`lib-` prefix, so the pages can be opened and judged by eye after a run. The assertions themselves are deliberately shallow — a file appears and is a structurally complete PDF — because asserting on glyph positions breaks on every deliberate design change while saying nothing about whether the page reads well. What they do catch is the class of failure unit tests cannot see: a template that stops validating at import, a bundling regression that drops the fonts, a flag that stops being wired, an optional peer that fails to load. ### Auth Helpers From 817154795e9145ef1430ec8a06313f00d763ae38 Mon Sep 17 00:00:00 2001 From: FlopBut Date: Sun, 30 Aug 2026 23:40:37 +0200 Subject: [PATCH 2/5] docs(changelog): date 0.12.0 The section has carried everything it is going to carry, so it stops being "Unreleased" and names the day it goes out. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M7sLete4AKPr5oHz2cMFqM --- packages/ksef-client-ts/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ksef-client-ts/CHANGELOG.md b/packages/ksef-client-ts/CHANGELOG.md index a56f09b3..ff7ab5b7 100644 --- a/packages/ksef-client-ts/CHANGELOG.md +++ b/packages/ksef-client-ts/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## [0.12.0] - Unreleased +## [0.12.0] - 2026-08-31 ### Added From db6c877f9a6b6ccafc77c1b1f59627acc90f3fda Mon Sep 17 00:00:00 2001 From: FlopBut Date: Sun, 30 Aug 2026 23:47:06 +0200 Subject: [PATCH 3/5] docs(cli): say where the pdfmake peer has to be installed The PDF section told the reader to run `npm i "pdfmake@^0.2.20"`, two screens below the page's own instruction to install the CLI globally. The peer is resolved from the installed package's location, not from the working directory, so that combination installs pdfmake somewhere the CLI never looks and the command keeps reporting a missing peer. Both forms are now given, with the rule that decides between them. Checked by resolving `pdfmake/build/pdfmake.js` from a package placed in a global-style tree: it finds a peer installed beside it, which is what `npm i -g` produces, and a project-local install in some other directory is not on that search path at all. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M7sLete4AKPr5oHz2cMFqM --- packages/ksef-client-ts/docs/cli.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/ksef-client-ts/docs/cli.md b/packages/ksef-client-ts/docs/cli.md index 0dc79a2a..75c9d78c 100644 --- a/packages/ksef-client-ts/docs/cli.md +++ b/packages/ksef-client-ts/docs/cli.md @@ -235,7 +235,14 @@ ksef invoice pdf faktura.xml --qr --ksef-number # with the KSeF Co ksef invoice pdf upo.xml --out upo.pdf # a receipt, detected as such ``` -Rendering needs the optional `pdfmake` peer (`npm i "pdfmake@^0.2.20"`); without it the command exits with an install hint instead of writing a file. +Rendering needs the optional `pdfmake` peer, and it has to sit where the CLI itself sits: the module is resolved from the installed package's own location, never from the working directory. Installing it into the current project does nothing for a CLI installed globally. + +```bash +npm i -g "pdfmake@^0.2.20" # the CLI was installed globally (see Installation above) +npm i "pdfmake@^0.2.20" # the CLI comes from this project's node_modules +``` + +Without it the command exits with an install hint instead of writing a file. | Flag | Description | |------|-------------| From 51b8ad80a0f38321ffbaf7e49940e4fcdb47f489 Mon Sep 17 00:00:00 2001 From: FlopBut Date: Sun, 30 Aug 2026 23:47:33 +0200 Subject: [PATCH 4/5] docs(architecture): describe both version detections, not one The PDF layer table said the document version comes from the namespace the root element is bound to. That is true of a UPO and false of an invoice: an invoice is read from the kodSystemowy attribute on KodFormularza and from WariantFormularza, which must agree with each other, and a contributor going by this line would have looked for a namespace check that is not there. Both mechanisms are now stated separately, matching detectInvoiceVersion and detectUpoVersion in src/pdf/parse.ts. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M7sLete4AKPr5oHz2cMFqM --- packages/ksef-client-ts/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ksef-client-ts/docs/architecture.md b/packages/ksef-client-ts/docs/architecture.md index 3435b67e..c259c37c 100644 --- a/packages/ksef-client-ts/docs/architecture.md +++ b/packages/ksef-client-ts/docs/architecture.md @@ -225,7 +225,7 @@ Nothing here calls the API. A render reads a local document and produces bytes. | File | Purpose | |------|---------| | `index.ts` | The public surface: `renderInvoicePdf`, `renderInvoicePdfFromTemplate`, `renderInvoicePdfFromFile`, `renderUpoPdf`, `getBuiltinTemplate`, `builtinTemplateNames`, plus the error classes this entry point throws | -| `parse.ts` | Reads the XML into a plain tree and detects which document it is — `FA(2)`/`FA(3)` or `UPO(4.2)`/`UPO(4.3)` — from the namespace the root element is bound to | +| `parse.ts` | Reads the XML into a plain tree and detects which document it is. An invoice says so twice — `Naglowek.KodFormularza`'s `kodSystemowy` attribute and `WariantFormularza` — and both must agree or the document is refused, since a mismatched pair would resolve every binding against the wrong schema. A UPO carries no such field, so its version comes from the namespace its root element is bound to | | `template/dsl.ts` | The template DSL: block/field types and the zod schema that validates an untrusted template, throwing `KSeFValidationError` | | `template/interpret.ts` | Walks a validated template against the document, resolving bindings, `when` conditions and repeaters into pdfmake content | | `template/blocks/*.ts` | One renderer per block kind — `header`, `parties`, `lines`, `totals`, `payment`, `annotations`, `notes`, `qr`, `table`, `each`, `image`, `footer` | From 3642d39bc7bdebdca5d70105a9c98623837b30b0 Mon Sep 17 00:00:00 2001 From: FlopBut Date: Sun, 30 Aug 2026 23:48:07 +0200 Subject: [PATCH 5/5] docs(architecture): name every binding a template can reach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "A template can only name paths the document already carries" described half of what a template sees. The renderer also exposes the resolved options and the flags derived from them, and the shipped layouts lean on both: the header prints a logo it was handed, the totals gate on which reading of P_15 this document supports, the page marks itself OFFLINE when no KSeF number was given. A reader taking the old sentence at face value would think none of that was possible without a code change. The claim that matters — data, not script — is unchanged, and now says what the data covers. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M7sLete4AKPr5oHz2cMFqM --- packages/ksef-client-ts/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ksef-client-ts/docs/architecture.md b/packages/ksef-client-ts/docs/architecture.md index c259c37c..0bf1152a 100644 --- a/packages/ksef-client-ts/docs/architecture.md +++ b/packages/ksef-client-ts/docs/architecture.md @@ -238,7 +238,7 @@ Nothing here calls the API. A render reads a local document and produces bytes. | `fonts.ts` | Lazily loads `pdfmake` and its font VFS, and turns a document definition into bytes | | `errors.ts` | `KSeFPdfError` — a missing or incompatible `pdfmake`, a template/document version mismatch, a nesting overflow | -**Templates are data, not code.** The DSL has bindings, conditions, repeaters and formatters, and deliberately no scripting: a template can only name paths the document already carries. +**Templates are data, not code.** The DSL has bindings, conditions, repeaters and formatters, and deliberately no scripting: a template can only name bindings the renderer already offers. Those are paths into the parsed document, the resolved render options (`opts.logo`, `opts.ksefNumber`, the QR URLs, the caller's notes), and the flags derived from both — which of `P_15`'s readings applies, whether the invoice is part-paid, whether it has a KSeF number yet. **Error identity across entry points.** Each entry point is bundled separately, so `/pdf` carries its own copies of the error classes. `KSeFError` recognises its own kind by a registered symbol and therefore matches across them, which keeps one catch-all working; a *subclass* check is per entry point, so import `KSeFValidationError` and `KSeFPdfError` from `ksef-client-ts/pdf` when telling one failure apart from another. See [Error Handling](./error-handling.md).