trustrender 0.3.3__tar.gz → 0.3.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {trustrender-0.3.3/src/trustrender.egg-info → trustrender-0.3.5}/PKG-INFO +82 -35
- {trustrender-0.3.3 → trustrender-0.3.5}/README.md +81 -34
- {trustrender-0.3.3 → trustrender-0.3.5}/pyproject.toml +8 -2
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/__init__.py +50 -42
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/cli.py +52 -39
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/contract.py +4 -9
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/dashboard.py +1 -4
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/doctor.py +5 -16
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/engine.py +104 -14
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/fingerprint.py +145 -104
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/invoice_aliases.py +59 -38
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/invoice_ingest.py +351 -265
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/invoice_schema.py +7 -6
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/provenance.py +8 -2
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/readiness.py +153 -119
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/regression.py +112 -98
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/semantic.py +172 -129
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/server.py +102 -26
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/trace.py +24 -27
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/zugferd.py +355 -175
- {trustrender-0.3.3 → trustrender-0.3.5/src/trustrender.egg-info}/PKG-INFO +82 -35
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_adapters.py +99 -76
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_adversarial_corpus.py +48 -30
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_audit_e2e.py +63 -36
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_contract.py +32 -34
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_doctor.py +21 -22
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_engine.py +154 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_fingerprint.py +3 -7
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_fingerprint_stress.py +27 -19
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_fonts.py +6 -18
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_invoice_ingest.py +106 -68
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_pagination.py +0 -1
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_provenance.py +9 -6
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_readiness.py +8 -12
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_real_payloads.py +6 -4
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_regression.py +26 -37
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_regression_stress.py +51 -34
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_semantic.py +20 -23
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_semantic_stress.py +8 -2
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_server.py +7 -1
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_templates.py +5 -12
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_trace.py +2 -4
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_ugly_data.py +5 -18
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_ugly_data_pressure.py +31 -28
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_zugferd.py +173 -25
- {trustrender-0.3.3 → trustrender-0.3.5}/LICENSE +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/setup.cfg +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/adapters/__init__.py +1 -1
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/adapters/shopify.py +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/adapters/stripe.py +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/einvoice.j2.typ +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/einvoice_data.json +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/invoice.j2.typ +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/invoice_data.json +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/letter.j2.typ +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/letter_data.json +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/receipt.j2.typ +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/receipt_data.json +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/report.j2.typ +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/report_data.json +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/statement.j2.typ +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/builtin_templates/statement_data.json +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/errors.py +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/filters.py +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/fonts/Inter/Inter-Bold.ttf +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/fonts/Inter/Inter-BoldItalic.ttf +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/fonts/Inter/Inter-Italic.ttf +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/fonts/Inter/Inter-Regular.ttf +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/fonts/Inter/LICENSE.txt +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/playground/assets/index-Bsf6YnD1.css +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/playground/assets/index-C6K0JsLw.js +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/playground/assets/pdf.worker-C1breYqn.mjs +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/playground/favicon.svg +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/playground/icons.svg +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/playground/index.html +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender/templates.py +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender.egg-info/SOURCES.txt +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender.egg-info/dependency_links.txt +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender.egg-info/entry_points.txt +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender.egg-info/requires.txt +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/src/trustrender.egg-info/top_level.txt +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_cli.py +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_error_pipeline.py +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_errors.py +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_filters.py +0 -0
- {trustrender-0.3.3 → trustrender-0.3.5}/tests/test_render.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trustrender
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Validate and normalize Stripe, Shopify, and custom billing data for Factur-X/ZUGFeRD
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://trustrender.dev
|
|
@@ -42,6 +42,11 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
# TrustRender
|
|
44
44
|
|
|
45
|
+
[](https://pypi.org/project/trustrender/)
|
|
46
|
+
[](https://pypi.org/project/trustrender/)
|
|
47
|
+
[](https://github.com/verityengine/trustrender/actions)
|
|
48
|
+
[](https://opensource.org/licenses/MIT)
|
|
49
|
+
|
|
45
50
|
Validate and normalize billing data before Factur-X/ZUGFeRD embedding.
|
|
46
51
|
|
|
47
52
|
## Quick start
|
|
@@ -59,7 +64,7 @@ Invoice: INV-2026-0187
|
|
|
59
64
|
From:
|
|
60
65
|
To: Rheingold Maschinenbau GmbH
|
|
61
66
|
Items: 3
|
|
62
|
-
Total: $2,
|
|
67
|
+
Total: $2,945.25
|
|
63
68
|
|
|
64
69
|
BLOCKED — 1 problem(s)
|
|
65
70
|
|
|
@@ -95,7 +100,7 @@ Invoice: INV-2026-0187
|
|
|
95
100
|
From: NovaTech Solutions GmbH
|
|
96
101
|
To: Rheingold Maschinenbau GmbH
|
|
97
102
|
Items: 3
|
|
98
|
-
Total: $2,
|
|
103
|
+
Total: $2,945.25
|
|
99
104
|
|
|
100
105
|
PASS — invoice data is valid
|
|
101
106
|
|
|
@@ -149,12 +154,14 @@ Requires Python 3.11+.
|
|
|
149
154
|
from trustrender import validate_invoice
|
|
150
155
|
from trustrender.adapters import from_stripe
|
|
151
156
|
|
|
152
|
-
# Raw Stripe API response →
|
|
153
|
-
result = validate_invoice(from_stripe(raw_stripe_response)
|
|
157
|
+
# Raw Stripe API response → canonical invoice (structural validation)
|
|
158
|
+
result = validate_invoice(from_stripe(raw_stripe_response))
|
|
154
159
|
|
|
155
|
-
if result["render_ready"]
|
|
160
|
+
if result["render_ready"]:
|
|
156
161
|
canonical = result["canonical"]
|
|
157
|
-
#
|
|
162
|
+
# canonical is now ready for either:
|
|
163
|
+
# - your own template renderer (most users)
|
|
164
|
+
# - the to_zugferd_data() bridge → drafthorse / factur-x (see end-to-end example below)
|
|
158
165
|
else:
|
|
159
166
|
for error in result["errors"]:
|
|
160
167
|
print(f"BLOCKED: {error['message']}")
|
|
@@ -167,7 +174,8 @@ else:
|
|
|
167
174
|
- `errors`: list of blocking issues with rule_id, path, expected/actual
|
|
168
175
|
- `warnings`: advisory issues
|
|
169
176
|
- `normalizations`: field-level provenance (what was renamed, coerced, computed)
|
|
170
|
-
|
|
177
|
+
|
|
178
|
+
The `zugferd=True` flag is also accepted but only useful if your input is already in ZUGFeRD shape (with `seller`/`buyer`/`tax_entries`/`payment` keys). For Stripe and Shopify, use the `to_zugferd_data()` bridge after `validate_invoice()` instead — see the end-to-end example below.
|
|
171
179
|
|
|
172
180
|
Also works with messy data from any source:
|
|
173
181
|
|
|
@@ -213,6 +221,51 @@ result = validate_invoice(from_shopify(raw_shopify_order))
|
|
|
213
221
|
|
|
214
222
|
Parses string amounts to floats, combines first_name + last_name, maps order fields to invoice structure, preserves structured address fields. Shopify orders have no seller info or due date — TrustRender flags the missing seller and handles the absent due date correctly.
|
|
215
223
|
|
|
224
|
+
## End-to-end example: Stripe → real Factur-X PDF
|
|
225
|
+
|
|
226
|
+
[`examples/with_drafthorse_facturx.py`](examples/with_drafthorse_facturx.py) runs the full pipeline:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from trustrender import validate_invoice
|
|
230
|
+
from trustrender.adapters import from_stripe
|
|
231
|
+
from trustrender.zugferd import to_zugferd_data, build_invoice_xml, apply_zugferd
|
|
232
|
+
|
|
233
|
+
# 1. Adapt + validate the Stripe payload (canonical structure check)
|
|
234
|
+
result = validate_invoice(from_stripe(stripe_invoice))
|
|
235
|
+
|
|
236
|
+
# 2. Bridge canonical → ZUGFeRD shape with the three things Stripe never includes:
|
|
237
|
+
# seller VAT/address, payment IBAN, applicable tax rate
|
|
238
|
+
zugferd_data = to_zugferd_data(
|
|
239
|
+
result["canonical"],
|
|
240
|
+
seller={"name": "...", "address": "...", "city": "...",
|
|
241
|
+
"postal_code": "...", "country": "DE", "vat_id": "DE..."},
|
|
242
|
+
payment={"means": "credit_transfer", "iban": "DE..."},
|
|
243
|
+
tax_rate=19,
|
|
244
|
+
)
|
|
245
|
+
# Real EN 16931 contract validation runs here — catches anything XML build would reject.
|
|
246
|
+
|
|
247
|
+
# 3. drafthorse builds UN/CEFACT CII XML
|
|
248
|
+
xml_bytes = build_invoice_xml(zugferd_data)
|
|
249
|
+
|
|
250
|
+
# 4. factur-x embeds the XML into a PDF/A-3b container
|
|
251
|
+
factur_x_pdf = apply_zugferd(your_visual_pdf_bytes, xml_bytes, lang="de")
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Output: a 15 KB Factur-X invoice PDF that validates against the EN 16931 XSD ([sample committed at `examples/invoice_facturx.pdf`](examples/invoice_facturx.pdf)).
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
$ python examples/with_drafthorse_facturx.py
|
|
258
|
+
Step 1: Adapt + validate via TrustRender (canonical structure) → status=ready
|
|
259
|
+
Step 2: Bridge canonical → ZUGFeRD shape (one call) → passes EN 16931 contract validation
|
|
260
|
+
Step 3: Build CII XML via drafthorse → 8,312 bytes of CII XML
|
|
261
|
+
Step 4: Render visual PDF (your template) → 1,599 bytes
|
|
262
|
+
Step 5: Embed CII XML as PDF/A-3b → 15,004 bytes
|
|
263
|
+
Step 6: Verify with factur-x library → ✓ passes EN 16931 XSD
|
|
264
|
+
✓ factur-x.xml is embedded in the PDF
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
What's TrustRender vs what's you: the seller VAT, payment IBAN, and tax rate come from your billing setup — Stripe's API doesn't include them, and TrustRender doesn't invent them. Everything else is automatic.
|
|
268
|
+
|
|
216
269
|
## What it normalizes
|
|
217
270
|
|
|
218
271
|
90+ vendor field aliases across QuickBooks, Xero, Stripe, and generic CSV/ERP formats:
|
|
@@ -257,53 +310,47 @@ trustrender serve --templates <dir> [--port 8190]
|
|
|
257
310
|
trustrender doctor [--smoke]
|
|
258
311
|
```
|
|
259
312
|
|
|
260
|
-
## Integration with factur-x / drafthorse
|
|
261
|
-
|
|
262
|
-
TrustRender validates and normalizes. You generate and embed with the library of your choice.
|
|
263
|
-
|
|
264
|
-
```python
|
|
265
|
-
from trustrender import validate_invoice
|
|
266
|
-
|
|
267
|
-
# Step 1: Validate with TrustRender
|
|
268
|
-
result = validate_invoice(messy_data, zugferd=True)
|
|
269
|
-
if not result["render_ready"] or not result["zugferd_ready"]:
|
|
270
|
-
raise ValueError(f"Invoice blocked: {result['errors']}")
|
|
271
|
-
|
|
272
|
-
# Step 2: Use the canonical payload with drafthorse or factur-x
|
|
273
|
-
canonical = result["canonical"]
|
|
274
|
-
# ... your existing ZUGFeRD generation code here
|
|
275
|
-
```
|
|
276
|
-
|
|
277
313
|
## EN 16931 e-invoicing (narrow scope)
|
|
278
314
|
|
|
279
|
-
Catches
|
|
315
|
+
Catches structural and contract-level EN 16931 issues before drafthorse builds the XML. Currently supports:
|
|
280
316
|
|
|
281
317
|
- **Domestic German B2B invoices** with standard VAT, EUR, SEPA payment
|
|
282
|
-
- Single
|
|
318
|
+
- Single VAT rate per invoice (mixed rates not yet supported in the bridge)
|
|
283
319
|
- Invoice type 380 and credit note 381
|
|
284
|
-
- PDF/A-3b with embedded CII XML (
|
|
320
|
+
- PDF/A-3b with embedded CII XML via `apply_zugferd()` (wraps `factur-x.attach_xml`)
|
|
321
|
+
|
|
322
|
+
Not supported, fails loudly: reverse charge, cross-border, allowances/charges, non-EUR currencies, non-DE seller country.
|
|
285
323
|
|
|
286
|
-
|
|
324
|
+
What still slips past TrustRender: things only the official Schematron rules catch (cross-field business rules, value range checks). Run [`xml_check_xsd`](https://github.com/akretion/factur-x) and `xml_check_schematron` on the produced XML for full compliance certainty.
|
|
287
325
|
|
|
288
326
|
See [docs/einvoice-scope.md](docs/einvoice-scope.md) for the full scope matrix.
|
|
289
327
|
|
|
290
|
-
##
|
|
328
|
+
## Also: deterministic PDF rendering
|
|
329
|
+
|
|
330
|
+
TrustRender ships a Jinja2 + Typst render engine alongside the validation layer. Used internally to produce the visual PDF for Factur-X embedding, and usable standalone if you want one Python package that goes from data → validated canonical → final PDF without bringing in a headless browser.
|
|
291
331
|
|
|
292
|
-
|
|
332
|
+
- 1,000-line invoice rendered in ~59 ms median (~72 ms with content varying every render; Apple M5 Pro, single core, v0.3.5, 2026-08-29)
|
|
333
|
+
- 1,011-page invoice rendered in 37 seconds
|
|
334
|
+
- No headless browser, no Chromium binary, no Java runtime — pure Python + the Typst CLI
|
|
335
|
+
- SHA-256 fingerprinting of input, template, and output for audit trails (see `trustrender audit`)
|
|
336
|
+
- Drift detection vs saved baselines for visual regression in CI
|
|
337
|
+
- Subprocess-based backend with real timeouts (killable renders, not just abandoned)
|
|
338
|
+
- Bundled fonts, six built-in templates (invoice, einvoice, statement, letter, receipt, report)
|
|
293
339
|
|
|
294
340
|
```
|
|
295
341
|
pip install "trustrender[render]"
|
|
296
342
|
trustrender render invoice.j2.typ data.json -o invoice.pdf --zugferd en16931
|
|
297
343
|
```
|
|
298
344
|
|
|
299
|
-
|
|
345
|
+
`render`, `preflight`, `audit`, `baseline`, and `serve` are all part of the rendering surface — see `trustrender --help` for the full set.
|
|
300
346
|
|
|
301
347
|
## What this is not
|
|
302
348
|
|
|
303
349
|
- Not a full AP automation platform
|
|
304
|
-
- Not an e-invoice compliance certification
|
|
305
|
-
- Not an AI-powered data fixer
|
|
306
|
-
- Not a replacement for factur-x or drafthorse — it
|
|
350
|
+
- Not an e-invoice compliance certification (run Schematron on the output XML for that)
|
|
351
|
+
- Not an AI-powered data fixer — all normalizations and validations are deterministic
|
|
352
|
+
- Not a replacement for factur-x or drafthorse — it validates billing data before them and wraps their entry points for convenience
|
|
353
|
+
- Not a billing platform replacement — it expects you already collect payment via Stripe, Shopify, or something else
|
|
307
354
|
|
|
308
355
|
## Development
|
|
309
356
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# TrustRender
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/trustrender/)
|
|
4
|
+
[](https://pypi.org/project/trustrender/)
|
|
5
|
+
[](https://github.com/verityengine/trustrender/actions)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
3
8
|
Validate and normalize billing data before Factur-X/ZUGFeRD embedding.
|
|
4
9
|
|
|
5
10
|
## Quick start
|
|
@@ -17,7 +22,7 @@ Invoice: INV-2026-0187
|
|
|
17
22
|
From:
|
|
18
23
|
To: Rheingold Maschinenbau GmbH
|
|
19
24
|
Items: 3
|
|
20
|
-
Total: $2,
|
|
25
|
+
Total: $2,945.25
|
|
21
26
|
|
|
22
27
|
BLOCKED — 1 problem(s)
|
|
23
28
|
|
|
@@ -53,7 +58,7 @@ Invoice: INV-2026-0187
|
|
|
53
58
|
From: NovaTech Solutions GmbH
|
|
54
59
|
To: Rheingold Maschinenbau GmbH
|
|
55
60
|
Items: 3
|
|
56
|
-
Total: $2,
|
|
61
|
+
Total: $2,945.25
|
|
57
62
|
|
|
58
63
|
PASS — invoice data is valid
|
|
59
64
|
|
|
@@ -107,12 +112,14 @@ Requires Python 3.11+.
|
|
|
107
112
|
from trustrender import validate_invoice
|
|
108
113
|
from trustrender.adapters import from_stripe
|
|
109
114
|
|
|
110
|
-
# Raw Stripe API response →
|
|
111
|
-
result = validate_invoice(from_stripe(raw_stripe_response)
|
|
115
|
+
# Raw Stripe API response → canonical invoice (structural validation)
|
|
116
|
+
result = validate_invoice(from_stripe(raw_stripe_response))
|
|
112
117
|
|
|
113
|
-
if result["render_ready"]
|
|
118
|
+
if result["render_ready"]:
|
|
114
119
|
canonical = result["canonical"]
|
|
115
|
-
#
|
|
120
|
+
# canonical is now ready for either:
|
|
121
|
+
# - your own template renderer (most users)
|
|
122
|
+
# - the to_zugferd_data() bridge → drafthorse / factur-x (see end-to-end example below)
|
|
116
123
|
else:
|
|
117
124
|
for error in result["errors"]:
|
|
118
125
|
print(f"BLOCKED: {error['message']}")
|
|
@@ -125,7 +132,8 @@ else:
|
|
|
125
132
|
- `errors`: list of blocking issues with rule_id, path, expected/actual
|
|
126
133
|
- `warnings`: advisory issues
|
|
127
134
|
- `normalizations`: field-level provenance (what was renamed, coerced, computed)
|
|
128
|
-
|
|
135
|
+
|
|
136
|
+
The `zugferd=True` flag is also accepted but only useful if your input is already in ZUGFeRD shape (with `seller`/`buyer`/`tax_entries`/`payment` keys). For Stripe and Shopify, use the `to_zugferd_data()` bridge after `validate_invoice()` instead — see the end-to-end example below.
|
|
129
137
|
|
|
130
138
|
Also works with messy data from any source:
|
|
131
139
|
|
|
@@ -171,6 +179,51 @@ result = validate_invoice(from_shopify(raw_shopify_order))
|
|
|
171
179
|
|
|
172
180
|
Parses string amounts to floats, combines first_name + last_name, maps order fields to invoice structure, preserves structured address fields. Shopify orders have no seller info or due date — TrustRender flags the missing seller and handles the absent due date correctly.
|
|
173
181
|
|
|
182
|
+
## End-to-end example: Stripe → real Factur-X PDF
|
|
183
|
+
|
|
184
|
+
[`examples/with_drafthorse_facturx.py`](examples/with_drafthorse_facturx.py) runs the full pipeline:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from trustrender import validate_invoice
|
|
188
|
+
from trustrender.adapters import from_stripe
|
|
189
|
+
from trustrender.zugferd import to_zugferd_data, build_invoice_xml, apply_zugferd
|
|
190
|
+
|
|
191
|
+
# 1. Adapt + validate the Stripe payload (canonical structure check)
|
|
192
|
+
result = validate_invoice(from_stripe(stripe_invoice))
|
|
193
|
+
|
|
194
|
+
# 2. Bridge canonical → ZUGFeRD shape with the three things Stripe never includes:
|
|
195
|
+
# seller VAT/address, payment IBAN, applicable tax rate
|
|
196
|
+
zugferd_data = to_zugferd_data(
|
|
197
|
+
result["canonical"],
|
|
198
|
+
seller={"name": "...", "address": "...", "city": "...",
|
|
199
|
+
"postal_code": "...", "country": "DE", "vat_id": "DE..."},
|
|
200
|
+
payment={"means": "credit_transfer", "iban": "DE..."},
|
|
201
|
+
tax_rate=19,
|
|
202
|
+
)
|
|
203
|
+
# Real EN 16931 contract validation runs here — catches anything XML build would reject.
|
|
204
|
+
|
|
205
|
+
# 3. drafthorse builds UN/CEFACT CII XML
|
|
206
|
+
xml_bytes = build_invoice_xml(zugferd_data)
|
|
207
|
+
|
|
208
|
+
# 4. factur-x embeds the XML into a PDF/A-3b container
|
|
209
|
+
factur_x_pdf = apply_zugferd(your_visual_pdf_bytes, xml_bytes, lang="de")
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Output: a 15 KB Factur-X invoice PDF that validates against the EN 16931 XSD ([sample committed at `examples/invoice_facturx.pdf`](examples/invoice_facturx.pdf)).
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
$ python examples/with_drafthorse_facturx.py
|
|
216
|
+
Step 1: Adapt + validate via TrustRender (canonical structure) → status=ready
|
|
217
|
+
Step 2: Bridge canonical → ZUGFeRD shape (one call) → passes EN 16931 contract validation
|
|
218
|
+
Step 3: Build CII XML via drafthorse → 8,312 bytes of CII XML
|
|
219
|
+
Step 4: Render visual PDF (your template) → 1,599 bytes
|
|
220
|
+
Step 5: Embed CII XML as PDF/A-3b → 15,004 bytes
|
|
221
|
+
Step 6: Verify with factur-x library → ✓ passes EN 16931 XSD
|
|
222
|
+
✓ factur-x.xml is embedded in the PDF
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
What's TrustRender vs what's you: the seller VAT, payment IBAN, and tax rate come from your billing setup — Stripe's API doesn't include them, and TrustRender doesn't invent them. Everything else is automatic.
|
|
226
|
+
|
|
174
227
|
## What it normalizes
|
|
175
228
|
|
|
176
229
|
90+ vendor field aliases across QuickBooks, Xero, Stripe, and generic CSV/ERP formats:
|
|
@@ -215,53 +268,47 @@ trustrender serve --templates <dir> [--port 8190]
|
|
|
215
268
|
trustrender doctor [--smoke]
|
|
216
269
|
```
|
|
217
270
|
|
|
218
|
-
## Integration with factur-x / drafthorse
|
|
219
|
-
|
|
220
|
-
TrustRender validates and normalizes. You generate and embed with the library of your choice.
|
|
221
|
-
|
|
222
|
-
```python
|
|
223
|
-
from trustrender import validate_invoice
|
|
224
|
-
|
|
225
|
-
# Step 1: Validate with TrustRender
|
|
226
|
-
result = validate_invoice(messy_data, zugferd=True)
|
|
227
|
-
if not result["render_ready"] or not result["zugferd_ready"]:
|
|
228
|
-
raise ValueError(f"Invoice blocked: {result['errors']}")
|
|
229
|
-
|
|
230
|
-
# Step 2: Use the canonical payload with drafthorse or factur-x
|
|
231
|
-
canonical = result["canonical"]
|
|
232
|
-
# ... your existing ZUGFeRD generation code here
|
|
233
|
-
```
|
|
234
|
-
|
|
235
271
|
## EN 16931 e-invoicing (narrow scope)
|
|
236
272
|
|
|
237
|
-
Catches
|
|
273
|
+
Catches structural and contract-level EN 16931 issues before drafthorse builds the XML. Currently supports:
|
|
238
274
|
|
|
239
275
|
- **Domestic German B2B invoices** with standard VAT, EUR, SEPA payment
|
|
240
|
-
- Single
|
|
276
|
+
- Single VAT rate per invoice (mixed rates not yet supported in the bridge)
|
|
241
277
|
- Invoice type 380 and credit note 381
|
|
242
|
-
- PDF/A-3b with embedded CII XML (
|
|
278
|
+
- PDF/A-3b with embedded CII XML via `apply_zugferd()` (wraps `factur-x.attach_xml`)
|
|
279
|
+
|
|
280
|
+
Not supported, fails loudly: reverse charge, cross-border, allowances/charges, non-EUR currencies, non-DE seller country.
|
|
243
281
|
|
|
244
|
-
|
|
282
|
+
What still slips past TrustRender: things only the official Schematron rules catch (cross-field business rules, value range checks). Run [`xml_check_xsd`](https://github.com/akretion/factur-x) and `xml_check_schematron` on the produced XML for full compliance certainty.
|
|
245
283
|
|
|
246
284
|
See [docs/einvoice-scope.md](docs/einvoice-scope.md) for the full scope matrix.
|
|
247
285
|
|
|
248
|
-
##
|
|
286
|
+
## Also: deterministic PDF rendering
|
|
287
|
+
|
|
288
|
+
TrustRender ships a Jinja2 + Typst render engine alongside the validation layer. Used internally to produce the visual PDF for Factur-X embedding, and usable standalone if you want one Python package that goes from data → validated canonical → final PDF without bringing in a headless browser.
|
|
249
289
|
|
|
250
|
-
|
|
290
|
+
- 1,000-line invoice rendered in ~59 ms median (~72 ms with content varying every render; Apple M5 Pro, single core, v0.3.5, 2026-08-29)
|
|
291
|
+
- 1,011-page invoice rendered in 37 seconds
|
|
292
|
+
- No headless browser, no Chromium binary, no Java runtime — pure Python + the Typst CLI
|
|
293
|
+
- SHA-256 fingerprinting of input, template, and output for audit trails (see `trustrender audit`)
|
|
294
|
+
- Drift detection vs saved baselines for visual regression in CI
|
|
295
|
+
- Subprocess-based backend with real timeouts (killable renders, not just abandoned)
|
|
296
|
+
- Bundled fonts, six built-in templates (invoice, einvoice, statement, letter, receipt, report)
|
|
251
297
|
|
|
252
298
|
```
|
|
253
299
|
pip install "trustrender[render]"
|
|
254
300
|
trustrender render invoice.j2.typ data.json -o invoice.pdf --zugferd en16931
|
|
255
301
|
```
|
|
256
302
|
|
|
257
|
-
|
|
303
|
+
`render`, `preflight`, `audit`, `baseline`, and `serve` are all part of the rendering surface — see `trustrender --help` for the full set.
|
|
258
304
|
|
|
259
305
|
## What this is not
|
|
260
306
|
|
|
261
307
|
- Not a full AP automation platform
|
|
262
|
-
- Not an e-invoice compliance certification
|
|
263
|
-
- Not an AI-powered data fixer
|
|
264
|
-
- Not a replacement for factur-x or drafthorse — it
|
|
308
|
+
- Not an e-invoice compliance certification (run Schematron on the output XML for that)
|
|
309
|
+
- Not an AI-powered data fixer — all normalizations and validations are deterministic
|
|
310
|
+
- Not a replacement for factur-x or drafthorse — it validates billing data before them and wraps their entry points for convenience
|
|
311
|
+
- Not a billing platform replacement — it expects you already collect payment via Stripe, Shopify, or something else
|
|
265
312
|
|
|
266
313
|
## Development
|
|
267
314
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trustrender"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.5"
|
|
8
8
|
description = "Validate and normalize Stripe, Shopify, and custom billing data for Factur-X/ZUGFeRD"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
classifiers = [
|
|
@@ -66,8 +66,14 @@ where = ["src"]
|
|
|
66
66
|
trustrender = ["playground/**/*", "fonts/**/*", "builtin_templates/*"]
|
|
67
67
|
|
|
68
68
|
[tool.ruff]
|
|
69
|
-
line-length =
|
|
69
|
+
line-length = 140
|
|
70
70
|
target-version = "py311"
|
|
71
71
|
|
|
72
72
|
[tool.ruff.lint]
|
|
73
73
|
select = ["E", "F", "I", "W"]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint.per-file-ignores]
|
|
76
|
+
# Dashboard generates HTML/CSS as inline string literals — long lines are intentional
|
|
77
|
+
"src/trustrender/dashboard.py" = ["E501"]
|
|
78
|
+
# Tests: long fixture strings, side-effect render() calls, ad-hoc fixture vars
|
|
79
|
+
"tests/*" = ["E501", "F841"]
|
|
@@ -23,6 +23,7 @@ from .invoice_ingest import IngestionReport, ingest_invoice
|
|
|
23
23
|
try:
|
|
24
24
|
from .engine import CompileBackend, compile_typst, compile_typst_file
|
|
25
25
|
from .templates import render_template
|
|
26
|
+
|
|
26
27
|
_HAS_RENDER = True
|
|
27
28
|
except ImportError:
|
|
28
29
|
_HAS_RENDER = False
|
|
@@ -36,22 +37,29 @@ class RenderResult:
|
|
|
36
37
|
trace_id: str | None = None
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
__version__ = "0.3.
|
|
40
|
+
__version__ = "0.3.5"
|
|
40
41
|
|
|
41
42
|
__all__ = [
|
|
42
43
|
# Core API (always available)
|
|
43
|
-
"validate_invoice",
|
|
44
|
+
"validate_invoice",
|
|
45
|
+
"ingest_invoice",
|
|
46
|
+
"IngestionReport",
|
|
44
47
|
# Rendering API (requires [render] extras)
|
|
45
|
-
"render",
|
|
48
|
+
"render",
|
|
49
|
+
"audit",
|
|
50
|
+
"AuditResult",
|
|
46
51
|
# Errors
|
|
47
|
-
"TrustRenderError",
|
|
52
|
+
"TrustRenderError",
|
|
53
|
+
"ErrorCode",
|
|
48
54
|
# Meta
|
|
49
|
-
"__version__",
|
|
55
|
+
"__version__",
|
|
56
|
+
"bundled_font_dir",
|
|
50
57
|
]
|
|
51
58
|
|
|
52
59
|
|
|
53
60
|
# ── Core API: validation (always available) ──────────────────────────
|
|
54
61
|
|
|
62
|
+
|
|
55
63
|
def validate_invoice(data: dict, *, zugferd: bool = False) -> dict:
|
|
56
64
|
"""Validate and normalize messy invoice data. Returns structured result.
|
|
57
65
|
|
|
@@ -100,10 +108,9 @@ def validate_invoice(data: dict, *, zugferd: bool = False) -> dict:
|
|
|
100
108
|
|
|
101
109
|
if zugferd and report.render_ready:
|
|
102
110
|
from .zugferd import validate_zugferd_invoice_data
|
|
111
|
+
|
|
103
112
|
zug_errors = validate_zugferd_invoice_data(report.canonical)
|
|
104
|
-
result["zugferd_errors"] = [
|
|
105
|
-
{"path": e.path, "message": e.message} for e in zug_errors
|
|
106
|
-
]
|
|
113
|
+
result["zugferd_errors"] = [{"path": e.path, "message": e.message} for e in zug_errors]
|
|
107
114
|
result["zugferd_ready"] = len(zug_errors) == 0
|
|
108
115
|
elif zugferd:
|
|
109
116
|
result["zugferd_errors"] = []
|
|
@@ -171,8 +178,7 @@ def _require_render():
|
|
|
171
178
|
"""Raise a clear error if rendering dependencies are not installed."""
|
|
172
179
|
if not _HAS_RENDER:
|
|
173
180
|
raise TrustRenderError(
|
|
174
|
-
"Rendering requires additional dependencies. "
|
|
175
|
-
"Install with: pip install trustrender[render]",
|
|
181
|
+
"Rendering requires additional dependencies. Install with: pip install trustrender[render]",
|
|
176
182
|
code=ErrorCode.BACKEND_ERROR,
|
|
177
183
|
stage="import",
|
|
178
184
|
)
|
|
@@ -221,7 +227,7 @@ def _render_document_pipeline(
|
|
|
221
227
|
trace = RenderTrace(
|
|
222
228
|
template_name=display_name or template_path.name,
|
|
223
229
|
template_hash=f"sha256:{hashlib.sha256(template_path.read_bytes()).hexdigest()[:16]}",
|
|
224
|
-
data_hash=f"sha256:{hashlib.sha256(json.dumps(data, sort_keys=True, separators=(',',':')).encode()).hexdigest()[:16]}",
|
|
230
|
+
data_hash=f"sha256:{hashlib.sha256(json.dumps(data, sort_keys=True, separators=(',', ':')).encode()).hexdigest()[:16]}",
|
|
225
231
|
engine_version=__version__,
|
|
226
232
|
zugferd_profile=zugferd or "",
|
|
227
233
|
validated=validate,
|
|
@@ -330,16 +336,18 @@ def _render_document_pipeline(
|
|
|
330
336
|
timeout=timeout,
|
|
331
337
|
pdf_standards=pdf_standards,
|
|
332
338
|
)
|
|
333
|
-
trace.stages.append(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
339
|
+
trace.stages.append(
|
|
340
|
+
StageTrace(
|
|
341
|
+
stage="compilation",
|
|
342
|
+
status="pass",
|
|
343
|
+
duration_ms=int((time.monotonic() - t0) * 1000),
|
|
344
|
+
metadata={
|
|
345
|
+
"template_type": "jinja2" if is_jinja else "raw",
|
|
346
|
+
"pdf_standards": pdf_standards or [],
|
|
347
|
+
"pdf_size": len(pdf_bytes),
|
|
348
|
+
},
|
|
349
|
+
)
|
|
350
|
+
)
|
|
343
351
|
|
|
344
352
|
# 4. ZUGFeRD post-processing
|
|
345
353
|
if zugferd:
|
|
@@ -363,12 +371,14 @@ def _render_document_pipeline(
|
|
|
363
371
|
)
|
|
364
372
|
|
|
365
373
|
pdf_bytes = apply_zugferd(pdf_bytes, xml_bytes)
|
|
366
|
-
trace.stages.append(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
374
|
+
trace.stages.append(
|
|
375
|
+
StageTrace(
|
|
376
|
+
stage="zugferd_postprocess",
|
|
377
|
+
status="pass",
|
|
378
|
+
duration_ms=int((time.monotonic() - t0) * 1000),
|
|
379
|
+
metadata={"xml_size": len(xml_bytes), "profile": zugferd},
|
|
380
|
+
)
|
|
381
|
+
)
|
|
372
382
|
except TrustRenderError:
|
|
373
383
|
raise
|
|
374
384
|
except Exception as exc:
|
|
@@ -387,12 +397,14 @@ def _render_document_pipeline(
|
|
|
387
397
|
t0 = time.monotonic()
|
|
388
398
|
prov_record = create_provenance(template_path, data)
|
|
389
399
|
pdf_bytes = embed_provenance(pdf_bytes, prov_record)
|
|
390
|
-
trace.stages.append(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
400
|
+
trace.stages.append(
|
|
401
|
+
StageTrace(
|
|
402
|
+
stage="provenance",
|
|
403
|
+
status="pass",
|
|
404
|
+
duration_ms=int((time.monotonic() - t0) * 1000),
|
|
405
|
+
metadata={"proof_hash": prov_record.proof[:30]},
|
|
406
|
+
)
|
|
407
|
+
)
|
|
396
408
|
trace.provenance_hash = prov_record.proof
|
|
397
409
|
|
|
398
410
|
# Output fingerprint: hash the final PDF bytes after all post-processing
|
|
@@ -545,7 +557,8 @@ def audit(
|
|
|
545
557
|
import time
|
|
546
558
|
|
|
547
559
|
from .fingerprint import InputFingerprint, compare, compute_fingerprint
|
|
548
|
-
from .regression import check_drift
|
|
560
|
+
from .regression import check_drift
|
|
561
|
+
from .regression import save_baseline as _save_baseline
|
|
549
562
|
from .semantic import SemanticReport, validate_semantics
|
|
550
563
|
|
|
551
564
|
_SUPPORTED_ZUGFERD = {"en16931"}
|
|
@@ -623,9 +636,7 @@ def audit(
|
|
|
623
636
|
pdf_bytes,
|
|
624
637
|
zugferd_valid=zugferd is not None,
|
|
625
638
|
contract_valid=validate,
|
|
626
|
-
semantic_issue_count=(
|
|
627
|
-
len(semantic_report.issues) if semantic_report else 0
|
|
628
|
-
),
|
|
639
|
+
semantic_issue_count=(len(semantic_report.issues) if semantic_report else 0),
|
|
629
640
|
)
|
|
630
641
|
|
|
631
642
|
# 6. Save baseline (if requested)
|
|
@@ -638,9 +649,7 @@ def audit(
|
|
|
638
649
|
render_duration_ms=render_duration_ms,
|
|
639
650
|
zugferd_valid=zugferd is not None if zugferd else None,
|
|
640
651
|
contract_valid=True if validate else None,
|
|
641
|
-
semantic_issue_count=(
|
|
642
|
-
len(semantic_report.issues) if semantic_report else 0
|
|
643
|
-
),
|
|
652
|
+
semantic_issue_count=(len(semantic_report.issues) if semantic_report else 0),
|
|
644
653
|
)
|
|
645
654
|
|
|
646
655
|
return AuditResult(
|
|
@@ -659,8 +668,7 @@ def _resolve_data(data: dict | str | os.PathLike) -> dict:
|
|
|
659
668
|
|
|
660
669
|
if not isinstance(data, (str, os.PathLike)):
|
|
661
670
|
raise TrustRenderError(
|
|
662
|
-
f"Data must be a dict, JSON string, or path to a .json file, "
|
|
663
|
-
f"got {type(data).__name__}",
|
|
671
|
+
f"Data must be a dict, JSON string, or path to a .json file, got {type(data).__name__}",
|
|
664
672
|
code=ErrorCode.INVALID_DATA,
|
|
665
673
|
stage="data_resolution",
|
|
666
674
|
)
|