prawn-swiss_qr_bill 0.4.2 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 427079ece5871b80503cf648e5f855d3ff1a0862e120fcf7ce9b125f641aaf0b
4
- data.tar.gz: f0a5e3e4e28ce8ef24b4888e56d06bf48ed0c98a23890c5b642195a6a92a275f
3
+ metadata.gz: 38df96b8ac1e36c19b8e887a1704dc3af8494bec29fa2cbd99f4e7d536079b09
4
+ data.tar.gz: a996978e775f4a7cbbdefce2ebec4b2e6fc1ba12d3916da6b6ee9eed22b4a754
5
5
  SHA512:
6
- metadata.gz: 7f1bf0ea06f6a9cf152763d0cf0f721efd403b066fd27bba3a4dde2635e72a2426708f799daa45f46eff49027435211f6b916ce08acba407786e186580b205a9
7
- data.tar.gz: 7408f2208b8c129392ce77233c32666eb283a39cd86af927aa66c12e79cd093d06147c64de30bba81b1f6e5ebaad5abee88556fd29b6d4f548cfa01f81a62a45
6
+ metadata.gz: b5f1530805035de0d2a744f1ac3ed83520710dfc4de4bb3b0b01fcaf430f41fbe3fb86720c0f1dcb41cfbdfeedf9be6bdf9d28bf9bb7fe2a4c90ed32f982d48a
7
+ data.tar.gz: d2b5985d953531ad271585cdd6f7bc8f4a223a764254c5b558c87de7f9adb2035e64450410d9d100a7ed0056eb3ef676e9db22c3e7fad79ba968502b56b93828
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
- @qr_data = {
33
+ @bill_data = {
34
34
  creditor: {
35
35
  iban: 'CH08 3080 8004 1110 4136 9',
36
36
  address: {
@@ -43,13 +43,15 @@ require 'prawn/swiss_qr_bill'
43
43
  amount: 9.90,
44
44
  currency: 'CHF',
45
45
  reference: '00 00000 00000 02202 20202 99991',
46
- reference_type: 'QRR'
46
+ reference_type: 'QRR',
47
+ unstructured_message: 'Bill number 2202.20202.9999',
48
+ bill_information: '//S1/10/2202202029999/11/220819'
47
49
  }
48
50
 
49
51
  Prawn::Document.generate('output.pdf', page_size: 'A4') do
50
52
  text 'A Swiss QR bill'
51
53
 
52
- swiss_qr_bill(@qr_data)
54
+ swiss_qr_bill(@bill_data)
53
55
  end
54
56
  ```
55
57
 
@@ -57,13 +59,13 @@ This will render the Swiss QR-bill at the bottom of the page:
57
59
 
58
60
  ![Swiss QR-bill Example, PDF](./images/sqb_example_01.png)
59
61
 
60
- ### Options
62
+ ### Bill data structure
61
63
 
62
- The following options are available:
64
+ The following data structure for the bill can be specified:
63
65
 
64
66
  ```ruby
65
67
  # *: mandatory
66
- @qr_data = {
68
+ @bill_data = {
67
69
  creditor: {
68
70
  iban: '<iban>', # *
69
71
  address: { # *
@@ -96,9 +98,37 @@ The following options are available:
96
98
 
97
99
  If `debtor` or `amount` amount is not given, a box will be printed.
98
100
 
101
+ ### Options
102
+
103
+ Calling `swiss_qr_bill()` method with options:
104
+
105
+ ```ruby
106
+ # ...
107
+ Prawn::Document.generate('output.pdf', page_size: 'A4') do
108
+ # ...
109
+
110
+ # raises InvalidIBANError when @bill_data[:creditor][:iban] is invalid
111
+ swiss_qr_bill(@bill_data, validate: true)
112
+ end
113
+ ```
114
+
115
+ Available options:
116
+
117
+ | Option | Data type | Description | Default |
118
+ | --- | --- | --- | --- |
119
+ | `validate` | boolean | Validates IBAN and Reference Number and raises several errors | `false` |
120
+
121
+ Errors which can be raised during validation:
122
+
123
+ * `MissingIBANError`: When IBAN is missing.
124
+ * `InvalidIBANError`: When IBAN is invalid. It checks for CH-IBAN only.
125
+ * `InvalidReferenceError`: When reference is invalid. It checks for a valid QRR or SCOR reference
126
+
99
127
  ## Important
100
128
 
101
- This library does not validate (yet) IBAN, reference or the given QR data.
129
+ This library can validate IBAN (switzerland only) and reference number (types QRR and SCOR).
130
+ It does not however validate, if the given data is fully valid according to the implementation guidelines.
131
+
102
132
  Please refer to the implementation guidelines and the Swiss QR-bill validaton
103
133
  portal by SIX below.
104
134
 
@@ -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
@@ -3,6 +3,8 @@
3
3
  module Prawn
4
4
  module SwissQRBill
5
5
  # Draws a box with corner ticks
6
+ #
7
+ # OPTIMIZE: rewrite @document to accessor, like the others
6
8
  class CornerBox
7
9
  TICK_SIZE = 3.mm
8
10
 
@@ -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
- # Simple field structure:
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
- Field = Struct.new(:default, :format, :skippable)
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
- # TODO: check for valid iban
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,22 +60,20 @@ 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, ->(value) { value && value.delete(' ') }),
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'),
66
-
67
- # additional:
68
-
69
67
  bill_information: Field.new(nil, nil, true),
70
68
  # key-value pairs:
71
69
  alternative_parameters: Field.new(nil, nil, true)
72
70
  }.freeze
73
71
 
74
- # TODO: check if all fields can be changed by user?
75
72
  attr_accessor(*FIELDS.keys)
76
73
 
77
- def initialize(fields = {})
74
+ def initialize(fields = {}, options = {})
75
+ @options = options || {}
76
+
78
77
  # set defaults
79
78
  FIELDS.each_key do |field|
80
79
  instance_variable_set("@#{field}", FIELDS[field].default)
@@ -87,18 +86,79 @@ module Prawn
87
86
  end
88
87
 
89
88
  def generate
89
+ validate if @options[:validate]
90
+
90
91
  stack = []
91
- FIELDS.keys.map do |k|
92
+ FIELDS.each_key do |k|
92
93
  var = instance_variable_get("@#{k}")
93
94
 
95
+ # TODO: fix possible wrong format if alt parameters (last one) is given
94
96
  next if FIELDS[k][:skippable] && var.nil?
95
97
 
98
+ # TODO: use #process; generate method should only call validate, then process, then render as string
96
99
  var = FIELDS[k][:format].call(var) if FIELDS[k][:format].is_a?(Proc)
97
100
 
98
101
  stack << var
99
102
  end
100
103
  stack.join("\r\n")
101
104
  end
105
+
106
+ def process
107
+ FIELDS.each_key do |k|
108
+ var = instance_variable_get("@#{k}")
109
+
110
+ instance_variable_set("@#{k}", FIELDS[k][:format].call(var)) if FIELDS[k][:format].is_a?(Proc)
111
+ end
112
+ end
113
+
114
+ def validate
115
+ FIELDS.each_key do |k|
116
+ next unless FIELDS[k][:validation]
117
+
118
+ var = instance_variable_get("@#{k}")
119
+
120
+ call_validator(FIELDS[k][:validation], var)
121
+ end
122
+
123
+ true
124
+ end
125
+
126
+ private
127
+
128
+ def call_validator(validator, value)
129
+ case validator
130
+ when Proc
131
+ # NOTE: currently not in use
132
+ # :nocov:
133
+ validator.call(value)
134
+ # :nocov:
135
+ when Symbol
136
+ send("#{validator}_validator", value)
137
+ end
138
+ end
139
+
140
+ def iban_validator(value)
141
+ # IBAN must be given
142
+ raise MissingIBANError, 'IBAN is missing' if value.nil? || value.empty?
143
+
144
+ # IBAN must be valid
145
+ iban = IBAN.new(value)
146
+ raise InvalidIBANError, "IBAN #{iban.prettify} is invalid" unless iban.valid?
147
+
148
+ true
149
+ end
150
+
151
+ def reference_validator(value)
152
+ reference = Reference.new(value, reference_type)
153
+
154
+ unless %w[QRR SCOR].include?(reference_type)
155
+ raise InvalidReferenceError, "Reference Type #{reference_type} invalid. Allowed: QRR, SCOR"
156
+ end
157
+
158
+ raise InvalidReferenceError, "Reference #{value} is invalid" unless reference.valid?
159
+
160
+ true
161
+ end
102
162
  end
103
163
  end
104
164
  end
@@ -0,0 +1,98 @@
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 == Reference.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
+ # Generate a check digit with modulo 10 recursive:
79
+ #
80
+ # Can be used as an instance method:
81
+ #
82
+ # Prawn::SwissQRBill::Reference.modulo10_recursive("2202202029999")
83
+ # # will return 1
84
+ def self.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
+
91
+ private
92
+
93
+ def standardize(reference)
94
+ reference.to_s.strip.gsub(/\s+/, '').upcase
95
+ end
96
+ end
97
+ end
98
+ end
@@ -13,7 +13,7 @@ module Prawn
13
13
  box do
14
14
  draw_payable_to
15
15
  draw_reference if @data.key?(:reference)
16
- draw_additional_information if @data.key?(:additional_information)
16
+ draw_additional_information if @data.key?(:unstructured_message) || @data.key?(:bill_information)
17
17
  draw_payable_by
18
18
  end
19
19
  end
@@ -37,7 +37,7 @@ module Prawn
37
37
 
38
38
  def draw_additional_information
39
39
  label I18n.t('additional_info', scope: i18n_scope)
40
- content @data[:additional_information]
40
+ content [@data[:unstructured_message], @data[:bill_information]].join(' ')
41
41
 
42
42
  line_spacing
43
43
  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],
@@ -37,7 +36,9 @@ module Prawn
37
36
  amount: [:amount],
38
37
  currency: [:currency],
39
38
  reference: [:reference],
40
- reference_type: [:reference_type]
39
+ reference_type: [:reference_type],
40
+ unstructured_message: [:unstructured_message],
41
+ bill_information: [:bill_information]
41
42
  }.freeze
42
43
 
43
44
  def draw
@@ -71,16 +72,17 @@ module Prawn
71
72
  end
72
73
 
73
74
  def generate_qr_data(data)
74
- options = {}
75
+ flat_data = {}
75
76
  MAPPING.each_key do |key|
76
- # check if the exists
77
+ # check if the key exists
77
78
  next unless deep_key?(data, MAPPING[key])
78
79
 
79
- options[key] = data.dig(*MAPPING[key])
80
+ flat_data[key] = data.dig(*MAPPING[key])
80
81
  end
81
82
 
82
83
  iban = IBAN.new(data[:creditor][:iban])
83
- qr_data = QR::Data.new(options.merge(iban: iban.code))
84
+
85
+ qr_data = QR::Data.new(flat_data.merge(iban: iban.code), validate: @options[:validate])
84
86
  qr_data.generate
85
87
  end
86
88
 
@@ -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
@@ -26,6 +26,7 @@ module Prawn
26
26
  }.freeze
27
27
 
28
28
  def initialize
29
+ # OPTIMIZE: unnessecary assignement
29
30
  @specs = load_specs
30
31
  end
31
32
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Prawn
4
4
  module SwissQRBill
5
- VERSION = '0.4.2'
5
+ VERSION = '0.5.2'
6
6
  end
7
7
  end
@@ -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.2
4
+ version: 0.5.2
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-22 00:00:00.000000000 Z
11
+ date: 2022-08-25 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
@@ -214,7 +215,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
214
215
  - !ruby/object:Gem::Version
215
216
  version: '0'
216
217
  requirements: []
217
- rubygems_version: 3.0.3.1
218
+ rubygems_version: 3.2.32
218
219
  signing_key:
219
220
  specification_version: 4
220
221
  summary: Swiss QR-Bill PDFs in Ruby with Prawn