ibandit 0.2.1 → 0.3.0
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 +4 -4
- data/CHANGELOG.md +10 -3
- data/README.md +3 -0
- data/bin/build_structure_file.rb +41 -5
- data/data/raw/structure_additions.yml +43 -0
- data/data/structures.yml +157 -0
- data/ibandit.gemspec +1 -1
- data/lib/ibandit/iban.rb +147 -60
- data/lib/ibandit/iban_assembler.rb +117 -0
- data/lib/ibandit/iban_splitter.rb +66 -0
- data/lib/ibandit/local_details_cleaner.rb +298 -0
- data/lib/ibandit/version.rb +1 -1
- data/lib/ibandit.rb +9 -1
- data/spec/ibandit/iban_assembler_spec.rb +567 -0
- data/spec/ibandit/iban_spec.rb +294 -13
- data/spec/ibandit/iban_splitter_spec.rb +41 -0
- data/spec/ibandit/local_details_cleaner_spec.rb +688 -0
- metadata +10 -6
- data/lib/ibandit/iban_builder.rb +0 -539
- data/spec/ibandit/iban_builder_spec.rb +0 -892
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
|
-
|
7
|
-
|
8
|
-
def
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
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
|
34
|
+
return unless decomposable?
|
68
35
|
|
69
|
-
national_id =
|
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
|
42
|
+
return unless decomposable? && structure[:local_check_digit_position]
|
75
43
|
|
76
|
-
iban.slice(
|
77
|
-
|
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
|
-
|
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
|
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
|
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.
|
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
|
-
|
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
|