trustrender 0.3.2__tar.gz → 0.3.4__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.2/src/trustrender.egg-info → trustrender-0.3.4}/PKG-INFO +82 -44
- {trustrender-0.3.2 → trustrender-0.3.4}/README.md +81 -43
- {trustrender-0.3.2 → trustrender-0.3.4}/pyproject.toml +1 -1
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/__init__.py +1 -1
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/adapters/shopify.py +9 -2
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/adapters/stripe.py +10 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/cli.py +2 -1
- {trustrender-0.3.2 → trustrender-0.3.4/src/trustrender.egg-info}/PKG-INFO +82 -44
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_adapters.py +41 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/LICENSE +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/setup.cfg +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/adapters/__init__.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/einvoice.j2.typ +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/einvoice_data.json +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/invoice.j2.typ +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/invoice_data.json +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/letter.j2.typ +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/letter_data.json +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/receipt.j2.typ +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/receipt_data.json +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/report.j2.typ +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/report_data.json +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/statement.j2.typ +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/statement_data.json +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/contract.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/dashboard.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/doctor.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/engine.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/errors.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/filters.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/fingerprint.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/fonts/Inter/Inter-Bold.ttf +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/fonts/Inter/Inter-BoldItalic.ttf +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/fonts/Inter/Inter-Italic.ttf +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/fonts/Inter/Inter-Regular.ttf +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/fonts/Inter/LICENSE.txt +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/invoice_aliases.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/invoice_ingest.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/invoice_schema.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/playground/assets/index-Bsf6YnD1.css +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/playground/assets/index-C6K0JsLw.js +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/playground/assets/pdf.worker-C1breYqn.mjs +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/playground/favicon.svg +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/playground/icons.svg +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/playground/index.html +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/provenance.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/readiness.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/regression.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/semantic.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/server.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/templates.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/trace.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/zugferd.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender.egg-info/SOURCES.txt +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender.egg-info/dependency_links.txt +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender.egg-info/entry_points.txt +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender.egg-info/requires.txt +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender.egg-info/top_level.txt +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_adversarial_corpus.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_audit_e2e.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_cli.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_contract.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_doctor.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_engine.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_error_pipeline.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_errors.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_filters.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_fingerprint.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_fingerprint_stress.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_fonts.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_invoice_ingest.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_pagination.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_provenance.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_readiness.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_real_payloads.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_regression.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_regression_stress.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_render.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_semantic.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_semantic_stress.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_server.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_templates.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_trace.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_ugly_data.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_ugly_data_pressure.py +0 -0
- {trustrender-0.3.2 → trustrender-0.3.4}/tests/test_zugferd.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.4
|
|
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,56 +42,90 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
# TrustRender
|
|
44
44
|
|
|
45
|
-
Validate and normalize billing data
|
|
45
|
+
Validate and normalize billing data before Factur-X/ZUGFeRD embedding.
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
## Quick start
|
|
48
48
|
|
|
49
49
|
```
|
|
50
50
|
pip install trustrender
|
|
51
|
-
trustrender validate invoice.json
|
|
52
51
|
```
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
Stripe and Shopify billing exports don't include the seller fields required for compliant invoices. TrustRender catches this:
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
```
|
|
56
|
+
$ trustrender validate examples/demo_stripe.json --source stripe
|
|
57
|
+
|
|
58
|
+
Invoice: INV-2026-0187
|
|
59
|
+
From:
|
|
60
|
+
To: Rheingold Maschinenbau GmbH
|
|
61
|
+
Items: 3
|
|
62
|
+
Total: $2,685.37
|
|
63
|
+
|
|
64
|
+
BLOCKED — 1 problem(s)
|
|
65
|
+
|
|
66
|
+
Missing vendor/sender name
|
|
67
|
+
Add a sender.name field to your invoice data.
|
|
68
|
+
|
|
69
|
+
This invoice cannot be processed until the problems above are fixed.
|
|
70
|
+
```
|
|
57
71
|
|
|
58
72
|
```
|
|
59
|
-
$ trustrender validate
|
|
73
|
+
$ trustrender validate examples/demo_shopify.json --source shopify
|
|
74
|
+
|
|
75
|
+
Invoice: 1047
|
|
76
|
+
From:
|
|
77
|
+
To: Klaus Berger
|
|
78
|
+
Items: 3
|
|
79
|
+
Total: $1,309.00
|
|
80
|
+
|
|
81
|
+
BLOCKED — 1 problem(s)
|
|
82
|
+
|
|
83
|
+
Missing vendor/sender name
|
|
84
|
+
Add a sender.name field to your invoice data.
|
|
85
|
+
|
|
86
|
+
This invoice cannot be processed until the problems above are fixed.
|
|
87
|
+
```
|
|
60
88
|
|
|
61
|
-
|
|
62
|
-
From: Summit Analytics Co.
|
|
63
|
-
To: Horizon Financial
|
|
64
|
-
Items: 2
|
|
65
|
-
Total: $10,524.50
|
|
89
|
+
Add your seller identity to the source payload and it passes:
|
|
66
90
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
91
|
+
```
|
|
92
|
+
$ trustrender validate examples/demo_stripe_ready.json --source stripe
|
|
93
|
+
|
|
94
|
+
Invoice: INV-2026-0187
|
|
95
|
+
From: NovaTech Solutions GmbH
|
|
96
|
+
To: Rheingold Maschinenbau GmbH
|
|
97
|
+
Items: 3
|
|
98
|
+
Total: $2,685.37
|
|
72
99
|
|
|
73
100
|
PASS — invoice data is valid
|
|
74
101
|
|
|
75
102
|
Safe to embed in Factur-X/ZUGFeRD PDF.
|
|
76
103
|
```
|
|
77
104
|
|
|
78
|
-
Bad data gets blocked:
|
|
79
|
-
|
|
80
105
|
```
|
|
81
|
-
$ trustrender validate
|
|
106
|
+
$ trustrender validate examples/demo_shopify_ready.json --source shopify
|
|
82
107
|
|
|
83
|
-
|
|
108
|
+
Invoice: 1047
|
|
109
|
+
From: Werkzeug-Kontor GmbH
|
|
110
|
+
To: Klaus Berger
|
|
111
|
+
Items: 3
|
|
112
|
+
Total: $1,309.00
|
|
84
113
|
|
|
85
|
-
|
|
86
|
-
You entered $459.00 but math says $450.00
|
|
87
|
-
Fix the line total or the price/quantity.
|
|
114
|
+
PASS — invoice data is valid
|
|
88
115
|
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
Safe to embed in Factur-X/ZUGFeRD PDF.
|
|
117
|
+
```
|
|
91
118
|
|
|
92
|
-
|
|
119
|
+
The only difference between the blocked and passing files is one added field:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
"sender": { "name": "NovaTech Solutions GmbH" }
|
|
93
123
|
```
|
|
94
124
|
|
|
125
|
+
## Why this exists
|
|
126
|
+
|
|
127
|
+
Stripe and Shopify billing exports are missing seller fields, use platform-specific formats (cents, Unix timestamps, decimal strings), and have no concept of tax compliance. [factur-x](https://github.com/akretion/factur-x) and [drafthorse](https://github.com/pretix/python-drafthorse) generate compliant Factur-X/ZUGFeRD XML, but they assume clean input. TrustRender validates and normalizes source billing data before handoff — it catches arithmetic mismatches, missing required fields, and structural problems so they don't silently produce non-compliant documents.
|
|
128
|
+
|
|
95
129
|
## Install
|
|
96
130
|
|
|
97
131
|
```
|
|
@@ -113,20 +147,14 @@ Requires Python 3.11+.
|
|
|
113
147
|
|
|
114
148
|
```python
|
|
115
149
|
from trustrender import validate_invoice
|
|
150
|
+
from trustrender.adapters import from_stripe
|
|
116
151
|
|
|
117
|
-
|
|
118
|
-
|
|
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)
|
|
152
|
+
# Raw Stripe API response → validated canonical invoice
|
|
153
|
+
result = validate_invoice(from_stripe(raw_stripe_response), zugferd=True)
|
|
126
154
|
|
|
127
155
|
if result["render_ready"] and result.get("zugferd_ready"):
|
|
128
|
-
|
|
129
|
-
|
|
156
|
+
canonical = result["canonical"]
|
|
157
|
+
# safe to hand off to factur-x / drafthorse
|
|
130
158
|
else:
|
|
131
159
|
for error in result["errors"]:
|
|
132
160
|
print(f"BLOCKED: {error['message']}")
|
|
@@ -141,6 +169,20 @@ else:
|
|
|
141
169
|
- `normalizations`: field-level provenance (what was renamed, coerced, computed)
|
|
142
170
|
- `zugferd_ready`: bool (if `zugferd=True`)
|
|
143
171
|
|
|
172
|
+
Also works with messy data from any source:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
result = validate_invoice({
|
|
176
|
+
"invoiceNo": "INV-001",
|
|
177
|
+
"vendor": {"companyName": "Acme Corp"},
|
|
178
|
+
"customer": {"Name": "Client Inc"},
|
|
179
|
+
"LineItems": [{"desc": "Widget", "qty": 2, "unitPrice": 50, "amount": 100}],
|
|
180
|
+
"SubTotal": 100,
|
|
181
|
+
"tax": 8.50,
|
|
182
|
+
"TotalAmt": 108.50,
|
|
183
|
+
}, zugferd=True)
|
|
184
|
+
```
|
|
185
|
+
|
|
144
186
|
## Stripe adapter
|
|
145
187
|
|
|
146
188
|
Raw Stripe Invoice API responses use cents, Unix timestamps, and nested structures. The adapter handles all of it:
|
|
@@ -150,13 +192,11 @@ trustrender validate stripe_invoice.json --source stripe --zugferd
|
|
|
150
192
|
```
|
|
151
193
|
|
|
152
194
|
```python
|
|
153
|
-
from trustrender import validate_invoice
|
|
154
195
|
from trustrender.adapters import from_stripe
|
|
155
|
-
|
|
156
196
|
result = validate_invoice(from_stripe(raw_stripe_response), zugferd=True)
|
|
157
197
|
```
|
|
158
198
|
|
|
159
|
-
|
|
199
|
+
Converts cents to dollars, timestamps to dates, extracts line items from `lines.data[]`, maps customer fields to recipient. If you enrich the source payload with `sender`, `vendor`, or `seller`, the adapter passes it through.
|
|
160
200
|
|
|
161
201
|
## Shopify adapter
|
|
162
202
|
|
|
@@ -167,13 +207,11 @@ trustrender validate shopify_order.json --source shopify
|
|
|
167
207
|
```
|
|
168
208
|
|
|
169
209
|
```python
|
|
170
|
-
from trustrender import validate_invoice
|
|
171
210
|
from trustrender.adapters import from_shopify
|
|
172
|
-
|
|
173
211
|
result = validate_invoice(from_shopify(raw_shopify_order))
|
|
174
212
|
```
|
|
175
213
|
|
|
176
|
-
|
|
214
|
+
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.
|
|
177
215
|
|
|
178
216
|
## What it normalizes
|
|
179
217
|
|
|
@@ -1,55 +1,89 @@
|
|
|
1
1
|
# TrustRender
|
|
2
2
|
|
|
3
|
-
Validate and normalize billing data
|
|
3
|
+
Validate and normalize billing data before Factur-X/ZUGFeRD embedding.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Quick start
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
pip install trustrender
|
|
9
|
-
trustrender validate invoice.json
|
|
10
9
|
```
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
Stripe and Shopify billing exports don't include the seller fields required for compliant invoices. TrustRender catches this:
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
```
|
|
14
|
+
$ trustrender validate examples/demo_stripe.json --source stripe
|
|
15
|
+
|
|
16
|
+
Invoice: INV-2026-0187
|
|
17
|
+
From:
|
|
18
|
+
To: Rheingold Maschinenbau GmbH
|
|
19
|
+
Items: 3
|
|
20
|
+
Total: $2,685.37
|
|
21
|
+
|
|
22
|
+
BLOCKED — 1 problem(s)
|
|
23
|
+
|
|
24
|
+
Missing vendor/sender name
|
|
25
|
+
Add a sender.name field to your invoice data.
|
|
26
|
+
|
|
27
|
+
This invoice cannot be processed until the problems above are fixed.
|
|
28
|
+
```
|
|
15
29
|
|
|
16
30
|
```
|
|
17
|
-
$ trustrender validate
|
|
31
|
+
$ trustrender validate examples/demo_shopify.json --source shopify
|
|
32
|
+
|
|
33
|
+
Invoice: 1047
|
|
34
|
+
From:
|
|
35
|
+
To: Klaus Berger
|
|
36
|
+
Items: 3
|
|
37
|
+
Total: $1,309.00
|
|
38
|
+
|
|
39
|
+
BLOCKED — 1 problem(s)
|
|
40
|
+
|
|
41
|
+
Missing vendor/sender name
|
|
42
|
+
Add a sender.name field to your invoice data.
|
|
43
|
+
|
|
44
|
+
This invoice cannot be processed until the problems above are fixed.
|
|
45
|
+
```
|
|
18
46
|
|
|
19
|
-
|
|
20
|
-
From: Summit Analytics Co.
|
|
21
|
-
To: Horizon Financial
|
|
22
|
-
Items: 2
|
|
23
|
-
Total: $10,524.50
|
|
47
|
+
Add your seller identity to the source payload and it passes:
|
|
24
48
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
49
|
+
```
|
|
50
|
+
$ trustrender validate examples/demo_stripe_ready.json --source stripe
|
|
51
|
+
|
|
52
|
+
Invoice: INV-2026-0187
|
|
53
|
+
From: NovaTech Solutions GmbH
|
|
54
|
+
To: Rheingold Maschinenbau GmbH
|
|
55
|
+
Items: 3
|
|
56
|
+
Total: $2,685.37
|
|
30
57
|
|
|
31
58
|
PASS — invoice data is valid
|
|
32
59
|
|
|
33
60
|
Safe to embed in Factur-X/ZUGFeRD PDF.
|
|
34
61
|
```
|
|
35
62
|
|
|
36
|
-
Bad data gets blocked:
|
|
37
|
-
|
|
38
63
|
```
|
|
39
|
-
$ trustrender validate
|
|
64
|
+
$ trustrender validate examples/demo_shopify_ready.json --source shopify
|
|
40
65
|
|
|
41
|
-
|
|
66
|
+
Invoice: 1047
|
|
67
|
+
From: Werkzeug-Kontor GmbH
|
|
68
|
+
To: Klaus Berger
|
|
69
|
+
Items: 3
|
|
70
|
+
Total: $1,309.00
|
|
42
71
|
|
|
43
|
-
|
|
44
|
-
You entered $459.00 but math says $450.00
|
|
45
|
-
Fix the line total or the price/quantity.
|
|
72
|
+
PASS — invoice data is valid
|
|
46
73
|
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
Safe to embed in Factur-X/ZUGFeRD PDF.
|
|
75
|
+
```
|
|
49
76
|
|
|
50
|
-
|
|
77
|
+
The only difference between the blocked and passing files is one added field:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
"sender": { "name": "NovaTech Solutions GmbH" }
|
|
51
81
|
```
|
|
52
82
|
|
|
83
|
+
## Why this exists
|
|
84
|
+
|
|
85
|
+
Stripe and Shopify billing exports are missing seller fields, use platform-specific formats (cents, Unix timestamps, decimal strings), and have no concept of tax compliance. [factur-x](https://github.com/akretion/factur-x) and [drafthorse](https://github.com/pretix/python-drafthorse) generate compliant Factur-X/ZUGFeRD XML, but they assume clean input. TrustRender validates and normalizes source billing data before handoff — it catches arithmetic mismatches, missing required fields, and structural problems so they don't silently produce non-compliant documents.
|
|
86
|
+
|
|
53
87
|
## Install
|
|
54
88
|
|
|
55
89
|
```
|
|
@@ -71,20 +105,14 @@ Requires Python 3.11+.
|
|
|
71
105
|
|
|
72
106
|
```python
|
|
73
107
|
from trustrender import validate_invoice
|
|
108
|
+
from trustrender.adapters import from_stripe
|
|
74
109
|
|
|
75
|
-
|
|
76
|
-
|
|
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)
|
|
110
|
+
# Raw Stripe API response → validated canonical invoice
|
|
111
|
+
result = validate_invoice(from_stripe(raw_stripe_response), zugferd=True)
|
|
84
112
|
|
|
85
113
|
if result["render_ready"] and result.get("zugferd_ready"):
|
|
86
|
-
|
|
87
|
-
|
|
114
|
+
canonical = result["canonical"]
|
|
115
|
+
# safe to hand off to factur-x / drafthorse
|
|
88
116
|
else:
|
|
89
117
|
for error in result["errors"]:
|
|
90
118
|
print(f"BLOCKED: {error['message']}")
|
|
@@ -99,6 +127,20 @@ else:
|
|
|
99
127
|
- `normalizations`: field-level provenance (what was renamed, coerced, computed)
|
|
100
128
|
- `zugferd_ready`: bool (if `zugferd=True`)
|
|
101
129
|
|
|
130
|
+
Also works with messy data from any source:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
result = validate_invoice({
|
|
134
|
+
"invoiceNo": "INV-001",
|
|
135
|
+
"vendor": {"companyName": "Acme Corp"},
|
|
136
|
+
"customer": {"Name": "Client Inc"},
|
|
137
|
+
"LineItems": [{"desc": "Widget", "qty": 2, "unitPrice": 50, "amount": 100}],
|
|
138
|
+
"SubTotal": 100,
|
|
139
|
+
"tax": 8.50,
|
|
140
|
+
"TotalAmt": 108.50,
|
|
141
|
+
}, zugferd=True)
|
|
142
|
+
```
|
|
143
|
+
|
|
102
144
|
## Stripe adapter
|
|
103
145
|
|
|
104
146
|
Raw Stripe Invoice API responses use cents, Unix timestamps, and nested structures. The adapter handles all of it:
|
|
@@ -108,13 +150,11 @@ trustrender validate stripe_invoice.json --source stripe --zugferd
|
|
|
108
150
|
```
|
|
109
151
|
|
|
110
152
|
```python
|
|
111
|
-
from trustrender import validate_invoice
|
|
112
153
|
from trustrender.adapters import from_stripe
|
|
113
|
-
|
|
114
154
|
result = validate_invoice(from_stripe(raw_stripe_response), zugferd=True)
|
|
115
155
|
```
|
|
116
156
|
|
|
117
|
-
|
|
157
|
+
Converts cents to dollars, timestamps to dates, extracts line items from `lines.data[]`, maps customer fields to recipient. If you enrich the source payload with `sender`, `vendor`, or `seller`, the adapter passes it through.
|
|
118
158
|
|
|
119
159
|
## Shopify adapter
|
|
120
160
|
|
|
@@ -125,13 +165,11 @@ trustrender validate shopify_order.json --source shopify
|
|
|
125
165
|
```
|
|
126
166
|
|
|
127
167
|
```python
|
|
128
|
-
from trustrender import validate_invoice
|
|
129
168
|
from trustrender.adapters import from_shopify
|
|
130
|
-
|
|
131
169
|
result = validate_invoice(from_shopify(raw_shopify_order))
|
|
132
170
|
```
|
|
133
171
|
|
|
134
|
-
|
|
172
|
+
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.
|
|
135
173
|
|
|
136
174
|
## What it normalizes
|
|
137
175
|
|
|
@@ -120,8 +120,15 @@ def from_shopify(raw: dict) -> dict:
|
|
|
120
120
|
if recipient:
|
|
121
121
|
out["recipient"] = recipient
|
|
122
122
|
|
|
123
|
-
# Seller
|
|
124
|
-
#
|
|
123
|
+
# ── Seller passthrough ────────────────────────────────────────
|
|
124
|
+
# Shopify orders never include seller info. If the user has enriched
|
|
125
|
+
# the source payload with sender/vendor/seller metadata, preserve it.
|
|
126
|
+
|
|
127
|
+
if not isinstance(out.get("sender"), dict):
|
|
128
|
+
for key in ("sender", "vendor", "seller"):
|
|
129
|
+
if isinstance(raw.get(key), dict):
|
|
130
|
+
out["sender"] = raw[key]
|
|
131
|
+
break
|
|
125
132
|
|
|
126
133
|
# ── Line items ───────────────────────────────────────────────
|
|
127
134
|
|
|
@@ -138,6 +138,16 @@ def from_stripe(raw: dict) -> dict:
|
|
|
138
138
|
if items:
|
|
139
139
|
out["items"] = items
|
|
140
140
|
|
|
141
|
+
# ── Seller passthrough ───────────────────────────────────────
|
|
142
|
+
# Stripe invoices never include seller info. If the user has enriched
|
|
143
|
+
# the source payload with sender/vendor/seller metadata, preserve it.
|
|
144
|
+
|
|
145
|
+
if not isinstance(out.get("sender"), dict):
|
|
146
|
+
for key in ("sender", "vendor", "seller"):
|
|
147
|
+
if isinstance(raw.get(key), dict):
|
|
148
|
+
out["sender"] = raw[key]
|
|
149
|
+
break
|
|
150
|
+
|
|
141
151
|
# ── Metadata passthrough ─────────────────────────────────────
|
|
142
152
|
|
|
143
153
|
if isinstance(raw.get("metadata"), dict) and raw["metadata"]:
|
|
@@ -401,7 +401,8 @@ def _print_validation_result(result: dict) -> None:
|
|
|
401
401
|
# ZUGFeRD status if checked
|
|
402
402
|
if "zugferd_ready" in result:
|
|
403
403
|
zr = result["zugferd_ready"]
|
|
404
|
-
|
|
404
|
+
no_msg = "no \u2014 fix blocking issues first"
|
|
405
|
+
print(f"ZUGFeRD ready: {'yes' if zr else no_msg}")
|
|
405
406
|
for ze in result.get("zugferd_errors", []):
|
|
406
407
|
print(f" {ze.get('path', '?')}: {ze.get('message', '')}")
|
|
407
408
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trustrender
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
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,56 +42,90 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
# TrustRender
|
|
44
44
|
|
|
45
|
-
Validate and normalize billing data
|
|
45
|
+
Validate and normalize billing data before Factur-X/ZUGFeRD embedding.
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
## Quick start
|
|
48
48
|
|
|
49
49
|
```
|
|
50
50
|
pip install trustrender
|
|
51
|
-
trustrender validate invoice.json
|
|
52
51
|
```
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
Stripe and Shopify billing exports don't include the seller fields required for compliant invoices. TrustRender catches this:
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
```
|
|
56
|
+
$ trustrender validate examples/demo_stripe.json --source stripe
|
|
57
|
+
|
|
58
|
+
Invoice: INV-2026-0187
|
|
59
|
+
From:
|
|
60
|
+
To: Rheingold Maschinenbau GmbH
|
|
61
|
+
Items: 3
|
|
62
|
+
Total: $2,685.37
|
|
63
|
+
|
|
64
|
+
BLOCKED — 1 problem(s)
|
|
65
|
+
|
|
66
|
+
Missing vendor/sender name
|
|
67
|
+
Add a sender.name field to your invoice data.
|
|
68
|
+
|
|
69
|
+
This invoice cannot be processed until the problems above are fixed.
|
|
70
|
+
```
|
|
57
71
|
|
|
58
72
|
```
|
|
59
|
-
$ trustrender validate
|
|
73
|
+
$ trustrender validate examples/demo_shopify.json --source shopify
|
|
74
|
+
|
|
75
|
+
Invoice: 1047
|
|
76
|
+
From:
|
|
77
|
+
To: Klaus Berger
|
|
78
|
+
Items: 3
|
|
79
|
+
Total: $1,309.00
|
|
80
|
+
|
|
81
|
+
BLOCKED — 1 problem(s)
|
|
82
|
+
|
|
83
|
+
Missing vendor/sender name
|
|
84
|
+
Add a sender.name field to your invoice data.
|
|
85
|
+
|
|
86
|
+
This invoice cannot be processed until the problems above are fixed.
|
|
87
|
+
```
|
|
60
88
|
|
|
61
|
-
|
|
62
|
-
From: Summit Analytics Co.
|
|
63
|
-
To: Horizon Financial
|
|
64
|
-
Items: 2
|
|
65
|
-
Total: $10,524.50
|
|
89
|
+
Add your seller identity to the source payload and it passes:
|
|
66
90
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
91
|
+
```
|
|
92
|
+
$ trustrender validate examples/demo_stripe_ready.json --source stripe
|
|
93
|
+
|
|
94
|
+
Invoice: INV-2026-0187
|
|
95
|
+
From: NovaTech Solutions GmbH
|
|
96
|
+
To: Rheingold Maschinenbau GmbH
|
|
97
|
+
Items: 3
|
|
98
|
+
Total: $2,685.37
|
|
72
99
|
|
|
73
100
|
PASS — invoice data is valid
|
|
74
101
|
|
|
75
102
|
Safe to embed in Factur-X/ZUGFeRD PDF.
|
|
76
103
|
```
|
|
77
104
|
|
|
78
|
-
Bad data gets blocked:
|
|
79
|
-
|
|
80
105
|
```
|
|
81
|
-
$ trustrender validate
|
|
106
|
+
$ trustrender validate examples/demo_shopify_ready.json --source shopify
|
|
82
107
|
|
|
83
|
-
|
|
108
|
+
Invoice: 1047
|
|
109
|
+
From: Werkzeug-Kontor GmbH
|
|
110
|
+
To: Klaus Berger
|
|
111
|
+
Items: 3
|
|
112
|
+
Total: $1,309.00
|
|
84
113
|
|
|
85
|
-
|
|
86
|
-
You entered $459.00 but math says $450.00
|
|
87
|
-
Fix the line total or the price/quantity.
|
|
114
|
+
PASS — invoice data is valid
|
|
88
115
|
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
Safe to embed in Factur-X/ZUGFeRD PDF.
|
|
117
|
+
```
|
|
91
118
|
|
|
92
|
-
|
|
119
|
+
The only difference between the blocked and passing files is one added field:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
"sender": { "name": "NovaTech Solutions GmbH" }
|
|
93
123
|
```
|
|
94
124
|
|
|
125
|
+
## Why this exists
|
|
126
|
+
|
|
127
|
+
Stripe and Shopify billing exports are missing seller fields, use platform-specific formats (cents, Unix timestamps, decimal strings), and have no concept of tax compliance. [factur-x](https://github.com/akretion/factur-x) and [drafthorse](https://github.com/pretix/python-drafthorse) generate compliant Factur-X/ZUGFeRD XML, but they assume clean input. TrustRender validates and normalizes source billing data before handoff — it catches arithmetic mismatches, missing required fields, and structural problems so they don't silently produce non-compliant documents.
|
|
128
|
+
|
|
95
129
|
## Install
|
|
96
130
|
|
|
97
131
|
```
|
|
@@ -113,20 +147,14 @@ Requires Python 3.11+.
|
|
|
113
147
|
|
|
114
148
|
```python
|
|
115
149
|
from trustrender import validate_invoice
|
|
150
|
+
from trustrender.adapters import from_stripe
|
|
116
151
|
|
|
117
|
-
|
|
118
|
-
|
|
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)
|
|
152
|
+
# Raw Stripe API response → validated canonical invoice
|
|
153
|
+
result = validate_invoice(from_stripe(raw_stripe_response), zugferd=True)
|
|
126
154
|
|
|
127
155
|
if result["render_ready"] and result.get("zugferd_ready"):
|
|
128
|
-
|
|
129
|
-
|
|
156
|
+
canonical = result["canonical"]
|
|
157
|
+
# safe to hand off to factur-x / drafthorse
|
|
130
158
|
else:
|
|
131
159
|
for error in result["errors"]:
|
|
132
160
|
print(f"BLOCKED: {error['message']}")
|
|
@@ -141,6 +169,20 @@ else:
|
|
|
141
169
|
- `normalizations`: field-level provenance (what was renamed, coerced, computed)
|
|
142
170
|
- `zugferd_ready`: bool (if `zugferd=True`)
|
|
143
171
|
|
|
172
|
+
Also works with messy data from any source:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
result = validate_invoice({
|
|
176
|
+
"invoiceNo": "INV-001",
|
|
177
|
+
"vendor": {"companyName": "Acme Corp"},
|
|
178
|
+
"customer": {"Name": "Client Inc"},
|
|
179
|
+
"LineItems": [{"desc": "Widget", "qty": 2, "unitPrice": 50, "amount": 100}],
|
|
180
|
+
"SubTotal": 100,
|
|
181
|
+
"tax": 8.50,
|
|
182
|
+
"TotalAmt": 108.50,
|
|
183
|
+
}, zugferd=True)
|
|
184
|
+
```
|
|
185
|
+
|
|
144
186
|
## Stripe adapter
|
|
145
187
|
|
|
146
188
|
Raw Stripe Invoice API responses use cents, Unix timestamps, and nested structures. The adapter handles all of it:
|
|
@@ -150,13 +192,11 @@ trustrender validate stripe_invoice.json --source stripe --zugferd
|
|
|
150
192
|
```
|
|
151
193
|
|
|
152
194
|
```python
|
|
153
|
-
from trustrender import validate_invoice
|
|
154
195
|
from trustrender.adapters import from_stripe
|
|
155
|
-
|
|
156
196
|
result = validate_invoice(from_stripe(raw_stripe_response), zugferd=True)
|
|
157
197
|
```
|
|
158
198
|
|
|
159
|
-
|
|
199
|
+
Converts cents to dollars, timestamps to dates, extracts line items from `lines.data[]`, maps customer fields to recipient. If you enrich the source payload with `sender`, `vendor`, or `seller`, the adapter passes it through.
|
|
160
200
|
|
|
161
201
|
## Shopify adapter
|
|
162
202
|
|
|
@@ -167,13 +207,11 @@ trustrender validate shopify_order.json --source shopify
|
|
|
167
207
|
```
|
|
168
208
|
|
|
169
209
|
```python
|
|
170
|
-
from trustrender import validate_invoice
|
|
171
210
|
from trustrender.adapters import from_shopify
|
|
172
|
-
|
|
173
211
|
result = validate_invoice(from_shopify(raw_shopify_order))
|
|
174
212
|
```
|
|
175
213
|
|
|
176
|
-
|
|
214
|
+
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.
|
|
177
215
|
|
|
178
216
|
## What it normalizes
|
|
179
217
|
|
|
@@ -100,6 +100,35 @@ class TestStripeAdapter:
|
|
|
100
100
|
result = from_stripe({"number": "INV-001", "customer_name": "Buyer"})
|
|
101
101
|
assert "sender" not in result
|
|
102
102
|
|
|
103
|
+
def test_sender_passthrough(self):
|
|
104
|
+
"""If user enriches source with sender dict, adapter preserves it."""
|
|
105
|
+
result = from_stripe({"number": "INV-001", "sender": {"name": "Acme GmbH"}})
|
|
106
|
+
assert result["sender"] == {"name": "Acme GmbH"}
|
|
107
|
+
|
|
108
|
+
def test_vendor_passthrough_as_sender(self):
|
|
109
|
+
"""vendor dict maps to sender if sender absent."""
|
|
110
|
+
result = from_stripe({"number": "INV-001", "vendor": {"name": "Acme GmbH"}})
|
|
111
|
+
assert result["sender"] == {"name": "Acme GmbH"}
|
|
112
|
+
|
|
113
|
+
def test_seller_passthrough_as_sender(self):
|
|
114
|
+
"""seller dict maps to sender if sender and vendor absent."""
|
|
115
|
+
result = from_stripe({"number": "INV-001", "seller": {"name": "Acme GmbH"}})
|
|
116
|
+
assert result["sender"] == {"name": "Acme GmbH"}
|
|
117
|
+
|
|
118
|
+
def test_sender_takes_priority_over_vendor(self):
|
|
119
|
+
"""First valid dict wins: sender > vendor > seller."""
|
|
120
|
+
result = from_stripe({
|
|
121
|
+
"number": "INV-001",
|
|
122
|
+
"sender": {"name": "Sender"},
|
|
123
|
+
"vendor": {"name": "Vendor"},
|
|
124
|
+
})
|
|
125
|
+
assert result["sender"]["name"] == "Sender"
|
|
126
|
+
|
|
127
|
+
def test_non_dict_sender_ignored(self):
|
|
128
|
+
"""String sender is not a valid enrichment — ignored."""
|
|
129
|
+
result = from_stripe({"number": "INV-001", "sender": "Acme GmbH"})
|
|
130
|
+
assert "sender" not in result
|
|
131
|
+
|
|
103
132
|
def test_missing_fields_left_missing(self):
|
|
104
133
|
"""Adapter does not guess. Missing data stays missing."""
|
|
105
134
|
result = from_stripe({})
|
|
@@ -284,6 +313,18 @@ class TestShopifyAdapter:
|
|
|
284
313
|
result = from_shopify({"name": "#1", "customer": {"first_name": "A", "last_name": "B"}})
|
|
285
314
|
assert "sender" not in result
|
|
286
315
|
|
|
316
|
+
def test_sender_passthrough(self):
|
|
317
|
+
result = from_shopify({"name": "#1", "sender": {"name": "Werkzeug GmbH"}})
|
|
318
|
+
assert result["sender"] == {"name": "Werkzeug GmbH"}
|
|
319
|
+
|
|
320
|
+
def test_vendor_passthrough_as_sender(self):
|
|
321
|
+
result = from_shopify({"name": "#1", "vendor": {"name": "Werkzeug GmbH"}})
|
|
322
|
+
assert result["sender"] == {"name": "Werkzeug GmbH"}
|
|
323
|
+
|
|
324
|
+
def test_non_dict_sender_ignored(self):
|
|
325
|
+
result = from_shopify({"name": "#1", "sender": "Werkzeug GmbH"})
|
|
326
|
+
assert "sender" not in result
|
|
327
|
+
|
|
287
328
|
def test_missing_fields_left_missing(self):
|
|
288
329
|
result = from_shopify({})
|
|
289
330
|
assert "invoice_number" not in result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/einvoice_data.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/builtin_templates/statement_data.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/playground/assets/index-Bsf6YnD1.css
RENAMED
|
File without changes
|
|
File without changes
|
{trustrender-0.3.2 → trustrender-0.3.4}/src/trustrender/playground/assets/pdf.worker-C1breYqn.mjs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|