trustrender 0.3.1__tar.gz → 0.3.3__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.1/src/trustrender.egg-info → trustrender-0.3.3}/PKG-INFO +96 -41
- {trustrender-0.3.1 → trustrender-0.3.3}/README.md +95 -40
- {trustrender-0.3.1 → trustrender-0.3.3}/pyproject.toml +1 -1
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/__init__.py +1 -1
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/adapters/__init__.py +2 -1
- trustrender-0.3.3/src/trustrender/adapters/shopify.py +185 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/adapters/stripe.py +20 -1
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/cli.py +6 -1
- {trustrender-0.3.1 → trustrender-0.3.3/src/trustrender.egg-info}/PKG-INFO +96 -41
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender.egg-info/SOURCES.txt +1 -0
- trustrender-0.3.3/tests/test_adapters.py +398 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_doctor.py +1 -1
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_fingerprint.py +1 -1
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_provenance.py +1 -1
- trustrender-0.3.1/tests/test_adapters.py +0 -170
- {trustrender-0.3.1 → trustrender-0.3.3}/LICENSE +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/setup.cfg +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/einvoice.j2.typ +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/einvoice_data.json +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/invoice.j2.typ +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/invoice_data.json +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/letter.j2.typ +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/letter_data.json +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/receipt.j2.typ +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/receipt_data.json +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/report.j2.typ +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/report_data.json +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/statement.j2.typ +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/statement_data.json +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/contract.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/dashboard.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/doctor.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/engine.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/errors.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/filters.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/fingerprint.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/fonts/Inter/Inter-Bold.ttf +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/fonts/Inter/Inter-BoldItalic.ttf +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/fonts/Inter/Inter-Italic.ttf +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/fonts/Inter/Inter-Regular.ttf +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/fonts/Inter/LICENSE.txt +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/invoice_aliases.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/invoice_ingest.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/invoice_schema.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/playground/assets/index-Bsf6YnD1.css +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/playground/assets/index-C6K0JsLw.js +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/playground/assets/pdf.worker-C1breYqn.mjs +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/playground/favicon.svg +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/playground/icons.svg +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/playground/index.html +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/provenance.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/readiness.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/regression.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/semantic.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/server.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/templates.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/trace.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/zugferd.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender.egg-info/dependency_links.txt +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender.egg-info/entry_points.txt +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender.egg-info/requires.txt +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender.egg-info/top_level.txt +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_adversarial_corpus.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_audit_e2e.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_cli.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_contract.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_engine.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_error_pipeline.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_errors.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_filters.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_fingerprint_stress.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_fonts.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_invoice_ingest.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_pagination.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_readiness.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_real_payloads.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_regression.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_regression_stress.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_render.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_semantic.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_semantic_stress.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_server.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_templates.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_trace.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_ugly_data.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_ugly_data_pressure.py +0 -0
- {trustrender-0.3.1 → trustrender-0.3.3}/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.3
|
|
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
|
|
60
80
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
```
|
|
88
|
+
|
|
89
|
+
Add your seller identity to the source payload and it passes:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
$ trustrender validate examples/demo_stripe_ready.json --source stripe
|
|
66
93
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,26 @@ 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.
|
|
200
|
+
|
|
201
|
+
## Shopify adapter
|
|
202
|
+
|
|
203
|
+
Shopify orders use decimal strings, split customer names, and a different structure from invoices:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
trustrender validate shopify_order.json --source shopify
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from trustrender.adapters import from_shopify
|
|
211
|
+
result = validate_invoice(from_shopify(raw_shopify_order))
|
|
212
|
+
```
|
|
213
|
+
|
|
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.
|
|
160
215
|
|
|
161
216
|
## What it normalizes
|
|
162
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
|
|
18
38
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
```
|
|
46
|
+
|
|
47
|
+
Add your seller identity to the source payload and it passes:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
$ trustrender validate examples/demo_stripe_ready.json --source stripe
|
|
24
51
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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,26 @@ 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.
|
|
158
|
+
|
|
159
|
+
## Shopify adapter
|
|
160
|
+
|
|
161
|
+
Shopify orders use decimal strings, split customer names, and a different structure from invoices:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
trustrender validate shopify_order.json --source shopify
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from trustrender.adapters import from_shopify
|
|
169
|
+
result = validate_invoice(from_shopify(raw_shopify_order))
|
|
170
|
+
```
|
|
171
|
+
|
|
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.
|
|
118
173
|
|
|
119
174
|
## What it normalizes
|
|
120
175
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Shopify Order adapter.
|
|
2
|
+
|
|
3
|
+
Converts a raw Shopify Admin API order response
|
|
4
|
+
(GET /admin/api/2024-01/orders/{id}.json) into a dict that
|
|
5
|
+
ingest_invoice() can normalize and validate.
|
|
6
|
+
|
|
7
|
+
Shopify has orders, not invoices. This adapter maps order data
|
|
8
|
+
to invoice-like structure for validation before e-invoice embedding.
|
|
9
|
+
|
|
10
|
+
Key transformations:
|
|
11
|
+
- Amounts: decimal strings ("100.00") → floats
|
|
12
|
+
- Dates: ISO 8601 strings → YYYY-MM-DD
|
|
13
|
+
- Customer name: first_name + last_name → combined name string
|
|
14
|
+
- Line items: title → description, price (per-unit string) → unit_price (float)
|
|
15
|
+
- Line totals: computed as quantity × unit_price (Shopify doesn't always include them)
|
|
16
|
+
- Billing address: structured fields preserved for ZUGFeRD handoff
|
|
17
|
+
- Seller: NOT included in Shopify orders. Adapter leaves sender empty.
|
|
18
|
+
The validation pipeline will flag missing sender data — this is correct behavior.
|
|
19
|
+
|
|
20
|
+
Usage::
|
|
21
|
+
|
|
22
|
+
from trustrender import validate_invoice
|
|
23
|
+
from trustrender.adapters import from_shopify
|
|
24
|
+
|
|
25
|
+
result = validate_invoice(from_shopify(raw_shopify_order))
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def from_shopify(raw: dict) -> dict:
|
|
32
|
+
"""Convert raw Shopify Order API response to ingest-ready dict.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
raw: The full JSON response from GET /admin/api/.../orders/{id}.json.
|
|
36
|
+
Amounts are decimal strings. Currency is uppercase.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A dict ready for ingest_invoice(). Not yet canonical,
|
|
40
|
+
not yet validated — just structurally bridged.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
ValueError: If raw is not a dict.
|
|
44
|
+
"""
|
|
45
|
+
if not isinstance(raw, dict):
|
|
46
|
+
raise ValueError(f"from_shopify expects a dict, got {type(raw).__name__}")
|
|
47
|
+
|
|
48
|
+
out: dict = {}
|
|
49
|
+
|
|
50
|
+
# ── Order identity ───────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
# Shopify uses "name" ("#1001") or "order_number" (1001)
|
|
53
|
+
if "name" in raw:
|
|
54
|
+
out["invoice_number"] = str(raw["name"]).lstrip("#")
|
|
55
|
+
elif "order_number" in raw:
|
|
56
|
+
out["invoice_number"] = str(raw["order_number"])
|
|
57
|
+
|
|
58
|
+
if "created_at" in raw and isinstance(raw["created_at"], str):
|
|
59
|
+
out["invoice_date"] = raw["created_at"][:10] # ISO 8601 → YYYY-MM-DD
|
|
60
|
+
|
|
61
|
+
# Shopify orders have no due_date — they are immediate payment
|
|
62
|
+
|
|
63
|
+
if "currency" in raw:
|
|
64
|
+
out["currency"] = raw["currency"]
|
|
65
|
+
|
|
66
|
+
# ── Amounts (strings → floats) ───────────────────────────────
|
|
67
|
+
|
|
68
|
+
for src, dst in [
|
|
69
|
+
("subtotal_price", "subtotal"),
|
|
70
|
+
("total_tax", "tax_amount"),
|
|
71
|
+
("total_price", "total"),
|
|
72
|
+
]:
|
|
73
|
+
val = raw.get(src)
|
|
74
|
+
if val is not None:
|
|
75
|
+
try:
|
|
76
|
+
out[dst] = float(val)
|
|
77
|
+
except (ValueError, TypeError):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
# ── Tax rate (from top-level tax_lines if available) ─────────
|
|
81
|
+
|
|
82
|
+
tax_lines = raw.get("tax_lines")
|
|
83
|
+
if isinstance(tax_lines, list) and len(tax_lines) == 1:
|
|
84
|
+
rate = tax_lines[0].get("rate")
|
|
85
|
+
if isinstance(rate, (int, float)):
|
|
86
|
+
out["tax_rate"] = rate
|
|
87
|
+
|
|
88
|
+
# ── Customer → recipient ─────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
recipient: dict = {}
|
|
91
|
+
|
|
92
|
+
if isinstance(raw.get("customer"), dict):
|
|
93
|
+
cust = raw["customer"]
|
|
94
|
+
name_parts = []
|
|
95
|
+
if cust.get("first_name"):
|
|
96
|
+
name_parts.append(cust["first_name"])
|
|
97
|
+
if cust.get("last_name"):
|
|
98
|
+
name_parts.append(cust["last_name"])
|
|
99
|
+
if name_parts:
|
|
100
|
+
recipient["name"] = " ".join(name_parts)
|
|
101
|
+
if cust.get("email"):
|
|
102
|
+
recipient["email"] = cust["email"]
|
|
103
|
+
|
|
104
|
+
# Billing address
|
|
105
|
+
if isinstance(raw.get("billing_address"), dict):
|
|
106
|
+
addr = raw["billing_address"]
|
|
107
|
+
# Use billing_address.name as fallback for recipient name
|
|
108
|
+
if not recipient.get("name") and addr.get("name"):
|
|
109
|
+
recipient["name"] = addr["name"]
|
|
110
|
+
# Flatten to string for canonical address field
|
|
111
|
+
recipient["address"] = _flatten_address(addr)
|
|
112
|
+
# Preserve structured fields for ZUGFeRD handoff
|
|
113
|
+
if addr.get("city"):
|
|
114
|
+
recipient["city"] = addr["city"]
|
|
115
|
+
if addr.get("zip"):
|
|
116
|
+
recipient["postal_code"] = addr["zip"]
|
|
117
|
+
if addr.get("country_code"):
|
|
118
|
+
recipient["country"] = addr["country_code"]
|
|
119
|
+
|
|
120
|
+
if recipient:
|
|
121
|
+
out["recipient"] = recipient
|
|
122
|
+
|
|
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
|
|
132
|
+
|
|
133
|
+
# ── Line items ───────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
if isinstance(raw.get("line_items"), list):
|
|
136
|
+
items = []
|
|
137
|
+
for li in raw["line_items"]:
|
|
138
|
+
if not isinstance(li, dict):
|
|
139
|
+
continue
|
|
140
|
+
item: dict = {}
|
|
141
|
+
|
|
142
|
+
if "title" in li:
|
|
143
|
+
item["description"] = li["title"]
|
|
144
|
+
if "quantity" in li:
|
|
145
|
+
item["quantity"] = li["quantity"]
|
|
146
|
+
|
|
147
|
+
# Price is per-unit, as a string
|
|
148
|
+
if "price" in li:
|
|
149
|
+
try:
|
|
150
|
+
unit_price = float(li["price"])
|
|
151
|
+
item["unit_price"] = unit_price
|
|
152
|
+
# Compute line_total (Shopify doesn't always include it)
|
|
153
|
+
if "quantity" in li:
|
|
154
|
+
item["line_total"] = unit_price * li["quantity"]
|
|
155
|
+
except (ValueError, TypeError):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
if item:
|
|
159
|
+
items.append(item)
|
|
160
|
+
|
|
161
|
+
if items:
|
|
162
|
+
out["items"] = items
|
|
163
|
+
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _flatten_address(addr: dict) -> str:
|
|
168
|
+
"""Flatten Shopify address object into a single string."""
|
|
169
|
+
parts = []
|
|
170
|
+
if addr.get("address1"):
|
|
171
|
+
parts.append(addr["address1"])
|
|
172
|
+
if addr.get("address2"):
|
|
173
|
+
parts.append(addr["address2"])
|
|
174
|
+
city_parts = []
|
|
175
|
+
if addr.get("city"):
|
|
176
|
+
city_parts.append(addr["city"])
|
|
177
|
+
if addr.get("province"):
|
|
178
|
+
city_parts.append(addr["province"])
|
|
179
|
+
if addr.get("zip"):
|
|
180
|
+
city_parts.append(addr["zip"])
|
|
181
|
+
if city_parts:
|
|
182
|
+
parts.append(", ".join(city_parts))
|
|
183
|
+
if addr.get("country"):
|
|
184
|
+
parts.append(addr["country"])
|
|
185
|
+
return ", ".join(parts)
|
|
@@ -80,7 +80,16 @@ def from_stripe(raw: dict) -> dict:
|
|
|
80
80
|
if raw.get("customer_email"):
|
|
81
81
|
recipient["email"] = raw["customer_email"]
|
|
82
82
|
if isinstance(raw.get("customer_address"), dict):
|
|
83
|
-
|
|
83
|
+
addr = raw["customer_address"]
|
|
84
|
+
# Flatten to string for the canonical address field
|
|
85
|
+
recipient["address"] = _flatten_address(addr)
|
|
86
|
+
# Preserve structured fields for ZUGFeRD XML generation
|
|
87
|
+
if addr.get("city"):
|
|
88
|
+
recipient["city"] = addr["city"]
|
|
89
|
+
if addr.get("postal_code"):
|
|
90
|
+
recipient["postal_code"] = addr["postal_code"]
|
|
91
|
+
if addr.get("country"):
|
|
92
|
+
recipient["country"] = addr["country"]
|
|
84
93
|
|
|
85
94
|
# Expanded customer object (if customer was expanded in the API call)
|
|
86
95
|
if isinstance(raw.get("customer"), dict):
|
|
@@ -129,6 +138,16 @@ def from_stripe(raw: dict) -> dict:
|
|
|
129
138
|
if items:
|
|
130
139
|
out["items"] = items
|
|
131
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
|
+
|
|
132
151
|
# ── Metadata passthrough ─────────────────────────────────────
|
|
133
152
|
|
|
134
153
|
if isinstance(raw.get("metadata"), dict) and raw["metadata"]:
|
|
@@ -62,7 +62,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
62
62
|
# ── Core: validation (no render deps required) ──
|
|
63
63
|
validate_cmd = sub.add_parser("validate", help="Validate invoice data before Factur-X/ZUGFeRD embedding")
|
|
64
64
|
validate_cmd.add_argument("data", help="Path to JSON invoice data (use '-' for stdin)")
|
|
65
|
-
validate_cmd.add_argument("--source", choices=["stripe"], help="Apply source adapter before validation (e.g. --source stripe)")
|
|
65
|
+
validate_cmd.add_argument("--source", choices=["stripe", "shopify"], help="Apply source adapter before validation (e.g. --source stripe)")
|
|
66
66
|
validate_cmd.add_argument("--zugferd", action="store_true", help="Run ZUGFeRD EN 16931 readiness checks")
|
|
67
67
|
validate_cmd.add_argument("--format", choices=["text", "json"], default="text", dest="output_format", help="Output format (default: text)")
|
|
68
68
|
|
|
@@ -308,6 +308,9 @@ def _run_validate(args: argparse.Namespace) -> int:
|
|
|
308
308
|
if args.source == "stripe":
|
|
309
309
|
from .adapters.stripe import from_stripe
|
|
310
310
|
raw = from_stripe(raw)
|
|
311
|
+
elif args.source == "shopify":
|
|
312
|
+
from .adapters.shopify import from_shopify
|
|
313
|
+
raw = from_shopify(raw)
|
|
311
314
|
|
|
312
315
|
result = validate_invoice(raw, zugferd=args.zugferd)
|
|
313
316
|
|
|
@@ -884,6 +887,8 @@ def _run_audit(args: argparse.Namespace) -> int:
|
|
|
884
887
|
|
|
885
888
|
def _run_baseline(args: argparse.Namespace) -> int:
|
|
886
889
|
"""Manage render baselines."""
|
|
890
|
+
from . import audit
|
|
891
|
+
|
|
887
892
|
import json as json_mod
|
|
888
893
|
|
|
889
894
|
if args.baseline_action is None:
|