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.
Files changed (87) hide show
  1. {trustrender-0.3.1/src/trustrender.egg-info → trustrender-0.3.3}/PKG-INFO +96 -41
  2. {trustrender-0.3.1 → trustrender-0.3.3}/README.md +95 -40
  3. {trustrender-0.3.1 → trustrender-0.3.3}/pyproject.toml +1 -1
  4. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/__init__.py +1 -1
  5. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/adapters/__init__.py +2 -1
  6. trustrender-0.3.3/src/trustrender/adapters/shopify.py +185 -0
  7. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/adapters/stripe.py +20 -1
  8. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/cli.py +6 -1
  9. {trustrender-0.3.1 → trustrender-0.3.3/src/trustrender.egg-info}/PKG-INFO +96 -41
  10. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender.egg-info/SOURCES.txt +1 -0
  11. trustrender-0.3.3/tests/test_adapters.py +398 -0
  12. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_doctor.py +1 -1
  13. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_fingerprint.py +1 -1
  14. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_provenance.py +1 -1
  15. trustrender-0.3.1/tests/test_adapters.py +0 -170
  16. {trustrender-0.3.1 → trustrender-0.3.3}/LICENSE +0 -0
  17. {trustrender-0.3.1 → trustrender-0.3.3}/setup.cfg +0 -0
  18. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/einvoice.j2.typ +0 -0
  19. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/einvoice_data.json +0 -0
  20. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/invoice.j2.typ +0 -0
  21. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/invoice_data.json +0 -0
  22. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/letter.j2.typ +0 -0
  23. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/letter_data.json +0 -0
  24. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/receipt.j2.typ +0 -0
  25. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/receipt_data.json +0 -0
  26. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/report.j2.typ +0 -0
  27. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/report_data.json +0 -0
  28. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/statement.j2.typ +0 -0
  29. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/builtin_templates/statement_data.json +0 -0
  30. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/contract.py +0 -0
  31. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/dashboard.py +0 -0
  32. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/doctor.py +0 -0
  33. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/engine.py +0 -0
  34. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/errors.py +0 -0
  35. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/filters.py +0 -0
  36. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/fingerprint.py +0 -0
  37. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/fonts/Inter/Inter-Bold.ttf +0 -0
  38. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/fonts/Inter/Inter-BoldItalic.ttf +0 -0
  39. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/fonts/Inter/Inter-Italic.ttf +0 -0
  40. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/fonts/Inter/Inter-Regular.ttf +0 -0
  41. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/fonts/Inter/LICENSE.txt +0 -0
  42. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/invoice_aliases.py +0 -0
  43. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/invoice_ingest.py +0 -0
  44. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/invoice_schema.py +0 -0
  45. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/playground/assets/index-Bsf6YnD1.css +0 -0
  46. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/playground/assets/index-C6K0JsLw.js +0 -0
  47. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/playground/assets/pdf.worker-C1breYqn.mjs +0 -0
  48. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/playground/favicon.svg +0 -0
  49. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/playground/icons.svg +0 -0
  50. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/playground/index.html +0 -0
  51. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/provenance.py +0 -0
  52. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/readiness.py +0 -0
  53. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/regression.py +0 -0
  54. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/semantic.py +0 -0
  55. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/server.py +0 -0
  56. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/templates.py +0 -0
  57. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/trace.py +0 -0
  58. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender/zugferd.py +0 -0
  59. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender.egg-info/dependency_links.txt +0 -0
  60. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender.egg-info/entry_points.txt +0 -0
  61. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender.egg-info/requires.txt +0 -0
  62. {trustrender-0.3.1 → trustrender-0.3.3}/src/trustrender.egg-info/top_level.txt +0 -0
  63. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_adversarial_corpus.py +0 -0
  64. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_audit_e2e.py +0 -0
  65. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_cli.py +0 -0
  66. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_contract.py +0 -0
  67. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_engine.py +0 -0
  68. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_error_pipeline.py +0 -0
  69. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_errors.py +0 -0
  70. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_filters.py +0 -0
  71. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_fingerprint_stress.py +0 -0
  72. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_fonts.py +0 -0
  73. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_invoice_ingest.py +0 -0
  74. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_pagination.py +0 -0
  75. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_readiness.py +0 -0
  76. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_real_payloads.py +0 -0
  77. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_regression.py +0 -0
  78. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_regression_stress.py +0 -0
  79. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_render.py +0 -0
  80. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_semantic.py +0 -0
  81. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_semantic_stress.py +0 -0
  82. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_server.py +0 -0
  83. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_templates.py +0 -0
  84. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_trace.py +0 -0
  85. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_ugly_data.py +0 -0
  86. {trustrender-0.3.1 → trustrender-0.3.3}/tests/test_ugly_data_pressure.py +0 -0
  87. {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.1
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 from Stripe, Shopify, and custom systems before Factur-X/ZUGFeRD embedding.
45
+ Validate and normalize billing data before Factur-X/ZUGFeRD embedding.
46
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.
47
+ ## Quick start
48
48
 
49
49
  ```
50
50
  pip install trustrender
51
- trustrender validate invoice.json
52
51
  ```
53
52
 
54
- ## What it does
53
+ Stripe and Shopify billing exports don't include the seller fields required for compliant invoices. TrustRender catches this:
55
54
 
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.
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 quickbooks_invoice.json
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
- Invoice: INV-2026-5541
62
- From: Summit Analytics Co.
63
- To: Horizon Financial
64
- Items: 2
65
- Total: $10,524.50
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
- 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
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 ocr_extracted_invoice.json
106
+ $ trustrender validate examples/demo_shopify_ready.json --source shopify
82
107
 
83
- BLOCKED — 2 problem(s)
108
+ Invoice: 1047
109
+ From: Werkzeug-Kontor GmbH
110
+ To: Klaus Berger
111
+ Items: 3
112
+ Total: $1,309.00
84
113
 
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.
114
+ PASS — invoice data is valid
88
115
 
89
- Subtotal is wrong
90
- Lines add up to $12,459.00 but you listed $12,450.00
116
+ Safe to embed in Factur-X/ZUGFeRD PDF.
117
+ ```
91
118
 
92
- This invoice cannot be processed until the problems above are fixed.
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
- 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)
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
- # safe to call factur-x / drafthorse
129
- print("All checks passed")
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
- The adapter converts cents to dollars, timestamps to dates, extracts line items from `lines.data[]`, and maps customer fields to recipient. Seller info is not included in Stripe invoices — TrustRender will flag it if required for ZUGFeRD compliance.
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 from Stripe, Shopify, and custom systems before Factur-X/ZUGFeRD embedding.
3
+ Validate and normalize billing data before Factur-X/ZUGFeRD embedding.
4
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.
5
+ ## Quick start
6
6
 
7
7
  ```
8
8
  pip install trustrender
9
- trustrender validate invoice.json
10
9
  ```
11
10
 
12
- ## What it does
11
+ Stripe and Shopify billing exports don't include the seller fields required for compliant invoices. TrustRender catches this:
13
12
 
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.
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 quickbooks_invoice.json
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
- Invoice: INV-2026-5541
20
- From: Summit Analytics Co.
21
- To: Horizon Financial
22
- Items: 2
23
- Total: $10,524.50
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
- 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
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 ocr_extracted_invoice.json
64
+ $ trustrender validate examples/demo_shopify_ready.json --source shopify
40
65
 
41
- BLOCKED — 2 problem(s)
66
+ Invoice: 1047
67
+ From: Werkzeug-Kontor GmbH
68
+ To: Klaus Berger
69
+ Items: 3
70
+ Total: $1,309.00
42
71
 
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.
72
+ PASS — invoice data is valid
46
73
 
47
- Subtotal is wrong
48
- Lines add up to $12,459.00 but you listed $12,450.00
74
+ Safe to embed in Factur-X/ZUGFeRD PDF.
75
+ ```
49
76
 
50
- This invoice cannot be processed until the problems above are fixed.
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
- 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)
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
- # safe to call factur-x / drafthorse
87
- print("All checks passed")
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
- The adapter converts cents to dollars, timestamps to dates, extracts line items from `lines.data[]`, and maps customer fields to recipient. Seller info is not included in Stripe invoices — TrustRender will flag it if required for ZUGFeRD compliance.
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trustrender"
7
- version = "0.3.1"
7
+ version = "0.3.3"
8
8
  description = "Validate and normalize Stripe, Shopify, and custom billing data for Factur-X/ZUGFeRD"
9
9
  license = "MIT"
10
10
  classifiers = [
@@ -36,7 +36,7 @@ class RenderResult:
36
36
  trace_id: str | None = None
37
37
 
38
38
 
39
- __version__ = "0.3.1"
39
+ __version__ = "0.3.3"
40
40
 
41
41
  __all__ = [
42
42
  # Core API (always available)
@@ -10,5 +10,6 @@ catches what's wrong.
10
10
  """
11
11
 
12
12
  from .stripe import from_stripe
13
+ from .shopify import from_shopify
13
14
 
14
- __all__ = ["from_stripe"]
15
+ __all__ = ["from_stripe", "from_shopify"]
@@ -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
- recipient["address"] = _flatten_address(raw["customer_address"])
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: