swiss_qr 0.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 +7 -0
- data/CHANGELOG.md +27 -0
- data/LICENSE.txt +21 -0
- data/README.md +402 -0
- data/Rakefile +12 -0
- data/docs/SPEC_NOTES.md +542 -0
- data/exe/swiss_qr +12 -0
- data/lib/swiss_qr/address.rb +57 -0
- data/lib/swiss_qr/bill.rb +88 -0
- data/lib/swiss_qr/billing_information.rb +226 -0
- data/lib/swiss_qr/checksum.rb +29 -0
- data/lib/swiss_qr/cli.rb +123 -0
- data/lib/swiss_qr/countries.rb +48 -0
- data/lib/swiss_qr/errors.rb +58 -0
- data/lib/swiss_qr/iban.rb +60 -0
- data/lib/swiss_qr/parser.rb +211 -0
- data/lib/swiss_qr/reader.rb +211 -0
- data/lib/swiss_qr/reference.rb +79 -0
- data/lib/swiss_qr/validation.rb +380 -0
- data/lib/swiss_qr/version.rb +5 -0
- data/lib/swiss_qr.rb +56 -0
- data/sig/manifest.yaml +2 -0
- data/sig/swiss_qr.rbs +226 -0
- metadata +89 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8277ee2838f01d43a46c5548d3398f06d2ff9a035d65b5063d3be912258c5d24
|
|
4
|
+
data.tar.gz: 5f594ffdbf468ed9904e671f87c5a7bf859e158858c51da381e215ce39289035
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 21908cccd4846e92b36747f59f4bcc0579dc90deb254802f1e34ca5170d564bf1be923de4a3f353c128043e01ae333a4b1a4232ad0819cb5994ee911c6b2a9b6
|
|
7
|
+
data.tar.gz: 3d417aea462376fb6dca46037f72be203c3d628d76dd3245b02df3dcc66f9dc0c32cdf7be310f6fa93ca261d877b3e2dc7066bcc6da82eb23c93ff46766ef22a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-10-16
|
|
6
|
+
|
|
7
|
+
First release.
|
|
8
|
+
|
|
9
|
+
- `SwissQr.parse(payload, strict: true)` parses the payload of the Swiss QR
|
|
10
|
+
Code on a Swiss QR-bill (SIX Implementation Guidelines 2.3/2.4, `SPC` /
|
|
11
|
+
`0200`) into immutable `Data` objects: `Bill`, `Address` and
|
|
12
|
+
`BillingInformation` (Swico S1). `SwissQr.valid?` and
|
|
13
|
+
`SwissQr.looks_like_qr_bill?` go with it.
|
|
14
|
+
- Every rule of the guidelines is validated: IBAN and QR-IBAN check digits,
|
|
15
|
+
reference type against account, QR reference (mod 10 recursive), ISO 11649
|
|
16
|
+
creditor reference, amount, currency, addresses, field lengths, character
|
|
17
|
+
set, the 997-character limit and line separators. Problems come back per
|
|
18
|
+
field as errors and warnings; `strict: false` returns the bill with them.
|
|
19
|
+
Deviations real generators make (CR LF or LF, a final line break, trailing
|
|
20
|
+
empty lines, a byte order mark) are accepted.
|
|
21
|
+
- `SwissQr.read` and `SwissQr.read_all` find QR-bills in images and PDFs
|
|
22
|
+
through a pluggable reader: qr_reader 0.0.3 or newer by default (optional,
|
|
23
|
+
loaded on first use), or any object with `decode_all` or any callable. A
|
|
24
|
+
`String` input is always a file path; image bytes come as an IO.
|
|
25
|
+
- `swiss_qr` command: prints the bills in images, PDFs or a payload as JSON.
|
|
26
|
+
- Runs on MRI 3.3 to 4.0, JRuby 10 and TruffleRuby (tested in CI), and on
|
|
27
|
+
ruby.wasm with bigdecimal-pure.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andi Idogawa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# SwissQr
|
|
2
|
+
|
|
3
|
+
Parses and validates the Swiss QR Code printed on a Swiss QR-bill
|
|
4
|
+
("QR-Rechnung", "QR-facture", "QR-fattura"), as defined by the SIX *Swiss
|
|
5
|
+
Implementation Guidelines for the QR-bill* (QR type `SPC`, version `0200`,
|
|
6
|
+
coding type `1`, trailer `EPD`). It can also find the code in images and PDFs.
|
|
7
|
+
|
|
8
|
+
- **Parser with no dependencies** other than `bigdecimal`. Values come back as
|
|
9
|
+
immutable `Data` objects: `SwissQr::Bill`, `SwissQr::Address` and
|
|
10
|
+
`SwissQr::BillingInformation` (Swico/SIX "S1" billing information).
|
|
11
|
+
- **Checks every payload rule:** header, IBAN and QR-IBAN check digits, the
|
|
12
|
+
reference type against the account, QR reference (modulo 10 recursive), ISO
|
|
13
|
+
11649 creditor reference (modulo 97), amount, currency, addresses, the
|
|
14
|
+
reserved ultimate creditor, field lengths, character set, the 997-character
|
|
15
|
+
limit and line separators.
|
|
16
|
+
- **Tolerant where real generators deviate:** CR LF or LF, a final line break
|
|
17
|
+
(of either kind), trailing empty lines, a byte order mark and blanks after
|
|
18
|
+
free text are accepted (SIX's own samples have some of these).
|
|
19
|
+
- **Reads QR codes from images and PDFs** through a pluggable reader. By
|
|
20
|
+
default this is the pure-Ruby [`qr_reader`](https://rubygems.org/gems/qr_reader)
|
|
21
|
+
gem, which is optional and loaded only when you call `SwissQr.read`. Any
|
|
22
|
+
other decoder (ZXing, ZBar, a cloud API, ...) can be plugged in instead.
|
|
23
|
+
- **`swiss_qr` command** that prints bills as JSON.
|
|
24
|
+
- **Runs on every major Ruby implementation:** MRI (CRuby) 3.3 to 4.0, JRuby 10,
|
|
25
|
+
TruffleRuby and ruby.wasm (with
|
|
26
|
+
[`bigdecimal-pure`](https://rubygems.org/gems/bigdecimal-pure)). All of them
|
|
27
|
+
run the test suite in CI except ruby.wasm, which was tested by hand; see
|
|
28
|
+
[Ruby versions and platforms](#ruby-versions-and-platforms).
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bundle add swiss_qr
|
|
34
|
+
bundle add qr_reader # optional: only needed for SwissQr.read / read_all and the CLI
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
or `gem install swiss_qr qr_reader`.
|
|
38
|
+
|
|
39
|
+
### Ruby versions and platforms
|
|
40
|
+
|
|
41
|
+
Ruby 3.3 or newer. Both gems are plain Ruby; neither has a C extension of its
|
|
42
|
+
own.
|
|
43
|
+
|
|
44
|
+
| Ruby | |
|
|
45
|
+
|---|---|
|
|
46
|
+
| MRI 3.3, 3.4, 4.0 | tested |
|
|
47
|
+
| TruffleRuby 25 | tested |
|
|
48
|
+
| JRuby 10 | tested |
|
|
49
|
+
| ruby.wasm (Ruby 4.0) | works with `bigdecimal-pure`, see below |
|
|
50
|
+
| MRI 3.2 and older, JRuby 9.4 and older | not supported (3.2 is end of life) |
|
|
51
|
+
|
|
52
|
+
Amounts, VAT rates and discounts are `BigDecimal`s, so swiss_qr depends on
|
|
53
|
+
the `bigdecimal` gem. That gem is a C extension. Where it cannot be built or
|
|
54
|
+
loaded, ruby.wasm above all, use
|
|
55
|
+
[`bigdecimal-pure`](https://rubygems.org/gems/bigdecimal-pure) instead: a
|
|
56
|
+
`BigDecimal` written in Ruby on top of `Rational`, exact like the original for
|
|
57
|
+
everything swiss_qr does. Its `require "bigdecimal"` loads the native
|
|
58
|
+
extension when there is one, and the pure class otherwise.
|
|
59
|
+
|
|
60
|
+
Put its `lib` on the load path ahead of swiss_qr, which is how gems are
|
|
61
|
+
usually loaded into ruby.wasm (for example with the `@ruby/4.0-wasm-wasi`
|
|
62
|
+
npm package):
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
$LOAD_PATH.unshift "/gems/bigdecimal-pure/lib", "/gems/swiss_qr/lib", "/gems/qr_reader/lib"
|
|
66
|
+
require "swiss_qr" # its require "bigdecimal" gets bigdecimal-pure's class
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Reading images and PDFs needs qr_reader 0.0.3 or newer (`SwissQr.read`
|
|
70
|
+
raises `SwissQr::ReaderUnavailable` with an older one). Parsing payloads does
|
|
71
|
+
not use qr_reader at all.
|
|
72
|
+
|
|
73
|
+
Under Bundler this does not work yet: swiss_qr's dependency installs the
|
|
74
|
+
`bigdecimal` gem itself (Bundler cannot substitute one gem for another), and
|
|
75
|
+
bigdecimal-pure 0.1.0 does not fall back to its pure class when that gem's
|
|
76
|
+
Ruby files are present but its C extension cannot be loaded.
|
|
77
|
+
|
|
78
|
+
## Parsing a payload
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
require "swiss_qr"
|
|
82
|
+
|
|
83
|
+
bill = SwissQr.parse(payload) # raises SwissQr::InvalidPayload on any rule violation
|
|
84
|
+
bill.iban # => "CH4431999123000889012"
|
|
85
|
+
bill.qr_iban? # => true
|
|
86
|
+
bill.formatted_iban # => "CH44 3199 9123 0008 8901 2"
|
|
87
|
+
bill.amount # => 0.194975e4 (BigDecimal, nil when open)
|
|
88
|
+
bill.formatted_amount # => "1949.75"
|
|
89
|
+
bill.currency # => "CHF"
|
|
90
|
+
bill.creditor.name # => "Max Muster & Söhne"
|
|
91
|
+
bill.creditor.lines # => ["Max Muster & Söhne", "Musterstrasse 123", "8000 Seldwyla"]
|
|
92
|
+
bill.debtor&.town # => "Seldwyla"
|
|
93
|
+
bill.reference_type # => :qrr (:qrr, :scor or :non)
|
|
94
|
+
bill.reference # => "210000000003139471430009017"
|
|
95
|
+
bill.formatted_reference # => "21 00000 00003 13947 14300 09017"
|
|
96
|
+
bill.unstructured_message # => "Order from 15.10.2020"
|
|
97
|
+
bill.billing_information&.invoice_number # => "1234"
|
|
98
|
+
bill.billing_information&.invoice_date # => Date.new(2020, 10, 21)
|
|
99
|
+
bill.billing_information&.payment_conditions # => [#<data PaymentCondition discount=0.0, days=30>]
|
|
100
|
+
bill.alternative_procedures # => ["Name AV1: UV;UltraPay005;12345", "Name AV2: XY;XYService;54321"]
|
|
101
|
+
bill.warnings # => [] (e.g. legacy address type "K")
|
|
102
|
+
bill.to_h # Ruby types (BigDecimal, Date, Symbol), nested hashes
|
|
103
|
+
bill.to_json # JSON (amount as "1949.75", symbols as strings)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The payload is the decoded text of the QR code: a String, ideally UTF-8.
|
|
107
|
+
Binary strings are taken as UTF-8, and strings in other encodings (UTF-16,
|
|
108
|
+
UTF-32, real Latin-1, ...) are transcoded. A string tagged ISO-8859-1 or
|
|
109
|
+
Windows-1252 whose bytes are valid UTF-8 is read as UTF-8 with a warning:
|
|
110
|
+
many QR decoders tag byte-mode data that way, but a QR-bill is always UTF-8.
|
|
111
|
+
Input longer than 4096 bytes (more than any QR code holds) is rejected with
|
|
112
|
+
`InvalidPayload` before it is parsed.
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
SwissQr.valid?(payload) # => true / false, never raises
|
|
116
|
+
SwissQr.looks_like_qr_bill?(text) # cheap "starts like SPC/0200" check, no validation
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Reading QR-bills from images and PDFs
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
require "swiss_qr"
|
|
123
|
+
|
|
124
|
+
bill = SwissQr.read("invoice.pdf") # first QR-bill found, or nil
|
|
125
|
+
bills = SwissQr.read_all("scans.pdf") # every QR-bill on every page, [] when none
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`input` can be:
|
|
129
|
+
|
|
130
|
+
- a file path (`String` or `Pathname`),
|
|
131
|
+
- an `IO` with the image or PDF: a `File`, an upload's `Tempfile`, or
|
|
132
|
+
`StringIO.new(bytes)` for bytes you already hold,
|
|
133
|
+
- with the default reader, also a `ChunkyPNG::Image`-like object (`#width`,
|
|
134
|
+
`#height`, `#pixels`).
|
|
135
|
+
|
|
136
|
+
A `String` is always a file path, never image data, as with `File.read`. Image
|
|
137
|
+
bytes in a `String` raise `ArgumentError` (wrap them in `StringIO.new`), so
|
|
138
|
+
bytes uploaded by a client can never be taken for a path on the server:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
SwissQr.read(params[:invoice].tempfile) # an upload
|
|
142
|
+
SwissQr.read(StringIO.new(pdf_bytes)) # bytes from an API or a database
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Supported formats are PNG, JPEG, JPEG 2000, GIF, BMP, PNM and TIFF (including
|
|
146
|
+
fax), and PDFs (scanned pages, as well as vector-drawn codes in generated
|
|
147
|
+
PDFs). The Swiss cross in the middle of the code is no problem.
|
|
148
|
+
|
|
149
|
+
Other QR codes on the page, such as a link to an online portal or a TWINT code,
|
|
150
|
+
are skipped: only texts for which `SwissQr.looks_like_qr_bill?` is true are parsed. `read`
|
|
151
|
+
stops at the first QR-bill. With the default reader it does not decode the
|
|
152
|
+
rest of a long PDF.
|
|
153
|
+
|
|
154
|
+
`strict:` works as for `parse` (see below). With the default `strict: true`, an
|
|
155
|
+
invalid QR-bill in the image raises `SwissQr::InvalidPayload`. A QR code that
|
|
156
|
+
does not start with `SPC` is not a QR-bill, so it is skipped and does not
|
|
157
|
+
raise. Errors from the reader itself propagate: `Errno::ENOENT` for a missing
|
|
158
|
+
file, and `QrReader::ImageError` or `QrReader::UnsupportedImageError` for
|
|
159
|
+
unreadable data.
|
|
160
|
+
|
|
161
|
+
### Choosing or replacing the QR code reader
|
|
162
|
+
|
|
163
|
+
`SwissQr` does not decode QR codes itself. It asks a *reader* for the texts of
|
|
164
|
+
all codes in the input. A reader is either:
|
|
165
|
+
|
|
166
|
+
- **an object with `decode_all(input)`** that returns an Array of Strings (the
|
|
167
|
+
`QrReader` module is one), or
|
|
168
|
+
- **a callable** (Proc, lambda, `Method`, any object with `#call(input)`) that
|
|
169
|
+
returns a String, an Array of Strings, or `nil`.
|
|
170
|
+
|
|
171
|
+
Elements may also be result objects with a `#text` method (for example
|
|
172
|
+
`QrReader::Result`). `nil` elements are ignored.
|
|
173
|
+
|
|
174
|
+
The reader gets the input as passed to `read`. A `String` it receives is
|
|
175
|
+
always a file path, because image bytes in a `String` are refused before any
|
|
176
|
+
reader is asked.
|
|
177
|
+
|
|
178
|
+
The default reader is `SwissQr::Reader::QrReaderAdapter`. Creating it does not
|
|
179
|
+
load anything. It runs `require "qr_reader"` on the first read. If the gem is
|
|
180
|
+
missing, the read raises `SwissQr::ReaderUnavailable`, and the message says how
|
|
181
|
+
to install the gem (`gem install qr_reader` or `bundle add qr_reader`) or to set
|
|
182
|
+
another reader. `require "swiss_qr"` never loads `qr_reader`.
|
|
183
|
+
|
|
184
|
+
Replace the reader for the whole process, or pass one per call:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
# qr_reader with options (fail fast on pages without a code); options are
|
|
188
|
+
# passed on to qr_reader, which is still loaded only on the first read
|
|
189
|
+
SwissQr.reader = SwissQr::Reader::QrReaderAdapter.new(try_harder: false)
|
|
190
|
+
|
|
191
|
+
# any object with decode_all
|
|
192
|
+
class MyZxingReader
|
|
193
|
+
def decode_all(path) = ZXing.decode_all(path) # e.g. the zxing gem (JRuby)
|
|
194
|
+
end
|
|
195
|
+
SwissQr.reader = MyZxingReader.new
|
|
196
|
+
|
|
197
|
+
# ZBar's command-line tool, for images that carry a single code
|
|
198
|
+
# (zbarimg --raw cannot separate several multi-line codes)
|
|
199
|
+
require "open3"
|
|
200
|
+
SwissQr.reader = lambda do |path|
|
|
201
|
+
out, status = Open3.capture2("zbarimg", "--quiet", "--raw", "-Sdisable", "-Sqrcode.enable", path.to_s)
|
|
202
|
+
status.success? ? out.chomp : nil
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# per call; the global reader is unchanged
|
|
206
|
+
SwissQr.read("scan.png", reader: ->(path) { my_cloud_ocr(path) })
|
|
207
|
+
|
|
208
|
+
SwissQr.reader = nil # back to the default (qr_reader)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Prefer `QrReaderAdapter.new(**options)` over `SwissQr.reader = QrReader` or a
|
|
212
|
+
lambda calling `QrReader.decode_all` (which also needs `require "qr_reader"`):
|
|
213
|
+
only the adapter returns no codes for a PDF without any image, where qr_reader
|
|
214
|
+
itself raises `QrReader::UnsupportedImageError`, and only the adapter lets
|
|
215
|
+
`read` stop decoding at the first QR-bill.
|
|
216
|
+
|
|
217
|
+
A callable may return a single result object (anything with `#text`, such as a
|
|
218
|
+
`Struct` or `Data`) instead of an Array; it counts as one code.
|
|
219
|
+
|
|
220
|
+
`SwissQr.reader=` checks that the object responds to `#decode_all` or `#call`
|
|
221
|
+
and raises `ArgumentError` otherwise. A reader that returns anything other
|
|
222
|
+
than Strings (or objects with `#text`) makes `read` raise `TypeError`.
|
|
223
|
+
|
|
224
|
+
Assigning `SwissQr.reader` is thread-safe, but the reader is global to the
|
|
225
|
+
process. Assign it once at boot, or pass `reader:` per call.
|
|
226
|
+
|
|
227
|
+
## Validation and strict mode
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
SwissQr.parse(payload) # strict: true (default) raises on any error
|
|
231
|
+
bill = SwissQr.parse(payload, strict: false)
|
|
232
|
+
bill.valid? # => false
|
|
233
|
+
bill.errors.map(&:to_s) # => ["reference: QR reference check digit (modulo 10 recursive) is wrong: ..."]
|
|
234
|
+
bill.warnings # problems that do not make the bill invalid
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
- **`strict: true`** raises `SwissQr::InvalidPayload` when there is any error.
|
|
238
|
+
`#errors` lists every `SwissQr::Problem`, and `#bill` holds the partially
|
|
239
|
+
parsed bill.
|
|
240
|
+
- **`strict: false`** returns the bill with `errors` filled in. It still raises
|
|
241
|
+
`InvalidPayload` when the text is not structurally a QR-bill: the QR type is
|
|
242
|
+
not `SPC`, or there are fewer than 31 lines. `#bill` is `nil` then.
|
|
243
|
+
- A **`SwissQr::Problem`** has a `field` symbol (`:iban`, `:"creditor.name"`,
|
|
244
|
+
`:reference`, `:payload`, ...) and a `message`. `problem.about?(:creditor)`
|
|
245
|
+
matches all creditor fields.
|
|
246
|
+
- **Warnings** never make a bill invalid. Billing information and alternative
|
|
247
|
+
procedures have status "A" in the guidelines (errors there must not lead to
|
|
248
|
+
rejection), so only warnings are given for them. Other warnings: legacy
|
|
249
|
+
address type `K`, a removed BOM, blanks around free text (names, street,
|
|
250
|
+
town, message), a UTF-8 payload tagged as Latin-1, a postal code that may
|
|
251
|
+
have a country prefix. Blanks around fixed-format elements (IBAN, amount,
|
|
252
|
+
codes, reference, country) are errors.
|
|
253
|
+
|
|
254
|
+
Errors: `SwissQr::Error` (base) → `SwissQr::InvalidPayload`,
|
|
255
|
+
`SwissQr::ReaderUnavailable`.
|
|
256
|
+
|
|
257
|
+
## Value objects
|
|
258
|
+
|
|
259
|
+
All three are immutable (`Data`) and support `#to_h`, `#as_json` and `==`.
|
|
260
|
+
|
|
261
|
+
### `SwissQr::Bill`
|
|
262
|
+
|
|
263
|
+
| Field | Type | Notes |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| `version` | String | `"0200"` |
|
|
266
|
+
| `coding_type` | String | `"1"` (UTF-8, Latin character set) |
|
|
267
|
+
| `iban` | String | IBAN or QR-IBAN, no spaces, upper case (`formatted_iban` adds spaces) |
|
|
268
|
+
| `qr_iban` | Boolean | Also `qr_iban?`. QR-IID 30000–31999 |
|
|
269
|
+
| `creditor` | Address | Required |
|
|
270
|
+
| `ultimate_creditor` | Address, nil | Reserved by SIX. Must be empty, so always `nil` in a valid bill |
|
|
271
|
+
| `amount` | BigDecimal, nil | `nil` = open amount (`open_amount?`). `0.00` = notification (`notification?`). `formatted_amount` gives `"50.00"` |
|
|
272
|
+
| `currency` | String | `"CHF"` or `"EUR"` |
|
|
273
|
+
| `debtor` | Address, nil | Optional |
|
|
274
|
+
| `reference_type` | Symbol | `:qrr` (QR reference, needs QR-IBAN), `:scor` (ISO 11649 creditor reference) or `:non` |
|
|
275
|
+
| `reference` | String, nil | As given. `formatted_reference` groups the digits |
|
|
276
|
+
| `unstructured_message` | String, nil | Free text |
|
|
277
|
+
| `trailer` | String | `"EPD"` |
|
|
278
|
+
| `raw_billing_information` | String, nil | Billing information as given (line 32) |
|
|
279
|
+
| `billing_information` | BillingInformation, nil | The billing information parsed, when it uses the Swico S1 syntax (`//S1/...`) |
|
|
280
|
+
| `alternative_procedures` | Array of String | At most 2 (e.g. `"eBill/B/..."`) |
|
|
281
|
+
| `errors`, `warnings` | Array of Problem | `valid?` is `errors.empty?` |
|
|
282
|
+
| `raw` | String | The payload as given (UTF-8) |
|
|
283
|
+
|
|
284
|
+
### `SwissQr::Address`
|
|
285
|
+
|
|
286
|
+
| Field | Notes |
|
|
287
|
+
|---|---|
|
|
288
|
+
| `type` | `:structured` (`S`) or `:combined` (`K`, legacy). Also `structured?` and `combined?` |
|
|
289
|
+
| `name` | Name or company |
|
|
290
|
+
| `street`, `building_number` | Type `S` only |
|
|
291
|
+
| `postal_code`, `town` | Type `S` only |
|
|
292
|
+
| `address_line1`, `address_line2` | Type `K` only (street and number, then postal code and town) |
|
|
293
|
+
| `country` | ISO 3166-1 alpha-2, e.g. `"CH"` |
|
|
294
|
+
|
|
295
|
+
`#lines` gives the postal lines for display. `#to_s` joins them with line
|
|
296
|
+
breaks.
|
|
297
|
+
|
|
298
|
+
### `SwissQr::BillingInformation` (Swico S1)
|
|
299
|
+
|
|
300
|
+
| Field | S1 tag | Type |
|
|
301
|
+
|---|---|---|
|
|
302
|
+
| `invoice_number` | `/10/` | String |
|
|
303
|
+
| `invoice_date` | `/11/` | Date |
|
|
304
|
+
| `customer_reference` | `/20/` | String |
|
|
305
|
+
| `vat_number` | `/30/` | String (UID digits without `CHE`) |
|
|
306
|
+
| `vat_dates` | `/31/` | Array of Date (one date, or start and end of a period) |
|
|
307
|
+
| `vat_details` | `/32/` | Array of `VatRate(rate, amount)` |
|
|
308
|
+
| `vat_import_taxes` | `/33/` | Array of `VatRate(rate, amount)` |
|
|
309
|
+
| `payment_conditions` | `/40/` | Array of `PaymentCondition(discount, days)` |
|
|
310
|
+
| `unknown_tags` | other tags | Hash |
|
|
311
|
+
| `warnings`, `raw` | | |
|
|
312
|
+
|
|
313
|
+
`SwissQr::BillingInformation.parse("//S1/10/10201409/11/190512/32/7.7/40/2:10;0:30")`
|
|
314
|
+
parses a string directly.
|
|
315
|
+
|
|
316
|
+
### Helpers
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
SwissQr::Iban.valid?("CH44 3199 9123 0008 8901 2") # => true
|
|
320
|
+
SwissQr::Iban.qr_iban?("CH4431999123000889012") # => true
|
|
321
|
+
SwissQr::Iban.format("CH4431999123000889012") # => "CH44 3199 9123 0008 8901 2"
|
|
322
|
+
SwissQr::Reference.valid_qrr?("210000000003139471430009017") # => true
|
|
323
|
+
SwissQr::Reference.valid_scor?("RF18 5390 0754 7034") # => true
|
|
324
|
+
SwissQr::Reference.build_qrr("21000000000313947143000901") # => "210000000003139471430009017" (adds the check digit)
|
|
325
|
+
SwissQr::Reference.build_scor("539007547034") # => "RF18539007547034"
|
|
326
|
+
SwissQr::Reference.format_qrr("210000000003139471430009017") # => "21 00000 00003 13947 14300 09017"
|
|
327
|
+
SwissQr::Reference.format_scor("RF18539007547034") # => "RF18 5390 0754 7034"
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Supported specification
|
|
331
|
+
|
|
332
|
+
- **Swiss Implementation Guidelines for the QR-bill, version 2.3** (in force
|
|
333
|
+
since November 2025). The payload version is `0200`.
|
|
334
|
+
- **Version 2.4** (in force from 14 November 2026) does not change the payload
|
|
335
|
+
version, so the two cannot be told apart from the payload. Its main new rule
|
|
336
|
+
(QR-IBAN with a QR reference only in CHF) is reported as a warning on
|
|
337
|
+
`currency`.
|
|
338
|
+
- **Version 2.2 and older:** address type `K` (combined address), removed in
|
|
339
|
+
2.3, is still parsed (`address_line1`, `address_line2`) with a warning.
|
|
340
|
+
- **Billing information:** the SIX/Swico "S1" syntax (IG Annex D).
|
|
341
|
+
- **Not yet published:** master version `03`, which is when the payload version
|
|
342
|
+
string will start to change. It is not supported.
|
|
343
|
+
|
|
344
|
+
`docs/SPEC_NOTES.md` digests the SIX documents (IG 2.2–2.4, processing rules,
|
|
345
|
+
QR-IID, S1 syntax), with a source for every rule. The test fixtures in
|
|
346
|
+
`test/fixtures` include the payloads of the official SIX sample QR codes.
|
|
347
|
+
|
|
348
|
+
## Command line
|
|
349
|
+
|
|
350
|
+
```console
|
|
351
|
+
$ swiss_qr invoice.pdf scan.png # all QR-bills in the files, as a JSON array
|
|
352
|
+
[
|
|
353
|
+
{
|
|
354
|
+
"file": "invoice.pdf",
|
|
355
|
+
"version": "0200",
|
|
356
|
+
"iban": "CH4431999123000889012",
|
|
357
|
+
"amount": "1949.75",
|
|
358
|
+
...
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
$ swiss_qr --text < payload.txt # parse a payload (the QR code's text) from stdin
|
|
362
|
+
$ swiss_qr - < scan.png # image from stdin
|
|
363
|
+
$ swiss_qr --lines *.pdf # one compact JSON object per line
|
|
364
|
+
$ swiss_qr --no-strict broken.png # print invalid bills too, with their "errors"
|
|
365
|
+
$ swiss_qr --version
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Each JSON object is `Bill#as_json` plus `"file"` (`"-"` for stdin). Messages
|
|
369
|
+
go to stderr.
|
|
370
|
+
|
|
371
|
+
| Exit status | Meaning |
|
|
372
|
+
|---|---|
|
|
373
|
+
| 0 | Every input held at least one QR-bill, and all of them were valid |
|
|
374
|
+
| 1 | An input held no QR-bill, or held an invalid one. With `--no-strict`, invalid bills are still printed |
|
|
375
|
+
| 2 | Usage error, unreadable file, or no QR code reader (`gem install qr_reader`) |
|
|
376
|
+
|
|
377
|
+
The command uses `SwissQr.reader`, so it needs `qr_reader` to be installed.
|
|
378
|
+
|
|
379
|
+
## Development
|
|
380
|
+
|
|
381
|
+
After checking out the repo, run `bin/setup` to install dependencies (this
|
|
382
|
+
includes `qr_reader`, `rqrcode` and `chunky_png` for the image tests). Then run
|
|
383
|
+
`bundle exec rake` for the tests and RuboCop. `bin/console` gives an
|
|
384
|
+
interactive prompt. `test/fixtures/README.md` explains how the fixtures are
|
|
385
|
+
generated.
|
|
386
|
+
|
|
387
|
+
### Releasing
|
|
388
|
+
|
|
389
|
+
Releases are published from GitHub Actions with RubyGems
|
|
390
|
+
[trusted publishing](https://guides.rubygems.org/trusted-publishing/) (no API
|
|
391
|
+
key is stored anywhere), and the gem carries a Sigstore attestation, so
|
|
392
|
+
RubyGems.org shows which repository, workflow and commit built it. Bump
|
|
393
|
+
`SwissQr::VERSION`, update `CHANGELOG.md`, then:
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
git tag v0.1.0
|
|
397
|
+
git push origin v0.1.0
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## License
|
|
401
|
+
|
|
402
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|