facturx 0.2.0 → 1.1.0

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -3
  3. data/DOCUMENTATION.md +271 -0
  4. data/README.md +79 -69
  5. data/lib/facturx/attach.rb +4 -20
  6. data/lib/facturx/builders/association.rb +8 -0
  7. data/lib/facturx/builders/base.rb +129 -0
  8. data/lib/facturx/builders/schema.rb +112 -0
  9. data/lib/facturx/builders.rb +25 -0
  10. data/lib/facturx/error.rb +7 -1
  11. data/lib/facturx/format.rb +85 -0
  12. data/lib/facturx/generate.rb +21 -0
  13. data/lib/facturx/pdf/composer.rb +32 -0
  14. data/lib/facturx/profile.rb +1 -1
  15. data/lib/facturx/profile_resolver.rb +40 -0
  16. data/lib/facturx/profiles.rb +1 -8
  17. data/lib/facturx/reader/semantic_mapper/line_mapping.rb +1 -1
  18. data/lib/facturx/reader/semantic_mapper/line_references.rb +19 -0
  19. data/lib/facturx/reader/semantic_mapper/payment_mapping.rb +48 -11
  20. data/lib/facturx/reader/semantic_mapper.rb +9 -0
  21. data/lib/facturx/reader/term_reader.rb +12 -4
  22. data/lib/facturx/reader.rb +3 -7
  23. data/lib/facturx/source_reader.rb +1 -1
  24. data/lib/facturx/validation.rb +61 -0
  25. data/lib/facturx/version.rb +1 -1
  26. data/lib/facturx/writer/context.rb +53 -0
  27. data/lib/facturx/writer/stage.rb +47 -0
  28. data/lib/facturx/writer/stage_support/contact_details.rb +57 -0
  29. data/lib/facturx/writer/stage_support/emission.rb +78 -0
  30. data/lib/facturx/writer/stage_support/identifiers.rb +129 -0
  31. data/lib/facturx/writer/stage_support/structure.rb +88 -0
  32. data/lib/facturx/writer/stages/document_context.rb +31 -0
  33. data/lib/facturx/writer/stages/exchanged_document.rb +32 -0
  34. data/lib/facturx/writer/stages/trade_agreement/parties.rb +105 -0
  35. data/lib/facturx/writer/stages/trade_agreement/references.rb +92 -0
  36. data/lib/facturx/writer/stages/trade_agreement.rb +45 -0
  37. data/lib/facturx/writer/stages/trade_delivery.rb +77 -0
  38. data/lib/facturx/writer/stages/trade_lines/agreement.rb +92 -0
  39. data/lib/facturx/writer/stages/trade_lines/product.rb +78 -0
  40. data/lib/facturx/writer/stages/trade_lines/quantity.rb +17 -0
  41. data/lib/facturx/writer/stages/trade_lines/settlement.rb +89 -0
  42. data/lib/facturx/writer/stages/trade_lines.rb +67 -0
  43. data/lib/facturx/writer/stages/trade_settlement/parties.rb +33 -0
  44. data/lib/facturx/writer/stages/trade_settlement/payment.rb +97 -0
  45. data/lib/facturx/writer/stages/trade_settlement/summary.rb +88 -0
  46. data/lib/facturx/writer/stages/trade_settlement/taxes.rb +121 -0
  47. data/lib/facturx/writer/stages/trade_settlement.rb +70 -0
  48. data/lib/facturx/writer/tracker.rb +131 -0
  49. data/lib/facturx/writer.rb +76 -0
  50. data/lib/facturx/xml/namespaces.rb +19 -0
  51. data/lib/facturx/xml/parser.rb +1 -1
  52. data/lib/facturx/xml/profile_detector.rb +2 -4
  53. data/lib/facturx/xml/report_builder.rb +44 -0
  54. data/lib/facturx/xml/schema_registry.rb +22 -0
  55. data/lib/facturx/xml/schema_validator.rb +7 -2
  56. data/lib/facturx/xml/validator.rb +32 -0
  57. data/lib/facturx/xml/verifier.rb +16 -11
  58. data/lib/facturx.rb +41 -4
  59. data/rbi/facturx.rbi +1755 -0
  60. metadata +44 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6318d858bdc82dd508e53e66c6f41ea56e455ce55df676b9e5268fa881f7a98
4
- data.tar.gz: 8b182edb44be499ec57bb1b0f98ecf892290584c0f959b73e7c787be460188d1
3
+ metadata.gz: 61ff49d55f56bb55d9b6624eb58fd59e16ed016672464f72507ea21b556c3050
4
+ data.tar.gz: 3f6be26b5cb8a65ea5d024cc68c709835154cd67fd48147d4a184182f7938c19
5
5
  SHA512:
6
- metadata.gz: b529dd611b8f5c71c6aa9b9d9458ffeea4352fee2e62030e74f543a464d008c4b97bd186313b13d7b1966abf4f1614b13b9797729d7ece243aa71045e032e03b
7
- data.tar.gz: 4a25ca0ef9d9f4df72150925753a250989316a2a31e171b0b14de85e342950408ebc43d5d4d3150a91863fe07b6a805271bbda7fa3a2f9c2502c68d18111918d
6
+ metadata.gz: 071dce1b5bb8ca3925152e2c0b5bcebaff0b2d73399d7e681f9407eeb59bb0ba981dbcbc194cb9b8faace203cdddc7ba5b332c22e92b377b3082d2641dacce8b
7
+ data.tar.gz: 1ffab4f27f2f0ba54e6ffcf3e5d949aafecde9dd92c758acb1f3a1c9f4a1792794b2c91372d564ec49c917114c715f061d90843a14393fabac44bb0058af03c7
data/CHANGELOG.md CHANGED
@@ -4,9 +4,47 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.1.0] - 2026-09-14
8
+
9
+ ### Add
10
+
11
+ - Add packaged reference documentation for the public API and operational constraints.
12
+ - Add structured XML and document validation reports with a shared issue model.
13
+ - Add precise errors for invalid input sources, invalid documents, and unavailable schemas.
14
+
15
+ ### Fix
16
+
17
+ - Validate generated XML only once before composing and verifying the PDF.
18
+ - Limit the supported public surface to facades, domain values, results, profiles, and emitted errors.
19
+ - Keep profile metadata independent from the bundled schema layout.
20
+ - Expose strict XML failures from `attach` through the shared validation report and issue model.
21
+
22
+ ### Del
23
+
24
+ - Remove `verify_xml`, `Conformance` reports, `ConformanceError`, and direct access to implementation services.
25
+ - Remove `Profile#xsd_path` and the public schema layout constants from `Profiles`.
26
+ - Make the internal `Profiles` lookup constants private; use `Profiles.all`, `.fetch`, or `.for_guideline_urn`.
27
+
28
+ ## [1.0.0] - 2026-09-13
29
+
30
+ ### Add
31
+
32
+ - Add immutable invoice construction through a typed nested DSL.
33
+ - Add aggregated document conformance validation for all five built-in profiles.
34
+ - Add profile-specific Factur-X XML generation and PDF generation from typed documents.
35
+ - Add strict RBI signatures for the public models, builders, reports, errors, and facades.
36
+ - Add maintainer verification of term membership and cardinalities against pinned official workbooks.
37
+
38
+ ### Fix
39
+
40
+ - Fix partial or unrepresentable values so supplied data is rejected instead of silently discarded.
41
+ - Fix repeated credit transfers and discriminated references across semantic round trips.
42
+ - Fix MINIMUM accounting-currency tax totals without emitting the forbidden tax currency term.
43
+ - Fix invalid profile objects to raise the documented typed error.
44
+
7
45
  ## [0.2.0] - 2026-09-12
8
46
 
9
- ### Added
47
+ ### Add
10
48
 
11
49
  - Read XML or PDF bytes into an immutable, typed EN16931 document model with structured diagnostics.
12
50
  - Map the complete EN16931 semantic term set and the MINIMUM, BASIC WL, and BASIC subsets.
@@ -14,12 +52,14 @@ All notable changes to this project will be documented in this file.
14
52
 
15
53
  ## [0.1.0] - 2026-09-11
16
54
 
17
- ### Added
55
+ ### Add
18
56
 
19
57
  - Validate Factur-X 1.09.2 XML against its profile XSD.
20
58
  - Compose PDF/A-3b Factur-X documents with Ghostscript.
21
59
  - Extract embedded Factur-X XML from PDF documents.
22
60
 
23
- [Unreleased]: https://github.com/AdVitam/facturx/compare/v0.2.0...HEAD
61
+ [Unreleased]: https://github.com/AdVitam/facturx/compare/v1.1.0...HEAD
62
+ [1.1.0]: https://github.com/AdVitam/facturx/compare/v1.0.0...v1.1.0
63
+ [1.0.0]: https://github.com/AdVitam/facturx/compare/v0.2.0...v1.0.0
24
64
  [0.2.0]: https://github.com/AdVitam/facturx/compare/v0.1.0...v0.2.0
25
65
  [0.1.0]: https://github.com/AdVitam/facturx/releases/tag/v0.1.0
data/DOCUMENTATION.md ADDED
@@ -0,0 +1,271 @@
1
+ # Facturx documentation
2
+
3
+ ## Contents
4
+
5
+ - [Public API](#public-api)
6
+ - [Build an invoice](#build-an-invoice)
7
+ - [Use an existing XML invoice](#use-an-existing-xml-invoice)
8
+ - [Read a received invoice](#read-a-received-invoice)
9
+ - [Profiles](#profiles)
10
+ - [Validation](#validation)
11
+ - [PDF composition](#pdf-composition)
12
+ - [Round-trip guarantees](#round-trip-guarantees)
13
+ - [Security and resource usage](#security-and-resource-usage)
14
+ - [Development](#development)
15
+
16
+ ## Public API
17
+
18
+ All PDF and XML inputs and outputs are byte strings. Facturx never interprets a string as a file path.
19
+
20
+ The three primary workflows are:
21
+
22
+ | Operation | API | Result |
23
+ |---|---|---|
24
+ | Build and embed XML | `Facturx.generate(pdf:, document:, profile:)` | PDF/A-3b bytes |
25
+ | Read XML or PDF | `Facturx.read(source)` | `Facturx::Reading` |
26
+ | Embed existing XML | `Facturx.attach(pdf:, xml:)` | PDF/A-3b bytes |
27
+
28
+ Focused operations are available when the complete workflow is not needed:
29
+
30
+ | Operation | API | Result |
31
+ |---|---|---|
32
+ | Validate XML | `Facturx.validate_xml(xml:)` | `Facturx::Validation::Report` |
33
+ | Validate typed data | `Facturx.validate_document(document:, profile:)` | `Facturx::Validation::Report` |
34
+ | Build XML | `Facturx.build_xml(document:, profile:)` | XML bytes |
35
+ | Extract embedded XML | `Facturx.extract_xml(pdf:)` | Exact embedded XML bytes |
36
+
37
+ ## Build an invoice
38
+
39
+ `Facturx::Document.build` provides a nested DSL for the immutable Factur-X model. Singular associations accept either keyword attributes or a block; repeating associations use singular helpers such as `line`, `note`, and `tax_breakdown`.
40
+
41
+ ```ruby
42
+ document = Facturx::Document.build(
43
+ invoice_number: 'INV-2026-0042',
44
+ type_code: '380',
45
+ issue_date: Date.new(2026, 9, 13),
46
+ currency: 'EUR'
47
+ ) do |invoice|
48
+ invoice.seller do |seller|
49
+ seller.name = 'Seller SAS'
50
+ seller.address(
51
+ line_one: '10 Example Street',
52
+ postcode: '75001',
53
+ city: 'Paris',
54
+ country_code: 'FR'
55
+ )
56
+ seller.vat_identifier(value: 'FR00123456789')
57
+ end
58
+
59
+ invoice.buyer do |buyer|
60
+ buyer.name = 'Buyer SAS'
61
+ buyer.address(country_code: 'FR')
62
+ end
63
+
64
+ invoice.line do |line|
65
+ line.id = '1'
66
+ line.product(name: 'Consulting services')
67
+ line.quantity(value: BigDecimal('1'), unit_code: 'C62')
68
+ line.net_price(amount: BigDecimal('100.00'))
69
+ line.tax(category_code: 'S', rate: BigDecimal('20.00'))
70
+ line.net_amount = BigDecimal('100.00')
71
+ end
72
+
73
+ invoice.tax_breakdown(
74
+ category_code: 'S',
75
+ rate: BigDecimal('20.00'),
76
+ basis_amount: BigDecimal('100.00'),
77
+ tax_amount: BigDecimal('20.00')
78
+ )
79
+
80
+ invoice.totals(
81
+ line_total: BigDecimal('100.00'),
82
+ tax_basis_total: BigDecimal('100.00'),
83
+ tax_total: BigDecimal('20.00'),
84
+ grand_total: BigDecimal('120.00'),
85
+ due_payable: BigDecimal('120.00')
86
+ )
87
+ end
88
+ ```
89
+
90
+ Choose one of `:minimum`, `:basic_wl`, `:basic`, `:en16931`, or `:extended` when validating or generating a document:
91
+
92
+ ```ruby
93
+ source_pdf = File.binread('invoice.pdf')
94
+ facturx_pdf = Facturx.generate(pdf: source_pdf, document:, profile: :en16931)
95
+ ```
96
+
97
+ `generate` performs document and XSD validation before composing the PDF. Use `validate_document` to collect every semantic issue without raising, or `build_xml` when only the XML is needed:
98
+
99
+ ```ruby
100
+ report = Facturx.validate_document(document:, profile: :en16931)
101
+
102
+ report.issues.each { |issue| warn issue.message } if report.invalid?
103
+ ```
104
+
105
+ Alternatively, call `build_xml` directly and rescue `Facturx::InvalidDocumentError` when the report is only needed on failure.
106
+
107
+ `build_xml` and `generate` raise `Facturx::InvalidDocumentError` with the same report in `error.details[:report]` when the document is invalid.
108
+
109
+ The writer inserts the selected profile's canonical BT-24 guideline URN when it is absent. It reports a profile mismatch when the document contains a conflicting value. Values are serialized from their declared semantic type; monetary values with more than two decimal places are rejected rather than rounded implicitly.
110
+
111
+ ## Use an existing XML invoice
112
+
113
+ Validate XML against the XSD selected by its BT-24 guideline URN:
114
+
115
+ ```ruby
116
+ xml = File.binread('invoice.xml')
117
+ report = Facturx.validate_xml(xml:)
118
+ report.valid?
119
+ ```
120
+
121
+ `validate_xml` reports malformed XML, a missing or unknown BT-24 profile, and every XSD violation without raising. A non-`String` input raises `Facturx::InvalidSourceError`; internal failures such as an unavailable bundled schema also raise a typed `Facturx::Error`.
122
+
123
+ Document validation reports all semantic issues in one pass. XSD validation runs only after that semantic layer succeeds, avoiding structural noise from XML already known to be incomplete.
124
+
125
+ Attach valid XML to an existing visual invoice PDF:
126
+
127
+ ```ruby
128
+ pdf = File.binread('invoice.pdf')
129
+ facturx_pdf = Facturx.attach(pdf:, xml:)
130
+
131
+ File.binwrite('invoice-facturx.pdf', facturx_pdf)
132
+ ```
133
+
134
+ `attach` validates the XML, converts the input to PDF/A-3b, embeds the original bytes as `factur-x.xml`, and verifies the attachment and Factur-X metadata. It rejects signed or encrypted PDFs because rewriting them would invalidate their protection.
135
+
136
+ Extracting XML is deliberately independent from validation:
137
+
138
+ ```ruby
139
+ embedded_xml = Facturx.extract_xml(pdf: facturx_pdf)
140
+ Facturx.validate_xml(xml: embedded_xml)
141
+ ```
142
+
143
+ This separation keeps malformed third-party invoices inspectable.
144
+
145
+ ## Read a received invoice
146
+
147
+ `read` accepts either XML or PDF bytes and returns an immutable `Facturx::Reading`:
148
+
149
+ ```ruby
150
+ reading = Facturx.read(File.binread('received-invoice.pdf'))
151
+
152
+ reading.document.invoice_number
153
+ reading.document.totals.grand_total
154
+ reading.profile.id
155
+ reading.diagnostics
156
+ reading.source
157
+ reading.source_type
158
+ ```
159
+
160
+ Dates are `Date`, decimals are `BigDecimal`, identifiers retain their schemes, and repeating groups are frozen arrays in XML order. `source` contains the exact XML bytes and `source_type` is either `:xml` or `:pdf`.
161
+
162
+ Reading is tolerant and does not run implicit XSD validation. Missing, duplicate, empty, invalid, or unmapped values produce immutable diagnostics while usable fields remain accessible. Call `validate_xml` when structural validation is required.
163
+
164
+ Unknown or missing BT-24 values fall back to the EN 16931 intersection represented by the EXTENDED profile and add a diagnostic. Reject them instead with:
165
+
166
+ ```ruby
167
+ Facturx.read(xml, on_unknown_profile: :raise)
168
+ ```
169
+
170
+ ## Profiles
171
+
172
+ The five canonical profiles are available through `Facturx::Profiles`:
173
+
174
+ ```ruby
175
+ profile = Facturx::Profiles.fetch(:en16931)
176
+
177
+ profile.id
178
+ profile.guideline_urn
179
+ profile.conformance_level
180
+ ```
181
+
182
+ Public writer methods accept either a profile symbol or its canonical `Facturx::Profile`. A separate profile object with matching attributes is rejected to keep profile resolution tied to the bundled XSD and metadata definitions.
183
+
184
+ The semantic registry covers all 184 EN 16931 business terms and the MINIMUM, BASIC WL, and BASIC subsets. EXTENDED-only fields remain available in `Reading#source` and produce diagnostics until represented in the typed model.
185
+
186
+ ## Validation
187
+
188
+ `validate_xml` and `validate_document` return the same immutable report. Each issue identifies its validation layer, severity, message, and available XML or business-term location. A report can therefore be inspected without rescuing expected validation failures:
189
+
190
+ ```ruby
191
+ report.issues.each do |issue|
192
+ warn "#{issue.layer}: #{issue.term_id || issue.path} #{issue.message}"
193
+ end
194
+ ```
195
+
196
+ All issues currently emitted use the `:error` severity. The `:warning` severity is reserved for planned Schematron diagnostics.
197
+
198
+ Generated documents pass through two layers:
199
+
200
+ 1. semantic conformance checks against the selected profile's business-term cardinalities and supported model mappings;
201
+ 2. structural validation against the bundled official profile XSD.
202
+
203
+ Incoming XML passed to `validate_xml` is parsed, resolved to a profile from BT-24, and checked against that profile's XSD.
204
+
205
+ Domain failures use a `Facturx::Error` subclass with structured context in `details`. Common errors include:
206
+
207
+ | Error | Meaning |
208
+ |---|---|
209
+ | `Facturx::ValidationError` | Base class for strict validation failures |
210
+ | `Facturx::InvalidXmlError` | XML cannot be parsed |
211
+ | `Facturx::UnknownProfileError` | BT-24 is absent or unsupported during strict XML validation |
212
+ | `Facturx::XsdValidationError` | XML does not satisfy the selected profile XSD |
213
+ | `Facturx::UnsupportedProfileError` | A writer profile is unsupported |
214
+ | `Facturx::InvalidDocumentError` | A typed document violates the selected profile |
215
+ | `Facturx::InvalidSourceError` | An XML or reader source is not a byte `String` |
216
+ | `Facturx::SchemaLoadError` | A bundled validation schema cannot be loaded |
217
+ | `Facturx::ProtectedPdfError` | The source PDF is signed or encrypted |
218
+ | `Facturx::ComposerUnavailableError` | Required Ghostscript resources are unavailable |
219
+ | `Facturx::CompositionError` | Ghostscript composition failed |
220
+ | `Facturx::ExtractionError` | The embedded Factur-X XML cannot be selected or decoded |
221
+ | `Facturx::VerificationError` | The composed PDF does not match the requested invoice |
222
+
223
+ XSD validation is not complete regulatory validation. Facturx does not yet run the official Schematron business rules, calculate totals, or evaluate accounting consistency.
224
+
225
+ ## PDF composition
226
+
227
+ PDF composition through `attach` or `generate` requires:
228
+
229
+ - Ghostscript 9.54 or newer;
230
+ - Ghostscript's `zugferd.ps` script;
231
+ - an RGB ICC profile.
232
+
233
+ These resources are not distributed with the gem. Facturx searches common installation paths and accepts explicit overrides when automatic discovery is not suitable:
234
+
235
+ ```bash
236
+ export GHOSTSCRIPT_BIN=/opt/ghostscript/bin/gs
237
+ export FACTURX_ZUGFERD_PS=/opt/ghostscript/share/ghostscript/lib/zugferd.ps
238
+ export FACTURX_ICC_PROFILE=/opt/ghostscript/share/ghostscript/iccprofiles/default_rgb.icc
239
+ ```
240
+
241
+ The composer invokes Ghostscript as an external process with a timeout, bounded output capture, and file access restricted to its staged inputs and outputs.
242
+
243
+ Facturx does not create the visual invoice. The supplied PDF remains the visual source that is converted to PDF/A-3b and enriched with Factur-X XML and metadata.
244
+
245
+ ## Round-trip guarantees
246
+
247
+ The reader preserves the exact incoming XML in `Reading#source`, but reading and rebuilding is not a fidelity round-trip. The writer cannot reproduce values outside the typed semantic model.
248
+
249
+ Retain and reuse `Reading#source` when reissuing an incoming invoice without intentional semantic changes.
250
+
251
+ ## Security and resource usage
252
+
253
+ The byte-string API materializes PDF streams in memory. Process untrusted PDFs in a resource-limited worker: limiting only the input file size does not prevent amplification by a compressed embedded stream.
254
+
255
+ Facturx does not communicate with a PDP or implement e-invoicing transport. Network submission, authentication, retries, storage, invoice calculations, and business approval workflows remain application responsibilities.
256
+
257
+ ## Development
258
+
259
+ ```bash
260
+ mise install
261
+ bundle install
262
+ bundle exec rubocop
263
+ bundle exec rspec
264
+ bundle exec rake build
265
+ ```
266
+
267
+ Maintainers can compare the semantic registry, D22B mappings, diagnostics, official XML examples, and paired PDF attachments with an extracted upstream package:
268
+
269
+ ```bash
270
+ FACTURX_REFERENCE_ROOT=/path/to/ZUGFeRD_2.5.2_EN bundle exec rake reference:verify
271
+ ```
data/README.md CHANGED
@@ -1,101 +1,111 @@
1
- # facturx
1
+ # Facturx
2
2
 
3
- `facturx` reads and validates Factur-X XML, creates PDF/A-3b Factur-X invoices from existing PDFs, and extracts their embedded XML.
3
+ [![Gem Version](https://badge.fury.io/rb/facturx.svg)](https://rubygems.org/gems/facturx)
4
+ [![Test](https://github.com/AdVitam/facturx/actions/workflows/test.yml/badge.svg)](https://github.com/AdVitam/facturx/actions/workflows/test.yml)
5
+ [![Ruby](https://img.shields.io/badge/Ruby-3.2%2B-CC342D.svg)](https://www.ruby-lang.org)
6
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.txt)
4
7
 
5
- The first release supports Factur-X 1.09.2 / ZUGFeRD 2.5.2 with the MINIMUM, BASIC WL, BASIC, EN 16931, and EXTENDED profiles.
8
+ Build, read, validate, and compose Factur-X / ZUGFeRD invoices in Ruby.
6
9
 
7
- ## Installation
10
+ Use typed Ruby objects to generate XSD-valid XML, embed it into an existing PDF, or inspect invoices received from third parties.
8
11
 
9
- Add the gem to your bundle:
12
+ ## Features
10
13
 
11
- ```ruby
12
- gem 'facturx'
13
- ```
14
-
15
- Then run `bundle install`.
14
+ ### Main workflows
16
15
 
17
- Composition requires Ghostscript 9.54 or newer with `zugferd.ps` and an RGB ICC profile installed on the system. Package contents vary by operating system and distribution: after installing Ghostscript, verify that both resources are present and configure their paths as described below when automatic discovery cannot find them.
16
+ | | What you need | API |
17
+ |:---:|---|---|
18
+ | ✅ | Generate a Factur-X invoice from typed data | `Facturx::Document.build` + `Facturx.generate` |
19
+ | ✅ | Read an XML or PDF invoice | `Facturx.read` |
20
+ | ✅ | Embed existing Factur-X XML into a PDF | `Facturx.attach` |
18
21
 
19
- ## Usage
22
+ ### Focused tools
20
23
 
21
- All inputs and outputs are byte strings. The gem does not interpret strings as file paths.
24
+ | | Capability | API |
25
+ |:---:|---|---|
26
+ | ✅ | Generate Factur-X XML | `Facturx.build_xml` |
27
+ | ✅ | Validate typed documents | `Facturx.validate_document` |
28
+ | ✅ | Validate XML against official XSDs | `Facturx.validate_xml` |
29
+ | ✅ | Extract the original embedded XML | `Facturx.extract_xml` |
22
30
 
23
- ```ruby
24
- require 'facturx'
31
+ ### Boundaries
25
32
 
26
- xml = File.binread('invoice.xml')
27
- pdf = File.binread('invoice.pdf')
33
+ | | Capability | Responsibility |
34
+ |:---:|---|---|
35
+ | ⏳ | Schematron business rules | Planned |
36
+ | ➖ | Invoice calculations | Application |
37
+ | ➖ | Visual PDF generation | Application |
38
+ | ➖ | PDP transport and e-invoicing | Application |
28
39
 
29
- Facturx.verify_xml(xml: xml) # => true
40
+ ✅ Supported · ⏳ Planned · ➖ Intentionally handled outside the gem
30
41
 
31
- reading = Facturx.read(xml)
32
- reading.document.invoice_number # => "F-2023-004"
33
- reading.document.totals.grand_total # => BigDecimal("120.00")
34
- reading.diagnostics # => [] for a complete EN16931 document
42
+ ## Supported profiles
35
43
 
36
- facturx_pdf = Facturx.attach(pdf: pdf, xml: xml)
37
- File.binwrite('invoice-facturx.pdf', facturx_pdf)
44
+ Facturx supports Factur-X 1.09.2 / ZUGFeRD 2.5.2.
38
45
 
39
- embedded_xml = Facturx.extract_xml(pdf: facturx_pdf)
40
- pdf_reading = Facturx.read(facturx_pdf)
41
- ```
46
+ | Profile | Read | Build | XSD validation |
47
+ |---|:---:|:---:|:---:|
48
+ | MINIMUM | ✅ | ✅ | ✅ |
49
+ | BASIC WL | ✅ | ✅ | ✅ |
50
+ | BASIC | ✅ | ✅ | ✅ |
51
+ | EN 16931 | ✅ | ✅ | ✅ |
52
+ | EXTENDED | ✅* | ✅* | ✅ |
42
53
 
43
- `verify_xml` infers the profile from BT-24 and validates the document against that profile's XSD. It returns `true` on success and raises a typed `Facturx::Error` on failure.
54
+ \* EXTENDED-only fields that are not represented by the typed model remain available in the original XML and are reported through diagnostics.
44
55
 
45
- `attach` always validates the XML first. It converts the input PDF to PDF/A-3b, embeds the original XML bytes as `factur-x.xml`, and verifies the resulting attachment and metadata. Signed and encrypted PDFs are rejected because rewriting them would invalidate their protection.
56
+ ## Requirements
46
57
 
47
- `extract_xml` returns the embedded bytes without validating them. This allows callers to inspect non-conforming invoices received from third parties and explicitly call `verify_xml` when appropriate.
58
+ - Ruby 3.2 or newer
59
+ - Ghostscript 9.54 or newer, `zugferd.ps`, and an RGB ICC profile for PDF composition only
48
60
 
49
- `read` accepts either XML or PDF bytes and returns an immutable `Facturx::Reading`. Its `document` contains typed immutable values: dates are `Date`, decimals are `BigDecimal`, identifiers retain their schemes, and repeating groups are frozen arrays in XML order. `source` always contains the exact embedded XML bytes and `source_type` is either `:xml` or `:pdf`.
61
+ XML building, reading, extraction, and validation require no external executable.
50
62
 
51
- Reading is intentionally tolerant and never performs implicit XSD validation. Missing, duplicate, empty, invalid, or unmapped values are reported through immutable diagnostics while usable fields remain accessible. Call `verify_xml` separately when strict structural validation is required.
63
+ ## Quick start
52
64
 
53
- Unknown or missing BT-24 values fall back to the EN16931 intersection supported for EXTENDED documents and add a diagnostic. Pass `on_unknown_profile: :raise` to reject them instead:
65
+ Turn an existing invoice PDF and typed business data into a PDF/A-3b Factur-X invoice with XSD-validated XML:
54
66
 
55
67
  ```ruby
56
- Facturx.read(xml, on_unknown_profile: :raise)
57
- ```
58
-
59
- The semantic registry covers all 184 EN16931 business terms and the MINIMUM, BASIC WL, and BASIC subsets. EXTENDED-only fields remain available in `Reading#source` and are reported as unmapped until their model is added.
60
-
61
- Reading is not a fidelity round-trip. To reissue an incoming invoice, retain and reuse `Reading#source`; a future writer built from `Reading#document` cannot reproduce fields outside the semantic model.
62
-
63
- The byte-string API materializes PDF streams in memory. Process untrusted PDFs in a resource-limited worker; limiting only the input file size does not prevent amplification by a compressed embedded stream.
64
-
65
- ## Ghostscript discovery
68
+ require 'facturx'
66
69
 
67
- The composer searches common installation paths. Override discovery when necessary:
70
+ document = Facturx::Document.build(
71
+ invoice_number: 'INV-2026-0042',
72
+ type_code: '380',
73
+ issue_date: Date.new(2026, 9, 13),
74
+ currency: 'EUR'
75
+ ) do |invoice|
76
+ invoice.seller do |seller|
77
+ seller.name = 'Seller SAS'
78
+ seller.address(country_code: 'FR')
79
+ end
80
+
81
+ invoice.buyer(name: 'Buyer SAS')
82
+
83
+ invoice.totals(
84
+ tax_basis_total: BigDecimal('100.00'),
85
+ tax_total: BigDecimal('20.00'),
86
+ grand_total: BigDecimal('120.00'),
87
+ due_payable: BigDecimal('120.00')
88
+ )
89
+ end
90
+
91
+ source_pdf = File.binread('invoice.pdf')
92
+ facturx_pdf = Facturx.generate(pdf: source_pdf, document:, profile: :minimum)
68
93
 
69
- ```bash
70
- export GHOSTSCRIPT_BIN=/opt/ghostscript/bin/gs
71
- export FACTURX_ZUGFERD_PS=/opt/ghostscript/share/ghostscript/lib/zugferd.ps
72
- export FACTURX_ICC_PROFILE=/opt/ghostscript/share/ghostscript/iccprofiles/default_rgb.icc
94
+ File.binwrite('invoice-facturx.pdf', facturx_pdf)
73
95
  ```
74
96
 
75
- The gem invokes Ghostscript as an external process. It does not distribute Ghostscript, `zugferd.ps`, or ICC profiles.
76
-
77
- ## Validation scope
97
+ `generate` validates the document and generated XML, embeds `factur-x.xml`, and verifies the resulting PDF metadata and attachment.
78
98
 
79
- Version 0.1 validates XML structure with the official XSDs. It does not yet run the Factur-X Schematron business rules. XSD validation alone must not be presented as complete semantic or regulatory validation.
99
+ ## Documentation
80
100
 
81
- The gem does not generate invoice XML or the visual invoice PDF, communicate with a PDP, or implement e-reporting.
101
+ See [DOCUMENTATION.md](DOCUMENTATION.md) to:
82
102
 
83
- ## Development
84
-
85
- ```bash
86
- mise install
87
- bundle install
88
- bundle exec rubocop
89
- bundle exec rspec
90
- bundle exec rake build
91
- ```
92
-
93
- Maintainers can verify the registry hashes, D22B element names, semantic diagnostics, official XML examples, and paired PDF attachments against an extracted upstream package:
94
-
95
- ```bash
96
- FACTURX_REFERENCE_ROOT=/path/to/ZUGFeRD_2.5.2_EN bundle exec rake reference:verify
97
- ```
103
+ - build richer typed invoices;
104
+ - validate or attach existing XML;
105
+ - read invoices and handle diagnostics;
106
+ - understand profiles, validation boundaries, and typed errors;
107
+ - configure Ghostscript for PDF/A-3b composition.
98
108
 
99
109
  ## License
100
110
 
101
- The gem code is available under the MIT License. Bundled standard artefacts retain their respective upstream notices; see `NOTICE.md`.
111
+ The gem code is available under the [MIT License](LICENSE.txt). Bundled standard artefacts retain their respective upstream notices; see [NOTICE.md](NOTICE.md).
@@ -1,34 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'xml/verifier'
4
- require_relative 'pdf/inspector'
5
- require_relative 'pdf/extractor'
6
- require_relative 'pdf/verifier'
7
- require_relative 'composers/ghostscript'
4
+ require_relative 'pdf/composer'
8
5
 
9
6
  module Facturx
10
7
  class Attach
11
- def initialize(
12
- xml_verifier: Xml::Verifier.new,
13
- pdf_inspector: Pdf::Inspector.new,
14
- composer: Composers::Ghostscript.new,
15
- extractor: Pdf::Extractor.new,
16
- pdf_verifier: Pdf::Verifier.new
17
- )
8
+ def initialize(xml_verifier: Xml::Verifier.new, pdf_composer: Pdf::Composer.new)
18
9
  @xml_verifier = xml_verifier
19
- @pdf_inspector = pdf_inspector
20
- @composer = composer
21
- @extractor = extractor
22
- @pdf_verifier = pdf_verifier
10
+ @pdf_composer = pdf_composer
23
11
  end
24
12
 
25
13
  def call(pdf:, xml:)
26
14
  profile = @xml_verifier.call(xml:)
27
- inspection = @pdf_inspector.call(pdf)
28
- output = @composer.call(pdf:, xml:, profile:)
29
- result = @extractor.call(output)
30
- @pdf_verifier.call(result:, expected_xml: xml, expected_page_count: inspection.page_count, profile:)
31
- output
15
+ @pdf_composer.call(pdf:, xml:, profile:)
32
16
  end
33
17
  end
34
18
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ module Builders
5
+ Association = Data.define(:model, :collection, :helper)
6
+ private_constant :Association
7
+ end
8
+ end