trustrender 0.2.0__tar.gz → 0.3.0__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.0/PKG-INFO +246 -0
- trustrender-0.3.0/README.md +204 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/pyproject.toml +21 -11
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/__init__.py +109 -11
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/cli.py +397 -104
- trustrender-0.3.0/src/trustrender/invoice_aliases.py +275 -0
- trustrender-0.3.0/src/trustrender/invoice_ingest.py +1002 -0
- trustrender-0.3.0/src/trustrender/invoice_schema.py +171 -0
- trustrender-0.3.0/src/trustrender/playground/assets/index-Bsf6YnD1.css +2 -0
- trustrender-0.3.0/src/trustrender/playground/assets/index-C6K0JsLw.js +97 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/playground/index.html +2 -2
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/server.py +42 -3
- trustrender-0.3.0/src/trustrender.egg-info/PKG-INFO +246 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender.egg-info/SOURCES.txt +8 -2
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender.egg-info/requires.txt +13 -5
- trustrender-0.3.0/tests/test_adversarial_corpus.py +278 -0
- trustrender-0.3.0/tests/test_invoice_ingest.py +646 -0
- trustrender-0.3.0/tests/test_real_payloads.py +101 -0
- trustrender-0.2.0/PKG-INFO +0 -274
- trustrender-0.2.0/README.md +0 -239
- trustrender-0.2.0/src/trustrender/playground/assets/index--cOwr7nw.css +0 -2
- trustrender-0.2.0/src/trustrender/playground/assets/index-CiBSP9E3.js +0 -81
- trustrender-0.2.0/src/trustrender.egg-info/PKG-INFO +0 -274
- {trustrender-0.2.0 → trustrender-0.3.0}/LICENSE +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/setup.cfg +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/builtin_templates/einvoice.j2.typ +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/builtin_templates/einvoice_data.json +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/builtin_templates/invoice.j2.typ +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/builtin_templates/invoice_data.json +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/builtin_templates/letter.j2.typ +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/builtin_templates/letter_data.json +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/builtin_templates/receipt.j2.typ +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/builtin_templates/receipt_data.json +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/builtin_templates/report.j2.typ +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/builtin_templates/report_data.json +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/builtin_templates/statement.j2.typ +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/builtin_templates/statement_data.json +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/contract.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/dashboard.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/doctor.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/engine.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/errors.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/filters.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/fingerprint.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/fonts/Inter/Inter-Bold.ttf +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/fonts/Inter/Inter-BoldItalic.ttf +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/fonts/Inter/Inter-Italic.ttf +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/fonts/Inter/Inter-Regular.ttf +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/fonts/Inter/LICENSE.txt +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/playground/assets/pdf.worker-C1breYqn.mjs +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/playground/favicon.svg +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/playground/icons.svg +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/provenance.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/readiness.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/regression.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/semantic.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/templates.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/trace.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender/zugferd.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender.egg-info/dependency_links.txt +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender.egg-info/entry_points.txt +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/src/trustrender.egg-info/top_level.txt +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_audit_e2e.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_cli.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_contract.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_doctor.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_engine.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_error_pipeline.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_errors.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_filters.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_fingerprint.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_fingerprint_stress.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_fonts.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_pagination.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_provenance.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_readiness.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_regression.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_regression_stress.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_render.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_semantic.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_semantic_stress.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_server.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_templates.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_trace.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_ugly_data.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_ugly_data_pressure.py +0 -0
- {trustrender-0.2.0 → trustrender-0.3.0}/tests/test_zugferd.py +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trustrender
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Validate and normalize Stripe, Shopify, and custom billing data for Factur-X/ZUGFeRD
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://trustrender.dev
|
|
7
|
+
Project-URL: Documentation, https://trustrender.dev
|
|
8
|
+
Project-URL: Source, https://github.com/verityengine/trustrender
|
|
9
|
+
Project-URL: Issues, https://github.com/verityengine/trustrender/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/verityengine/trustrender/releases
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: drafthorse>=2024.0
|
|
24
|
+
Provides-Extra: render
|
|
25
|
+
Requires-Dist: typst>=0.14; extra == "render"
|
|
26
|
+
Requires-Dist: jinja2>=3.1; extra == "render"
|
|
27
|
+
Requires-Dist: pypdf>=4.0; extra == "render"
|
|
28
|
+
Provides-Extra: serve
|
|
29
|
+
Requires-Dist: starlette>=0.40; extra == "serve"
|
|
30
|
+
Requires-Dist: uvicorn>=0.30; extra == "serve"
|
|
31
|
+
Provides-Extra: zugferd
|
|
32
|
+
Requires-Dist: factur-x>=3.0; extra == "zugferd"
|
|
33
|
+
Provides-Extra: all
|
|
34
|
+
Requires-Dist: trustrender[render,serve,zugferd]; extra == "all"
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
37
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
38
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
39
|
+
Requires-Dist: factur-x>=3.0; extra == "dev"
|
|
40
|
+
Requires-Dist: trustrender[all]; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# TrustRender
|
|
44
|
+
|
|
45
|
+
Validate and normalize billing data from Stripe, Shopify, and custom systems before Factur-X/ZUGFeRD embedding.
|
|
46
|
+
|
|
47
|
+
If you're bridging a non-compliant billing platform into EU e-invoicing, libraries like [factur-x](https://github.com/akretion/factur-x) and [drafthorse](https://github.com/pretix/python-drafthorse) assume your data is already correct. TrustRender catches arithmetic mismatches, field misalignment, missing required fields, and structural problems before compliant XML gets embedded.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
pip install trustrender
|
|
51
|
+
trustrender validate invoice.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## What it does
|
|
55
|
+
|
|
56
|
+
Takes invoice JSON from Stripe, Shopify, custom billing APIs, or legacy exports and tells you whether it's safe to embed as Factur-X/ZUGFeRD.
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
$ trustrender validate quickbooks_invoice.json
|
|
60
|
+
|
|
61
|
+
Invoice: INV-2026-5541
|
|
62
|
+
From: Summit Analytics Co.
|
|
63
|
+
To: Horizon Financial
|
|
64
|
+
Items: 2
|
|
65
|
+
Total: $10,524.50
|
|
66
|
+
|
|
67
|
+
Normalizations (25):
|
|
68
|
+
DocNumber → invoice_number CompanyName → sender.name
|
|
69
|
+
TxnDate → invoice_date customer.Name → recipient.name
|
|
70
|
+
Line → items SubTotal → subtotal
|
|
71
|
+
... and 19 more
|
|
72
|
+
|
|
73
|
+
PASS — invoice data is valid
|
|
74
|
+
|
|
75
|
+
Safe to embed in Factur-X/ZUGFeRD PDF.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Bad data gets blocked:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
$ trustrender validate ocr_extracted_invoice.json
|
|
82
|
+
|
|
83
|
+
BLOCKED — 2 problem(s)
|
|
84
|
+
|
|
85
|
+
items[1] total is wrong
|
|
86
|
+
You entered $459.00 but math says $450.00
|
|
87
|
+
Fix the line total or the price/quantity.
|
|
88
|
+
|
|
89
|
+
Subtotal is wrong
|
|
90
|
+
Lines add up to $12,459.00 but you listed $12,450.00
|
|
91
|
+
|
|
92
|
+
This invoice cannot be processed until the problems above are fixed.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Install
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
pip install trustrender
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Core install requires only `drafthorse`. No Typst, no browser, no heavy deps.
|
|
102
|
+
|
|
103
|
+
Optional extras:
|
|
104
|
+
```
|
|
105
|
+
pip install "trustrender[zugferd]" # XSD/Schematron validation
|
|
106
|
+
pip install "trustrender[render]" # PDF rendering via Typst
|
|
107
|
+
pip install "trustrender[all]" # everything
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Requires Python 3.11+.
|
|
111
|
+
|
|
112
|
+
## Python API
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from trustrender import validate_invoice
|
|
116
|
+
|
|
117
|
+
result = validate_invoice({
|
|
118
|
+
"invoiceNo": "INV-001",
|
|
119
|
+
"vendor": {"companyName": "Acme Corp"},
|
|
120
|
+
"customer": {"Name": "Client Inc"},
|
|
121
|
+
"LineItems": [{"desc": "Widget", "qty": 2, "unitPrice": 50, "amount": 100}],
|
|
122
|
+
"SubTotal": 100,
|
|
123
|
+
"tax": 8.50,
|
|
124
|
+
"TotalAmt": 108.50,
|
|
125
|
+
}, zugferd=True)
|
|
126
|
+
|
|
127
|
+
if result["render_ready"] and result.get("zugferd_ready"):
|
|
128
|
+
# safe to call factur-x / drafthorse
|
|
129
|
+
print("All checks passed")
|
|
130
|
+
else:
|
|
131
|
+
for error in result["errors"]:
|
|
132
|
+
print(f"BLOCKED: {error['message']}")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`validate_invoice()` returns:
|
|
136
|
+
- `status`: "ready" | "ready_with_warnings" | "blocked"
|
|
137
|
+
- `render_ready`: bool
|
|
138
|
+
- `canonical`: normalized invoice dict (all fields in canonical names)
|
|
139
|
+
- `errors`: list of blocking issues with rule_id, path, expected/actual
|
|
140
|
+
- `warnings`: advisory issues
|
|
141
|
+
- `normalizations`: field-level provenance (what was renamed, coerced, computed)
|
|
142
|
+
- `zugferd_ready`: bool (if `zugferd=True`)
|
|
143
|
+
|
|
144
|
+
## What it normalizes
|
|
145
|
+
|
|
146
|
+
90+ vendor field aliases across QuickBooks, Xero, Stripe, and generic CSV/ERP formats:
|
|
147
|
+
|
|
148
|
+
| Source field | Canonical field |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `DocNumber`, `invoiceNo`, `inv_no`, `ref` | `invoice_number` |
|
|
151
|
+
| `CompanyName`, `account_name`, `bill_from_name` | `sender.name` |
|
|
152
|
+
| `customer`, `billTo`, `Contact` | `recipient` |
|
|
153
|
+
| `Line`, `LineItems`, `entries`, `products` | `items` |
|
|
154
|
+
| `UnitPrice`, `cost`, `rate`, `unitCost` | `unit_price` |
|
|
155
|
+
| `Amount`, `LineAmount`, `line_total` | `line_total` |
|
|
156
|
+
| `SubTotal`, `net_total`, `sub_total` | `subtotal` |
|
|
157
|
+
| `TotalAmt`, `grand_total`, `amount_due` | `total` |
|
|
158
|
+
|
|
159
|
+
Plus: type coercion (`"$1,234.56"` → `1234.56`), date parsing (`"April 10, 2026"` → `2026-04-10`), computed defaults (missing `line_total` = `qty × price`), near-match typo detection (`invioce_number` → suggests `invoice_number`).
|
|
160
|
+
|
|
161
|
+
## What it checks
|
|
162
|
+
|
|
163
|
+
7 deterministic semantic checks, all arithmetic:
|
|
164
|
+
|
|
165
|
+
| Check | What it catches |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `identity.invoice_number` | Missing or empty invoice number |
|
|
168
|
+
| `identity.sender_name` | Missing vendor/sender name |
|
|
169
|
+
| `identity.recipient_name` | Missing recipient/buyer name |
|
|
170
|
+
| `items.non_empty` | No line items |
|
|
171
|
+
| `arithmetic.line_total` | line_total ≠ qty × unit_price |
|
|
172
|
+
| `arithmetic.subtotal` | subtotal ≠ sum of line_totals |
|
|
173
|
+
| `arithmetic.total` | total ≠ subtotal + tax_amount |
|
|
174
|
+
|
|
175
|
+
No AI. No heuristics. Every check is deterministic and objectively verifiable.
|
|
176
|
+
|
|
177
|
+
## CLI
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
trustrender validate <data.json> [--zugferd] [--format text|json]
|
|
181
|
+
trustrender ingest <data.json> [-o canonical.json]
|
|
182
|
+
trustrender render <template> <data.json> -o <output.pdf> [--zugferd en16931]
|
|
183
|
+
trustrender preflight <template> <data.json> [--zugferd en16931]
|
|
184
|
+
trustrender serve --templates <dir> [--port 8190]
|
|
185
|
+
trustrender doctor [--smoke]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Integration with factur-x / drafthorse
|
|
189
|
+
|
|
190
|
+
TrustRender validates and normalizes. You generate and embed with the library of your choice.
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from trustrender import validate_invoice
|
|
194
|
+
|
|
195
|
+
# Step 1: Validate with TrustRender
|
|
196
|
+
result = validate_invoice(messy_data, zugferd=True)
|
|
197
|
+
if not result["render_ready"] or not result["zugferd_ready"]:
|
|
198
|
+
raise ValueError(f"Invoice blocked: {result['errors']}")
|
|
199
|
+
|
|
200
|
+
# Step 2: Use the canonical payload with drafthorse or factur-x
|
|
201
|
+
canonical = result["canonical"]
|
|
202
|
+
# ... your existing ZUGFeRD generation code here
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## EN 16931 e-invoicing (narrow scope)
|
|
206
|
+
|
|
207
|
+
Catches many document-level and ZUGFeRD/EN 16931 readiness issues before embedding. Currently supports:
|
|
208
|
+
|
|
209
|
+
- **Domestic German B2B invoices** with standard VAT, EUR, SEPA payment
|
|
210
|
+
- Single or mixed VAT rates (7% + 19%)
|
|
211
|
+
- Invoice type 380 and credit note 381
|
|
212
|
+
- PDF/A-3b with embedded CII XML (requires `trustrender[render]`)
|
|
213
|
+
|
|
214
|
+
Not supported (fails loudly): reverse charge, cross-border, allowances/charges, non-EUR currencies.
|
|
215
|
+
|
|
216
|
+
See [docs/einvoice-scope.md](docs/einvoice-scope.md) for the full scope matrix.
|
|
217
|
+
|
|
218
|
+
## Optional: PDF rendering
|
|
219
|
+
|
|
220
|
+
If you also want TrustRender to generate PDFs (not just validate):
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
pip install "trustrender[render]"
|
|
224
|
+
trustrender render invoice.j2.typ data.json -o invoice.pdf --zugferd en16931
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Rendering uses Typst — no browser, no Chromium. Fast and deterministic.
|
|
228
|
+
|
|
229
|
+
## What this is not
|
|
230
|
+
|
|
231
|
+
- Not a full AP automation platform
|
|
232
|
+
- Not an e-invoice compliance certification
|
|
233
|
+
- Not an AI-powered data fixer (all corrections are deterministic)
|
|
234
|
+
- Not a replacement for factur-x or drafthorse — it's the validation layer you run before them
|
|
235
|
+
|
|
236
|
+
## Development
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
pip install -e ".[dev]"
|
|
240
|
+
trustrender doctor --smoke
|
|
241
|
+
pytest
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# TrustRender
|
|
2
|
+
|
|
3
|
+
Validate and normalize billing data from Stripe, Shopify, and custom systems before Factur-X/ZUGFeRD embedding.
|
|
4
|
+
|
|
5
|
+
If you're bridging a non-compliant billing platform into EU e-invoicing, libraries like [factur-x](https://github.com/akretion/factur-x) and [drafthorse](https://github.com/pretix/python-drafthorse) assume your data is already correct. TrustRender catches arithmetic mismatches, field misalignment, missing required fields, and structural problems before compliant XML gets embedded.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install trustrender
|
|
9
|
+
trustrender validate invoice.json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## What it does
|
|
13
|
+
|
|
14
|
+
Takes invoice JSON from Stripe, Shopify, custom billing APIs, or legacy exports and tells you whether it's safe to embed as Factur-X/ZUGFeRD.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
$ trustrender validate quickbooks_invoice.json
|
|
18
|
+
|
|
19
|
+
Invoice: INV-2026-5541
|
|
20
|
+
From: Summit Analytics Co.
|
|
21
|
+
To: Horizon Financial
|
|
22
|
+
Items: 2
|
|
23
|
+
Total: $10,524.50
|
|
24
|
+
|
|
25
|
+
Normalizations (25):
|
|
26
|
+
DocNumber → invoice_number CompanyName → sender.name
|
|
27
|
+
TxnDate → invoice_date customer.Name → recipient.name
|
|
28
|
+
Line → items SubTotal → subtotal
|
|
29
|
+
... and 19 more
|
|
30
|
+
|
|
31
|
+
PASS — invoice data is valid
|
|
32
|
+
|
|
33
|
+
Safe to embed in Factur-X/ZUGFeRD PDF.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Bad data gets blocked:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
$ trustrender validate ocr_extracted_invoice.json
|
|
40
|
+
|
|
41
|
+
BLOCKED — 2 problem(s)
|
|
42
|
+
|
|
43
|
+
items[1] total is wrong
|
|
44
|
+
You entered $459.00 but math says $450.00
|
|
45
|
+
Fix the line total or the price/quantity.
|
|
46
|
+
|
|
47
|
+
Subtotal is wrong
|
|
48
|
+
Lines add up to $12,459.00 but you listed $12,450.00
|
|
49
|
+
|
|
50
|
+
This invoice cannot be processed until the problems above are fixed.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
pip install trustrender
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Core install requires only `drafthorse`. No Typst, no browser, no heavy deps.
|
|
60
|
+
|
|
61
|
+
Optional extras:
|
|
62
|
+
```
|
|
63
|
+
pip install "trustrender[zugferd]" # XSD/Schematron validation
|
|
64
|
+
pip install "trustrender[render]" # PDF rendering via Typst
|
|
65
|
+
pip install "trustrender[all]" # everything
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Requires Python 3.11+.
|
|
69
|
+
|
|
70
|
+
## Python API
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from trustrender import validate_invoice
|
|
74
|
+
|
|
75
|
+
result = validate_invoice({
|
|
76
|
+
"invoiceNo": "INV-001",
|
|
77
|
+
"vendor": {"companyName": "Acme Corp"},
|
|
78
|
+
"customer": {"Name": "Client Inc"},
|
|
79
|
+
"LineItems": [{"desc": "Widget", "qty": 2, "unitPrice": 50, "amount": 100}],
|
|
80
|
+
"SubTotal": 100,
|
|
81
|
+
"tax": 8.50,
|
|
82
|
+
"TotalAmt": 108.50,
|
|
83
|
+
}, zugferd=True)
|
|
84
|
+
|
|
85
|
+
if result["render_ready"] and result.get("zugferd_ready"):
|
|
86
|
+
# safe to call factur-x / drafthorse
|
|
87
|
+
print("All checks passed")
|
|
88
|
+
else:
|
|
89
|
+
for error in result["errors"]:
|
|
90
|
+
print(f"BLOCKED: {error['message']}")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`validate_invoice()` returns:
|
|
94
|
+
- `status`: "ready" | "ready_with_warnings" | "blocked"
|
|
95
|
+
- `render_ready`: bool
|
|
96
|
+
- `canonical`: normalized invoice dict (all fields in canonical names)
|
|
97
|
+
- `errors`: list of blocking issues with rule_id, path, expected/actual
|
|
98
|
+
- `warnings`: advisory issues
|
|
99
|
+
- `normalizations`: field-level provenance (what was renamed, coerced, computed)
|
|
100
|
+
- `zugferd_ready`: bool (if `zugferd=True`)
|
|
101
|
+
|
|
102
|
+
## What it normalizes
|
|
103
|
+
|
|
104
|
+
90+ vendor field aliases across QuickBooks, Xero, Stripe, and generic CSV/ERP formats:
|
|
105
|
+
|
|
106
|
+
| Source field | Canonical field |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `DocNumber`, `invoiceNo`, `inv_no`, `ref` | `invoice_number` |
|
|
109
|
+
| `CompanyName`, `account_name`, `bill_from_name` | `sender.name` |
|
|
110
|
+
| `customer`, `billTo`, `Contact` | `recipient` |
|
|
111
|
+
| `Line`, `LineItems`, `entries`, `products` | `items` |
|
|
112
|
+
| `UnitPrice`, `cost`, `rate`, `unitCost` | `unit_price` |
|
|
113
|
+
| `Amount`, `LineAmount`, `line_total` | `line_total` |
|
|
114
|
+
| `SubTotal`, `net_total`, `sub_total` | `subtotal` |
|
|
115
|
+
| `TotalAmt`, `grand_total`, `amount_due` | `total` |
|
|
116
|
+
|
|
117
|
+
Plus: type coercion (`"$1,234.56"` → `1234.56`), date parsing (`"April 10, 2026"` → `2026-04-10`), computed defaults (missing `line_total` = `qty × price`), near-match typo detection (`invioce_number` → suggests `invoice_number`).
|
|
118
|
+
|
|
119
|
+
## What it checks
|
|
120
|
+
|
|
121
|
+
7 deterministic semantic checks, all arithmetic:
|
|
122
|
+
|
|
123
|
+
| Check | What it catches |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `identity.invoice_number` | Missing or empty invoice number |
|
|
126
|
+
| `identity.sender_name` | Missing vendor/sender name |
|
|
127
|
+
| `identity.recipient_name` | Missing recipient/buyer name |
|
|
128
|
+
| `items.non_empty` | No line items |
|
|
129
|
+
| `arithmetic.line_total` | line_total ≠ qty × unit_price |
|
|
130
|
+
| `arithmetic.subtotal` | subtotal ≠ sum of line_totals |
|
|
131
|
+
| `arithmetic.total` | total ≠ subtotal + tax_amount |
|
|
132
|
+
|
|
133
|
+
No AI. No heuristics. Every check is deterministic and objectively verifiable.
|
|
134
|
+
|
|
135
|
+
## CLI
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
trustrender validate <data.json> [--zugferd] [--format text|json]
|
|
139
|
+
trustrender ingest <data.json> [-o canonical.json]
|
|
140
|
+
trustrender render <template> <data.json> -o <output.pdf> [--zugferd en16931]
|
|
141
|
+
trustrender preflight <template> <data.json> [--zugferd en16931]
|
|
142
|
+
trustrender serve --templates <dir> [--port 8190]
|
|
143
|
+
trustrender doctor [--smoke]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Integration with factur-x / drafthorse
|
|
147
|
+
|
|
148
|
+
TrustRender validates and normalizes. You generate and embed with the library of your choice.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from trustrender import validate_invoice
|
|
152
|
+
|
|
153
|
+
# Step 1: Validate with TrustRender
|
|
154
|
+
result = validate_invoice(messy_data, zugferd=True)
|
|
155
|
+
if not result["render_ready"] or not result["zugferd_ready"]:
|
|
156
|
+
raise ValueError(f"Invoice blocked: {result['errors']}")
|
|
157
|
+
|
|
158
|
+
# Step 2: Use the canonical payload with drafthorse or factur-x
|
|
159
|
+
canonical = result["canonical"]
|
|
160
|
+
# ... your existing ZUGFeRD generation code here
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## EN 16931 e-invoicing (narrow scope)
|
|
164
|
+
|
|
165
|
+
Catches many document-level and ZUGFeRD/EN 16931 readiness issues before embedding. Currently supports:
|
|
166
|
+
|
|
167
|
+
- **Domestic German B2B invoices** with standard VAT, EUR, SEPA payment
|
|
168
|
+
- Single or mixed VAT rates (7% + 19%)
|
|
169
|
+
- Invoice type 380 and credit note 381
|
|
170
|
+
- PDF/A-3b with embedded CII XML (requires `trustrender[render]`)
|
|
171
|
+
|
|
172
|
+
Not supported (fails loudly): reverse charge, cross-border, allowances/charges, non-EUR currencies.
|
|
173
|
+
|
|
174
|
+
See [docs/einvoice-scope.md](docs/einvoice-scope.md) for the full scope matrix.
|
|
175
|
+
|
|
176
|
+
## Optional: PDF rendering
|
|
177
|
+
|
|
178
|
+
If you also want TrustRender to generate PDFs (not just validate):
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
pip install "trustrender[render]"
|
|
182
|
+
trustrender render invoice.j2.typ data.json -o invoice.pdf --zugferd en16931
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Rendering uses Typst — no browser, no Chromium. Fast and deterministic.
|
|
186
|
+
|
|
187
|
+
## What this is not
|
|
188
|
+
|
|
189
|
+
- Not a full AP automation platform
|
|
190
|
+
- Not an e-invoice compliance certification
|
|
191
|
+
- Not an AI-powered data fixer (all corrections are deterministic)
|
|
192
|
+
- Not a replacement for factur-x or drafthorse — it's the validation layer you run before them
|
|
193
|
+
|
|
194
|
+
## Development
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
pip install -e ".[dev]"
|
|
198
|
+
trustrender doctor --smoke
|
|
199
|
+
pytest
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trustrender"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Validate and normalize Stripe, Shopify, and custom billing data for Factur-X/ZUGFeRD"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Development Status :: 3 - Alpha",
|
|
@@ -15,17 +15,14 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3.11",
|
|
16
16
|
"Programming Language :: Python :: 3.12",
|
|
17
17
|
"Topic :: Office/Business :: Financial",
|
|
18
|
-
"Topic ::
|
|
18
|
+
"Topic :: Office/Business :: Financial :: Accounting",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
19
21
|
]
|
|
20
22
|
readme = "README.md"
|
|
21
23
|
requires-python = ">=3.11"
|
|
22
24
|
dependencies = [
|
|
23
|
-
"typst>=0.14",
|
|
24
|
-
"jinja2>=3.1",
|
|
25
|
-
"starlette>=0.40",
|
|
26
|
-
"uvicorn>=0.30",
|
|
27
25
|
"drafthorse>=2024.0",
|
|
28
|
-
"pypdf>=4.0",
|
|
29
26
|
]
|
|
30
27
|
|
|
31
28
|
[project.urls]
|
|
@@ -39,14 +36,27 @@ Changelog = "https://github.com/verityengine/trustrender/releases"
|
|
|
39
36
|
trustrender = "trustrender.cli:main"
|
|
40
37
|
|
|
41
38
|
[project.optional-dependencies]
|
|
39
|
+
render = [
|
|
40
|
+
"typst>=0.14",
|
|
41
|
+
"jinja2>=3.1",
|
|
42
|
+
"pypdf>=4.0",
|
|
43
|
+
]
|
|
44
|
+
serve = [
|
|
45
|
+
"starlette>=0.40",
|
|
46
|
+
"uvicorn>=0.30",
|
|
47
|
+
]
|
|
48
|
+
zugferd = [
|
|
49
|
+
"factur-x>=3.0",
|
|
50
|
+
]
|
|
51
|
+
all = [
|
|
52
|
+
"trustrender[render,serve,zugferd]",
|
|
53
|
+
]
|
|
42
54
|
dev = [
|
|
43
55
|
"pytest>=8.0",
|
|
44
56
|
"ruff>=0.4",
|
|
45
57
|
"httpx>=0.27",
|
|
46
58
|
"factur-x>=3.0",
|
|
47
|
-
]
|
|
48
|
-
zugferd = [
|
|
49
|
-
"factur-x>=3.0",
|
|
59
|
+
"trustrender[all]",
|
|
50
60
|
]
|
|
51
61
|
|
|
52
62
|
[tool.setuptools.packages.find]
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
"""TrustRender
|
|
1
|
+
"""TrustRender — validate and normalize messy invoice data for Factur-X/ZUGFeRD compliance.
|
|
2
|
+
|
|
3
|
+
Core API (always available, no rendering deps required):
|
|
4
|
+
validate_invoice(data, zugferd=False) → structured validation result
|
|
5
|
+
ingest_invoice(data) → IngestionReport with canonical payload + provenance
|
|
6
|
+
|
|
7
|
+
Rendering API (requires `pip install trustrender[render]`):
|
|
8
|
+
render(template, data, ...) → PDF bytes
|
|
9
|
+
audit(template, data, ...) → AuditResult with fingerprint + drift detection
|
|
10
|
+
"""
|
|
2
11
|
|
|
3
12
|
from __future__ import annotations
|
|
4
13
|
|
|
@@ -7,8 +16,16 @@ import os
|
|
|
7
16
|
from dataclasses import dataclass
|
|
8
17
|
from pathlib import Path
|
|
9
18
|
|
|
10
|
-
from .engine import CompileBackend, compile_typst, compile_typst_file
|
|
11
19
|
from .errors import ErrorCode, TrustRenderError
|
|
20
|
+
from .invoice_ingest import IngestionReport, ingest_invoice
|
|
21
|
+
|
|
22
|
+
# Rendering imports — optional, guarded
|
|
23
|
+
try:
|
|
24
|
+
from .engine import CompileBackend, compile_typst, compile_typst_file
|
|
25
|
+
from .templates import render_template
|
|
26
|
+
_HAS_RENDER = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
_HAS_RENDER = False
|
|
12
29
|
|
|
13
30
|
|
|
14
31
|
@dataclass
|
|
@@ -18,18 +35,83 @@ class RenderResult:
|
|
|
18
35
|
pdf_bytes: bytes
|
|
19
36
|
trace_id: str | None = None
|
|
20
37
|
|
|
21
|
-
# Re-export for public API
|
|
22
|
-
from .errors import ErrorCode as ErrorCode # noqa: F811
|
|
23
|
-
from .templates import render_template
|
|
24
38
|
|
|
25
|
-
__version__ = "0.
|
|
39
|
+
__version__ = "0.3.0"
|
|
26
40
|
|
|
27
41
|
__all__ = [
|
|
42
|
+
# Core API (always available)
|
|
43
|
+
"validate_invoice", "ingest_invoice", "IngestionReport",
|
|
44
|
+
# Rendering API (requires [render] extras)
|
|
28
45
|
"render", "audit", "AuditResult",
|
|
29
|
-
|
|
46
|
+
# Errors
|
|
47
|
+
"TrustRenderError", "ErrorCode",
|
|
48
|
+
# Meta
|
|
49
|
+
"__version__", "bundled_font_dir",
|
|
30
50
|
]
|
|
31
51
|
|
|
32
52
|
|
|
53
|
+
# ── Core API: validation (always available) ──────────────────────────
|
|
54
|
+
|
|
55
|
+
def validate_invoice(data: dict, *, zugferd: bool = False) -> dict:
|
|
56
|
+
"""Validate and normalize messy invoice data. Returns structured result.
|
|
57
|
+
|
|
58
|
+
This is the primary API for using TrustRender as a validation layer
|
|
59
|
+
before Factur-X/ZUGFeRD embedding. No rendering deps required.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
data: Messy invoice data as a dict (any vendor format — QuickBooks,
|
|
63
|
+
Xero, Stripe, CSV, OCR output, etc.).
|
|
64
|
+
zugferd: If True, also run ZUGFeRD EN 16931 readiness checks
|
|
65
|
+
on the canonical output.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dict with keys:
|
|
69
|
+
- status: "ready" | "ready_with_warnings" | "blocked"
|
|
70
|
+
- render_ready: bool
|
|
71
|
+
- canonical: normalized invoice dict
|
|
72
|
+
- errors: list of blocking issues
|
|
73
|
+
- warnings: list of advisory issues
|
|
74
|
+
- normalizations: list of field changes with provenance
|
|
75
|
+
- computed_fields: list of auto-computed fields
|
|
76
|
+
- unknown_fields: list of unrecognized fields with classification
|
|
77
|
+
- zugferd_ready: bool (only if zugferd=True)
|
|
78
|
+
- zugferd_errors: list (only if zugferd=True)
|
|
79
|
+
|
|
80
|
+
Example::
|
|
81
|
+
|
|
82
|
+
from trustrender import validate_invoice
|
|
83
|
+
|
|
84
|
+
result = validate_invoice({
|
|
85
|
+
"invoiceNo": "INV-001",
|
|
86
|
+
"vendor": {"companyName": "Acme Corp"},
|
|
87
|
+
"customer": {"Name": "Client Inc"},
|
|
88
|
+
"LineItems": [{"desc": "Widget", "qty": 2, "unitPrice": 50, "amount": 100}],
|
|
89
|
+
"SubTotal": 100, "tax": 8.50, "TotalAmt": 108.50,
|
|
90
|
+
}, zugferd=True)
|
|
91
|
+
|
|
92
|
+
if result["render_ready"] and result.get("zugferd_ready"):
|
|
93
|
+
print("Safe to embed — all checks passed")
|
|
94
|
+
else:
|
|
95
|
+
for error in result["errors"]:
|
|
96
|
+
print(f"BLOCKED: {error['message']}")
|
|
97
|
+
"""
|
|
98
|
+
report = ingest_invoice(data)
|
|
99
|
+
result = report.to_dict()
|
|
100
|
+
|
|
101
|
+
if zugferd and report.render_ready:
|
|
102
|
+
from .zugferd import validate_zugferd_invoice_data
|
|
103
|
+
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
|
+
]
|
|
107
|
+
result["zugferd_ready"] = len(zug_errors) == 0
|
|
108
|
+
elif zugferd:
|
|
109
|
+
result["zugferd_errors"] = []
|
|
110
|
+
result["zugferd_ready"] = False # can't be zugferd-ready if base validation failed
|
|
111
|
+
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
|
|
33
115
|
# Resolved once at import time — deterministic across local, test, and container.
|
|
34
116
|
# Check multiple locations: env var, installed package, dev layout.
|
|
35
117
|
def _find_bundled_fonts() -> Path | None:
|
|
@@ -85,6 +167,17 @@ def _build_font_paths(
|
|
|
85
167
|
return result or None
|
|
86
168
|
|
|
87
169
|
|
|
170
|
+
def _require_render():
|
|
171
|
+
"""Raise a clear error if rendering dependencies are not installed."""
|
|
172
|
+
if not _HAS_RENDER:
|
|
173
|
+
raise TrustRenderError(
|
|
174
|
+
"Rendering requires additional dependencies. "
|
|
175
|
+
"Install with: pip install trustrender[render]",
|
|
176
|
+
code=ErrorCode.BACKEND_ERROR,
|
|
177
|
+
stage="import",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
88
181
|
def _render_document_pipeline(
|
|
89
182
|
template_path: Path,
|
|
90
183
|
data: dict,
|
|
@@ -94,7 +187,7 @@ def _render_document_pipeline(
|
|
|
94
187
|
validate: bool = True,
|
|
95
188
|
zugferd: str | None = None,
|
|
96
189
|
provenance: bool = False,
|
|
97
|
-
backend: CompileBackend | None = None,
|
|
190
|
+
backend: "CompileBackend | None" = None,
|
|
98
191
|
timeout: float | None = None,
|
|
99
192
|
display_name: str | None = None,
|
|
100
193
|
) -> RenderResult:
|
|
@@ -117,6 +210,8 @@ def _render_document_pipeline(
|
|
|
117
210
|
If ``TRUSTRENDER_HISTORY`` is set, a stage-by-stage RenderTrace is
|
|
118
211
|
recorded to the trace store after each render (success or failure).
|
|
119
212
|
"""
|
|
213
|
+
_require_render()
|
|
214
|
+
|
|
120
215
|
import hashlib
|
|
121
216
|
import time
|
|
122
217
|
|
|
@@ -574,9 +669,12 @@ def _resolve_data(data: dict | str | os.PathLike) -> dict:
|
|
|
574
669
|
path = Path(data) if not isinstance(data, str) else None
|
|
575
670
|
if path is None:
|
|
576
671
|
# Could be a JSON string or a file path string
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
672
|
+
try:
|
|
673
|
+
candidate = Path(data)
|
|
674
|
+
if candidate.exists() and candidate.suffix == ".json":
|
|
675
|
+
path = candidate
|
|
676
|
+
except OSError:
|
|
677
|
+
pass # String too long to be a path — treat as JSON below
|
|
580
678
|
|
|
581
679
|
if path is not None and path.exists():
|
|
582
680
|
with open(path) as f:
|