prawn-swiss_qr_bill 0.4.2 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
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