facturx 1.0.0 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10cd4d86db6d643606dbd85da9ced31d20e884249505e8a608ebb864f9a2f2ef
4
- data.tar.gz: 3e0665c807043cdca83550452ab2b1e3ca7d40b759105eea8856916a316576fd
3
+ metadata.gz: 8beb5aea7a14256c4d062dfbf732462016ec2e0eaacafedf016235f085353eee
4
+ data.tar.gz: 24ea8be809ed5b980cb01b2dee2a775c76c21ae717dda9c056182880bfc44e3d
5
5
  SHA512:
6
- metadata.gz: 0cf6cb2b75adb6a668a5746420ee82177aada0c87d9d40bd625387a59d1409b63ef53916df3480aa7adaf539b59c99fb635667134e3cefcbd66590f1a19aeecd
7
- data.tar.gz: 3e7715e0181e11b9b0589ab85da8194e692647fcb4be0d42d177968584d0cc1f1e2114a782e8d533c7c5a2fbac0943bc2bd4964e59633773c9dd864e5a42cee4
6
+ metadata.gz: 93279c7e5faaebe8aa8d2b4c645f739e6a70517fa530340186621af496b95e3ec29b5f84a99fcb6edfadb0601013e0b945d3aaf2440eb02aa30c395eb06f5f6e
7
+ data.tar.gz: 04af7d886516bebfa086252a99892721b9a9a775271e35abc94641ac40fbfbb70865be020cf93b41123c317ae794d823ec7ce0104119f0a7cdb8829d6ac3ba64
data/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.2.0] - 2026-09-14
8
+
9
+ ### Add
10
+
11
+ - Add optional official Schematron validation through the lightweight `facturx-schematron` companion gem.
12
+ - Add Schematron issues and strict errors to the existing validation model for all writing and attachment workflows.
13
+ - Add shared bounded subprocess execution for Ghostscript and SaxonC without loading optional rules in the core gem.
14
+
15
+ ### Fix
16
+
17
+ - Preserve non-UTF-8 invoice text when passing parsed XML to SaxonC.
18
+ - Report oversized Schematron output as a resource limit instead of malformed validation output.
19
+ - Keep subprocess timeout diagnostics valid UTF-8 while preserving raw successful output bytes.
20
+
21
+ ## [1.1.0] - 2026-09-14
22
+
23
+ ### Add
24
+
25
+ - Add packaged reference documentation for the public API and operational constraints.
26
+ - Add structured XML and document validation reports with a shared issue model.
27
+ - Add precise errors for invalid input sources, invalid documents, and unavailable schemas.
28
+
29
+ ### Fix
30
+
31
+ - Validate generated XML only once before composing and verifying the PDF.
32
+ - Limit the supported public surface to facades, domain values, results, profiles, and emitted errors.
33
+ - Keep profile metadata independent from the bundled schema layout.
34
+ - Expose strict XML failures from `attach` through the shared validation report and issue model.
35
+
36
+ ### Del
37
+
38
+ - Remove `verify_xml`, `Conformance` reports, `ConformanceError`, and direct access to implementation services.
39
+ - Remove `Profile#xsd_path` and the public schema layout constants from `Profiles`.
40
+ - Make the internal `Profiles` lookup constants private; use `Profiles.all`, `.fetch`, or `.for_guideline_urn`.
41
+
7
42
  ## [1.0.0] - 2026-09-13
8
43
 
9
44
  ### Add
@@ -37,7 +72,9 @@ All notable changes to this project will be documented in this file.
37
72
  - Compose PDF/A-3b Factur-X documents with Ghostscript.
38
73
  - Extract embedded Factur-X XML from PDF documents.
39
74
 
40
- [Unreleased]: https://github.com/AdVitam/facturx/compare/v1.0.0...HEAD
75
+ [Unreleased]: https://github.com/AdVitam/facturx/compare/v1.2.0...HEAD
76
+ [1.2.0]: https://github.com/AdVitam/facturx/compare/v1.1.0...v1.2.0
77
+ [1.1.0]: https://github.com/AdVitam/facturx/compare/v1.0.0...v1.1.0
41
78
  [1.0.0]: https://github.com/AdVitam/facturx/compare/v0.2.0...v1.0.0
42
79
  [0.2.0]: https://github.com/AdVitam/facturx/compare/v0.1.0...v0.2.0
43
80
  [0.1.0]: https://github.com/AdVitam/facturx/releases/tag/v0.1.0
data/DOCUMENTATION.md ADDED
@@ -0,0 +1,302 @@
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
+ Core validation issues use the `:error` severity. The optional Schematron validator also preserves official `:warning` findings, which do not invalidate a report.
197
+
198
+ Generated documents always 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. When enabled, Schematron runs after these core checks as a third validation layer.
204
+
205
+ ### Optional Schematron validation
206
+
207
+ Install the companion gem to add the official Factur-X 1.09.2 business rules without increasing the core gem's package or runtime footprint:
208
+
209
+ ```ruby
210
+ gem 'facturx'
211
+ gem 'facturx-schematron', require: 'facturx/schematron'
212
+ ```
213
+
214
+ Alternatively, require it explicitly after Bundler setup:
215
+
216
+ ```ruby
217
+ require 'facturx'
218
+ require 'facturx/schematron'
219
+ ```
220
+
221
+ Loading the companion enables Schematron after XSD validation for `validate_xml`, `validate_document`, `attach`, `build_xml`, and `generate`. Public method signatures and report types remain unchanged. `read` stays tolerant and does not validate implicitly.
222
+
223
+ The companion requires SaxonC-HE's `Transform` executable, version 12.10 or newer. Put it in `PATH`, or provide its absolute path:
224
+
225
+ ```bash
226
+ export FACTURX_SAXONC_TRANSFORM=/opt/saxonc/bin/Transform
227
+ ```
228
+
229
+ Loading fails immediately with `Facturx::Schematron::UnavailableError` when the executable is missing, cannot start, or is unsupported. Runtime engine failures raise `Facturx::Schematron::ExecutionError`; they never fall back silently to XSD-only validation.
230
+
231
+ Schematron findings use the `:schematron` layer and expose the official rule ID, test, and flag in `issue.details`. Official warnings keep reports valid; assertions without a warning flag are errors. Strict XML workflows raise `Facturx::SchematronValidationError` for blocking findings.
232
+
233
+ The companion ships only the five official compiled XSLT stylesheets and adjacent code databases. Each successful validation starts one isolated SaxonC subprocess, so applications processing large batches should account for native process startup and memory in their worker sizing.
234
+
235
+ Domain failures use a `Facturx::Error` subclass with structured context in `details`. Common errors include:
236
+
237
+ | Error | Meaning |
238
+ |---|---|
239
+ | `Facturx::ValidationError` | Base class for strict validation failures |
240
+ | `Facturx::InvalidXmlError` | XML cannot be parsed |
241
+ | `Facturx::UnknownProfileError` | BT-24 is absent or unsupported during strict XML validation |
242
+ | `Facturx::XsdValidationError` | XML does not satisfy the selected profile XSD |
243
+ | `Facturx::SchematronValidationError` | XML violates an enabled Schematron business rule |
244
+ | `Facturx::UnsupportedProfileError` | A writer profile is unsupported |
245
+ | `Facturx::InvalidDocumentError` | A typed document violates the selected profile |
246
+ | `Facturx::InvalidSourceError` | An XML or reader source is not a byte `String` |
247
+ | `Facturx::SchemaLoadError` | A bundled validation schema cannot be loaded |
248
+ | `Facturx::ProtectedPdfError` | The source PDF is signed or encrypted |
249
+ | `Facturx::ComposerUnavailableError` | Required Ghostscript resources are unavailable |
250
+ | `Facturx::CompositionError` | Ghostscript composition failed |
251
+ | `Facturx::ExtractionError` | The embedded Factur-X XML cannot be selected or decoded |
252
+ | `Facturx::VerificationError` | The composed PDF does not match the requested invoice |
253
+
254
+ XSD validation alone is not complete regulatory validation. The optional companion runs the official Schematron rules, but Facturx does not calculate invoice values or replace application-level accounting controls.
255
+
256
+ ## PDF composition
257
+
258
+ PDF composition through `attach` or `generate` requires:
259
+
260
+ - Ghostscript 9.54 or newer;
261
+ - Ghostscript's `zugferd.ps` script;
262
+ - an RGB ICC profile.
263
+
264
+ These resources are not distributed with the gem. Facturx searches common installation paths and accepts explicit overrides when automatic discovery is not suitable:
265
+
266
+ ```bash
267
+ export GHOSTSCRIPT_BIN=/opt/ghostscript/bin/gs
268
+ export FACTURX_ZUGFERD_PS=/opt/ghostscript/share/ghostscript/lib/zugferd.ps
269
+ export FACTURX_ICC_PROFILE=/opt/ghostscript/share/ghostscript/iccprofiles/default_rgb.icc
270
+ ```
271
+
272
+ The composer invokes Ghostscript as an external process with a timeout, bounded output capture, and file access restricted to its staged inputs and outputs. The optional Schematron integration reuses the same bounded process lifecycle for SaxonC.
273
+
274
+ 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.
275
+
276
+ ## Round-trip guarantees
277
+
278
+ 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.
279
+
280
+ Retain and reuse `Reading#source` when reissuing an incoming invoice without intentional semantic changes.
281
+
282
+ ## Security and resource usage
283
+
284
+ 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.
285
+
286
+ 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.
287
+
288
+ ## Development
289
+
290
+ ```bash
291
+ mise install
292
+ bundle install
293
+ bundle exec rubocop
294
+ bundle exec rake
295
+ bundle exec rake build_all
296
+ ```
297
+
298
+ Maintainers can compare the semantic registry, D22B mappings, diagnostics, official XML examples, and paired PDF attachments with an extracted upstream package:
299
+
300
+ ```bash
301
+ FACTURX_REFERENCE_ROOT=/path/to/ZUGFeRD_2.5.2_EN bundle exec rake reference:verify
302
+ ```
data/README.md CHANGED
@@ -1,133 +1,115 @@
1
- # facturx
1
+ # Facturx
2
2
 
3
- `facturx` builds, 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
+ ### Main workflows
14
15
 
15
- Then run `bundle install`.
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` |
16
21
 
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.
22
+ ### Focused tools
18
23
 
19
- ## Usage
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
+ | 🔌 | Add official Schematron business-rule validation | `facturx-schematron` |
30
+ | ✅ | Extract the original embedded XML | `Facturx.extract_xml` |
20
31
 
21
- All inputs and outputs are byte strings. The gem does not interpret strings as file paths.
32
+ ### Boundaries
22
33
 
23
- ```ruby
24
- require 'facturx'
34
+ | | Capability | Responsibility |
35
+ |:---:|---|---|
36
+ | ➖ | Invoice calculations | Application |
37
+ | ➖ | Visual PDF generation | Application |
38
+ | ➖ | PDP transport and e-invoicing | Application |
25
39
 
26
- xml = File.binread('invoice.xml')
27
- pdf = File.binread('invoice.pdf')
40
+ ✅ Included · 🔌 Optional companion · ➖ Intentionally handled outside the gem
28
41
 
29
- Facturx.verify_xml(xml: xml)
42
+ ## Supported profiles
30
43
 
31
- reading = Facturx.read(xml)
32
- reading.document.invoice_number
33
- reading.document.totals.grand_total
34
- reading.diagnostics
44
+ Facturx supports Factur-X 1.09.2 / ZUGFeRD 2.5.2.
35
45
 
36
- facturx_pdf = Facturx.attach(pdf: pdf, xml: xml)
37
- File.binwrite('invoice-facturx.pdf', facturx_pdf)
46
+ | Profile | Read | Build | XSD | Schematron** |
47
+ |---|:---:|:---:|:---:|:---:|
48
+ | MINIMUM | ✅ | ✅ | ✅ | ✅ |
49
+ | BASIC WL | ✅ | ✅ | ✅ | ✅ |
50
+ | BASIC | ✅ | ✅ | ✅ | ✅ |
51
+ | EN 16931 | ✅ | ✅ | ✅ | ✅ |
52
+ | EXTENDED | ✅* | ✅* | ✅ | ✅ |
38
53
 
39
- embedded_xml = Facturx.extract_xml(pdf: facturx_pdf)
40
- pdf_reading = Facturx.read(facturx_pdf)
41
- ```
54
+ \* EXTENDED-only fields that are not represented by the typed model remain available in the original XML and are reported through diagnostics.
55
+
56
+ \** Available through the optional `facturx-schematron` gem.
57
+
58
+ ## Requirements
59
+
60
+ - Ruby 3.2 or newer
61
+ - Ghostscript 9.54 or newer, `zugferd.ps`, and an RGB ICC profile for PDF composition only
62
+ - SaxonC-HE 12.10 or newer only when `facturx-schematron` is enabled
63
+
64
+ Core XML building, reading, extraction, and XSD validation require no external executable.
42
65
 
43
- Build a typed document with the nested DSL, validate it without generating XML, or generate XML and attach it to an existing PDF:
66
+ ## Quick start
67
+
68
+ Turn an existing invoice PDF and typed business data into a PDF/A-3b Factur-X invoice with XSD-validated XML:
44
69
 
45
70
  ```ruby
71
+ require 'facturx'
72
+
46
73
  document = Facturx::Document.build(
47
74
  invoice_number: 'INV-2026-0042',
48
75
  type_code: '380',
49
- issue_date: Date.new(2026, 9, 12),
76
+ issue_date: Date.new(2026, 9, 13),
50
77
  currency: 'EUR'
51
78
  ) do |invoice|
52
79
  invoice.seller do |seller|
53
80
  seller.name = 'Seller SAS'
54
81
  seller.address(country_code: 'FR')
55
82
  end
83
+
56
84
  invoice.buyer(name: 'Buyer SAS')
85
+
57
86
  invoice.totals(
58
87
  tax_basis_total: BigDecimal('100.00'),
88
+ tax_total: BigDecimal('20.00'),
59
89
  grand_total: BigDecimal('120.00'),
60
90
  due_payable: BigDecimal('120.00')
61
91
  )
62
92
  end
63
93
 
64
- report = Facturx.validate_document(document:, profile: :minimum)
65
- report.valid?
66
-
67
- xml = Facturx.build_xml(document:, profile: :minimum)
68
- facturx_pdf = Facturx.generate(pdf:, document:, profile: :minimum)
69
- ```
70
-
71
- `profile:` accepts one of `:minimum`, `:basic_wl`, `:basic`, `:en16931`, or `:extended`, as well as the corresponding canonical `Facturx::Profile`. When BT-24 is absent, the writer inserts the selected profile's guideline URN. A conflicting BT-24 is reported as a profile mismatch.
72
-
73
- `validate_document` returns an immutable report containing every conformance issue found. `build_xml` and `generate` raise `Facturx::ConformanceError` with that report when the document is invalid. Values are serialized from their declared semantic type; monetary values with more than two decimal places are rejected instead of being rounded implicitly. Generated XML is always checked against the selected profile's XSD.
74
-
75
- `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.
94
+ source_pdf = File.binread('invoice.pdf')
95
+ facturx_pdf = Facturx.generate(pdf: source_pdf, document:, profile: :minimum)
76
96
 
77
- `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.
78
-
79
- `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.
80
-
81
- `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`.
82
-
83
- 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.
84
-
85
- 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:
86
-
87
- ```ruby
88
- Facturx.read(xml, on_unknown_profile: :raise)
89
- ```
90
-
91
- 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.
92
-
93
- Reading is not a fidelity round-trip. To reissue an incoming invoice, retain and reuse `Reading#source`; the writer cannot reproduce fields outside the semantic model.
94
-
95
- 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.
96
-
97
- ## Ghostscript discovery
98
-
99
- The composer searches common installation paths. Override discovery when necessary:
100
-
101
- ```bash
102
- export GHOSTSCRIPT_BIN=/opt/ghostscript/bin/gs
103
- export FACTURX_ZUGFERD_PS=/opt/ghostscript/share/ghostscript/lib/zugferd.ps
104
- export FACTURX_ICC_PROFILE=/opt/ghostscript/share/ghostscript/iccprofiles/default_rgb.icc
97
+ File.binwrite('invoice-facturx.pdf', facturx_pdf)
105
98
  ```
106
99
 
107
- The gem invokes Ghostscript as an external process. It does not distribute Ghostscript, `zugferd.ps`, or ICC profiles.
108
-
109
- ## Validation scope
100
+ `generate` validates the document and generated XML, embeds `factur-x.xml`, and verifies the resulting PDF metadata and attachment.
110
101
 
111
- The gem validates generated documents against its EN16931 semantic registry and the official profile XSDs. It does not yet run the Factur-X Schematron business rules, calculate totals, or evaluate accounting consistency. These checks must not be presented as complete regulatory validation.
102
+ ## Documentation
112
103
 
113
- The gem does not generate the visual invoice PDF, communicate with a PDP, or implement e-reporting. EXTENDED-only fields are not generated until they are represented in the typed model.
104
+ See [DOCUMENTATION.md](DOCUMENTATION.md) to:
114
105
 
115
- ## Development
116
-
117
- ```bash
118
- mise install
119
- bundle install
120
- bundle exec rubocop
121
- bundle exec rspec
122
- bundle exec rake build
123
- ```
124
-
125
- Maintainers can verify the registry hashes, D22B element names, semantic diagnostics, official XML examples, and paired PDF attachments against an extracted upstream package:
126
-
127
- ```bash
128
- FACTURX_REFERENCE_ROOT=/path/to/ZUGFeRD_2.5.2_EN bundle exec rake reference:verify
129
- ```
106
+ - build richer typed invoices;
107
+ - validate or attach existing XML;
108
+ - read invoices and handle diagnostics;
109
+ - understand profiles, validation boundaries, and typed errors;
110
+ - enable official Schematron business-rule validation;
111
+ - configure Ghostscript for PDF/A-3b composition.
130
112
 
131
113
  ## License
132
114
 
133
- The gem code is available under the MIT License. Bundled standard artefacts retain their respective upstream notices; see `NOTICE.md`.
115
+ 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