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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 427079ece5871b80503cf648e5f855d3ff1a0862e120fcf7ce9b125f641aaf0b
4
- data.tar.gz: f0a5e3e4e28ce8ef24b4888e56d06bf48ed0c98a23890c5b642195a6a92a275f
3
+ metadata.gz: 5871d8607c6b36fc4a2d7d102b1ad2809db65c5b802b0bb578daa52d738ed7e2
4
+ data.tar.gz: 437c213e736683b1caee25462817141593d927347da80a1a21a8f4846a24d696
5
5
  SHA512:
6
- metadata.gz: 7f1bf0ea06f6a9cf152763d0cf0f721efd403b066fd27bba3a4dde2635e72a2426708f799daa45f46eff49027435211f6b916ce08acba407786e186580b205a9
7
- data.tar.gz: 7408f2208b8c129392ce77233c32666eb283a39cd86af927aa66c12e79cd093d06147c64de30bba81b1f6e5ebaad5abee88556fd29b6d4f548cfa01f81a62a45
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
- @qr_data = {
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(@qr_data)
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
- ### Options
60
+ ### Bill data structure
61
61
 
62
- The following options are available:
62
+ The following data structure for the bill can be specified:
63
63
 
64
64
  ```ruby
65
65
  # *: mandatory
66
- @qr_data = {
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 does not validate (yet) IBAN, reference or the given QR data.
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
@@ -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,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, ->(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'),
@@ -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.keys.map do |k|
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
- options = {}
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
- options[key] = data.dig(*MAPPING[key])
78
+ flat_data[key] = data.dig(*MAPPING[key])
80
79
  end
81
80
 
82
81
  iban = IBAN.new(data[:creditor][:iban])
83
- qr_data = QR::Data.new(options.merge(iban: iban.code))
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
@@ -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.0'
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.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-22 00:00:00.000000000 Z
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