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 +4 -4
- data/CHANGELOG.md +38 -1
- data/DOCUMENTATION.md +302 -0
- data/README.md +71 -89
- data/lib/facturx/attach.rb +4 -20
- data/lib/facturx/composers/ghostscript/runner.rb +19 -80
- data/lib/facturx/error.rb +6 -2
- data/lib/facturx/generate.rb +4 -4
- data/lib/facturx/pdf/composer.rb +32 -0
- data/lib/facturx/profile.rb +1 -1
- data/lib/facturx/profiles.rb +1 -8
- data/lib/facturx/schematron_adapter.rb +30 -0
- data/lib/facturx/source_reader.rb +1 -1
- data/lib/facturx/subprocess.rb +154 -0
- data/lib/facturx/validation.rb +61 -0
- data/lib/facturx/version.rb +1 -1
- data/lib/facturx/{conformance.rb → writer/tracker.rb} +53 -85
- data/lib/facturx/writer.rb +22 -14
- data/lib/facturx/xml/conformance_validator.rb +20 -0
- data/lib/facturx/xml/parser.rb +1 -1
- data/lib/facturx/xml/report_builder.rb +44 -0
- data/lib/facturx/xml/schema_registry.rb +22 -0
- data/lib/facturx/xml/schema_validator.rb +7 -2
- data/lib/facturx/xml/validator.rb +36 -0
- data/lib/facturx/xml/verifier.rb +17 -11
- data/lib/facturx.rb +18 -6
- data/rbi/facturx.rbi +702 -36
- metadata +11 -3
- data/lib/facturx/composers/ghostscript/output_capture.rb +0 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8beb5aea7a14256c4d062dfbf732462016ec2e0eaacafedf016235f085353eee
|
|
4
|
+
data.tar.gz: 24ea8be809ed5b980cb01b2dee2a775c76c21ae717dda9c056182880bfc44e3d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
#
|
|
1
|
+
# Facturx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://rubygems.org/gems/facturx)
|
|
4
|
+
[](https://github.com/AdVitam/facturx/actions/workflows/test.yml)
|
|
5
|
+
[](https://www.ruby-lang.org)
|
|
6
|
+
[](LICENSE.txt)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
Build, read, validate, and compose Factur-X / ZUGFeRD invoices in Ruby.
|
|
6
9
|
|
|
7
|
-
|
|
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
|
-
|
|
12
|
+
## Features
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
gem 'facturx'
|
|
13
|
-
```
|
|
14
|
+
### Main workflows
|
|
14
15
|
|
|
15
|
-
|
|
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
|
-
|
|
22
|
+
### Focused tools
|
|
18
23
|
|
|
19
|
-
|
|
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
|
-
|
|
32
|
+
### Boundaries
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
| | Capability | Responsibility |
|
|
35
|
+
|:---:|---|---|
|
|
36
|
+
| ➖ | Invoice calculations | Application |
|
|
37
|
+
| ➖ | Visual PDF generation | Application |
|
|
38
|
+
| ➖ | PDP transport and e-invoicing | Application |
|
|
25
39
|
|
|
26
|
-
|
|
27
|
-
pdf = File.binread('invoice.pdf')
|
|
40
|
+
✅ Included · 🔌 Optional companion · ➖ Intentionally handled outside the gem
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
## Supported profiles
|
|
30
43
|
|
|
31
|
-
|
|
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
|
-
|
|
37
|
-
|
|
46
|
+
| Profile | Read | Build | XSD | Schematron** |
|
|
47
|
+
|---|:---:|:---:|:---:|:---:|
|
|
48
|
+
| MINIMUM | ✅ | ✅ | ✅ | ✅ |
|
|
49
|
+
| BASIC WL | ✅ | ✅ | ✅ | ✅ |
|
|
50
|
+
| BASIC | ✅ | ✅ | ✅ | ✅ |
|
|
51
|
+
| EN 16931 | ✅ | ✅ | ✅ | ✅ |
|
|
52
|
+
| EXTENDED | ✅* | ✅* | ✅ | ✅ |
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
+
## Documentation
|
|
112
103
|
|
|
113
|
-
|
|
104
|
+
See [DOCUMENTATION.md](DOCUMENTATION.md) to:
|
|
114
105
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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).
|
data/lib/facturx/attach.rb
CHANGED
|
@@ -1,34 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'xml/verifier'
|
|
4
|
-
require_relative 'pdf/
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|