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 diff --git a/packages/ksef-client-ts/docs/architecture.md b/packages/ksef-client-ts/docs/architecture.md index 3e9fc4e7..0bf1152a 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. 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` | +| `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 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). + +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..75c9d78c 100644 --- a/packages/ksef-client-ts/docs/cli.md +++ b/packages/ksef-client-ts/docs/cli.md @@ -224,6 +224,47 @@ 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, 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 | +|------|-------------| +| `--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