prawn-swiss_qr_bill 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +34 -6
- data/lib/prawn/swiss_qr_bill/bill.rb +3 -2
- data/lib/prawn/swiss_qr_bill/corner_box.rb +2 -0
- data/lib/prawn/swiss_qr_bill/extension.rb +2 -2
- data/lib/prawn/swiss_qr_bill/qr/data.rb +76 -13
- data/lib/prawn/swiss_qr_bill/reference.rb +92 -0
- data/lib/prawn/swiss_qr_bill/sections/qr_code.rb +6 -6
- data/lib/prawn/swiss_qr_bill/sections/section.rb +2 -1
- data/lib/prawn/swiss_qr_bill/sections.rb +2 -2
- data/lib/prawn/swiss_qr_bill/specifications.rb +1 -0
- data/lib/prawn/swiss_qr_bill/version.rb +1 -1
- data/lib/prawn/swiss_qr_bill.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5871d8607c6b36fc4a2d7d102b1ad2809db65c5b802b0bb578daa52d738ed7e2
|
4
|
+
data.tar.gz: 437c213e736683b1caee25462817141593d927347da80a1a21a8f4846a24d696
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0570742ae29ea8c8b84166bdfd341495b065ac4fa537c197c41a5c04e62c6b9b7907d77e7be9cf51508802f8c9343cbb7795de7c4c9336de8ea78d001b772e0
|
7
|
+
data.tar.gz: 93990db475afdf4851a98a552ad8be43235e365b0cc79506b613c20f2f1e865b3b8e7b6950d16f45ffa6ddc06e935786210875edac5778c2facd6f627ee0c5d8
|
data/README.md
CHANGED
@@ -30,7 +30,7 @@ Define the relevant information for the Swiss QR-bill and render it inside the P
|
|
30
30
|
require 'prawn'
|
31
31
|
require 'prawn/swiss_qr_bill'
|
32
32
|
|
33
|
-
@
|
33
|
+
@bill_data = {
|
34
34
|
creditor: {
|
35
35
|
iban: 'CH08 3080 8004 1110 4136 9',
|
36
36
|
address: {
|
@@ -49,7 +49,7 @@ require 'prawn/swiss_qr_bill'
|
|
49
49
|
Prawn::Document.generate('output.pdf', page_size: 'A4') do
|
50
50
|
text 'A Swiss QR bill'
|
51
51
|
|
52
|
-
swiss_qr_bill(@
|
52
|
+
swiss_qr_bill(@bill_data)
|
53
53
|
end
|
54
54
|
```
|
55
55
|
|
@@ -57,13 +57,13 @@ This will render the Swiss QR-bill at the bottom of the page:
|
|
57
57
|
|
58
58
|
![Swiss QR-bill Example, PDF](./images/sqb_example_01.png)
|
59
59
|
|
60
|
-
###
|
60
|
+
### Bill data structure
|
61
61
|
|
62
|
-
The following
|
62
|
+
The following data structure for the bill can be specified:
|
63
63
|
|
64
64
|
```ruby
|
65
65
|
# *: mandatory
|
66
|
-
@
|
66
|
+
@bill_data = {
|
67
67
|
creditor: {
|
68
68
|
iban: '<iban>', # *
|
69
69
|
address: { # *
|
@@ -96,9 +96,37 @@ The following options are available:
|
|
96
96
|
|
97
97
|
If `debtor` or `amount` amount is not given, a box will be printed.
|
98
98
|
|
99
|
+
### Options
|
100
|
+
|
101
|
+
Calling `swiss_qr_bill()` method with options:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
# ...
|
105
|
+
Prawn::Document.generate('output.pdf', page_size: 'A4') do
|
106
|
+
# ...
|
107
|
+
|
108
|
+
# raises InvalidIBANError when @bill_data[:creditor][:iban] is invalid
|
109
|
+
swiss_qr_bill(@bill_data, validate: true)
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
Available options:
|
114
|
+
|
115
|
+
| Option | Data type | Description | Default |
|
116
|
+
| --- | --- | --- | --- |
|
117
|
+
| `validate` | boolean | Validates IBAN and Reference Number and raises several errors | `false` |
|
118
|
+
|
119
|
+
Errors which can be raised during validation:
|
120
|
+
|
121
|
+
* `MissingIBANError`: When IBAN is missing.
|
122
|
+
* `InvalidIBANError`: When IBAN is invalid. It checks for CH-IBAN only.
|
123
|
+
* `InvalidReferenceError`: When reference is invalid. It checks for a valid QRR or SCOR reference
|
124
|
+
|
99
125
|
## Important
|
100
126
|
|
101
|
-
This library
|
127
|
+
This library can validate IBAN (switzerland only) and reference number (types QRR and SCOR).
|
128
|
+
It does not however validate, if the given data is fully valid according to the implementation guidelines.
|
129
|
+
|
102
130
|
Please refer to the implementation guidelines and the Swiss QR-bill validaton
|
103
131
|
portal by SIX below.
|
104
132
|
|
@@ -6,16 +6,17 @@ module Prawn
|
|
6
6
|
class Bill
|
7
7
|
FONT_DIR = File.expand_path("#{__dir__}/../../../assets/fonts")
|
8
8
|
|
9
|
-
def initialize(document, data)
|
9
|
+
def initialize(document, data, options = {})
|
10
10
|
@doc = document
|
11
11
|
@data = data
|
12
|
+
@options = options || {}
|
12
13
|
end
|
13
14
|
|
14
15
|
def draw
|
15
16
|
set_font
|
16
17
|
|
17
18
|
@doc.canvas do
|
18
|
-
Sections.draw_all(@doc, @data)
|
19
|
+
Sections.draw_all(@doc, @data, @options)
|
19
20
|
CuttingLines.new(@doc).draw
|
20
21
|
end
|
21
22
|
end
|
@@ -4,8 +4,8 @@ module Prawn
|
|
4
4
|
module SwissQRBill
|
5
5
|
# Extend prawn with *swiss_qr_bill* methods
|
6
6
|
module Extension
|
7
|
-
def swiss_qr_bill(data)
|
8
|
-
Prawn::SwissQRBill::Bill.new(self, data).draw
|
7
|
+
def swiss_qr_bill(data, options = {})
|
8
|
+
Prawn::SwissQRBill::Bill.new(self, data, options).draw
|
9
9
|
end
|
10
10
|
|
11
11
|
def swiss_qr_bill_sections
|
@@ -3,21 +3,24 @@
|
|
3
3
|
module Prawn
|
4
4
|
module SwissQRBill
|
5
5
|
module QR
|
6
|
+
class MissingIBANError < StandardError; end
|
7
|
+
class InvalidIBANError < StandardError; end
|
8
|
+
class InvalidReferenceError < StandardError; end
|
9
|
+
|
6
10
|
# The data of the Swiss QR-bill
|
7
11
|
#
|
8
12
|
# References:
|
9
13
|
# https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf
|
10
14
|
# * Chapter 4, Page 25
|
11
|
-
#
|
12
|
-
# TODO: implement skippable (alt schemas, bill info. fields after trailer)
|
13
|
-
# TODO: check if addr-type has to be mentioned if not given?
|
14
|
-
# OPTIMIZE: implement non-changable?
|
15
15
|
class Data
|
16
|
-
#
|
16
|
+
# Field structure:
|
17
17
|
# * :default => default value to be set, if key is not given
|
18
18
|
# * :format => Proc to call when generating output
|
19
19
|
# * :skippable => Do not output in QR data if not given
|
20
|
-
|
20
|
+
# * :validation => Proc or symbol for validation
|
21
|
+
# * if Proc given: will be called with value as argument
|
22
|
+
# * if symbol given: method "#{:symbol}_validator" will be called with value as argument
|
23
|
+
Field = Struct.new(:default, :format, :skippable, :validation)
|
21
24
|
|
22
25
|
# Available fields of the QR code data, ordered.
|
23
26
|
FIELDS = {
|
@@ -26,9 +29,7 @@ module Prawn
|
|
26
29
|
version: Field.new('0200'),
|
27
30
|
# fixed: 1
|
28
31
|
coding: Field.new('1'),
|
29
|
-
|
30
|
-
# iban: Field.new(nil, ->(value) { value.delete(' ') }),
|
31
|
-
iban: Field.new,
|
32
|
+
iban: Field.new(nil, nil, false, :iban),
|
32
33
|
# enum: S, K
|
33
34
|
creditor_address_type: Field.new('K'),
|
34
35
|
creditor_address_name: Field.new,
|
@@ -59,7 +60,7 @@ module Prawn
|
|
59
60
|
debtor_address_country: Field.new,
|
60
61
|
# enum: QRR, SCOR, NON
|
61
62
|
reference_type: Field.new('NON'),
|
62
|
-
reference: Field.new(nil, ->(
|
63
|
+
reference: Field.new(nil, ->(v) { v && v.delete(' ') }, false, :reference),
|
63
64
|
unstructured_message: Field.new,
|
64
65
|
# fixed: EPD
|
65
66
|
trailer: Field.new('EPD'),
|
@@ -71,10 +72,11 @@ module Prawn
|
|
71
72
|
alternative_parameters: Field.new(nil, nil, true)
|
72
73
|
}.freeze
|
73
74
|
|
74
|
-
# TODO: check if all fields can be changed by user?
|
75
75
|
attr_accessor(*FIELDS.keys)
|
76
76
|
|
77
|
-
def initialize(fields = {})
|
77
|
+
def initialize(fields = {}, options = {})
|
78
|
+
@options = options || {}
|
79
|
+
|
78
80
|
# set defaults
|
79
81
|
FIELDS.each_key do |field|
|
80
82
|
instance_variable_set("@#{field}", FIELDS[field].default)
|
@@ -87,18 +89,79 @@ module Prawn
|
|
87
89
|
end
|
88
90
|
|
89
91
|
def generate
|
92
|
+
validate if @options[:validate]
|
93
|
+
|
90
94
|
stack = []
|
91
|
-
FIELDS.
|
95
|
+
FIELDS.each_key do |k|
|
92
96
|
var = instance_variable_get("@#{k}")
|
93
97
|
|
98
|
+
# TODO: fix possible wrong format if alt parameters (last one) is given
|
94
99
|
next if FIELDS[k][:skippable] && var.nil?
|
95
100
|
|
101
|
+
# TODO: use #process; generate method should only call validate, then process, then render as string
|
96
102
|
var = FIELDS[k][:format].call(var) if FIELDS[k][:format].is_a?(Proc)
|
97
103
|
|
98
104
|
stack << var
|
99
105
|
end
|
100
106
|
stack.join("\r\n")
|
101
107
|
end
|
108
|
+
|
109
|
+
def process
|
110
|
+
FIELDS.each_key do |k|
|
111
|
+
var = instance_variable_get("@#{k}")
|
112
|
+
|
113
|
+
instance_variable_set("@#{k}", FIELDS[k][:format].call(var)) if FIELDS[k][:format].is_a?(Proc)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def validate
|
118
|
+
FIELDS.each_key do |k|
|
119
|
+
next unless FIELDS[k][:validation]
|
120
|
+
|
121
|
+
var = instance_variable_get("@#{k}")
|
122
|
+
|
123
|
+
call_validator(FIELDS[k][:validation], var)
|
124
|
+
end
|
125
|
+
|
126
|
+
true
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def call_validator(validator, value)
|
132
|
+
case validator
|
133
|
+
when Proc
|
134
|
+
# NOTE: currently not in use
|
135
|
+
# :nocov:
|
136
|
+
validator.call(value)
|
137
|
+
# :nocov:
|
138
|
+
when Symbol
|
139
|
+
send("#{validator}_validator", value)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def iban_validator(value)
|
144
|
+
# IBAN must be given
|
145
|
+
raise MissingIBANError, 'IBAN is missing' if value.nil? || value.empty?
|
146
|
+
|
147
|
+
# IBAN must be valid
|
148
|
+
iban = IBAN.new(value)
|
149
|
+
raise InvalidIBANError, "IBAN #{iban.prettify} is invalid" unless iban.valid?
|
150
|
+
|
151
|
+
true
|
152
|
+
end
|
153
|
+
|
154
|
+
def reference_validator(value)
|
155
|
+
reference = Reference.new(value, reference_type)
|
156
|
+
|
157
|
+
unless %w[QRR SCOR].include?(reference_type)
|
158
|
+
raise InvalidReferenceError, "Reference Type #{reference_type} invalid. Allowed: QRR, SCOR"
|
159
|
+
end
|
160
|
+
|
161
|
+
raise InvalidReferenceError, "Reference #{value} is invalid" unless reference.valid?
|
162
|
+
|
163
|
+
true
|
164
|
+
end
|
102
165
|
end
|
103
166
|
end
|
104
167
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prawn
|
4
|
+
module SwissQRBill
|
5
|
+
# Check validity of reference number
|
6
|
+
#
|
7
|
+
# QRR reference:
|
8
|
+
# Refer to the implementation guides of SIX: https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf
|
9
|
+
class Reference
|
10
|
+
MODULO_TABLE = [
|
11
|
+
[0, 9, 4, 6, 8, 2, 7, 1, 3, 5],
|
12
|
+
[9, 4, 6, 8, 2, 7, 1, 3, 5, 0],
|
13
|
+
[4, 6, 8, 2, 7, 1, 3, 5, 0, 9],
|
14
|
+
[6, 8, 2, 7, 1, 3, 5, 0, 9, 4],
|
15
|
+
[8, 2, 7, 1, 3, 5, 0, 9, 4, 6],
|
16
|
+
[2, 7, 1, 3, 5, 0, 9, 4, 6, 8],
|
17
|
+
[7, 1, 3, 5, 0, 9, 4, 6, 8, 2],
|
18
|
+
[1, 3, 5, 0, 9, 4, 6, 8, 2, 7],
|
19
|
+
[3, 5, 0, 9, 4, 6, 8, 2, 7, 1],
|
20
|
+
[5, 0, 9, 4, 6, 8, 2, 7, 1, 3]
|
21
|
+
].freeze
|
22
|
+
|
23
|
+
def initialize(reference, type = 'QRR')
|
24
|
+
@type = type
|
25
|
+
@reference = standardize(reference)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Number without check digit
|
29
|
+
def number
|
30
|
+
case @type
|
31
|
+
when 'QRR'
|
32
|
+
@reference[0...-1]
|
33
|
+
when 'SCOR'
|
34
|
+
@reference[4..-1]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# QRR: Last number, the check digit
|
39
|
+
# SCOR: 2 numbers after RF
|
40
|
+
def check_digits
|
41
|
+
case @type
|
42
|
+
when 'QRR'
|
43
|
+
@reference[-1].to_i
|
44
|
+
when 'SCOR'
|
45
|
+
@reference[2..3]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid?
|
50
|
+
valid_check_digits? && valid_length?
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid_check_digits?
|
54
|
+
case @type
|
55
|
+
when 'QRR'
|
56
|
+
check_digits == modulo10_recursive(number)
|
57
|
+
when 'SCOR'
|
58
|
+
scor_to_i % 97 == 1
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# NOTE: for SCOR only
|
63
|
+
def scor_to_i
|
64
|
+
"#{number}RF#{check_digits}".gsub(/[A-Z]/) { |c| c.ord - 55 }.to_i
|
65
|
+
end
|
66
|
+
|
67
|
+
# According to the payment standards (PDF, Annex B):
|
68
|
+
# The QR reference consists of 27 positions and is numerical.
|
69
|
+
def valid_length?
|
70
|
+
case @type
|
71
|
+
when 'QRR'
|
72
|
+
@reference.length <= 27
|
73
|
+
when 'SCOR'
|
74
|
+
@reference.length <= 25
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def standardize(reference)
|
81
|
+
reference.to_s.strip.gsub(/\s+/, '').upcase
|
82
|
+
end
|
83
|
+
|
84
|
+
def modulo10_recursive(reference)
|
85
|
+
numbers = reference.to_s.chars.map(&:to_i)
|
86
|
+
report = numbers.inject(0) { |memo, c| MODULO_TABLE[memo][c] }
|
87
|
+
|
88
|
+
(10 - report) % 10
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -11,8 +11,6 @@ module Prawn
|
|
11
11
|
#
|
12
12
|
# swiss_cross.png => 166px ~> 7.mm
|
13
13
|
# QR-code: 1090px <~ 46.mm
|
14
|
-
#
|
15
|
-
# TODO: iban check -> raise exception if configured
|
16
14
|
class QRCode < Section
|
17
15
|
KEY = 'payment.qr_code'
|
18
16
|
|
@@ -26,6 +24,7 @@ module Prawn
|
|
26
24
|
creditor_address_line1: %i[creditor address line1],
|
27
25
|
creditor_address_line2: %i[creditor address line2],
|
28
26
|
creditor_address_postal_code: %i[creditor address postal_code],
|
27
|
+
creditor_address_city: %i[creditor address city],
|
29
28
|
creditor_address_country: %i[creditor address country],
|
30
29
|
debtor_address_type: %i[debtor address type],
|
31
30
|
debtor_address_name: %i[debtor address name],
|
@@ -71,16 +70,17 @@ module Prawn
|
|
71
70
|
end
|
72
71
|
|
73
72
|
def generate_qr_data(data)
|
74
|
-
|
73
|
+
flat_data = {}
|
75
74
|
MAPPING.each_key do |key|
|
76
|
-
# check if the exists
|
75
|
+
# check if the key exists
|
77
76
|
next unless deep_key?(data, MAPPING[key])
|
78
77
|
|
79
|
-
|
78
|
+
flat_data[key] = data.dig(*MAPPING[key])
|
80
79
|
end
|
81
80
|
|
82
81
|
iban = IBAN.new(data[:creditor][:iban])
|
83
|
-
|
82
|
+
|
83
|
+
qr_data = QR::Data.new(flat_data.merge(iban: iban.code), validate: @options[:validate])
|
84
84
|
qr_data.generate
|
85
85
|
end
|
86
86
|
|
@@ -19,13 +19,14 @@ module Prawn
|
|
19
19
|
# specifications for subclass' section, @see Specification for details
|
20
20
|
attr_accessor :specs
|
21
21
|
|
22
|
-
def initialize(document, data)
|
22
|
+
def initialize(document, data, options = {})
|
23
23
|
unless self.class.const_defined?(:KEY)
|
24
24
|
raise NotImplementedError, "constant KEY not defined in class #{self.class}"
|
25
25
|
end
|
26
26
|
|
27
27
|
@doc = document
|
28
28
|
@data = data
|
29
|
+
@options = options || {}
|
29
30
|
|
30
31
|
load_specs
|
31
32
|
end
|
@@ -19,9 +19,9 @@ module Prawn
|
|
19
19
|
].freeze
|
20
20
|
|
21
21
|
# Draw all sections in the right order.
|
22
|
-
def self.draw_all(document, data)
|
22
|
+
def self.draw_all(document, data, options = {})
|
23
23
|
SECTION_CLASSES.each do |klass|
|
24
|
-
klass.new(document, data).draw
|
24
|
+
klass.new(document, data, options).draw
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
data/lib/prawn/swiss_qr_bill.rb
CHANGED
@@ -17,6 +17,7 @@ require 'prawn/swiss_qr_bill/helpers/box_helper'
|
|
17
17
|
|
18
18
|
require 'prawn/swiss_qr_bill/qr/data'
|
19
19
|
require 'prawn/swiss_qr_bill/iban'
|
20
|
+
require 'prawn/swiss_qr_bill/reference'
|
20
21
|
require 'prawn/swiss_qr_bill/padded_box'
|
21
22
|
require 'prawn/swiss_qr_bill/corner_box'
|
22
23
|
require 'prawn/swiss_qr_bill/cutting_lines'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prawn-swiss_qr_bill
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mischa Schindowski
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-02-
|
11
|
+
date: 2022-02-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: i18n
|
@@ -179,6 +179,7 @@ files:
|
|
179
179
|
- lib/prawn/swiss_qr_bill/iban.rb
|
180
180
|
- lib/prawn/swiss_qr_bill/padded_box.rb
|
181
181
|
- lib/prawn/swiss_qr_bill/qr/data.rb
|
182
|
+
- lib/prawn/swiss_qr_bill/reference.rb
|
182
183
|
- lib/prawn/swiss_qr_bill/sections.rb
|
183
184
|
- lib/prawn/swiss_qr_bill/sections/payment_amount.rb
|
184
185
|
- lib/prawn/swiss_qr_bill/sections/payment_further_information.rb
|