ibandit 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ibandit/iban.rb CHANGED
@@ -2,23 +2,25 @@ require 'yaml'
2
2
 
3
3
  module Ibandit
4
4
  class IBAN
5
- attr_reader :iban
6
- attr_reader :errors
7
-
8
- def self.structures
9
- @structures ||= YAML.load_file(
10
- File.expand_path('../../../data/structures.yml', __FILE__)
11
- )
12
- end
5
+ attr_reader :errors, :iban, :country_code, :check_digits, :bank_code,
6
+ :branch_code, :account_number
7
+
8
+ def initialize(argument)
9
+ if argument.is_a?(String)
10
+ @iban = argument.to_s.gsub(/\s+/, '').upcase
11
+ extract_local_details_from_iban!
12
+ elsif argument.is_a?(Hash)
13
+ build_iban_from_local_details(argument)
14
+ else
15
+ raise TypeError, 'Must pass an IBAN string or hash of local details'
16
+ end
13
17
 
14
- def initialize(iban)
15
- @iban = iban.to_s.gsub(/\s+/, '').upcase
16
18
  @errors = {}
17
19
  end
18
20
 
19
21
  def to_s(format = :compact)
20
22
  case format
21
- when :compact then iban
23
+ when :compact then iban.to_s
22
24
  when :formatted then formatted
23
25
  else raise ArgumentError, "invalid format '#{format}'"
24
26
  end
@@ -28,58 +30,25 @@ module Ibandit
28
30
  # Component parts #
29
31
  ###################
30
32
 
31
- def country_code
32
- iban[0, 2]
33
- end
34
-
35
- def check_digits
36
- iban[2, 2] || ''
37
- end
38
-
39
- def bank_code
40
- return '' unless structure
41
-
42
- iban.slice(
43
- structure[:bank_code_position] - 1,
44
- structure[:bank_code_length]
45
- ) || ''
46
- end
47
-
48
- def branch_code
49
- return '' unless structure && structure[:branch_code_length] > 0
50
-
51
- iban.slice(
52
- structure[:branch_code_position] - 1,
53
- structure[:branch_code_length]
54
- ) || ''
55
- end
56
-
57
- def account_number
58
- return '' unless structure
59
-
60
- iban.slice(
61
- structure[:account_number_position] - 1,
62
- structure[:account_number_length]
63
- ) || ''
64
- end
65
-
66
33
  def iban_national_id
67
- return '' unless structure
34
+ return unless decomposable?
68
35
 
69
- national_id = branch_code.nil? ? bank_code : bank_code + branch_code
36
+ national_id = bank_code.to_s
37
+ national_id += branch_code.to_s
70
38
  national_id.slice(0, structure[:iban_national_id_length])
71
39
  end
72
40
 
73
41
  def local_check_digits
74
- return '' unless structure && structure[:local_check_digit_position]
42
+ return unless decomposable? && structure[:local_check_digit_position]
75
43
 
76
- iban.slice(structure[:local_check_digit_position] - 1,
77
- structure[:local_check_digit_length]
78
- ) || ''
44
+ iban.slice(
45
+ structure[:local_check_digit_position] - 1,
46
+ structure[:local_check_digit_length]
47
+ )
79
48
  end
80
49
 
81
50
  def bban
82
- iban[4..-1] || ''
51
+ iban[4..-1] unless iban.nil?
83
52
  end
84
53
 
85
54
  ###############
@@ -92,12 +61,18 @@ module Ibandit
92
61
  valid_characters?,
93
62
  valid_check_digits?,
94
63
  valid_length?,
95
- valid_format?
64
+ valid_bank_code_length?,
65
+ valid_branch_code_length?,
66
+ valid_account_number_length?,
67
+ valid_format?,
68
+ valid_bank_code_format?,
69
+ valid_branch_code_format?,
70
+ valid_account_number_format?
96
71
  ].all?
97
72
  end
98
73
 
99
74
  def valid_country_code?
100
- if IBAN.structures.key?(country_code)
75
+ if Ibandit.structures.key?(country_code)
101
76
  true
102
77
  else
103
78
  @errors[:country_code] = "'#{country_code}' is not a valid " \
@@ -107,7 +82,7 @@ module Ibandit
107
82
  end
108
83
 
109
84
  def valid_check_digits?
110
- return unless valid_country_code? && valid_characters?
85
+ return unless decomposable? && valid_characters?
111
86
 
112
87
  expected_check_digits = CheckDigit.iban(country_code, bban)
113
88
  if check_digits == expected_check_digits
@@ -121,9 +96,9 @@ module Ibandit
121
96
  end
122
97
 
123
98
  def valid_length?
124
- return unless valid_country_code?
99
+ return unless valid_country_code? && !iban.nil?
125
100
 
126
- if iban.size == structure[:total_length]
101
+ if iban.length == structure[:total_length]
127
102
  true
128
103
  else
129
104
  @errors[:length] = "Length doesn't match SWIFT specification " \
@@ -133,7 +108,54 @@ module Ibandit
133
108
  end
134
109
  end
135
110
 
111
+ def valid_bank_code_length?
112
+ return unless valid_country_code?
113
+
114
+ if bank_code.nil? || bank_code.length == 0
115
+ @errors[:bank_code] = 'is required'
116
+ return false
117
+ end
118
+
119
+ return true if bank_code.length == structure[:bank_code_length]
120
+
121
+ @errors[:bank_code] = 'is the wrong length (should be ' \
122
+ "#{structure[:bank_code_length]} characters)"
123
+ false
124
+ end
125
+
126
+ def valid_branch_code_length?
127
+ return unless valid_country_code?
128
+ return true if branch_code.to_s.length == structure[:branch_code_length]
129
+
130
+ if structure[:branch_code_length] == 0
131
+ @errors[:branch_code] = "is not used in #{country_code}"
132
+ elsif branch_code.nil? || branch_code.length == 0
133
+ @errors[:branch_code] = 'is required'
134
+ else
135
+ @errors[:branch_code] = 'is the wrong length (should be ' \
136
+ "#{structure[:branch_code_length]} characters)"
137
+ end
138
+ false
139
+ end
140
+
141
+ def valid_account_number_length?
142
+ return unless valid_country_code?
143
+
144
+ if account_number.nil?
145
+ @errors[:account_number] = 'is required'
146
+ return false
147
+ end
148
+
149
+ return true if account_number.length == structure[:account_number_length]
150
+
151
+ @errors[:account_number] = 'is the wrong length (should be ' \
152
+ "#{structure[:account_number_length]} " \
153
+ 'characters)'
154
+ false
155
+ end
156
+
136
157
  def valid_characters?
158
+ return if iban.nil?
137
159
  if iban.scan(/[^A-Z0-9]/).any?
138
160
  @errors[:characters] = 'Non-alphanumeric characters found: ' \
139
161
  "#{iban.scan(/[^A-Z\d]/).join(' ')}"
@@ -154,18 +176,83 @@ module Ibandit
154
176
  end
155
177
  end
156
178
 
179
+ def valid_bank_code_format?
180
+ return unless valid_bank_code_length?
181
+
182
+ if bank_code =~ Regexp.new(structure[:bank_code_format])
183
+ true
184
+ else
185
+ @errors[:bank_code] = 'is invalid'
186
+ false
187
+ end
188
+ end
189
+
190
+ def valid_branch_code_format?
191
+ return unless valid_branch_code_length?
192
+ return true unless structure[:branch_code_format]
193
+
194
+ if branch_code =~ Regexp.new(structure[:branch_code_format])
195
+ true
196
+ else
197
+ @errors[:branch_code] = 'is invalid'
198
+ false
199
+ end
200
+ end
201
+
202
+ def valid_account_number_format?
203
+ return unless valid_account_number_length?
204
+
205
+ if account_number =~ Regexp.new(structure[:account_number_format])
206
+ true
207
+ else
208
+ @errors[:account_number] = 'is invalid'
209
+ false
210
+ end
211
+ end
212
+
157
213
  ###################
158
214
  # Private methods #
159
215
  ###################
160
216
 
161
217
  private
162
218
 
219
+ def decomposable?
220
+ [iban, country_code, bank_code, account_number].none?(&:nil?)
221
+ end
222
+
223
+ def build_iban_from_local_details(details_hash)
224
+ local_details = LocalDetailsCleaner.clean(details_hash)
225
+
226
+ @country_code = try_dup(local_details[:country_code])
227
+ @account_number = try_dup(local_details[:account_number])
228
+ @branch_code = try_dup(local_details[:branch_code])
229
+ @bank_code = try_dup(local_details[:bank_code])
230
+ @iban = IBANAssembler.assemble(local_details)
231
+ @check_digits = @iban.slice(2, 2) unless @iban.nil?
232
+ end
233
+
234
+ def extract_local_details_from_iban!
235
+ local_details = IBANSplitter.split(@iban)
236
+
237
+ @country_code = local_details[:country_code]
238
+ @check_digits = local_details[:check_digits]
239
+ @bank_code = local_details[:bank_code]
240
+ @branch_code = local_details[:branch_code]
241
+ @account_number = local_details[:account_number]
242
+ end
243
+
244
+ def try_dup(object)
245
+ object.dup
246
+ rescue TypeError
247
+ object
248
+ end
249
+
163
250
  def structure
164
- IBAN.structures[country_code]
251
+ Ibandit.structures[country_code]
165
252
  end
166
253
 
167
254
  def formatted
168
- iban.gsub(/(.{4})/, '\1 ').strip
255
+ iban.to_s.gsub(/(.{4})/, '\1 ').strip
169
256
  end
170
257
  end
171
258
  end
@@ -0,0 +1,117 @@
1
+ module Ibandit
2
+ module IBANAssembler
3
+ SUPPORTED_COUNTRY_CODES = %w(AT BE CY DE EE ES FI FR GB IE IT LT LU LV MC NL
4
+ PT SI SK SM).freeze
5
+
6
+ EXCEPTION_COUNTRY_CODES = %w(IT SM BE).freeze
7
+
8
+ def self.assemble(local_details)
9
+ country_code = local_details[:country_code]
10
+
11
+ return unless can_assemble?(local_details)
12
+
13
+ bban =
14
+ if EXCEPTION_COUNTRY_CODES.include?(country_code)
15
+ public_send(:"assemble_#{country_code.downcase}_bban", local_details)
16
+ else
17
+ assemble_general_bban(local_details)
18
+ end
19
+
20
+ assemble_iban(country_code, bban)
21
+ end
22
+
23
+ ##############################
24
+ # General case BBAN creation #
25
+ ##############################
26
+
27
+ def self.assemble_general_bban(opts)
28
+ [opts[:bank_code], opts[:branch_code], opts[:account_number]].join
29
+ end
30
+
31
+ ##################################
32
+ # Country-specific BBAN creation #
33
+ ##################################
34
+
35
+ def self.assemble_be_bban(opts)
36
+ # The first three digits of Belgian account numbers are the bank_code,
37
+ # but the account number is not considered complete without these three
38
+ # numbers and the IBAN structure file includes them in its definition of
39
+ # the account number. As a result, this method ignores all arguments
40
+ # other than the account number.
41
+ opts[:account_number]
42
+ end
43
+
44
+ def self.assemble_it_bban(opts)
45
+ # The Italian check digit is NOT included in the any of the other SWIFT
46
+ # elements, so should be passed explicitly or left blank for it to be
47
+ # calculated implicitly
48
+ partial_bban = [
49
+ opts[:bank_code],
50
+ opts[:branch_code],
51
+ opts[:account_number]
52
+ ].join
53
+
54
+ check_digit = opts[:check_digit] || CheckDigit.italian(partial_bban)
55
+
56
+ [check_digit, partial_bban].join
57
+ end
58
+
59
+ def self.assemble_sm_bban(opts)
60
+ # San Marino uses the same BBAN construction method as Italy
61
+ assemble_it_bban(opts)
62
+ end
63
+
64
+ ##################
65
+ # Helper methods #
66
+ ##################
67
+
68
+ def self.can_assemble?(local_details)
69
+ SUPPORTED_COUNTRY_CODES.include?(local_details[:country_code]) &&
70
+ valid_arguments?(local_details)
71
+ end
72
+
73
+ def self.valid_arguments?(local_details)
74
+ country_code = local_details[:country_code]
75
+
76
+ supplied = local_details.keys.select { |key| local_details[key] }
77
+ supplied.delete(:country_code)
78
+
79
+ allowed = allowed_fields(country_code)
80
+
81
+ required_fields(country_code).all? { |key| supplied.include?(key) } &&
82
+ supplied.all? { |key| allowed.include?(key) }
83
+ end
84
+
85
+ def self.required_fields(country_code)
86
+ case country_code
87
+ when 'AT', 'CY', 'DE', 'EE', 'FI', 'LT', 'LU', 'LV', 'NL', 'SI', 'SK'
88
+ %i(bank_code account_number)
89
+ when 'BE'
90
+ %i(account_number)
91
+ else
92
+ %i(bank_code branch_code account_number)
93
+ end
94
+ end
95
+
96
+ def self.allowed_fields(country_code)
97
+ # Some countries have additional optional fields
98
+ case country_code
99
+ when 'BE' then %i(bank_code account_number)
100
+ when 'CY' then %i(bank_code branch_code account_number)
101
+ when 'IT' then %i(bank_code branch_code account_number check_digit)
102
+ when 'SK' then %i(bank_code account_number account_number_prefix)
103
+ else required_fields(country_code)
104
+ end
105
+ end
106
+
107
+ def self.assemble_iban(country_code, bban)
108
+ [
109
+ country_code,
110
+ CheckDigit.iban(country_code, bban),
111
+ bban
112
+ ].join
113
+ rescue InvalidCharacterError
114
+ nil
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,66 @@
1
+ module Ibandit
2
+ module IBANSplitter
3
+ def self.split(iban)
4
+ {
5
+ country_code: country_code_from(iban),
6
+ check_digits: check_digits_from(iban),
7
+ bank_code: bank_code_from(iban),
8
+ branch_code: branch_code_from(iban),
9
+ account_number: account_number_from(iban)
10
+ }
11
+ end
12
+
13
+ ###################
14
+ # Component parts #
15
+ ###################
16
+
17
+ def self.country_code_from(iban)
18
+ return if iban.nil? || iban.empty?
19
+
20
+ iban.slice(0, 2)
21
+ end
22
+
23
+ def self.check_digits_from(iban)
24
+ return unless decomposable?(iban)
25
+
26
+ iban.slice(2, 2)
27
+ end
28
+
29
+ def self.bank_code_from(iban)
30
+ return unless decomposable?(iban)
31
+
32
+ iban.slice(
33
+ structure(iban)[:bank_code_position] - 1,
34
+ structure(iban)[:bank_code_length]
35
+ )
36
+ end
37
+
38
+ def self.branch_code_from(iban)
39
+ unless decomposable?(iban) && structure(iban)[:branch_code_length] > 0
40
+ return
41
+ end
42
+
43
+ iban.slice(
44
+ structure(iban)[:branch_code_position] - 1,
45
+ structure(iban)[:branch_code_length]
46
+ )
47
+ end
48
+
49
+ def self.account_number_from(iban)
50
+ return unless decomposable?(iban)
51
+
52
+ iban.slice(
53
+ structure(iban)[:account_number_position] - 1,
54
+ structure(iban)[:account_number_length]
55
+ )
56
+ end
57
+
58
+ def self.decomposable?(iban)
59
+ structure(iban) && iban.length == structure(iban)[:total_length]
60
+ end
61
+
62
+ def self.structure(iban)
63
+ Ibandit.structures[country_code_from(iban)]
64
+ end
65
+ end
66
+ end