trustrender 0.3.0__tar.gz → 0.3.2__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 (86) hide show
  1. {trustrender-0.3.0/src/trustrender.egg-info → trustrender-0.3.2}/PKG-INFO +36 -2
  2. {trustrender-0.3.0 → trustrender-0.3.2}/README.md +35 -1
  3. {trustrender-0.3.0 → trustrender-0.3.2}/pyproject.toml +1 -1
  4. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/__init__.py +1 -1
  5. trustrender-0.3.2/src/trustrender/adapters/__init__.py +15 -0
  6. trustrender-0.3.2/src/trustrender/adapters/shopify.py +178 -0
  7. trustrender-0.3.2/src/trustrender/adapters/stripe.py +172 -0
  8. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/cli.py +12 -0
  9. {trustrender-0.3.0 → trustrender-0.3.2/src/trustrender.egg-info}/PKG-INFO +36 -2
  10. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender.egg-info/SOURCES.txt +4 -0
  11. trustrender-0.3.2/tests/test_adapters.py +357 -0
  12. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_doctor.py +1 -1
  13. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_fingerprint.py +1 -1
  14. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_provenance.py +1 -1
  15. {trustrender-0.3.0 → trustrender-0.3.2}/LICENSE +0 -0
  16. {trustrender-0.3.0 → trustrender-0.3.2}/setup.cfg +0 -0
  17. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/builtin_templates/einvoice.j2.typ +0 -0
  18. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/builtin_templates/einvoice_data.json +0 -0
  19. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/builtin_templates/invoice.j2.typ +0 -0
  20. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/builtin_templates/invoice_data.json +0 -0
  21. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/builtin_templates/letter.j2.typ +0 -0
  22. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/builtin_templates/letter_data.json +0 -0
  23. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/builtin_templates/receipt.j2.typ +0 -0
  24. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/builtin_templates/receipt_data.json +0 -0
  25. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/builtin_templates/report.j2.typ +0 -0
  26. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/builtin_templates/report_data.json +0 -0
  27. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/builtin_templates/statement.j2.typ +0 -0
  28. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/builtin_templates/statement_data.json +0 -0
  29. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/contract.py +0 -0
  30. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/dashboard.py +0 -0
  31. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/doctor.py +0 -0
  32. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/engine.py +0 -0
  33. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/errors.py +0 -0
  34. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/filters.py +0 -0
  35. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/fingerprint.py +0 -0
  36. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/fonts/Inter/Inter-Bold.ttf +0 -0
  37. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/fonts/Inter/Inter-BoldItalic.ttf +0 -0
  38. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/fonts/Inter/Inter-Italic.ttf +0 -0
  39. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/fonts/Inter/Inter-Regular.ttf +0 -0
  40. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/fonts/Inter/LICENSE.txt +0 -0
  41. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/invoice_aliases.py +0 -0
  42. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/invoice_ingest.py +0 -0
  43. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/invoice_schema.py +0 -0
  44. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/playground/assets/index-Bsf6YnD1.css +0 -0
  45. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/playground/assets/index-C6K0JsLw.js +0 -0
  46. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/playground/assets/pdf.worker-C1breYqn.mjs +0 -0
  47. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/playground/favicon.svg +0 -0
  48. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/playground/icons.svg +0 -0
  49. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/playground/index.html +0 -0
  50. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/provenance.py +0 -0
  51. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/readiness.py +0 -0
  52. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/regression.py +0 -0
  53. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/semantic.py +0 -0
  54. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/server.py +0 -0
  55. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/templates.py +0 -0
  56. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/trace.py +0 -0
  57. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender/zugferd.py +0 -0
  58. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender.egg-info/dependency_links.txt +0 -0
  59. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender.egg-info/entry_points.txt +0 -0
  60. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender.egg-info/requires.txt +0 -0
  61. {trustrender-0.3.0 → trustrender-0.3.2}/src/trustrender.egg-info/top_level.txt +0 -0
  62. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_adversarial_corpus.py +0 -0
  63. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_audit_e2e.py +0 -0
  64. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_cli.py +0 -0
  65. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_contract.py +0 -0
  66. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_engine.py +0 -0
  67. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_error_pipeline.py +0 -0
  68. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_errors.py +0 -0
  69. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_filters.py +0 -0
  70. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_fingerprint_stress.py +0 -0
  71. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_fonts.py +0 -0
  72. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_invoice_ingest.py +0 -0
  73. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_pagination.py +0 -0
  74. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_readiness.py +0 -0
  75. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_real_payloads.py +0 -0
  76. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_regression.py +0 -0
  77. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_regression_stress.py +0 -0
  78. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_render.py +0 -0
  79. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_semantic.py +0 -0
  80. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_semantic_stress.py +0 -0
  81. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_server.py +0 -0
  82. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_templates.py +0 -0
  83. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_trace.py +0 -0
  84. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_ugly_data.py +0 -0
  85. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_ugly_data_pressure.py +0 -0
  86. {trustrender-0.3.0 → trustrender-0.3.2}/tests/test_zugferd.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trustrender
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -44,7 +44,7 @@ Dynamic: license-file
44
44
 
45
45
  Validate and normalize billing data from Stripe, Shopify, and custom systems 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
+ If you're bridging a non-compliant billing platform into EU e-invoicing, TrustRender adds a validation and normalization layer before tools like [factur-x](https://github.com/akretion/factur-x) and [drafthorse](https://github.com/pretix/python-drafthorse) generate or embed compliant XML. It catches arithmetic mismatches, field misalignment, missing required fields, and structural problems before handoff.
48
48
 
49
49
  ```
50
50
  pip install trustrender
@@ -141,6 +141,40 @@ else:
141
141
  - `normalizations`: field-level provenance (what was renamed, coerced, computed)
142
142
  - `zugferd_ready`: bool (if `zugferd=True`)
143
143
 
144
+ ## Stripe adapter
145
+
146
+ Raw Stripe Invoice API responses use cents, Unix timestamps, and nested structures. The adapter handles all of it:
147
+
148
+ ```bash
149
+ trustrender validate stripe_invoice.json --source stripe --zugferd
150
+ ```
151
+
152
+ ```python
153
+ from trustrender import validate_invoice
154
+ from trustrender.adapters import from_stripe
155
+
156
+ result = validate_invoice(from_stripe(raw_stripe_response), zugferd=True)
157
+ ```
158
+
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.
160
+
161
+ ## Shopify adapter
162
+
163
+ Shopify orders use decimal strings, split customer names, and a different structure from invoices:
164
+
165
+ ```bash
166
+ trustrender validate shopify_order.json --source shopify
167
+ ```
168
+
169
+ ```python
170
+ from trustrender import validate_invoice
171
+ from trustrender.adapters import from_shopify
172
+
173
+ result = validate_invoice(from_shopify(raw_shopify_order))
174
+ ```
175
+
176
+ The adapter parses string amounts to floats, combines first_name + last_name, maps order fields to invoice structure, and preserves structured address fields. Shopify orders have no seller info or due date — TrustRender handles both correctly.
177
+
144
178
  ## What it normalizes
145
179
 
146
180
  90+ vendor field aliases across QuickBooks, Xero, Stripe, and generic CSV/ERP formats:
@@ -2,7 +2,7 @@
2
2
 
3
3
  Validate and normalize billing data from Stripe, Shopify, and custom systems 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
+ If you're bridging a non-compliant billing platform into EU e-invoicing, TrustRender adds a validation and normalization layer before tools like [factur-x](https://github.com/akretion/factur-x) and [drafthorse](https://github.com/pretix/python-drafthorse) generate or embed compliant XML. It catches arithmetic mismatches, field misalignment, missing required fields, and structural problems before handoff.
6
6
 
7
7
  ```
8
8
  pip install trustrender
@@ -99,6 +99,40 @@ else:
99
99
  - `normalizations`: field-level provenance (what was renamed, coerced, computed)
100
100
  - `zugferd_ready`: bool (if `zugferd=True`)
101
101
 
102
+ ## Stripe adapter
103
+
104
+ Raw Stripe Invoice API responses use cents, Unix timestamps, and nested structures. The adapter handles all of it:
105
+
106
+ ```bash
107
+ trustrender validate stripe_invoice.json --source stripe --zugferd
108
+ ```
109
+
110
+ ```python
111
+ from trustrender import validate_invoice
112
+ from trustrender.adapters import from_stripe
113
+
114
+ result = validate_invoice(from_stripe(raw_stripe_response), zugferd=True)
115
+ ```
116
+
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.
118
+
119
+ ## Shopify adapter
120
+
121
+ Shopify orders use decimal strings, split customer names, and a different structure from invoices:
122
+
123
+ ```bash
124
+ trustrender validate shopify_order.json --source shopify
125
+ ```
126
+
127
+ ```python
128
+ from trustrender import validate_invoice
129
+ from trustrender.adapters import from_shopify
130
+
131
+ result = validate_invoice(from_shopify(raw_shopify_order))
132
+ ```
133
+
134
+ The adapter parses string amounts to floats, combines first_name + last_name, maps order fields to invoice structure, and preserves structured address fields. Shopify orders have no seller info or due date — TrustRender handles both correctly.
135
+
102
136
  ## What it normalizes
103
137
 
104
138
  90+ vendor field aliases across QuickBooks, Xero, Stripe, and generic CSV/ERP formats:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trustrender"
7
- version = "0.3.0"
7
+ version = "0.3.2"
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.0"
39
+ __version__ = "0.3.2"
40
40
 
41
41
  __all__ = [
42
42
  # Core API (always available)
@@ -0,0 +1,15 @@
1
+ """Source adapters for common billing platforms.
2
+
3
+ Convert raw API responses into the dict shape that
4
+ ingest_invoice() and validate_invoice() already understand.
5
+
6
+ Adapters are pure functions. No network, no filesystem,
7
+ no side effects. They do minimal structural transformation
8
+ and leave missing data missing — the validation pipeline
9
+ catches what's wrong.
10
+ """
11
+
12
+ from .stripe import from_stripe
13
+ from .shopify import from_shopify
14
+
15
+ __all__ = ["from_stripe", "from_shopify"]
@@ -0,0 +1,178 @@
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 is NOT available in Shopify order objects.
124
+ # The validation pipeline will flag this as blocked if sender.name is required.
125
+
126
+ # ── Line items ───────────────────────────────────────────────
127
+
128
+ if isinstance(raw.get("line_items"), list):
129
+ items = []
130
+ for li in raw["line_items"]:
131
+ if not isinstance(li, dict):
132
+ continue
133
+ item: dict = {}
134
+
135
+ if "title" in li:
136
+ item["description"] = li["title"]
137
+ if "quantity" in li:
138
+ item["quantity"] = li["quantity"]
139
+
140
+ # Price is per-unit, as a string
141
+ if "price" in li:
142
+ try:
143
+ unit_price = float(li["price"])
144
+ item["unit_price"] = unit_price
145
+ # Compute line_total (Shopify doesn't always include it)
146
+ if "quantity" in li:
147
+ item["line_total"] = unit_price * li["quantity"]
148
+ except (ValueError, TypeError):
149
+ pass
150
+
151
+ if item:
152
+ items.append(item)
153
+
154
+ if items:
155
+ out["items"] = items
156
+
157
+ return out
158
+
159
+
160
+ def _flatten_address(addr: dict) -> str:
161
+ """Flatten Shopify address object into a single string."""
162
+ parts = []
163
+ if addr.get("address1"):
164
+ parts.append(addr["address1"])
165
+ if addr.get("address2"):
166
+ parts.append(addr["address2"])
167
+ city_parts = []
168
+ if addr.get("city"):
169
+ city_parts.append(addr["city"])
170
+ if addr.get("province"):
171
+ city_parts.append(addr["province"])
172
+ if addr.get("zip"):
173
+ city_parts.append(addr["zip"])
174
+ if city_parts:
175
+ parts.append(", ".join(city_parts))
176
+ if addr.get("country"):
177
+ parts.append(addr["country"])
178
+ return ", ".join(parts)
@@ -0,0 +1,172 @@
1
+ """Stripe Invoice adapter.
2
+
3
+ Converts a raw Stripe Invoice API response (GET /v1/invoices/:id)
4
+ into a dict that ingest_invoice() can normalize and validate.
5
+
6
+ Key transformations:
7
+ - Amounts: cents (int) → dollars (float), divide by 100
8
+ - Dates: Unix timestamps (int) → YYYY-MM-DD strings
9
+ - Line items: extract from lines.data[], pull unit_amount from nested price object
10
+ - Customer: Stripe's customer is the buyer → maps to recipient
11
+ - Currency: lowercased in Stripe → uppercased for canonical
12
+ - Seller: NOT included in Stripe invoice objects. Adapter leaves sender empty.
13
+ The validation pipeline will flag missing sender data — this is correct behavior,
14
+ not a bug. Users must supply seller identity separately.
15
+
16
+ Usage::
17
+
18
+ from trustrender import validate_invoice
19
+ from trustrender.adapters import from_stripe
20
+
21
+ result = validate_invoice(from_stripe(raw_stripe_response))
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from datetime import datetime, timezone
27
+
28
+
29
+ def from_stripe(raw: dict) -> dict:
30
+ """Convert raw Stripe Invoice API response to ingest-ready dict.
31
+
32
+ Args:
33
+ raw: The full JSON response from GET /v1/invoices/:id.
34
+ Amounts must be in cents (Stripe's native format).
35
+
36
+ Returns:
37
+ A dict ready for ingest_invoice(). Not yet canonical,
38
+ not yet validated — just structurally bridged.
39
+
40
+ Raises:
41
+ ValueError: If raw is not a dict.
42
+ """
43
+ if not isinstance(raw, dict):
44
+ raise ValueError(f"from_stripe expects a dict, got {type(raw).__name__}")
45
+
46
+ out: dict = {}
47
+
48
+ # ── Invoice identity ─────────────────────────────────────────
49
+
50
+ if "number" in raw:
51
+ out["invoice_number"] = raw["number"]
52
+
53
+ if "created" in raw and isinstance(raw["created"], (int, float)):
54
+ out["invoice_date"] = _unix_to_date(raw["created"])
55
+
56
+ if "due_date" in raw and isinstance(raw["due_date"], (int, float)):
57
+ out["due_date"] = _unix_to_date(raw["due_date"])
58
+
59
+ if "currency" in raw:
60
+ out["currency"] = raw["currency"].upper()
61
+
62
+ # ── Amounts (cents → dollars) ────────────────────────────────
63
+
64
+ if "subtotal" in raw and isinstance(raw["subtotal"], (int, float)):
65
+ out["subtotal"] = raw["subtotal"] / 100
66
+
67
+ if "tax" in raw and isinstance(raw["tax"], (int, float)):
68
+ out["tax_amount"] = raw["tax"] / 100
69
+
70
+ if "total" in raw and isinstance(raw["total"], (int, float)):
71
+ out["total"] = raw["total"] / 100
72
+
73
+ # ── Customer → recipient ─────────────────────────────────────
74
+ # Stripe's "customer" is the buyer. Maps to recipient, not sender.
75
+
76
+ recipient: dict = {}
77
+
78
+ if raw.get("customer_name"):
79
+ recipient["name"] = raw["customer_name"]
80
+ if raw.get("customer_email"):
81
+ recipient["email"] = raw["customer_email"]
82
+ if isinstance(raw.get("customer_address"), dict):
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"]
93
+
94
+ # Expanded customer object (if customer was expanded in the API call)
95
+ if isinstance(raw.get("customer"), dict):
96
+ cust = raw["customer"]
97
+ if not recipient.get("name") and cust.get("name"):
98
+ recipient["name"] = cust["name"]
99
+ if not recipient.get("email") and cust.get("email"):
100
+ recipient["email"] = cust["email"]
101
+
102
+ if recipient:
103
+ out["recipient"] = recipient
104
+
105
+ # Sender is NOT available in Stripe invoice objects.
106
+ # The validation pipeline will flag this as blocked if sender.name is required.
107
+ # This is intentional — users must supply seller identity separately.
108
+
109
+ # ── Line items ───────────────────────────────────────────────
110
+
111
+ lines_obj = raw.get("lines")
112
+ if isinstance(lines_obj, dict) and isinstance(lines_obj.get("data"), list):
113
+ items = []
114
+ for line in lines_obj["data"]:
115
+ if not isinstance(line, dict):
116
+ continue
117
+ item: dict = {}
118
+
119
+ if "description" in line:
120
+ item["description"] = line["description"]
121
+ if "quantity" in line and line["quantity"] is not None:
122
+ item["quantity"] = line["quantity"]
123
+
124
+ # unit_amount lives on the nested price object
125
+ price_obj = line.get("price")
126
+ if isinstance(price_obj, dict):
127
+ ua = price_obj.get("unit_amount")
128
+ if isinstance(ua, (int, float)):
129
+ item["unit_price"] = ua / 100
130
+
131
+ # line-level amount (cents → dollars)
132
+ if "amount" in line and isinstance(line["amount"], (int, float)):
133
+ item["line_total"] = line["amount"] / 100
134
+
135
+ if item:
136
+ items.append(item)
137
+
138
+ if items:
139
+ out["items"] = items
140
+
141
+ # ── Metadata passthrough ─────────────────────────────────────
142
+
143
+ if isinstance(raw.get("metadata"), dict) and raw["metadata"]:
144
+ out["_metadata"] = raw["metadata"]
145
+
146
+ return out
147
+
148
+
149
+ def _unix_to_date(ts: int | float) -> str:
150
+ """Convert Unix timestamp to YYYY-MM-DD string."""
151
+ return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d")
152
+
153
+
154
+ def _flatten_address(addr: dict) -> str:
155
+ """Flatten Stripe address object into a single string."""
156
+ parts = []
157
+ if addr.get("line1"):
158
+ parts.append(addr["line1"])
159
+ if addr.get("line2"):
160
+ parts.append(addr["line2"])
161
+ city_state = []
162
+ if addr.get("city"):
163
+ city_state.append(addr["city"])
164
+ if addr.get("state"):
165
+ city_state.append(addr["state"])
166
+ if addr.get("postal_code"):
167
+ city_state.append(addr["postal_code"])
168
+ if city_state:
169
+ parts.append(", ".join(city_state))
170
+ if addr.get("country"):
171
+ parts.append(addr["country"])
172
+ return ", ".join(parts)
@@ -62,6 +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", "shopify"], help="Apply source adapter before validation (e.g. --source stripe)")
65
66
  validate_cmd.add_argument("--zugferd", action="store_true", help="Run ZUGFeRD EN 16931 readiness checks")
66
67
  validate_cmd.add_argument("--format", choices=["text", "json"], default="text", dest="output_format", help="Output format (default: text)")
67
68
 
@@ -302,6 +303,15 @@ def _run_validate(args: argparse.Namespace) -> int:
302
303
  print(f"error: file not found: {args.data}", file=sys.stderr)
303
304
  return 1
304
305
 
306
+ # Apply source adapter if specified
307
+ if getattr(args, "source", None):
308
+ if args.source == "stripe":
309
+ from .adapters.stripe import from_stripe
310
+ raw = from_stripe(raw)
311
+ elif args.source == "shopify":
312
+ from .adapters.shopify import from_shopify
313
+ raw = from_shopify(raw)
314
+
305
315
  result = validate_invoice(raw, zugferd=args.zugferd)
306
316
 
307
317
  if args.output_format == "json":
@@ -877,6 +887,8 @@ def _run_audit(args: argparse.Namespace) -> int:
877
887
 
878
888
  def _run_baseline(args: argparse.Namespace) -> int:
879
889
  """Manage render baselines."""
890
+ from . import audit
891
+
880
892
  import json as json_mod
881
893
 
882
894
  if args.baseline_action is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trustrender
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -44,7 +44,7 @@ Dynamic: license-file
44
44
 
45
45
  Validate and normalize billing data from Stripe, Shopify, and custom systems 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
+ If you're bridging a non-compliant billing platform into EU e-invoicing, TrustRender adds a validation and normalization layer before tools like [factur-x](https://github.com/akretion/factur-x) and [drafthorse](https://github.com/pretix/python-drafthorse) generate or embed compliant XML. It catches arithmetic mismatches, field misalignment, missing required fields, and structural problems before handoff.
48
48
 
49
49
  ```
50
50
  pip install trustrender
@@ -141,6 +141,40 @@ else:
141
141
  - `normalizations`: field-level provenance (what was renamed, coerced, computed)
142
142
  - `zugferd_ready`: bool (if `zugferd=True`)
143
143
 
144
+ ## Stripe adapter
145
+
146
+ Raw Stripe Invoice API responses use cents, Unix timestamps, and nested structures. The adapter handles all of it:
147
+
148
+ ```bash
149
+ trustrender validate stripe_invoice.json --source stripe --zugferd
150
+ ```
151
+
152
+ ```python
153
+ from trustrender import validate_invoice
154
+ from trustrender.adapters import from_stripe
155
+
156
+ result = validate_invoice(from_stripe(raw_stripe_response), zugferd=True)
157
+ ```
158
+
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.
160
+
161
+ ## Shopify adapter
162
+
163
+ Shopify orders use decimal strings, split customer names, and a different structure from invoices:
164
+
165
+ ```bash
166
+ trustrender validate shopify_order.json --source shopify
167
+ ```
168
+
169
+ ```python
170
+ from trustrender import validate_invoice
171
+ from trustrender.adapters import from_shopify
172
+
173
+ result = validate_invoice(from_shopify(raw_shopify_order))
174
+ ```
175
+
176
+ The adapter parses string amounts to floats, combines first_name + last_name, maps order fields to invoice structure, and preserves structured address fields. Shopify orders have no seller info or due date — TrustRender handles both correctly.
177
+
144
178
  ## What it normalizes
145
179
 
146
180
  90+ vendor field aliases across QuickBooks, Xero, Stripe, and generic CSV/ERP formats:
@@ -27,6 +27,9 @@ src/trustrender.egg-info/dependency_links.txt
27
27
  src/trustrender.egg-info/entry_points.txt
28
28
  src/trustrender.egg-info/requires.txt
29
29
  src/trustrender.egg-info/top_level.txt
30
+ src/trustrender/adapters/__init__.py
31
+ src/trustrender/adapters/shopify.py
32
+ src/trustrender/adapters/stripe.py
30
33
  src/trustrender/builtin_templates/einvoice.j2.typ
31
34
  src/trustrender/builtin_templates/einvoice_data.json
32
35
  src/trustrender/builtin_templates/invoice.j2.typ
@@ -50,6 +53,7 @@ src/trustrender/playground/index.html
50
53
  src/trustrender/playground/assets/index-Bsf6YnD1.css
51
54
  src/trustrender/playground/assets/index-C6K0JsLw.js
52
55
  src/trustrender/playground/assets/pdf.worker-C1breYqn.mjs
56
+ tests/test_adapters.py
53
57
  tests/test_adversarial_corpus.py
54
58
  tests/test_audit_e2e.py
55
59
  tests/test_cli.py