facturx 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10cd4d86db6d643606dbd85da9ced31d20e884249505e8a608ebb864f9a2f2ef
4
- data.tar.gz: 3e0665c807043cdca83550452ab2b1e3ca7d40b759105eea8856916a316576fd
3
+ metadata.gz: 61ff49d55f56bb55d9b6624eb58fd59e16ed016672464f72507ea21b556c3050
4
+ data.tar.gz: 3f6be26b5cb8a65ea5d024cc68c709835154cd67fd48147d4a184182f7938c19
5
5
  SHA512:
6
- metadata.gz: 0cf6cb2b75adb6a668a5746420ee82177aada0c87d9d40bd625387a59d1409b63ef53916df3480aa7adaf539b59c99fb635667134e3cefcbd66590f1a19aeecd
7
- data.tar.gz: 3e7715e0181e11b9b0589ab85da8194e692647fcb4be0d42d177968584d0cc1f1e2114a782e8d533c7c5a2fbac0943bc2bd4964e59633773c9dd864e5a42cee4
6
+ metadata.gz: 071dce1b5bb8ca3925152e2c0b5bcebaff0b2d73399d7e681f9407eeb59bb0ba981dbcbc194cb9b8faace203cdddc7ba5b332c22e92b377b3082d2641dacce8b
7
+ data.tar.gz: 1ffab4f27f2f0ba54e6ffcf3e5d949aafecde9dd92c758acb1f3a1c9f4a1792794b2c91372d564ec49c917114c715f061d90843a14393fabac44bb0058af03c7
data/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ 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
+
7
28
  ## [1.0.0] - 2026-09-13
8
29
 
9
30
  ### Add
@@ -37,7 +58,8 @@ All notable changes to this project will be documented in this file.
37
58
  - Compose PDF/A-3b Factur-X documents with Ghostscript.
38
59
  - Extract embedded Factur-X XML from PDF documents.
39
60
 
40
- [Unreleased]: https://github.com/AdVitam/facturx/compare/v1.0.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
41
63
  [1.0.0]: https://github.com/AdVitam/facturx/compare/v0.2.0...v1.0.0
42
64
  [0.2.0]: https://github.com/AdVitam/facturx/compare/v0.1.0...v0.2.0
43
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,133 +1,111 @@
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
+ | ✅ | Extract the original embedded XML | `Facturx.extract_xml` |
20
30
 
21
- All inputs and outputs are byte strings. The gem does not interpret strings as file paths.
31
+ ### Boundaries
22
32
 
23
- ```ruby
24
- require 'facturx'
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 |
25
39
 
26
- xml = File.binread('invoice.xml')
27
- pdf = File.binread('invoice.pdf')
40
+ Supported · ⏳ Planned · ➖ 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 validation |
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
+ ## Requirements
57
+
58
+ - Ruby 3.2 or newer
59
+ - Ghostscript 9.54 or newer, `zugferd.ps`, and an RGB ICC profile for PDF composition only
60
+
61
+ XML building, reading, extraction, and validation require no external executable.
62
+
63
+ ## Quick start
42
64
 
43
- Build a typed document with the nested DSL, validate it without generating XML, or generate XML and attach it to an existing PDF:
65
+ Turn an existing invoice PDF and typed business data into a PDF/A-3b Factur-X invoice with XSD-validated XML:
44
66
 
45
67
  ```ruby
68
+ require 'facturx'
69
+
46
70
  document = Facturx::Document.build(
47
71
  invoice_number: 'INV-2026-0042',
48
72
  type_code: '380',
49
- issue_date: Date.new(2026, 9, 12),
73
+ issue_date: Date.new(2026, 9, 13),
50
74
  currency: 'EUR'
51
75
  ) do |invoice|
52
76
  invoice.seller do |seller|
53
77
  seller.name = 'Seller SAS'
54
78
  seller.address(country_code: 'FR')
55
79
  end
80
+
56
81
  invoice.buyer(name: 'Buyer SAS')
82
+
57
83
  invoice.totals(
58
84
  tax_basis_total: BigDecimal('100.00'),
85
+ tax_total: BigDecimal('20.00'),
59
86
  grand_total: BigDecimal('120.00'),
60
87
  due_payable: BigDecimal('120.00')
61
88
  )
62
89
  end
63
90
 
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.
76
-
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.
91
+ source_pdf = File.binread('invoice.pdf')
92
+ facturx_pdf = Facturx.generate(pdf: source_pdf, document:, profile: :minimum)
78
93
 
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
94
+ File.binwrite('invoice-facturx.pdf', facturx_pdf)
105
95
  ```
106
96
 
107
- The gem invokes Ghostscript as an external process. It does not distribute Ghostscript, `zugferd.ps`, or ICC profiles.
108
-
109
- ## Validation scope
110
-
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.
97
+ `generate` validates the document and generated XML, embeds `factur-x.xml`, and verifies the resulting PDF metadata and attachment.
112
98
 
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.
99
+ ## Documentation
114
100
 
115
- ## Development
101
+ See [DOCUMENTATION.md](DOCUMENTATION.md) to:
116
102
 
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
- ```
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.
130
108
 
131
109
  ## License
132
110
 
133
- 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
data/lib/facturx/error.rb CHANGED
@@ -10,12 +10,15 @@ module Facturx
10
10
  end
11
11
  end
12
12
 
13
- class InvalidXmlError < Error; end
13
+ class ValidationError < Error; end
14
+ class InvalidDocumentError < ValidationError; end
15
+ class InvalidXmlError < ValidationError; end
14
16
  class UnknownProfileError < InvalidXmlError; end
15
17
  class XsdValidationError < InvalidXmlError; end
18
+ class SchemaLoadError < Error; end
19
+ class InvalidSourceError < Error; end
16
20
  class UnsupportedProfileError < Error; end
17
21
  class FormattingError < Error; end
18
- class ConformanceError < Error; end
19
22
  class InvalidPdfError < Error; end
20
23
  class ProtectedPdfError < InvalidPdfError; end
21
24
  class ComposerUnavailableError < Error; end
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'attach'
3
+ require_relative 'pdf/composer'
4
4
  require_relative 'profile_resolver'
5
5
  require_relative 'writer'
6
6
 
7
7
  module Facturx
8
8
  class Generate
9
- def initialize(writer: Writer.new, attacher: Attach.new, profile_resolver: ProfileResolver.new)
9
+ def initialize(writer: Writer.new, pdf_composer: Pdf::Composer.new, profile_resolver: ProfileResolver.new)
10
10
  @writer = writer
11
- @attacher = attacher
11
+ @pdf_composer = pdf_composer
12
12
  @profile_resolver = profile_resolver
13
13
  end
14
14
 
15
15
  def call(pdf:, document:, profile:)
16
16
  canonical_profile = @profile_resolver.call(profile)
17
17
  xml = @writer.call(document:, profile: canonical_profile)
18
- @attacher.call(pdf:, xml:)
18
+ @pdf_composer.call(pdf:, xml:, profile: canonical_profile)
19
19
  end
20
20
  end
21
21
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'inspector'
4
+ require_relative 'extractor'
5
+ require_relative 'verifier'
6
+ require_relative '../composers/ghostscript'
7
+
8
+ module Facturx
9
+ module Pdf
10
+ class Composer
11
+ def initialize(
12
+ inspector: Inspector.new,
13
+ backend: Composers::Ghostscript.new,
14
+ extractor: Extractor.new,
15
+ verifier: Verifier.new
16
+ )
17
+ @inspector = inspector
18
+ @backend = backend
19
+ @extractor = extractor
20
+ @verifier = verifier
21
+ end
22
+
23
+ def call(pdf:, xml:, profile:)
24
+ inspection = @inspector.call(pdf)
25
+ output = @backend.call(pdf:, xml:, profile:)
26
+ result = @extractor.call(output)
27
+ @verifier.call(result:, expected_xml: xml, expected_page_count: inspection.page_count, profile:)
28
+ output
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Facturx
4
- Profile = Data.define(:id, :guideline_urn, :xsd_path, :conformance_level)
4
+ Profile = Data.define(:id, :guideline_urn, :conformance_level)
5
5
  end