hexapdf-extras 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 +4 -4
- data/Rakefile +1 -1
- data/lib/hexapdf/extras/layout/qr_code_box.rb +3 -1
- data/lib/hexapdf/extras/layout/swiss_qr_bill.rb +683 -0
- data/lib/hexapdf/extras/layout.rb +1 -0
- data/lib/hexapdf/extras/version.rb +1 -1
- data/lib/hexapdf/extras.rb +3 -0
- data/test/hexapdf/extras/layout/test_qr_code_box.rb +20 -6
- data/test/hexapdf/extras/layout/test_swiss_qr_bill.rb +233 -0
- metadata +11 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18aea5b94e331d639be753fc306bf52a24a348356c05e958063a0066e896f1e4
|
4
|
+
data.tar.gz: fa64c74156bfe91e7692825741f85199383a3d56987fb32db8dc30063e802d55
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6115bc7a6a964b76466dd030b33a7d0d394c0eec5c847875960fcf945d8fe253600c36837dbb5f18a756617a4308f162bb482c8a28d884460b7b15bd63b42ba7
|
7
|
+
data.tar.gz: df41b43ba6afb9cb76b20c87f05658f7c5d250228d66f87a6906660156cfe1099eb331632304d7a82dfb3d3529757fcd44f4cbd34ce1988a0a17105cc8c3815d
|
data/Rakefile
CHANGED
@@ -27,7 +27,7 @@ task publish_files: [:package] do
|
|
27
27
|
end
|
28
28
|
|
29
29
|
task :test_all do
|
30
|
-
versions = `rbenv versions --bare | grep -i 2.[
|
30
|
+
versions = `rbenv versions --bare | grep -i 2.[7]\\\\\\|3.`.split("\n")
|
31
31
|
versions.each do |version|
|
32
32
|
sh "eval \"$(rbenv init -)\"; rbenv shell #{version} && ruby -v && rake test"
|
33
33
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'hexapdf/layout/box'
|
4
|
+
require 'hexapdf/extras/graphic_object/qr_code'
|
4
5
|
|
5
6
|
module HexaPDF
|
6
7
|
module Extras
|
@@ -44,8 +45,9 @@ module HexaPDF
|
|
44
45
|
# Fits the QRCode into the given area.
|
45
46
|
def fit(available_width, available_height, _frame)
|
46
47
|
super
|
47
|
-
@width = @height = [@width, @height].min
|
48
48
|
@qr_code.size = [content_width, content_height].min
|
49
|
+
@width = @qr_code.size + reserved_width
|
50
|
+
@height = @qr_code.size + reserved_height
|
49
51
|
@fit_successful = (@width <= available_width && @height <= available_height)
|
50
52
|
end
|
51
53
|
|
@@ -0,0 +1,683 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hexapdf/error'
|
4
|
+
require 'hexapdf/layout/box'
|
5
|
+
require 'hexapdf/extras/layout'
|
6
|
+
|
7
|
+
module HexaPDF::Extras::Layout::NumericMeasurementHelper #:nodoc:
|
8
|
+
refine Numeric do
|
9
|
+
def mm
|
10
|
+
self * 72 / 25.4
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
using HexaPDF::Extras::Layout::NumericMeasurementHelper
|
16
|
+
|
17
|
+
# These values comes from the "Style Guide QR-bill", available at
|
18
|
+
# https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/style-guide-qr-bill-en.pdf
|
19
|
+
font = '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf'
|
20
|
+
font_bold = '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf'
|
21
|
+
HexaPDF::DefaultDocumentConfiguration['layout.swiss_qr_bill'] = {
|
22
|
+
'section.heading.font' => font_bold,
|
23
|
+
'section.heading.font_size' => 11,
|
24
|
+
'payment.heading.font' => font_bold,
|
25
|
+
'payment.heading.font_size' => 8,
|
26
|
+
'payment.heading.line_height' => 11,
|
27
|
+
'payment.value.font' => font,
|
28
|
+
'payment.value.font_size' => 10,
|
29
|
+
'payment.value.line_height' => 11,
|
30
|
+
'receipt.heading.font' => font_bold,
|
31
|
+
'receipt.heading.font_size' => 6,
|
32
|
+
'receipt.heading.line_height' => 9,
|
33
|
+
'receipt.value.font' => font,
|
34
|
+
'receipt.value.font_size' => 8,
|
35
|
+
'receipt.value.line_height' => 9,
|
36
|
+
'alternative_procedures.heading.font' => font_bold,
|
37
|
+
'alternative_procedures.heading.font_size' => 7,
|
38
|
+
'alternative_procedures.heading.line_height' => 8,
|
39
|
+
'alternative_procedures.value.font' => font,
|
40
|
+
'alternative_procedures.value.font_size' => 7,
|
41
|
+
'alternative_procedures.value.line_height' => 8,
|
42
|
+
}
|
43
|
+
|
44
|
+
module HexaPDF
|
45
|
+
module Extras
|
46
|
+
module Layout
|
47
|
+
|
48
|
+
# Displays a Swiss QR-bill.
|
49
|
+
#
|
50
|
+
# This class implements version 2.2 of the Swiss QR-bill specification but takes into account
|
51
|
+
# version 2.3 where appropriate.
|
52
|
+
#
|
53
|
+
#
|
54
|
+
# == Requirements
|
55
|
+
#
|
56
|
+
# * Liberation Sans TrueType font installed in standard Linux path (or changed via the
|
57
|
+
# configuration option, see next section)
|
58
|
+
# * Rubygem +rqrcode_core+ for generating the QR code
|
59
|
+
#
|
60
|
+
#
|
61
|
+
# == Configuration option 'layout.swiss_qr_bill'
|
62
|
+
#
|
63
|
+
# The configuration option 'layout.swiss_qr_bill' is a hash containing styling information for
|
64
|
+
# the various text parts: section heading, payment heading/value, receipt heading/value and
|
65
|
+
# the alternative procedures. The default values are taking from the QR-bill style guide.
|
66
|
+
#
|
67
|
+
# The keys of this hash are strings of the form 'part.subpart.property' where
|
68
|
+
#
|
69
|
+
# * 'part' can be 'section', 'payment', 'receipt', or 'alternative_procedures',
|
70
|
+
# * 'subpart' can be 'heading' or 'value' (the latter not for 'section'),
|
71
|
+
# * 'property' can be 'font', 'font_size' or 'line_height' (the latter not for 'section').
|
72
|
+
#
|
73
|
+
# The default font is Liberation Sans which is one of the four allowed fonts (the others being
|
74
|
+
# Arial, Frutiger, and Helvetica). The font files themselves are *not* included and the 'font'
|
75
|
+
# property, by default, references the standard Linux path where the fonts would be found.
|
76
|
+
# Note that all '*.heading.font' values should reference the bold version of the font whereas
|
77
|
+
# the '*.value.font' values should reference the regular version.
|
78
|
+
#
|
79
|
+
#
|
80
|
+
# == Data Structure
|
81
|
+
#
|
82
|
+
# All the necessary information for generating the Swiss QR-bill is provided on
|
83
|
+
# initialization. The following keys can be used:
|
84
|
+
#
|
85
|
+
# :lang::
|
86
|
+
# The language to use for the literal text strings appearing in the QR-bill. One of :en,
|
87
|
+
# :de, :fr or :it.
|
88
|
+
#
|
89
|
+
# Defaults to :en if not specified.
|
90
|
+
#
|
91
|
+
# :creditor::
|
92
|
+
# (required) The creditor of the transaction. This is a hash that can contain the
|
93
|
+
# following elements:
|
94
|
+
#
|
95
|
+
# :iban::
|
96
|
+
# (required) The IBAN of the creditor (21 characters, no spaces, only IBANs for CH or
|
97
|
+
# LI). Note that the IBAN is not checked for validity.
|
98
|
+
#
|
99
|
+
# :name::
|
100
|
+
# (required) The name of the creditor (maximum 70 characters).
|
101
|
+
#
|
102
|
+
# :address_type::
|
103
|
+
# (required) The type of address, either :structured or :combined. Defaults to
|
104
|
+
# :structured which is the only choice in version 2.3 of the specification.
|
105
|
+
#
|
106
|
+
# :address_line1::
|
107
|
+
# The first line of the creditor's address (maximum 70 characters). In case of a
|
108
|
+
# structured address, this is the street. Otherwise this has to be the street and
|
109
|
+
# building number together.
|
110
|
+
#
|
111
|
+
# :address_line2::
|
112
|
+
# The second line of the creditor's address. In case of a structured address, this
|
113
|
+
# has to be the building number (maximum 16 characters). Otherwise it has to be the
|
114
|
+
# postal code and town (maximum 70 characters).
|
115
|
+
#
|
116
|
+
# :postal_code::
|
117
|
+
# The postal code of the creditor's address (maximum 16 characters, only for
|
118
|
+
# structured addresses).
|
119
|
+
#
|
120
|
+
# :town::
|
121
|
+
# The town from the creditor's address (maximum 35 characters, only for structured
|
122
|
+
# addresses).
|
123
|
+
#
|
124
|
+
# :country::
|
125
|
+
# (required) The country from the creditor's address (ISO 3166-1 two-letter country
|
126
|
+
# code).
|
127
|
+
#
|
128
|
+
# :debtor::
|
129
|
+
# The debtor information for the transaction. This information is optional but if used
|
130
|
+
# some elements are required. The value is a hash that can contain the same elements as
|
131
|
+
# the +:creditor+ key with the exception of the +:iban+.
|
132
|
+
#
|
133
|
+
# :amount::
|
134
|
+
# The payment amount (between 0.01 and 999,999,999.99). If not filled in, a blank field is
|
135
|
+
# shown for adding the amount by hand later. If the amount is set to zero, it means that
|
136
|
+
# the QR-bill should be used as notification and the :message is set according to the
|
137
|
+
# specification.
|
138
|
+
#
|
139
|
+
# :currency::
|
140
|
+
# (required) The payment currency (either CHF or EUR).
|
141
|
+
#
|
142
|
+
# :reference_type::
|
143
|
+
# The payment reference type (either QRR, SCOR or NON). Defaults to NON.
|
144
|
+
#
|
145
|
+
# :reference::
|
146
|
+
# The structured reference data. All whitespace is removed before processing.
|
147
|
+
#
|
148
|
+
# In case of a QRR reference, the value has to be a 26 digit reference without check digit
|
149
|
+
# or a 27 digit reference with check digit. The check digit is validated.
|
150
|
+
#
|
151
|
+
# In case of a SCOR reference, the value has to contain between 5 and 25 alpha-numeric
|
152
|
+
# characters. The check digits are validated.
|
153
|
+
#
|
154
|
+
# :message::
|
155
|
+
# Additional, unstructured information (maximum 140 characters).
|
156
|
+
#
|
157
|
+
# :billing_information::
|
158
|
+
# Billing information for automated booking of the payment (maximum 140 characters).
|
159
|
+
#
|
160
|
+
# :alternative_schemes::
|
161
|
+
# Alternative schemes parameters. Either a single string with a maximum of 100 characters
|
162
|
+
# or an array of two such strings.
|
163
|
+
#
|
164
|
+
# == Example
|
165
|
+
#
|
166
|
+
# HexaPDF::Composer.create("sample-qr-bill.pdf", margin: 0) do |composer|
|
167
|
+
# data = {
|
168
|
+
# lang: :de,
|
169
|
+
# creditor: {
|
170
|
+
# iban: "CH44 3199 9123 0008 8901 2",
|
171
|
+
# name: "Max Muster & Söhne",
|
172
|
+
# address_line1: "Musterstrasse",
|
173
|
+
# address_line2: "123",
|
174
|
+
# postal_code: "8000",
|
175
|
+
# town: "Seldwyla",
|
176
|
+
# country: "CH",
|
177
|
+
# },
|
178
|
+
# debtor: {
|
179
|
+
# address_type: :combined,
|
180
|
+
# name: "Simon Muster",
|
181
|
+
# address_line1: "Musterstrasse 1",
|
182
|
+
# address_line2: "8000 Seldwyla",
|
183
|
+
# country: "CH"
|
184
|
+
# },
|
185
|
+
# amount: 2500.25,
|
186
|
+
# currency: 'CHF',
|
187
|
+
# }
|
188
|
+
# composer.swiss_qr_bill(data: data, valign: :bottom)
|
189
|
+
# end
|
190
|
+
#
|
191
|
+
# == References
|
192
|
+
#
|
193
|
+
# * Website https://www.six-group.com/en/products-services/banking-services/billing-and-payments/qr-bill.html
|
194
|
+
# * 2.2 Specification https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/ig-qr-bill-v2.2-en.pdf
|
195
|
+
# * 2.3 Specification https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/ig-qr-bill-v2.3-en.pdf
|
196
|
+
# * Style guide https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/style-guide-qr-bill-en.pdf
|
197
|
+
class SwissQRBill < HexaPDF::Layout::Box
|
198
|
+
|
199
|
+
# Thrown when an error occurs when working with the SwissQRBill class.
|
200
|
+
class Error < HexaPDF::Error
|
201
|
+
end
|
202
|
+
|
203
|
+
# Mapping of the English text literals to their German, French and Italian counterparts,
|
204
|
+
# taken from Annex C of the specification.
|
205
|
+
TEXT_LITERALS = { #:nodoc:
|
206
|
+
de: {
|
207
|
+
'Payment part' => 'Zahlteil',
|
208
|
+
'Receipt' => 'Empfangsschein',
|
209
|
+
'Account / Payable to' => 'Konto / Zahlbar an',
|
210
|
+
'Reference' => 'Referenz',
|
211
|
+
'Additional information' => 'Zusätzliche Informationen',
|
212
|
+
'Payable by' => 'Zahlbar durch',
|
213
|
+
'Payable by (name/address)' => 'Zahlbar durch (Name/Adresse)',
|
214
|
+
'Currency' => 'Währung',
|
215
|
+
'Amount' => 'Betrag',
|
216
|
+
'Acceptance point' => 'Annahmestelle',
|
217
|
+
'In favour of' => 'Zugunsten',
|
218
|
+
'DO NOT USE FOR PAYMENT' => 'NICHT ZUR ZAHLUNG VERWENDEN',
|
219
|
+
},
|
220
|
+
fr: {
|
221
|
+
'Payment part' => 'Section paiement',
|
222
|
+
'Receipt' => 'Récépissé',
|
223
|
+
'Account / Payable to' => 'Compte / Payable à',
|
224
|
+
'Reference' => 'Référence',
|
225
|
+
'Additional information' => 'Information supplémentaires',
|
226
|
+
'Payable by' => 'Payable par',
|
227
|
+
'Payable by (name/address)' => 'Payable par (nom/address)',
|
228
|
+
'Currency' => 'Monnaie',
|
229
|
+
'Amount' => 'Montant',
|
230
|
+
'Acceptance point' => 'Point de dépôt',
|
231
|
+
'In favour of' => 'En faveur de',
|
232
|
+
'DO NOT USE FOR PAYMENT' => 'NE PAS UTILISER POUR LE PAIEMENT',
|
233
|
+
},
|
234
|
+
it: {
|
235
|
+
'Payment part' => 'Sezione pagamento',
|
236
|
+
'Receipt' => 'Ricevuta',
|
237
|
+
'Account / Payable to' => 'Conto / Pagabile a',
|
238
|
+
'Reference' => 'Riferimento',
|
239
|
+
'Additional information' => 'Informazioni supplementari',
|
240
|
+
'Payable by' => 'Pagabile da',
|
241
|
+
'Payable by (name/address)' => 'Pagabile da (nome/indirizzo)',
|
242
|
+
'Currency' => 'Value date ',
|
243
|
+
'Amount' => 'Importo',
|
244
|
+
'Acceptance point' => 'Punto di accettazione',
|
245
|
+
'In favour of' => 'A favore di',
|
246
|
+
'DO NOT USE FOR PAYMENT' => 'NON UTILIZZARE PER IL PAGAMENTO',
|
247
|
+
}
|
248
|
+
}
|
249
|
+
|
250
|
+
# The payment data - see the SwissQRBill class documentation for details.
|
251
|
+
attr_reader :data
|
252
|
+
|
253
|
+
# Creates a new SwissQRBill object for the given payment +data+ (see the class documentation
|
254
|
+
# for details).
|
255
|
+
#
|
256
|
+
# If the arguments +width+ and +height+ are provided, they are ignored since the QR-bill has
|
257
|
+
# a fixed size of 210mm x 105mm.
|
258
|
+
def initialize(data:, **kwargs)
|
259
|
+
super(**kwargs, width: 210.mm, height: 105.mm)
|
260
|
+
validate_data(data)
|
261
|
+
end
|
262
|
+
|
263
|
+
private
|
264
|
+
|
265
|
+
QRR_MODULO10_TABLE = [ #:nodoc:
|
266
|
+
[0, 9, 4, 6, 8, 2, 7, 1, 3, 5],
|
267
|
+
[9, 4, 6, 8, 2, 7, 1, 3, 5, 0],
|
268
|
+
[4, 6, 8, 2, 7, 1, 3, 5, 0, 9],
|
269
|
+
[6, 8, 2, 7, 1, 3, 5, 0, 9, 4],
|
270
|
+
[8, 2, 7, 1, 3, 5, 0, 9, 4, 6],
|
271
|
+
[2, 7, 1, 3, 5, 0, 9, 4, 6, 8],
|
272
|
+
[7, 1, 3, 5, 0, 9, 4, 6, 8, 2],
|
273
|
+
[1, 3, 5, 0, 9, 4, 6, 8, 2, 7],
|
274
|
+
[3, 5, 0, 9, 4, 6, 8, 2, 7, 1],
|
275
|
+
[5, 0, 9, 4, 6, 8, 2, 7, 1, 3]
|
276
|
+
].freeze
|
277
|
+
|
278
|
+
# Validates the given data and raises an error if the data is not valid
|
279
|
+
def validate_data(data)
|
280
|
+
@data = data
|
281
|
+
|
282
|
+
if @data[:currency] != 'EUR' && @data[:currency] != 'CHF'
|
283
|
+
raise Error, "Data field :currency must be EUR or CHR, not #{@data[:currency].inspect}"
|
284
|
+
end
|
285
|
+
|
286
|
+
if @data[:amount] == 0
|
287
|
+
@data[:message] = text('DO NOT USE FOR PAYMENT')
|
288
|
+
elsif @data[:amount] && (@data[:amount] < 0.01 || @data[:amount] > 999_999_999.99)
|
289
|
+
raise Error, "Data field :amount must be between 0.01 and 999_999_999.99"
|
290
|
+
end
|
291
|
+
|
292
|
+
validate_address = lambda do |hash|
|
293
|
+
hash[:address_type] ||= :structured
|
294
|
+
if hash[:address_type] != :structured && hash[:address_type] != :combined
|
295
|
+
raise Error, "Address type must be :structured or :combined, not #{hash[:address_type]}"
|
296
|
+
end
|
297
|
+
structured = (hash[:address_type] == :structured)
|
298
|
+
|
299
|
+
if (value = hash[:name]) && value.size > 70
|
300
|
+
raise Error, "Name in addresss must not contain more than 70 characters"
|
301
|
+
elsif !value
|
302
|
+
raise Error, "Name in address must be provided"
|
303
|
+
end
|
304
|
+
|
305
|
+
if hash[:address_line1] && hash[:address_line1].size > 70
|
306
|
+
raise Error, "Address line 1 must not contain more than 70 characters"
|
307
|
+
end
|
308
|
+
|
309
|
+
if (value = hash[:address_line2])
|
310
|
+
if structured && value.size > 16
|
311
|
+
raise Error, "Address line 2 of a structured address must not contain " \
|
312
|
+
"more than 16 characters"
|
313
|
+
elsif value.size > 70
|
314
|
+
raise Error, "Address line 2 of a combined address must not contain " \
|
315
|
+
"more than 70 characters"
|
316
|
+
end
|
317
|
+
elsif !structured
|
318
|
+
raise Error, "Address line 2 must be provided for a combined address"
|
319
|
+
end
|
320
|
+
|
321
|
+
if (value = hash[:postal_code])
|
322
|
+
if !structured
|
323
|
+
raise Error, "Postal code must not be provided for a combined address"
|
324
|
+
elsif value.size > 16
|
325
|
+
raise Error, "Postal code must not contain more than 16 characters"
|
326
|
+
end
|
327
|
+
elsif structured
|
328
|
+
raise Error, "Postal code must be provided for a structured address"
|
329
|
+
end
|
330
|
+
|
331
|
+
if (value = hash[:town])
|
332
|
+
if !structured
|
333
|
+
raise Error, "Town must not be provided for a combined address"
|
334
|
+
elsif value.size > 35
|
335
|
+
raise Error, "Town must not contain more than 35 characters"
|
336
|
+
end
|
337
|
+
elsif structured
|
338
|
+
raise Error, "Town must be provided for a structured address"
|
339
|
+
end
|
340
|
+
|
341
|
+
if (value = hash[:country]) && value.size != 2
|
342
|
+
raise Error, "Country must be a two-letter ISO-3166-1 code"
|
343
|
+
elsif !value
|
344
|
+
raise Error, "Country must be provided"
|
345
|
+
end
|
346
|
+
end
|
347
|
+
validate_address.call(@data[:creditor])
|
348
|
+
validate_address.call(@data[:debtor]) if @data[:debtor]
|
349
|
+
|
350
|
+
@data[:reference_type] ||= "NON"
|
351
|
+
case @data[:reference_type]
|
352
|
+
when "QRR"
|
353
|
+
value = @data[:reference]
|
354
|
+
unless value
|
355
|
+
raise Error, "Data field :reference must be filled in for QRR reference type"
|
356
|
+
end
|
357
|
+
value.gsub!(/\s*/, '')
|
358
|
+
if value !~ /\A\d{26,27}\z/
|
359
|
+
raise Error, "Data field :reference for QRR must contain 26 or 27 digits"
|
360
|
+
end
|
361
|
+
result = value[0, 26].each_codepoint.inject(0) do |memo, codepoint|
|
362
|
+
QRR_MODULO10_TABLE[memo][codepoint - 48]
|
363
|
+
end
|
364
|
+
check_digit = (10 - result) % 10
|
365
|
+
value << check_digit.to_s if value.size == 26
|
366
|
+
if value[26].to_i != check_digit
|
367
|
+
raise Error, "Data field :reference for QRR contains an invalid check digit, " \
|
368
|
+
"should be #{check_digit}"
|
369
|
+
end
|
370
|
+
when "SCOR"
|
371
|
+
value = @data[:reference]
|
372
|
+
unless value
|
373
|
+
raise Error, "Data field :reference must be filled in for SCOR reference type"
|
374
|
+
end
|
375
|
+
value.gsub!(/\s*/, '')
|
376
|
+
if value !~ /\A\w{5,25}\z/
|
377
|
+
raise Error, "Data field :reference for SCOR must contain between 5 and 25 " \
|
378
|
+
"alpha-numeric characters"
|
379
|
+
elsif value !~ /\ARF\d\d/
|
380
|
+
raise Error, "Data field :reference for SCOR must start with RF and check digits"
|
381
|
+
end
|
382
|
+
# See https://www.mobilefish.com/services/creditor_reference/creditor_reference.php
|
383
|
+
result = "#{value[4..-1]}#{value[0, 4]}".upcase.gsub(/[A-Z]/) {|c| c.ord - 55 }.to_i % 97
|
384
|
+
unless result == 1
|
385
|
+
raise Error, "Data field :reference for SCOR has invalid check digits"
|
386
|
+
end
|
387
|
+
when "NON"
|
388
|
+
if @data[:reference]
|
389
|
+
raise Error, "Data field :reference must not be provided for NON reference type"
|
390
|
+
end
|
391
|
+
else
|
392
|
+
raise Error, "Data field :reference_type must be one of QRR, SCOR or NON"
|
393
|
+
end
|
394
|
+
|
395
|
+
if @data[:message] && @data[:message].size > 140
|
396
|
+
raise Error, "Data field :message must not contain more than 140 characters"
|
397
|
+
end
|
398
|
+
if @data[:billing_information] && @data[:billing_information].size > 140
|
399
|
+
raise Error, "Data field :billing_information must not contain more than 140 characters"
|
400
|
+
end
|
401
|
+
if (@data[:message].to_s + @data[:billing_information].to_s).size > 140
|
402
|
+
raise Error, "Data fields :message and :billing_information together must not " \
|
403
|
+
"contain more than 140 characters"
|
404
|
+
end
|
405
|
+
|
406
|
+
@data[:alternative_schemes] = Array(@data[:alternative_schemes])
|
407
|
+
if @data[:alternative_schemes].any? {|as| as.size > 100 } ||
|
408
|
+
@data[:alternative_schemes].size > 2
|
409
|
+
raise Error, "Data field :alternative_schemes must at most contain 2 strings with " \
|
410
|
+
"not more than 100 characters each"
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# Draws the SwissQRBill onto the canvas at position [x, y].
|
415
|
+
def draw_content(canvas, x, y)
|
416
|
+
layout = canvas.context.document.layout
|
417
|
+
frame = HexaPDF::Layout::Frame.new(0, 0, width, height, context: canvas.context)
|
418
|
+
box_fitter = HexaPDF::Layout::BoxFitter.new([frame])
|
419
|
+
styles = set_up_styles(canvas.context.document.config['layout.swiss_qr_bill'], layout)
|
420
|
+
|
421
|
+
box_fitter.fit(receipt(layout, styles))
|
422
|
+
box_fitter.fit(payment(layout, styles, qr_code_cross(canvas.context.document)))
|
423
|
+
unless box_fitter.fit_successful?
|
424
|
+
raise HexaPDF::Error, "The Swiss QR-bill could not be fit"
|
425
|
+
end
|
426
|
+
|
427
|
+
canvas.translate(x, y) do
|
428
|
+
box_fitter.fit_results.each {|fit_result| fit_result.draw(canvas)}
|
429
|
+
canvas.stroke_color(0).line_width(0.5).line_dash_pattern(2).
|
430
|
+
line(62.mm, 0, 62.mm, 105.mm).
|
431
|
+
line(0, 105.mm, 210.mm, 105.mm).stroke
|
432
|
+
canvas.font('ZapfDingbats', size: 15).text("✂", at: [5.mm, 103.1.mm])
|
433
|
+
canvas.font('ZapfDingbats', size: 15).text_matrix(0, -1, 1, 0, 60.175.mm, 100.mm).text("✂")
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
# Returns a hash with styles that are used throughout the QR-bill creation for consistency.
|
438
|
+
def set_up_styles(config, layout)
|
439
|
+
{
|
440
|
+
section_heading: {font_size: config['section.heading.font_size'],
|
441
|
+
font: config['section.heading.font']},
|
442
|
+
payment_heading: {font_size: config['payment.heading.font_size'],
|
443
|
+
font: config['payment.heading.font'],
|
444
|
+
line_height: config['payment.heading.line_height'],
|
445
|
+
line_spacing: {type: :fixed, value: config['payment.heading.line_height']}},
|
446
|
+
payment_value: {font_size: config['payment.value.font_size'],
|
447
|
+
font: config['payment.value.font'],
|
448
|
+
line_height: config['payment.value.line_height'],
|
449
|
+
line_spacing: {type: :fixed, value: config['payment.value.line_height']},
|
450
|
+
padding: [0, 0, config['payment.value.line_height']]},
|
451
|
+
receipt_heading: {font_size: config['receipt.heading.font_size'],
|
452
|
+
font: config['receipt.heading.font'],
|
453
|
+
line_height: config['receipt.heading.line_height'],
|
454
|
+
line_spacing: {type: :fixed, value: config['receipt.heading.line_height']}},
|
455
|
+
receipt_value: {font_size: config['receipt.value.font_size'],
|
456
|
+
font: config['receipt.value.font'],
|
457
|
+
line_height: config['receipt.value.line_height'],
|
458
|
+
line_spacing: {type: :fixed, value: config['receipt.value.line_height']},
|
459
|
+
padding: [0, 0, config['receipt.value.line_height']]},
|
460
|
+
alternative_procedures_heading: {
|
461
|
+
font_size: config['alternative_procedures.heading.font_size'],
|
462
|
+
font: config['alternative_procedures.heading.font'],
|
463
|
+
line_height: config['alternative_procedures.heading.line_height'],
|
464
|
+
line_spacing: {type: :fixed, value: config['alternative_procedures.heading.line_height']}
|
465
|
+
},
|
466
|
+
alternative_procedures_value: {
|
467
|
+
font_size: config['alternative_procedures.value.font_size'],
|
468
|
+
font: config['alternative_procedures.value.font'],
|
469
|
+
line_height: config['alternative_procedures.value.line_height'],
|
470
|
+
line_spacing: {type: :fixed, value: config['alternative_procedures.value.line_height']},
|
471
|
+
padding: [0, 0, config['alternative_procedures.value.line_height']]
|
472
|
+
},
|
473
|
+
}.transform_values! {|value| layout.style(:base).dup.update(**value) }
|
474
|
+
end
|
475
|
+
|
476
|
+
# Returns a box containing the receipt part of the QR-bill.
|
477
|
+
def receipt(layout, styles)
|
478
|
+
layout.container(width: 62.mm, style: {padding: 5.mm, mask_mode: :fill_vertical}) do |col|
|
479
|
+
col.text(text('Receipt'), height: 7.mm, style: styles[:section_heading])
|
480
|
+
col.container(height: 56.mm) do |info|
|
481
|
+
info.text(text('Account / Payable to'), style: styles[:receipt_heading])
|
482
|
+
info.text("#{@data[:creditor][:iban]}\n#{address(@data[:creditor])}", style: styles[:receipt_value])
|
483
|
+
|
484
|
+
if @data[:reference_type] != 'NON'
|
485
|
+
info.text(text('Reference'), style: styles[:receipt_heading])
|
486
|
+
info.text(@data[:reference], style: styles[:receipt_value])
|
487
|
+
end
|
488
|
+
|
489
|
+
if @data[:debtor]
|
490
|
+
info.text(text('Payable by'), style: styles[:receipt_heading])
|
491
|
+
info.text(address(@data[:debtor]), style: styles[:receipt_value])
|
492
|
+
else
|
493
|
+
info.text(text('Payable by (name/address)'), style: styles[:receipt_heading])
|
494
|
+
blank_field(info, 52.mm, 20.mm)
|
495
|
+
end
|
496
|
+
end
|
497
|
+
receipt_amount(col, styles)
|
498
|
+
col.text(text('Acceptance point'), style: styles[:receipt_heading], text_align: :right)
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
# Adds the appropriate boxes to the given composer for the amount part of the receipt
|
503
|
+
# section.
|
504
|
+
def receipt_amount(composer, styles)
|
505
|
+
composer.container(height: 14.mm) do |amount|
|
506
|
+
if @data[:amount]
|
507
|
+
amount.column(columns: [26.mm, -1], gaps: 0) do |inner|
|
508
|
+
inner.text(text('Currency'), style: styles[:receipt_heading], padding: [0, 0, 1.mm])
|
509
|
+
inner.text(@data[:currency], style: styles[:receipt_value])
|
510
|
+
inner.text(text('Amount'), style: styles[:receipt_heading], padding: [0, 0, 1.mm])
|
511
|
+
inner.text(formatted_amount, style: styles[:receipt_value])
|
512
|
+
end
|
513
|
+
else
|
514
|
+
amount.column(columns: [-1, 31.mm], gaps: 0) do |inner|
|
515
|
+
inner.text(text('Currency') + " " + text("Amount"),
|
516
|
+
style: styles[:receipt_heading], padding: [0, 0, 1.mm])
|
517
|
+
inner.text(@data[:currency], style: styles[:receipt_value], mask_mode: :fill)
|
518
|
+
inner.box(:base, height: 2)
|
519
|
+
blank_field(inner, 30.mm, 10.mm)
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
# Returns a box containing the payment part of the QR-bill.
|
526
|
+
def payment(layout, styles, cross)
|
527
|
+
layout.container(width: 148.mm, style: {padding: 5.mm}) do |col|
|
528
|
+
col.container(width: 51.mm, height: 85.mm, style: {mask_mode: :box}) do |left_col|
|
529
|
+
left_col.text(text('Payment part'), height: 7.mm, style: styles[:section_heading])
|
530
|
+
left_col.box(:qrcode, data: qr_code_data, level: :m, style: {padding: [5.mm, 5.mm, 5.mm, 0.mm]})
|
531
|
+
left_col.image(cross, width: 7.mm, position: [19.5.mm, 46.5.mm])
|
532
|
+
payment_amount(left_col, styles)
|
533
|
+
end
|
534
|
+
col.container(height: 85.mm) do |info|
|
535
|
+
info.text(text('Account / Payable to'), style: styles[:payment_heading])
|
536
|
+
info.text("#{@data[:creditor][:iban]}\n#{address(@data[:creditor])}", style: styles[:payment_value])
|
537
|
+
|
538
|
+
if @data[:reference_type] != 'NON'
|
539
|
+
info.text(text('Reference'), style: styles[:payment_heading])
|
540
|
+
info.text(@data[:reference], style: styles[:payment_value])
|
541
|
+
end
|
542
|
+
|
543
|
+
if @data[:message] || @data[:billing_information]
|
544
|
+
info.text(text('Additional information'), style: styles[:payment_heading])
|
545
|
+
info.text([@data[:message], @data[:billing_information]].compact.join("\n"), style: styles[:payment_value])
|
546
|
+
end
|
547
|
+
|
548
|
+
if @data[:debtor]
|
549
|
+
info.text(text('Payable by'), style: styles[:payment_heading])
|
550
|
+
info.text(address(@data[:debtor]), style: styles[:payment_value])
|
551
|
+
else
|
552
|
+
info.text(text('Payable by (name/address)'), style: styles[:payment_heading])
|
553
|
+
blank_field(info, 65.mm, 25.mm)
|
554
|
+
end
|
555
|
+
end
|
556
|
+
if @data[:alternative_schemes].size > 0
|
557
|
+
@data[:alternative_schemes].each do |as|
|
558
|
+
provider, data = *as.split('/', 2)
|
559
|
+
col.formatted_text([{text: provider, style: styles[:alternative_procedures_heading]},
|
560
|
+
{text: data, style: styles[:alternative_procedures_value]}])
|
561
|
+
end
|
562
|
+
end
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
# Adds the appropriate boxes to the given composer for the amount part of the payment
|
567
|
+
# section.
|
568
|
+
def payment_amount(composer, styles)
|
569
|
+
composer.container(height: 22.mm) do |amount|
|
570
|
+
if @data[:amount]
|
571
|
+
amount.column(columns: [23.mm, -1], gaps: 0) do |inner|
|
572
|
+
inner.text(text('Currency'), style: styles[:payment_heading], padding: [0, 0, 1.mm])
|
573
|
+
inner.text(@data[:currency], style: styles[:payment_value])
|
574
|
+
inner.text(text('Amount'), style: styles[:payment_heading], padding: [0, 0, 1.mm])
|
575
|
+
inner.text(formatted_amount, style: styles[:payment_value])
|
576
|
+
end
|
577
|
+
else
|
578
|
+
amount.text(text('Currency') + " " + text("Amount"),
|
579
|
+
style: styles[:payment_heading], padding: [0, 0, 1.mm])
|
580
|
+
amount.column(columns: [-1, 41.mm], gaps: 0) do |inner|
|
581
|
+
inner.text(@data[:currency], style: styles[:payment_value])
|
582
|
+
blank_field(inner, 40.mm, 15.mm)
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
# Returns the correctly localized text for the given string +str+.
|
589
|
+
def text(str)
|
590
|
+
TEXT_LITERALS.dig(@data[:lang], str) || str
|
591
|
+
end
|
592
|
+
|
593
|
+
# Returns a string containing the formatted address for output using the provided data.
|
594
|
+
def address(data)
|
595
|
+
result = +''
|
596
|
+
result << "#{data[:name]}\n"
|
597
|
+
if data[:address_type] == :structured
|
598
|
+
addr = [data[:address_line1], data[:address_line2]].compact
|
599
|
+
result << "#{addr.join(' ')}\n" unless addr.empty?
|
600
|
+
result << "#{data[:country]}-#{data[:postal_code]} #{data[:town]}"
|
601
|
+
else
|
602
|
+
result << "#{data[:address_line1]}\n" if data.key?(:address_line1)
|
603
|
+
result << "#{data[:country]}-#{data[:address_line2]}\n"
|
604
|
+
end
|
605
|
+
result
|
606
|
+
end
|
607
|
+
|
608
|
+
# Returns the amount formatted according to the specification.
|
609
|
+
def formatted_amount
|
610
|
+
a, b = format('%.2f', @data[:amount]).split('.')
|
611
|
+
a.gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1 ") << '.' << b
|
612
|
+
end
|
613
|
+
|
614
|
+
# Creates the content of the QR code using the information provided in #data.
|
615
|
+
def qr_code_data
|
616
|
+
qr_code_data = []
|
617
|
+
add_address_data = lambda do |hash|
|
618
|
+
qr_code_data << (hash[:address_type] == :structured ? "S" : "K")
|
619
|
+
qr_code_data << hash[:name] <<
|
620
|
+
hash[:address_line1].to_s << hash[:address_line2].to_s <<
|
621
|
+
hash[:postal_code].to_s << hash[:town].to_s << hash[:country]
|
622
|
+
end
|
623
|
+
|
624
|
+
# Header information
|
625
|
+
qr_code_data.concat(["SPC", "0200", "1"])
|
626
|
+
|
627
|
+
# Creditor information
|
628
|
+
qr_code_data << @data[:creditor][:iban]
|
629
|
+
add_address_data.call(@data[:creditor])
|
630
|
+
|
631
|
+
# Ultimate creditor information
|
632
|
+
qr_code_data.concat(["", "", "", "", "", "", ""])
|
633
|
+
|
634
|
+
# Amount information
|
635
|
+
qr_code_data << (@data[:amount] ? format('%.2f', @data[:amount]) : "") << @data[:currency]
|
636
|
+
|
637
|
+
# Debtor information
|
638
|
+
add_address_data.call(@data[:debtor]) if @data[:debtor]
|
639
|
+
|
640
|
+
# Payment reference
|
641
|
+
qr_code_data << @data[:reference_type] << @data[:reference].to_s <<
|
642
|
+
@data[:message].to_s << "EPD"
|
643
|
+
qr_code_data << @data[:billing_information] if @data[:billing_information]
|
644
|
+
|
645
|
+
qr_code_data.join("\r\n")
|
646
|
+
end
|
647
|
+
|
648
|
+
# Creates a Form XObject for the Swiss cross that is to be overlaid over the QR code.
|
649
|
+
#
|
650
|
+
# The measurements are reverse-engineered from the images provided at
|
651
|
+
# https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/swiss-cross-graphic-en.zip
|
652
|
+
def qr_code_cross(document)
|
653
|
+
cross = document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 7.mm, 7.mm]})
|
654
|
+
canvas = cross.canvas
|
655
|
+
canvas.fill_color(1.0).rectangle(0, 0, 7.mm, 7.mm).fill
|
656
|
+
canvas.fill_color(0.0).rectangle(0.5.mm, 0.5.mm, 6.mm, 6.mm).fill
|
657
|
+
canvas.fill_color(1.0).
|
658
|
+
rectangle(2.93.mm, 1.67.mm, 1.17.mm, 3.9.mm).
|
659
|
+
rectangle(1.57.mm, 3.04.mm, 3.9.mm, 1.17.mm).
|
660
|
+
fill
|
661
|
+
cross
|
662
|
+
end
|
663
|
+
|
664
|
+
CORNER_MARK_LENGTH = 3.mm # :nodoc:
|
665
|
+
|
666
|
+
# Creates a blank rectangle of the given +width+ and +height+, with corner marks as
|
667
|
+
# specified by the specification.
|
668
|
+
def blank_field(layout, width, height)
|
669
|
+
layout.box(width: width, height: height) do |canvas, box|
|
670
|
+
canvas.stroke_color(0).line_width(0.75).
|
671
|
+
polyline(0, CORNER_MARK_LENGTH, 0, 0, CORNER_MARK_LENGTH, 0).
|
672
|
+
polyline(width - CORNER_MARK_LENGTH, 0, width, 0, width, CORNER_MARK_LENGTH).
|
673
|
+
polyline(width, height - CORNER_MARK_LENGTH, width, height, width - CORNER_MARK_LENGTH, height).
|
674
|
+
polyline(CORNER_MARK_LENGTH, height, 0, height, 0, height - CORNER_MARK_LENGTH).
|
675
|
+
stroke
|
676
|
+
end
|
677
|
+
end
|
678
|
+
|
679
|
+
end
|
680
|
+
|
681
|
+
end
|
682
|
+
end
|
683
|
+
end
|
data/lib/hexapdf/extras.rb
CHANGED
@@ -14,3 +14,6 @@ HexaPDF::DefaultDocumentConfiguration['graphic_object.map'][:qrcode] =
|
|
14
14
|
|
15
15
|
HexaPDF::DefaultDocumentConfiguration['layout.boxes.map'][:qrcode] =
|
16
16
|
'HexaPDF::Extras::Layout::QRCodeBox'
|
17
|
+
|
18
|
+
HexaPDF::DefaultDocumentConfiguration['layout.boxes.map'][:swiss_qr_bill] =
|
19
|
+
'HexaPDF::Extras::Layout::SwissQRBill'
|
@@ -7,6 +7,10 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
|
|
7
7
|
HexaPDF::Extras::Layout::QRCodeBox.new(**kwargs)
|
8
8
|
end
|
9
9
|
|
10
|
+
before do
|
11
|
+
@frame = HexaPDF::Layout::Frame.new(0, 0, 100, 100)
|
12
|
+
end
|
13
|
+
|
10
14
|
describe "initialize" do
|
11
15
|
it "can take the common box arguments" do
|
12
16
|
box = create_box(width: 10, height: 15)
|
@@ -25,7 +29,7 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
|
|
25
29
|
it "uses the smaller value of width/height for the dimensions if smaller than available_width/height" do
|
26
30
|
[{width: 10}, {width: 10, height: 50}, {height: 10}, {width: 50, height: 10}].each do |args|
|
27
31
|
box = create_box(**args)
|
28
|
-
assert(box.fit(100, 100,
|
32
|
+
assert(box.fit(100, 100, @frame))
|
29
33
|
assert_equal(10, box.width)
|
30
34
|
assert_equal(10, box.height)
|
31
35
|
assert_equal(10, box.qr_code.size)
|
@@ -34,12 +38,12 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
|
|
34
38
|
|
35
39
|
it "uses the smaller value of available_width/height for the dimensions" do
|
36
40
|
box = create_box
|
37
|
-
assert(box.fit(10, 20,
|
41
|
+
assert(box.fit(10, 20, @frame))
|
38
42
|
assert_equal(10, box.width)
|
39
43
|
assert_equal(10, box.height)
|
40
44
|
assert_equal(10, box.qr_code.size)
|
41
45
|
|
42
|
-
assert(box.fit(20, 15,
|
46
|
+
assert(box.fit(20, 15, @frame))
|
43
47
|
assert_equal(15, box.width)
|
44
48
|
assert_equal(15, box.height)
|
45
49
|
assert_equal(15, box.qr_code.size)
|
@@ -47,19 +51,29 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
|
|
47
51
|
|
48
52
|
it "takes the border and padding into account for the QR code size" do
|
49
53
|
box = create_box(style: {padding: [1, 2], border: {width: [3, 4]}})
|
50
|
-
assert(box.fit(100, 100,
|
54
|
+
assert(box.fit(100, 100, @frame))
|
51
55
|
assert_equal(88, box.qr_code.size)
|
56
|
+
assert_equal(100, box.width)
|
57
|
+
assert_equal(96, box.height)
|
52
58
|
|
53
59
|
box = create_box(style: {padding: [2, 1], border: {width: [4, 3]}})
|
54
|
-
assert(box.fit(100, 100,
|
60
|
+
assert(box.fit(100, 100, @frame))
|
55
61
|
assert_equal(88, box.qr_code.size)
|
62
|
+
assert_equal(96, box.width)
|
63
|
+
assert_equal(100, box.height)
|
64
|
+
|
65
|
+
box = create_box(style: {padding: [5, 5, 5, 0], border: {width: [2, 2, 2, 0]}})
|
66
|
+
assert(box.fit(50, 100, @frame))
|
67
|
+
assert_equal(43, box.qr_code.size)
|
68
|
+
assert_equal(50, box.width)
|
69
|
+
assert_equal(57, box.height)
|
56
70
|
end
|
57
71
|
end
|
58
72
|
|
59
73
|
describe "draw" do
|
60
74
|
it "draws the qrcode" do
|
61
75
|
box = create_box(width: 10)
|
62
|
-
box.fit(100, 100,
|
76
|
+
box.fit(100, 100, @frame)
|
63
77
|
|
64
78
|
canvas = Minitest::Mock.new
|
65
79
|
canvas.expect(:draw, nil, [box.qr_code])
|
@@ -0,0 +1,233 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'hexapdf'
|
3
|
+
require 'hexapdf/extras'
|
4
|
+
require 'hexapdf/extras/layout/swiss_qr_bill'
|
5
|
+
|
6
|
+
using HexaPDF::Extras::Layout::NumericMeasurementHelper
|
7
|
+
|
8
|
+
describe HexaPDF::Extras::Layout::SwissQRBill do
|
9
|
+
def create_box(data, **kwargs)
|
10
|
+
HexaPDF::Extras::Layout::SwissQRBill.new(data: data, **kwargs)
|
11
|
+
end
|
12
|
+
|
13
|
+
def data
|
14
|
+
@data ||= {
|
15
|
+
creditor: {
|
16
|
+
iban: "CH44 3199 9123 0008 8901 2",
|
17
|
+
name: "Max Muster & Söhne",
|
18
|
+
address_line1: "Musterstrasse",
|
19
|
+
address_line2: "123",
|
20
|
+
postal_code: "8000",
|
21
|
+
town: "Seldwyla",
|
22
|
+
country: "CH",
|
23
|
+
},
|
24
|
+
debtor: {
|
25
|
+
address_type: :combined,
|
26
|
+
name: "Simon Muster",
|
27
|
+
address_line1: "Musterstrasse 1",
|
28
|
+
address_line2: "8000 Seldwyla",
|
29
|
+
country: "CH"
|
30
|
+
},
|
31
|
+
lang: :de,
|
32
|
+
amount: 2500.25,
|
33
|
+
currency: 'CHF',
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
before do
|
38
|
+
@error_class = HexaPDF::Extras::Layout::SwissQRBill::Error
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "initialize" do
|
42
|
+
it "overrides any set width or height" do
|
43
|
+
box = create_box(data, width: 10, height: 15)
|
44
|
+
assert_equal(210.mm, box.width)
|
45
|
+
assert_equal(105.mm, box.height)
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "data validation" do
|
49
|
+
def assert_invalid_data(data = self.data, message)
|
50
|
+
err = assert_raises(@error_class) { create_box(data) }
|
51
|
+
assert_match(message, err.message)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "ensures a correct currency value" do
|
55
|
+
assert_equal('EUR', create_box(data.update(currency: 'EUR')).data[:currency])
|
56
|
+
assert_equal('CHF', create_box(data.update(currency: 'CHF')).data[:currency])
|
57
|
+
assert_invalid_data(data.update(currency: 'USD'), /field :currency/)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "ensure a correct amount value" do
|
61
|
+
assert_invalid_data(data.update(amount: 0.001), /field :amount/)
|
62
|
+
assert_invalid_data(data.update(amount: 1_000_000_000), /field :amount/)
|
63
|
+
data.update(amount: 0.00)
|
64
|
+
assert_equal('NICHT ZUR ZAHLUNG VERWENDEN', create_box(data).data[:message])
|
65
|
+
end
|
66
|
+
|
67
|
+
it "sets the address type to structured by default" do
|
68
|
+
assert_equal(:structured, create_box(data, width: 10, height: 15).data[:creditor][:address_type])
|
69
|
+
end
|
70
|
+
|
71
|
+
it "ensures a correct address type" do
|
72
|
+
data[:creditor].update(address_type: :invalid)
|
73
|
+
assert_invalid_data(/Address type must be/)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "ensures a name for an address exists and has a valid length" do
|
77
|
+
data[:creditor][:name] = 'a' * 71
|
78
|
+
assert_invalid_data(/Name in address.*70/)
|
79
|
+
data[:creditor].delete(:name)
|
80
|
+
assert_invalid_data(/Name in address must be provided/)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "ensures that address line 1 has a valid length" do
|
84
|
+
data[:creditor][:address_line1] = 'a' * 71
|
85
|
+
assert_invalid_data(/Address line 1.*70/)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "ensures a correct country code" do
|
89
|
+
data[:creditor][:country] = 'AMS'
|
90
|
+
assert_invalid_data(/Country must.*ISO-3166-1/)
|
91
|
+
data[:creditor].delete(:country)
|
92
|
+
assert_invalid_data(/Country must be provided/)
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "ensures a correct structured address" do
|
96
|
+
it "ensures that address line 2 has a valid length" do
|
97
|
+
data[:creditor][:address_line2] = 'a' * 17
|
98
|
+
assert_invalid_data(/Address line 2.*structured.*16/)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "ensures that the postal code exists and has a valid length" do
|
102
|
+
data[:creditor][:postal_code] = 'a' * 17
|
103
|
+
assert_invalid_data(/Postal code.*16/)
|
104
|
+
data[:creditor].delete(:postal_code)
|
105
|
+
assert_invalid_data(/Postal code must be provided.*structured/)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "ensures that the town exists and has a valid length" do
|
109
|
+
data[:creditor][:town] = 'a' * 36
|
110
|
+
assert_invalid_data(/Town.*35/)
|
111
|
+
data[:creditor].delete(:town)
|
112
|
+
assert_invalid_data(/Town must be provided.*structured/)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "ensures a correct combined address" do
|
117
|
+
it "ensures that address line 2 exists and has a valid length" do
|
118
|
+
data[:debtor][:address_line2] = 'a' * 71
|
119
|
+
assert_invalid_data(/Address line 2.*combined.*70/)
|
120
|
+
data[:debtor].delete(:address_line2)
|
121
|
+
assert_invalid_data(/Address line 2 must be provided.*combined/)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "ensures that the postal code does not exist" do
|
125
|
+
data[:debtor][:postal_code] = 'a'
|
126
|
+
assert_invalid_data(/Postal code must not be provided.*combined/)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "ensures that the town exists and has a valid length" do
|
130
|
+
data[:debtor][:town] = 'a'
|
131
|
+
assert_invalid_data(/Town must not be provided.*combined/)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "reference" do
|
136
|
+
it "ensures the QRR reference value exists and is valid" do
|
137
|
+
data[:reference_type] = 'QRR'
|
138
|
+
assert_invalid_data(/:reference must be filled.*QRR/)
|
139
|
+
data[:reference] = 'adsfads'
|
140
|
+
assert_invalid_data(/:reference for QRR.*27/)
|
141
|
+
data[:reference] = '210000000003139471430009011'
|
142
|
+
assert_invalid_data(/:reference for QRR.*invalid check digit.*7/)
|
143
|
+
data[:reference] = '21000000000313947143000901'
|
144
|
+
assert_equal('7', create_box(data).data[:reference][26])
|
145
|
+
data[:reference] = '210000000 0031394 71430009017'
|
146
|
+
assert(create_box(data))
|
147
|
+
end
|
148
|
+
|
149
|
+
it "ensures the SCOR reference value exists and is valid" do
|
150
|
+
data[:reference_type] = 'SCOR'
|
151
|
+
assert_invalid_data(/:reference must be filled.*SCOR/)
|
152
|
+
data[:reference] = 'RF11'
|
153
|
+
assert_invalid_data(/:reference for SCOR.*5 and 25/)
|
154
|
+
data[:reference] = 'RFa' * 9
|
155
|
+
assert_invalid_data(/:reference for SCOR.*5 and 25/)
|
156
|
+
data[:reference] = 'RF123323;ö'
|
157
|
+
assert_invalid_data(/:reference for SCOR.*alpha-numeric/)
|
158
|
+
data[:reference] = 'a' * 20
|
159
|
+
assert_invalid_data(/:reference for SCOR must start.*RF/)
|
160
|
+
data[:reference] = 'RFabcdefgh'
|
161
|
+
assert_invalid_data(/:reference for SCOR must start.*RF and check digits/)
|
162
|
+
data[:reference] = 'RF 48 5000056789012345d'
|
163
|
+
assert_invalid_data(/:reference for SCOR has invalid check digits/)
|
164
|
+
data[:reference] = 'RF 48 5000056789012345'
|
165
|
+
assert(create_box(data))
|
166
|
+
end
|
167
|
+
|
168
|
+
it "ensures that no reference value is specified for a NON type" do
|
169
|
+
data[:reference_type] = 'NON'
|
170
|
+
data[:reference] = 'something'
|
171
|
+
assert_invalid_data(/:reference must not be provided.*NON/)
|
172
|
+
end
|
173
|
+
|
174
|
+
it "ensures a valid reference_type value" do
|
175
|
+
data[:reference_type] = 'OTHER'
|
176
|
+
assert_invalid_data(/:reference_type must be one of/)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
it "ensures a correct message value" do
|
181
|
+
data[:message] = 'a' * 141
|
182
|
+
assert_invalid_data(/:message must not contain.*140/)
|
183
|
+
end
|
184
|
+
|
185
|
+
it "ensures a correct billing information value" do
|
186
|
+
data[:billing_information] = 'a' * 141
|
187
|
+
assert_invalid_data(/:billing_information must not contain.*140/)
|
188
|
+
end
|
189
|
+
|
190
|
+
it "ensures a total combined length of message + billing_information of max 140 characters" do
|
191
|
+
data[:message] = 'a' * 80
|
192
|
+
data[:billing_information] = 'a' * 80
|
193
|
+
assert_invalid_data(/:message and :billing_information together.*140/)
|
194
|
+
end
|
195
|
+
|
196
|
+
it "ensures a correct alternative schemes value" do
|
197
|
+
data[:alternative_schemes] = 'a' * 101
|
198
|
+
assert_invalid_data(/alternative_schemes.*not more than 100/)
|
199
|
+
data[:alternative_schemes] = ['a', 'b', 'c']
|
200
|
+
assert_invalid_data(/alternative_schemes.*at most contain 2 strings/)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe "draw" do
|
206
|
+
before do
|
207
|
+
@doc = HexaPDF::Document.new
|
208
|
+
@composer = @doc.pages.add.canvas.composer(margin: 0)
|
209
|
+
end
|
210
|
+
|
211
|
+
it "works with all information filled in" do
|
212
|
+
data[:reference_type] = 'QRR'
|
213
|
+
data[:reference] = '210000000 0031394 71430009017'
|
214
|
+
data[:message] = 'Please just pay the bills, Jim!'
|
215
|
+
data[:billing_information] = '//S/hit/30/50/what/do/I/do/'
|
216
|
+
data[:alternative_schemes] = 'ebill/here/comes/data'
|
217
|
+
assert(@composer.box(:swiss_qr_bill, data: data))
|
218
|
+
end
|
219
|
+
|
220
|
+
it "works with no amount and no debtor" do
|
221
|
+
data.delete(:debtor)
|
222
|
+
data.delete(:amount)
|
223
|
+
assert(@composer.box(:swiss_qr_bill, data: data))
|
224
|
+
end
|
225
|
+
|
226
|
+
it "fails if the content is too big for the box" do
|
227
|
+
box = create_box(data)
|
228
|
+
box.data[:message] = 'x' * 1400
|
229
|
+
err = assert_raises(HexaPDF::Error) { @composer.draw_box(box) }
|
230
|
+
assert_match(/Swiss QR-bill could not be fit/, err.message)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hexapdf-extras
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Leitner
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hexapdf
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0.
|
19
|
+
version: '0.36'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0.
|
26
|
+
version: '0.36'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rqrcode_core
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,7 +38,7 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.2'
|
41
|
-
description:
|
41
|
+
description:
|
42
42
|
email: t_leitner@gmx.at
|
43
43
|
executables: []
|
44
44
|
extensions: []
|
@@ -53,15 +53,17 @@ files:
|
|
53
53
|
- lib/hexapdf/extras/graphic_object/qr_code.rb
|
54
54
|
- lib/hexapdf/extras/layout.rb
|
55
55
|
- lib/hexapdf/extras/layout/qr_code_box.rb
|
56
|
+
- lib/hexapdf/extras/layout/swiss_qr_bill.rb
|
56
57
|
- lib/hexapdf/extras/version.rb
|
57
58
|
- test/hexapdf/extras/graphic_object/test_qr_code.rb
|
58
59
|
- test/hexapdf/extras/layout/test_qr_code_box.rb
|
60
|
+
- test/hexapdf/extras/layout/test_swiss_qr_bill.rb
|
59
61
|
- test/test_helper.rb
|
60
62
|
homepage: https://hexapdf-extras.gettalong.org
|
61
63
|
licenses:
|
62
64
|
- MIT
|
63
65
|
metadata: {}
|
64
|
-
post_install_message:
|
66
|
+
post_install_message:
|
65
67
|
rdoc_options: []
|
66
68
|
require_paths:
|
67
69
|
- lib
|
@@ -76,8 +78,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
78
|
- !ruby/object:Gem::Version
|
77
79
|
version: '0'
|
78
80
|
requirements: []
|
79
|
-
rubygems_version: 3.
|
80
|
-
signing_key:
|
81
|
+
rubygems_version: 3.5.3
|
82
|
+
signing_key:
|
81
83
|
specification_version: 4
|
82
84
|
summary: Additional functionality for HexaPDF
|
83
85
|
test_files: []
|