konto_check_ruby 1.0.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 +7 -0
- data/CHANGELOG.md +26 -0
- data/COPYRIGHT +22 -0
- data/LICENSE +504 -0
- data/README.md +295 -0
- data/bin/konto_check_ruby +202 -0
- data/data/README.md +17 -0
- data/data/blz.lut2f +0 -0
- data/data/iban_regeln.txt +1260 -0
- data/lib/konto_check_ruby/bank_data.rb +421 -0
- data/lib/konto_check_ruby/blz_file.rb +705 -0
- data/lib/konto_check_ruby/check_methods/part1.rb +1634 -0
- data/lib/konto_check_ruby/check_methods/part2.rb +1604 -0
- data/lib/konto_check_ruby/check_methods/part3.rb +1905 -0
- data/lib/konto_check_ruby/check_methods/part4.rb +1738 -0
- data/lib/konto_check_ruby/check_methods/part5.rb +1407 -0
- data/lib/konto_check_ruby/check_methods.rb +147 -0
- data/lib/konto_check_ruby/collation.rb +52 -0
- data/lib/konto_check_ruby/engine.rb +1006 -0
- data/lib/konto_check_ruby/iban_rules.rb +1060 -0
- data/lib/konto_check_ruby/konto_check.rb +1578 -0
- data/lib/konto_check_ruby/lut_file.rb +457 -0
- data/lib/konto_check_ruby/raw.rb +614 -0
- data/lib/konto_check_ruby/retvals.rb +826 -0
- data/lib/konto_check_ruby/search.rb +440 -0
- data/lib/konto_check_ruby/update.rb +176 -0
- data/lib/konto_check_ruby/version.rb +18 -0
- data/lib/konto_check_ruby.rb +42 -0
- metadata +112 -0
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file is part of konto_check_ruby, a Ruby port of the C library
|
|
4
|
+
# konto_check, Copyright (C) 2002-2023 Michael Plugge <konto_check@yahoo.com>.
|
|
5
|
+
# Ruby port Copyright (C) 2026 tickettoaster GmbH <https://tickettoaster.de>.
|
|
6
|
+
#
|
|
7
|
+
# konto_check_ruby is free software; you can redistribute it and/or modify it
|
|
8
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
9
|
+
# the Free Software Foundation; either version 2.1 of the License, or (at
|
|
10
|
+
# your option) any later version. It is distributed WITHOUT ANY WARRANTY; see
|
|
11
|
+
# the file LICENSE for details.
|
|
12
|
+
|
|
13
|
+
require_relative "retvals"
|
|
14
|
+
require_relative "lut_file"
|
|
15
|
+
require_relative "blz_file"
|
|
16
|
+
require_relative "bank_data"
|
|
17
|
+
require_relative "check_methods"
|
|
18
|
+
require_relative "collation"
|
|
19
|
+
require_relative "iban_rules"
|
|
20
|
+
require_relative "search"
|
|
21
|
+
require_relative "update"
|
|
22
|
+
|
|
23
|
+
module KontoCheckRuby
|
|
24
|
+
# The Engine holds one loaded bank directory (BankData) and implements the
|
|
25
|
+
# functions of the C library konto_check on top of it: account number
|
|
26
|
+
# checks, IBAN generation/validation, lookups and searches.
|
|
27
|
+
#
|
|
28
|
+
# Method names and return conventions follow the C library closely; the
|
|
29
|
+
# modules KontoCheckRaw and KontoCheck (see konto_check_ruby.rb) provide the
|
|
30
|
+
# interface of the original Ruby gem on top of a default Engine instance.
|
|
31
|
+
#
|
|
32
|
+
# Functions that return a value plus a status return [value, status]
|
|
33
|
+
# (the C functions take an `int *retval` parameter for the status).
|
|
34
|
+
class Engine
|
|
35
|
+
include IbanRules
|
|
36
|
+
include Search
|
|
37
|
+
|
|
38
|
+
DEFAULT_INIT_LEVEL = 5
|
|
39
|
+
# The LUT file shipped with the gem; used whenever no file is given
|
|
40
|
+
# explicitly (the C library searches ./blz.lut2f, /etc/blz.lut2f ...
|
|
41
|
+
# instead).
|
|
42
|
+
BUNDLED_LUT = File.expand_path("../../data/blz.lut2f", __dir__)
|
|
43
|
+
EMPTY_BIC = BankData::EMPTY_BIC
|
|
44
|
+
|
|
45
|
+
# Date of the check digit methods / IBAN rules implemented (from konto_check 6.15)
|
|
46
|
+
PZ_METHODS_DATE = "09.12.2019"
|
|
47
|
+
IBAN_RULES_DATE = "09.09.2019"
|
|
48
|
+
C_LIBRARY_DATE = "13. April 2023"
|
|
49
|
+
|
|
50
|
+
attr_reader :data
|
|
51
|
+
# Fixed "current date" (Integer JJJJMMTT) for validity tests, nil = today
|
|
52
|
+
attr_accessor :current_date
|
|
53
|
+
|
|
54
|
+
def initialize
|
|
55
|
+
@data = nil
|
|
56
|
+
@encoding = 2 # UTF-8
|
|
57
|
+
@pz_aenderungen_2019_12 = true
|
|
58
|
+
@extra_init_done = 0
|
|
59
|
+
@current_date = nil
|
|
60
|
+
@level = -1
|
|
61
|
+
reset_search_cache
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------ initialisation
|
|
65
|
+
|
|
66
|
+
def initialized?
|
|
67
|
+
!@data.nil?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Loads a LUT file (lut_init in C). Without a file name the LUT file
|
|
71
|
+
# bundled with the gem (BUNDLED_LUT) is loaded. level 0..9 selects the
|
|
72
|
+
# blocks to load (see BlzFile::LUT_SETS), set 0 = automatically choose
|
|
73
|
+
# the valid data set.
|
|
74
|
+
# If the same file (by file id) is already loaded, only missing blocks are
|
|
75
|
+
# loaded incrementally. Returns OK, LUT2_PARTIAL_OK, ... or an error code.
|
|
76
|
+
def init(lut_name = nil, level = DEFAULT_INIT_LEVEL, set = 0)
|
|
77
|
+
level = DEFAULT_INIT_LEVEL if level.nil?
|
|
78
|
+
set = 0 if set.nil?
|
|
79
|
+
lut_name = BUNDLED_LUT if lut_name.nil? || lut_name.to_s.empty?
|
|
80
|
+
return NO_LUT_FILE unless File.file?(lut_name)
|
|
81
|
+
required = required_blocks(level)
|
|
82
|
+
if @data && @data.source == lut_name && !@data.lut_id.empty?
|
|
83
|
+
id = file_id_of(lut_name, set)
|
|
84
|
+
if id == @data.lut_id && (set == 0 || set == @data.set)
|
|
85
|
+
return OK if level <= @level
|
|
86
|
+
code = @data.load_blocks(required)
|
|
87
|
+
@level = level if code > 0 || code == LUT2_PARTIAL_OK
|
|
88
|
+
@extra_init_done = 0
|
|
89
|
+
return code
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
# (unlike the C library the previously loaded data is kept if the new
|
|
93
|
+
# file cannot be loaded)
|
|
94
|
+
code, data = BankData.from_lut(lut_name, required: required, set: set, current_date: today)
|
|
95
|
+
if code > 0 || [LUT2_PARTIAL_OK, LUT2_NO_LONGER_VALID_PARTIAL_OK, LUT2_NOT_YET_VALID_PARTIAL_OK].include?(code)
|
|
96
|
+
free
|
|
97
|
+
@data = data
|
|
98
|
+
@data.level = level
|
|
99
|
+
@level = level
|
|
100
|
+
end
|
|
101
|
+
code
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Loads the bank directory directly from a Bundesbank file (TXT fixed
|
|
105
|
+
# width, CSV or XML; no LUT file needed).
|
|
106
|
+
# gueltigkeit validity "JJJJMMTT-JJJJMMTT" (optional; XML files carry
|
|
107
|
+
# their validity themselves)
|
|
108
|
+
# iban_rules :default (built-in table of konto_check 6.15) applies
|
|
109
|
+
# IBAN rules to files without the IBAN rule column, false
|
|
110
|
+
# disables that, a Hash {blz => rule*100+version} or a
|
|
111
|
+
# file name supplies own rules
|
|
112
|
+
def load_blz_file(filename, gueltigkeit: nil, iban_rules: :default)
|
|
113
|
+
code, records, file_format, v1, v2 = BlzFile.parse_any(filename)
|
|
114
|
+
return code unless code == OK
|
|
115
|
+
if gueltigkeit && !gueltigkeit.empty?
|
|
116
|
+
return LUT2_INVALID_GUELTIGKEIT unless gueltigkeit =~ /\A(\d{8})[ -](\d{8})\z/
|
|
117
|
+
v1 = Regexp.last_match(1).to_i
|
|
118
|
+
v2 = Regexp.last_match(2).to_i
|
|
119
|
+
return LUT2_GUELTIGKEIT_SWAPPED if v2 < v1
|
|
120
|
+
end
|
|
121
|
+
if file_format != 2 && (table = BlzFile.iban_rule_table(iban_rules))
|
|
122
|
+
BlzFile.apply_iban_rules!(records, table)
|
|
123
|
+
end
|
|
124
|
+
load_records(records, source: filename, valid_from: v1, valid_to: v2)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Loads the current Bundesbank file from the local cache (see Update),
|
|
128
|
+
# downloading it first if no cached file is valid for today (or for
|
|
129
|
+
# current_date). If the download fails, the newest cached file is used,
|
|
130
|
+
# and if there is none, the bundled LUT file. Returns the load status
|
|
131
|
+
# (OK ...) or a negative code; the reason of a failed download is
|
|
132
|
+
# available in #last_update_error.
|
|
133
|
+
# dir: cache directory (default Update.cache_dir)
|
|
134
|
+
# format: :xml (default), :txt or :csv
|
|
135
|
+
# refresh: :auto (download only if nothing valid is cached, default),
|
|
136
|
+
# :always (check the download page every time), :never
|
|
137
|
+
def load_current(dir: Update.cache_dir, format: :xml, refresh: :auto, iban_rules: :default)
|
|
138
|
+
@last_update_error = nil
|
|
139
|
+
file = refresh == :always ? nil : Update.cached_file(dir: dir, current_date: today)
|
|
140
|
+
if file.nil? && refresh != :never
|
|
141
|
+
begin
|
|
142
|
+
file = Update.update(dir: dir, format: format).path
|
|
143
|
+
rescue Update::Error => e
|
|
144
|
+
@last_update_error = e.message
|
|
145
|
+
file = nil
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
file ||= Update.cached_file(dir: dir)
|
|
149
|
+
return init(BUNDLED_LUT, 9) if file.nil?
|
|
150
|
+
load_blz_file(file, iban_rules: iban_rules)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Message of the last failed download in load_current, or nil
|
|
154
|
+
attr_reader :last_update_error
|
|
155
|
+
|
|
156
|
+
# Loads the bank directory from an Array of BlzFile::BankRecord
|
|
157
|
+
def load_records(records, source: nil, valid_from: 0, valid_to: 0)
|
|
158
|
+
free
|
|
159
|
+
@data = BankData.from_records(records, source: source, valid_from: valid_from, valid_to: valid_to)
|
|
160
|
+
@data.level = 9
|
|
161
|
+
@level = 9
|
|
162
|
+
OK
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Releases the loaded data (lut_cleanup in C)
|
|
166
|
+
def free
|
|
167
|
+
@data = nil
|
|
168
|
+
@level = -1
|
|
169
|
+
@extra_init_done = 0
|
|
170
|
+
reset_search_cache
|
|
171
|
+
OK
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# [filename, set, level, retval]
|
|
175
|
+
def current_lutfile_name
|
|
176
|
+
return [nil, 0, -1, LUT2_NOT_INITIALIZED] unless @data
|
|
177
|
+
[@data.source, @data.set, @level, OK]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Validity of the loaded data set for the current date
|
|
181
|
+
def lut_valid
|
|
182
|
+
return LUT2_NOT_INITIALIZED unless @data
|
|
183
|
+
@data.valid(today)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Info blocks: [code, info1, info2, valid1, valid2]. Without a file name
|
|
187
|
+
# the info of the loaded data set is returned.
|
|
188
|
+
def lut_info(lut_name = nil)
|
|
189
|
+
if lut_name.nil? || lut_name.empty?
|
|
190
|
+
return [LUT2_NOT_INITIALIZED, nil, nil, LUT2_NOT_INITIALIZED, LUT2_NOT_INITIALIZED] unless @data
|
|
191
|
+
return [OK, @data.info_text, nil, @data.valid(today), LUT2_BLOCK_NOT_IN_FILE]
|
|
192
|
+
end
|
|
193
|
+
LutFile.info(lut_name, today)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Status of the loaded blocks: [code, filename, blocks_ok, blocks_failed]
|
|
197
|
+
def lut_blocks(mode = 1)
|
|
198
|
+
return [LUT2_NOT_INITIALIZED, nil, nil, nil] unless @data
|
|
199
|
+
ok = []
|
|
200
|
+
fail = []
|
|
201
|
+
offset = @data.set == 2 ? LutFile::SET_OFFSET : 0
|
|
202
|
+
(1...LutFile::SET_OFFSET).each do |t|
|
|
203
|
+
st = @data.block_status[t + offset] || @data.block_status[t]
|
|
204
|
+
next if st.nil?
|
|
205
|
+
name = LutFile.block_name(t + offset, mode)
|
|
206
|
+
if st == OK
|
|
207
|
+
ok << name
|
|
208
|
+
else
|
|
209
|
+
fail << name
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
[fail.empty? ? OK : LUT2_BLOCKS_MISSING, @data.source, ok.join(", "), fail.join(", ")]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def dump_lutfile(lut_name)
|
|
216
|
+
LutFile.dump(lut_name)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# generate_lut2_p in C
|
|
220
|
+
def generate_lutfile(input, output, user_info = "", gueltigkeit = nil, felder = 9, filialen = false, set = 0)
|
|
221
|
+
BlzFile.generate_lut(input, output, user_info: user_info.to_s, gueltigkeit: gueltigkeit,
|
|
222
|
+
felder: felder.to_i, filialen: filialen.to_i != 0, set: set.to_i)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Rebuilds a Bundesbank text file from the loaded data (rebuild_blzfile in C).
|
|
226
|
+
# set 0: input is a Bundesbank file, 1/2: input is a LUT file (set 1/2).
|
|
227
|
+
def rebuild_blzfile(input, output, set = 0)
|
|
228
|
+
set = set.to_i
|
|
229
|
+
if set == 0
|
|
230
|
+
code = load_blz_file(input)
|
|
231
|
+
else
|
|
232
|
+
code = init(input, 9, set > 2 ? 2 : set)
|
|
233
|
+
end
|
|
234
|
+
return code if code <= 0 && code != LUT2_PARTIAL_OK
|
|
235
|
+
d = @data
|
|
236
|
+
File.open(output, "wb") do |out|
|
|
237
|
+
d.cnt_hs.times do |i|
|
|
238
|
+
s = d.startidx[i]
|
|
239
|
+
d.filialen_at(i).times do |k|
|
|
240
|
+
j = s + k
|
|
241
|
+
regel = d.iban_regel ? d.iban_regel[j] : nil
|
|
242
|
+
pan = (d.pan && d.pan[j] != 0) ? format("%05d", d.pan[j]) : ""
|
|
243
|
+
nr = (d.nr && d.nr[j] != 0) ? format("%06d", d.nr[j]) : ""
|
|
244
|
+
line = format("%8d%1s%-58s%05d%-35s%-27s%5s%-11s%2s%6s%1s%1s%08d",
|
|
245
|
+
d.blz[i], k == 0 ? "1" : "2", d.name ? d.name[j] : "", d.plz ? d.plz[j] : 0,
|
|
246
|
+
d.ort ? d.ort[j] : "", d.name_kurz ? d.name_kurz[j] : "", pan,
|
|
247
|
+
d.bic ? d.bic[j] : "", BlzFile.pz_to_string(d.pz[i]), nr,
|
|
248
|
+
d.aenderung ? d.aenderung[j] : " ", d.loeschung ? d.loeschung[j] : " ",
|
|
249
|
+
d.nachfolge_blz ? d.nachfolge_blz[j] : 0)
|
|
250
|
+
line += format("%06d", regel) if regel
|
|
251
|
+
out.write(line.encode("ISO-8859-1", undef: :replace) + "\n")
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
OK
|
|
256
|
+
rescue SystemCallError
|
|
257
|
+
FILE_WRITE_ERROR
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# ------------------------------------------------------------- BLZ lookup
|
|
261
|
+
|
|
262
|
+
# Index of a BLZ (String) in the main office arrays or a negative error
|
|
263
|
+
# code (lut_index in C). Leading blanks/tabs are skipped, exactly 8 digits
|
|
264
|
+
# followed by end of string, blank or tab are required.
|
|
265
|
+
def lut_index(blz)
|
|
266
|
+
return LUT2_NOT_INITIALIZED unless @data
|
|
267
|
+
s = blz.to_s.sub(/\A[ \t]+/, "")
|
|
268
|
+
m = s.match(/\A(\d{8})(?:[ \t]|\z)/)
|
|
269
|
+
return INVALID_BLZ_LENGTH unless m
|
|
270
|
+
@data.index(m[1].to_i) || INVALID_BLZ
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def lut_index_i(blz_int)
|
|
274
|
+
return LUT2_NOT_INITIALIZED unless @data
|
|
275
|
+
return INVALID_BLZ_LENGTH if blz_int < 10_000_000 || blz_int > 99_999_999
|
|
276
|
+
@data.index(blz_int) || INVALID_BLZ
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Tests whether a BLZ (and branch) exists (lut_blz in C)
|
|
280
|
+
def lut_blz(blz, zweigstelle = 0)
|
|
281
|
+
return LUT2_BLZ_NOT_INITIALIZED unless @data
|
|
282
|
+
idx = lut_index(blz)
|
|
283
|
+
return idx if idx < 0
|
|
284
|
+
return LUT2_INDEX_OUT_OF_RANGE unless branch_ok?(idx, zweigstelle)
|
|
285
|
+
OK
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# [count, retval]
|
|
289
|
+
def lut_filialen(blz)
|
|
290
|
+
return [0, LUT2_BLZ_NOT_INITIALIZED] unless @data
|
|
291
|
+
return [0, LUT2_FILIALEN_NOT_INITIALIZED] unless @data.filialen
|
|
292
|
+
idx = lut_index(blz)
|
|
293
|
+
return [0, idx] if idx < 0
|
|
294
|
+
[@data.filialen[idx], OK]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def lut_name(blz, zweigstelle = 0)
|
|
298
|
+
field_s(:name, LUT2_NAME_NOT_INITIALIZED, blz, zweigstelle)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def lut_name_kurz(blz, zweigstelle = 0)
|
|
302
|
+
field_s(:name_kurz, LUT2_NAME_KURZ_NOT_INITIALIZED, blz, zweigstelle)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def lut_ort(blz, zweigstelle = 0)
|
|
306
|
+
field_s(:ort, LUT2_ORT_NOT_INITIALIZED, blz, zweigstelle)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def lut_plz(blz, zweigstelle = 0)
|
|
310
|
+
field_i(:plz, LUT2_PLZ_NOT_INITIALIZED, blz, zweigstelle)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def lut_pan(blz, zweigstelle = 0)
|
|
314
|
+
field_i(:pan, LUT2_PAN_NOT_INITIALIZED, blz, zweigstelle)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def lut_nr(blz, zweigstelle = 0)
|
|
318
|
+
field_i(:nr, LUT2_NR_NOT_INITIALIZED, blz, zweigstelle)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def lut_nachfolge_blz(blz, zweigstelle = 0)
|
|
322
|
+
field_i(:nachfolge_blz, LUT2_NACHFOLGE_BLZ_NOT_INITIALIZED, blz, zweigstelle)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# [character "A"/"D"/"M"/"U", retval]
|
|
326
|
+
def lut_aenderung(blz, zweigstelle = 0)
|
|
327
|
+
v, r = field_s(:aenderung, LUT2_AENDERUNG_NOT_INITIALIZED, blz, zweigstelle)
|
|
328
|
+
[v.nil? || v.empty? ? nil : v, r]
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# [0/1, retval]
|
|
332
|
+
def lut_loeschung(blz, zweigstelle = 0)
|
|
333
|
+
v, r = field_s(:loeschung, LUT2_LOESCHUNG_NOT_INITIALIZED, blz, zweigstelle)
|
|
334
|
+
[v.nil? || v.empty? ? nil : v.to_i, r]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# [check method (Integer), retval]
|
|
338
|
+
def lut_pz(blz, zweigstelle = 0)
|
|
339
|
+
return [0, LUT2_BLZ_NOT_INITIALIZED] unless @data
|
|
340
|
+
return [0, LUT2_PZ_NOT_INITIALIZED] unless @data.pz
|
|
341
|
+
idx = lut_index(blz)
|
|
342
|
+
return [0, idx] if idx < 0
|
|
343
|
+
return [0, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
|
|
344
|
+
[@data.pz[idx], OK]
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# [rule*100+version, retval]. Leading "@" or "+" in the BLZ are ignored.
|
|
348
|
+
def lut_iban_regel(blz, zweigstelle = 0)
|
|
349
|
+
ret = iban_init
|
|
350
|
+
return [0, ret] if ret < OK
|
|
351
|
+
return [0, LUT2_IBAN_REGEL_NOT_INITIALIZED] unless @data.iban_regel
|
|
352
|
+
b = blz.to_s.sub(/\A[@+]+/, "")
|
|
353
|
+
idx = lut_index(b)
|
|
354
|
+
return [0, idx] if idx < 0
|
|
355
|
+
return [0, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
|
|
356
|
+
[@data.iban_regel[@data.startidx[idx] + zweigstelle], OK]
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def lut_iban_regel_i(blz_int, zweigstelle = 0)
|
|
360
|
+
return [0, LUT2_NOT_INITIALIZED] unless @data
|
|
361
|
+
return [0, LUT2_IBAN_REGEL_NOT_INITIALIZED] unless @data.iban_regel
|
|
362
|
+
idx = lut_index_i(blz_int)
|
|
363
|
+
return [0, idx] if idx < 0
|
|
364
|
+
return [0, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
|
|
365
|
+
[@data.iban_regel[@data.startidx[idx] + zweigstelle], OK]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# BIC of a bank: [bic, retval]. If the BLZ has a successor BLZ, the BIC of
|
|
369
|
+
# the successor is returned (retval OK_NACHFOLGE_BLZ_USED) unless the BLZ
|
|
370
|
+
# starts with "!". retval is OK_INVALID_FOR_IBAN if an IBAN rule replaces
|
|
371
|
+
# the BIC, OK_HYPO_REQUIRES_KTO for the rules 31..35.
|
|
372
|
+
def lut_bic(blz, zweigstelle = 0)
|
|
373
|
+
ret = iban_init
|
|
374
|
+
return [nil, ret] if ret < OK
|
|
375
|
+
bic, retval = lut_bic_int(blz, zweigstelle)
|
|
376
|
+
regel, ret = lut_iban_regel(blz, 0)
|
|
377
|
+
# like the C library the rule check is done even if the lookup failed
|
|
378
|
+
# (the C function returns "" instead of NULL then)
|
|
379
|
+
if ret == OK
|
|
380
|
+
# The C library compares the raw value (rule * 100 + version) with
|
|
381
|
+
# 31..35, so OK_HYPO_REQUIRES_KTO is practically never returned; this
|
|
382
|
+
# is reproduced for compatibility.
|
|
383
|
+
if regel >= 31 && regel <= 35
|
|
384
|
+
retval = OK_HYPO_REQUIRES_KTO
|
|
385
|
+
else
|
|
386
|
+
b2 = blz.to_s.sub(/\A[!@+]+/, "").dup
|
|
387
|
+
k2 = +"0000000000"
|
|
388
|
+
_r, bic_neu = iban_regel_cvt(b2, k2, regel, nil)
|
|
389
|
+
retval = OK_INVALID_FOR_IBAN if bic_neu && (bic || "").casecmp(bic_neu) != 0
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
[bic, retval]
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# BIC from the LUT data without IBAN rule check (lut_bic_int in C)
|
|
396
|
+
def lut_bic_int(blz, zweigstelle = 0)
|
|
397
|
+
return [nil, LUT2_BLZ_NOT_INITIALIZED] unless @data
|
|
398
|
+
return [nil, LUT2_BIC_NOT_INITIALIZED] unless @data.bic
|
|
399
|
+
b = blz.to_s
|
|
400
|
+
force_old = false
|
|
401
|
+
if b.start_with?("!")
|
|
402
|
+
b = b[1..]
|
|
403
|
+
force_old = true
|
|
404
|
+
end
|
|
405
|
+
idx = lut_index(b)
|
|
406
|
+
return [nil, idx] if idx < 0
|
|
407
|
+
return [nil, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
|
|
408
|
+
return [nil, LUT2_NACHFOLGE_BLZ_NOT_INITIALIZED] if @data.nachfolge_blz.nil? && !force_old
|
|
409
|
+
retval = OK
|
|
410
|
+
if !force_old && @data.nachfolge_blz && (nb = @data.nachfolge_blz[@data.startidx[idx]]) != 0
|
|
411
|
+
idx2 = lut_index_i(nb)
|
|
412
|
+
return [nil, idx2] if idx2 < 0
|
|
413
|
+
idx = idx2
|
|
414
|
+
retval = OK_NACHFOLGE_BLZ_USED
|
|
415
|
+
end
|
|
416
|
+
[@data.bic[@data.startidx[idx] + zweigstelle], retval]
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# BIC of the main office (lut_bic_h in C)
|
|
420
|
+
def lut_bic_h(blz, zweigstelle = 0)
|
|
421
|
+
return [nil, LUT2_BLZ_NOT_INITIALIZED] unless @data
|
|
422
|
+
return [nil, LUT2_BIC_NOT_INITIALIZED] unless @data.bic_h
|
|
423
|
+
b = blz.to_s
|
|
424
|
+
force_old = false
|
|
425
|
+
if b.start_with?("!")
|
|
426
|
+
b = b[1..]
|
|
427
|
+
force_old = true
|
|
428
|
+
end
|
|
429
|
+
idx = lut_index(b)
|
|
430
|
+
return [nil, idx] if idx < 0
|
|
431
|
+
return [nil, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
|
|
432
|
+
return [nil, LUT2_NACHFOLGE_BLZ_NOT_INITIALIZED] if @data.nachfolge_blz.nil? && !force_old
|
|
433
|
+
retval = OK
|
|
434
|
+
if !force_old && @data.nachfolge_blz && (nb = @data.nachfolge_blz[@data.startidx[idx]]) != 0
|
|
435
|
+
idx2 = lut_index_i(nb)
|
|
436
|
+
return [nil, idx2] if idx2 < 0
|
|
437
|
+
idx = idx2
|
|
438
|
+
retval = OK_NACHFOLGE_BLZ_USED
|
|
439
|
+
end
|
|
440
|
+
[@data.bic_h[@data.startidx[idx] + zweigstelle], retval]
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Integer variants used by the IBAN rules
|
|
444
|
+
def lut_aenderung_i(blz_int)
|
|
445
|
+
return nil unless @data && @data.aenderung
|
|
446
|
+
idx = lut_index_i(blz_int)
|
|
447
|
+
return nil if idx < 0
|
|
448
|
+
@data.aenderung[@data.startidx[idx]]
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def lut_nachfolge_blz_i(blz_int)
|
|
452
|
+
return 0 unless @data && @data.nachfolge_blz
|
|
453
|
+
idx = lut_index_i(blz_int)
|
|
454
|
+
return 0 if idx < 0
|
|
455
|
+
@data.nachfolge_blz[@data.startidx[idx]]
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# All fields of a bank (lut_multiple in C). Returns a Hash with
|
|
459
|
+
# :retval, :cnt (branches), :idx and per field an Array over the branches
|
|
460
|
+
# (:name, :name_kurz, :plz, :ort, :pan, :bic, :nr, :aenderung, :loeschung,
|
|
461
|
+
# :nachfolge_blz, :iban_regel) plus :pz (one value). Fields of blocks that
|
|
462
|
+
# are not loaded are nil (then :retval is LUT2_PARTIAL_OK).
|
|
463
|
+
def lut_multiple(blz)
|
|
464
|
+
return { retval: LUT2_NOT_INITIALIZED } unless @data
|
|
465
|
+
idx = lut_index(blz)
|
|
466
|
+
return { retval: idx } if idx < 0
|
|
467
|
+
cnt = @data.filialen_at(idx)
|
|
468
|
+
s = @data.startidx[idx]
|
|
469
|
+
res = { retval: OK, cnt: cnt, idx: idx, blz: @data.blz[idx], pz: @data.pz ? @data.pz[idx] : nil }
|
|
470
|
+
res[:retval] = LUT2_PARTIAL_OK if @data.pz.nil?
|
|
471
|
+
%i[name name_kurz plz ort pan bic nr aenderung loeschung nachfolge_blz iban_regel].each do |f|
|
|
472
|
+
arr = @data.public_send(f)
|
|
473
|
+
if arr
|
|
474
|
+
res[f] = arr[s, cnt]
|
|
475
|
+
else
|
|
476
|
+
res[f] = nil
|
|
477
|
+
res[:retval] = LUT2_PARTIAL_OK
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
res
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# -------------------------------------------------------- account checks
|
|
484
|
+
|
|
485
|
+
# Checks an account number with the check method of the given BLZ
|
|
486
|
+
# (kto_check_blz in C).
|
|
487
|
+
def kto_check_blz(blz, kto)
|
|
488
|
+
return MISSING_PARAMETER if blz.nil? || kto.nil?
|
|
489
|
+
return LUT2_NOT_INITIALIZED unless @data && @data.pz
|
|
490
|
+
idx = lut_index(blz)
|
|
491
|
+
return idx if idx < 0
|
|
492
|
+
return BLZ_MARKED_AS_DELETED if @data.aenderung && @data.aenderung[@data.startidx[idx]] == "D"
|
|
493
|
+
check_int(blz.to_s.strip, @data.pz[idx], kto, 0, nil)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Same with debug information: [retval, Retvals]
|
|
497
|
+
def kto_check_blz_dbg(blz, kto)
|
|
498
|
+
rv = CheckMethods::Retvals.new("(-)", -1, -1, -1)
|
|
499
|
+
return [MISSING_PARAMETER, rv] if blz.nil? || kto.nil?
|
|
500
|
+
return [LUT2_NOT_INITIALIZED, rv] unless @data && @data.pz
|
|
501
|
+
idx = lut_index(blz)
|
|
502
|
+
return [idx, rv] if idx < 0
|
|
503
|
+
[check_int(blz.to_s.strip, @data.pz[idx], kto, 0, rv), rv]
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Checks an account number with an explicitly given check method
|
|
507
|
+
# ("00".."E4", optionally with sub-method letter, e.g. "51c"). The BLZ is
|
|
508
|
+
# only needed for the methods 52, 53, B6 and C0 (kto_check_pz in C).
|
|
509
|
+
def kto_check_pz(pz, kto, blz = nil, rv = nil)
|
|
510
|
+
return MISSING_PARAMETER if pz.nil? || kto.nil?
|
|
511
|
+
s = pz.to_s
|
|
512
|
+
return UNDEFINED_SUBMETHOD if s.length > 3
|
|
513
|
+
parsed = CheckMethods.parse_method(s)
|
|
514
|
+
unless parsed
|
|
515
|
+
return UNDEFINED_SUBMETHOD if s.length == 3
|
|
516
|
+
return NOT_IMPLEMENTED
|
|
517
|
+
end
|
|
518
|
+
methode, um = parsed
|
|
519
|
+
b = blz.to_s
|
|
520
|
+
b = nil if b.empty? || b.start_with?("0")
|
|
521
|
+
check_int(b, methode, kto, um, rv)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# [retval, Retvals]
|
|
525
|
+
def kto_check_pz_dbg(pz, kto, blz = nil)
|
|
526
|
+
rv = CheckMethods::Retvals.new("(-)", -1, -1, -1)
|
|
527
|
+
[kto_check_pz(pz, kto, blz, rv), rv]
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Universal check (kto_check in C): pz_or_blz with 2 or 3 characters is a
|
|
531
|
+
# check method, otherwise a BLZ.
|
|
532
|
+
def kto_check(pz_or_blz, kto, lut_name = nil)
|
|
533
|
+
return MISSING_PARAMETER if pz_or_blz.nil? || kto.nil?
|
|
534
|
+
s = pz_or_blz.to_s
|
|
535
|
+
if s.length == 2 || s.length == 3
|
|
536
|
+
parsed = CheckMethods.parse_method(s)
|
|
537
|
+
return NOT_IMPLEMENTED unless parsed
|
|
538
|
+
return check_int(nil, parsed[0], kto, parsed[1], nil)
|
|
539
|
+
end
|
|
540
|
+
unless @data
|
|
541
|
+
code = init(lut_name, 1, 0)
|
|
542
|
+
return code if code <= 0 && code != LUT2_PARTIAL_OK && code != LUT1_SET_LOADED
|
|
543
|
+
return LUT2_NOT_INITIALIZED unless @data
|
|
544
|
+
end
|
|
545
|
+
kto_check_blz(s, kto)
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Check with IBAN rules applied first (kto_check_regel in C)
|
|
549
|
+
def kto_check_regel(blz, kto)
|
|
550
|
+
kto_check_regel_dbg(blz, kto)[0]
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# [retval, blz2, kto2, bic, regel*100+version, Retvals]
|
|
554
|
+
def kto_check_regel_dbg(blz, kto)
|
|
555
|
+
rv = CheckMethods::Retvals.new("-", -1, -1, -1)
|
|
556
|
+
return [MISSING_PARAMETER, nil, nil, nil, 0, rv] if blz.nil? || kto.nil?
|
|
557
|
+
k = kto.to_s
|
|
558
|
+
return [INVALID_KTO_LENGTH, nil, nil, nil, 0, rv] if k.length > 10
|
|
559
|
+
blz_n = blz.to_s[0, 8].dup
|
|
560
|
+
kto_n = k.rjust(10, "0")
|
|
561
|
+
blz_o = blz_n.dup
|
|
562
|
+
kto_o = kto_n.dup
|
|
563
|
+
regel, ret = lut_iban_regel(blz_n, 0)
|
|
564
|
+
regel_out = ret > 0 ? regel : 0
|
|
565
|
+
ret_regel, bic = iban_regel_cvt(blz_n, kto_n, regel, rv)
|
|
566
|
+
bic ||= lut_bic(blz_n, 0)[0]
|
|
567
|
+
return [ret_regel, blz_n, kto_n, bic, regel_out, rv] if ret_regel < OK
|
|
568
|
+
ret, = kto_check_blz_dbg(blz_n, kto_n).then { |r, rv2| rv.methode = rv2.methode; rv.pz_methode = rv2.pz_methode; rv.pz = rv2.pz; rv.pz_pos = rv2.pz_pos; [r] }
|
|
569
|
+
if blz_n != blz_o || kto_n != kto_o
|
|
570
|
+
result = ret_regel > 3 ? ret_regel : ret
|
|
571
|
+
else
|
|
572
|
+
result = ret
|
|
573
|
+
end
|
|
574
|
+
[result, blz_n, kto_n, bic, regel_out, rv]
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# ----------------------------------------------------------- IBAN, BIC
|
|
578
|
+
|
|
579
|
+
# Generates the IBAN for a German bank account (iban_bic_gen in C).
|
|
580
|
+
# Returns [retval, iban ("DE89 3704 0044 0532 0130 00" with blanks) or nil,
|
|
581
|
+
# bic, blz2, kto2]. blz2/kto2 are the BLZ and account actually used
|
|
582
|
+
# (they may have been replaced by IBAN rules). Prefixes for the BLZ:
|
|
583
|
+
# "+" skip the account check, "@" ignore the blacklist, "!" do not
|
|
584
|
+
# replace the BLZ by its successor.
|
|
585
|
+
def iban_bic_gen(blz, kto)
|
|
586
|
+
return [LUT2_NO_ACCOUNT_GIVEN, nil, nil, "", ""] if blz.nil? || kto.nil? || blz.to_s.empty? || kto.to_s.empty?
|
|
587
|
+
ret = iban_init
|
|
588
|
+
return [ret, nil, nil, nil, nil] if ret < OK
|
|
589
|
+
b = blz.to_s
|
|
590
|
+
flags = 0
|
|
591
|
+
b.each_char do |c|
|
|
592
|
+
break if c =~ /\d/
|
|
593
|
+
flags |= 1 if c == "+"
|
|
594
|
+
flags |= 2 if c == "@"
|
|
595
|
+
flags |= 4 if c == "!"
|
|
596
|
+
end
|
|
597
|
+
b = b.sub(/\A[@+!]+/, "")
|
|
598
|
+
blz2 = b.dup
|
|
599
|
+
kto2 = kto.to_s.dup
|
|
600
|
+
k = kto.to_s
|
|
601
|
+
return [INVALID_KTO_LENGTH, nil, nil, blz2, kto2] if k.length > 10
|
|
602
|
+
blz_i = b =~ /\A\d{8}\z/ ? b.to_i : 100_000_000
|
|
603
|
+
blz_n = b.dup
|
|
604
|
+
kto_n = k.rjust(10, "0")
|
|
605
|
+
regel, ret = lut_iban_regel_i(blz_i, 0)
|
|
606
|
+
if ret <= 0 && ret != LUT2_IBAN_REGEL_NOT_INITIALIZED
|
|
607
|
+
return [ret, nil, nil, blz2, kto2]
|
|
608
|
+
end
|
|
609
|
+
bic = nil
|
|
610
|
+
if ret != LUT2_IBAN_REGEL_NOT_INITIALIZED
|
|
611
|
+
if regel == 0 && (flags & 2) == 0 && @data.own_iban && @data.own_iban.first == 2718281 && @data.own_iban.include?(blz_i)
|
|
612
|
+
return [BLZ_BLACKLISTED, nil, nil, blz2, kto2]
|
|
613
|
+
end
|
|
614
|
+
ret_regel, bic = iban_regel_cvt(blz_n, kto_n, regel, nil)
|
|
615
|
+
if ret_regel < OK
|
|
616
|
+
bic ||= lut_bic(blz_n, 0)[0]
|
|
617
|
+
bic = "" if bic.nil? || bic.start_with?(" ")
|
|
618
|
+
return [ret_regel, nil, bic, blz_n, kto_n]
|
|
619
|
+
end
|
|
620
|
+
else
|
|
621
|
+
regel = 0
|
|
622
|
+
ret_regel = OK
|
|
623
|
+
end
|
|
624
|
+
flags |= 1 if ret_regel == OK_IBAN_WITHOUT_KC_TEST || ret_regel == OK_KTO_REPLACED_NO_PZ
|
|
625
|
+
nb, ret = lut_nachfolge_blz(blz_n, 0)
|
|
626
|
+
if regel == 0 && (flags & 4) == 0 && ret == OK && nb > 0
|
|
627
|
+
ret_old = kto_check_blz(blz_n, kto_n)
|
|
628
|
+
blz_n = format("%8d", nb)
|
|
629
|
+
ret_neu = kto_check_blz(blz_n, kto_n)
|
|
630
|
+
return [OLD_BLZ_OK_NEW_NOT, nil, nil, blz2, kto2] if ret_old == OK && ret_neu < OK
|
|
631
|
+
end
|
|
632
|
+
bic ||= lut_bic(blz_n, 0)[0]
|
|
633
|
+
bic = "" if bic.nil? || bic.start_with?(" ")
|
|
634
|
+
blz2 = blz_n
|
|
635
|
+
kto2 = kto_n
|
|
636
|
+
if (flags & 1) == 0
|
|
637
|
+
ret = kto_check_blz(blz_n, kto_n)
|
|
638
|
+
return [ret, nil, bic, blz2, kto2] if ret <= 0
|
|
639
|
+
elsif ret_regel != OK_IBAN_WITHOUT_KC_TEST && ret_regel != OK_KTO_REPLACED_NO_PZ
|
|
640
|
+
ret_regel = LUT2_KTO_NOT_CHECKED
|
|
641
|
+
end
|
|
642
|
+
bban = format("%8s%10s", blz_n, kto_n).tr(" ", "0")
|
|
643
|
+
iban = "DE" + iban_checksum("DE", bban) + bban
|
|
644
|
+
[ret_regel, iban.scan(/.{1,4}/).join(" "), bic, blz2, kto2]
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# IBAN without blanks or nil (iban_gen in C): [iban, retval]
|
|
648
|
+
def iban_gen(blz, kto)
|
|
649
|
+
ret, iban, = iban_bic_gen(blz, kto)
|
|
650
|
+
[iban&.delete(" "), ret]
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# Checks an IBAN (checksum, length by country and for German IBANs also
|
|
654
|
+
# the account number and the IBAN rules). Returns [retval, retval_kc]
|
|
655
|
+
# where retval_kc is the result of the account check.
|
|
656
|
+
def iban_check(iban)
|
|
657
|
+
return [LUT2_NO_ACCOUNT_GIVEN, LUT2_NO_ACCOUNT_GIVEN] if iban.nil? || iban.to_s.empty?
|
|
658
|
+
s = iban.to_s.gsub(/[^A-Za-z0-9]/, "")
|
|
659
|
+
country = s[0, 2].to_s.upcase
|
|
660
|
+
retval = LUT2_KTO_NOT_CHECKED
|
|
661
|
+
expected = IBAN_LENGTHS[country]
|
|
662
|
+
return [INVALID_IBAN_LENGTH, retval] if expected && s.length != expected
|
|
663
|
+
test = iban_checksum_ok?(s) ? 1 : 0
|
|
664
|
+
ret_kc = 0
|
|
665
|
+
if country == "DE"
|
|
666
|
+
digits = s[4..].to_s.gsub(/\D/, "")
|
|
667
|
+
blz2 = digits[0, 8].to_s
|
|
668
|
+
kto2 = digits[8, 10].to_s
|
|
669
|
+
ret = ret_kc = kto_check_blz(blz2, kto2)
|
|
670
|
+
test |= 2 if ret > 0
|
|
671
|
+
retval = ret
|
|
672
|
+
if test & 1 == 1
|
|
673
|
+
j = lut_index(blz2)
|
|
674
|
+
if j >= 0
|
|
675
|
+
ret = iban_init
|
|
676
|
+
return [ret, retval] if ret < OK
|
|
677
|
+
uk = CheckMethods::UK_PZ_METHODEN.include?(@data.pz[j])
|
|
678
|
+
nachfolge = @data.nachfolge_blz ? @data.nachfolge_blz[@data.startidx[j]] : 0
|
|
679
|
+
regel = @data.iban_regel ? @data.iban_regel[@data.startidx[j]] : 0
|
|
680
|
+
if uk || nachfolge != 0 || regel != 0
|
|
681
|
+
ret, papier2, = iban_bic_gen(blz2, kto2)
|
|
682
|
+
return [IBAN_CHKSUM_OK_NO_IBAN_CALCULATION, retval] if ret == NO_IBAN_CALCULATION
|
|
683
|
+
test = 4 if ret == OK_IBAN_WITHOUT_KC_TEST || ret == OK_KTO_REPLACED_NO_PZ
|
|
684
|
+
if papier2
|
|
685
|
+
iban2 = papier2.delete(" ")
|
|
686
|
+
if s.casecmp(iban2) != 0
|
|
687
|
+
if regel > 0
|
|
688
|
+
return [IBAN_CHKSUM_OK_RULE_IGNORED_BLZ, retval] if s[12..] == iban2[12..]
|
|
689
|
+
return [IBAN_CHKSUM_OK_RULE_IGNORED, retval]
|
|
690
|
+
elsif nachfolge != 0
|
|
691
|
+
return [IBAN_CHKSUM_OK_NACHFOLGE_BLZ_DEFINED, retval]
|
|
692
|
+
else
|
|
693
|
+
return [IBAN_CHKSUM_OK_UNTERKTO_MISSING, retval]
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
else
|
|
701
|
+
test |= 2 if test != 0
|
|
702
|
+
retval = NO_GERMAN_BIC
|
|
703
|
+
end
|
|
704
|
+
case test
|
|
705
|
+
when 1
|
|
706
|
+
if ret_kc == INVALID_BLZ
|
|
707
|
+
[IBAN_CHKSUM_OK_BLZ_INVALID, retval]
|
|
708
|
+
elsif ret_kc == LUT2_NOT_INITIALIZED
|
|
709
|
+
[IBAN_CHKSUM_OK_KC_NOT_INITIALIZED, retval]
|
|
710
|
+
else
|
|
711
|
+
[IBAN_OK_KTO_NOT, retval]
|
|
712
|
+
end
|
|
713
|
+
when 2 then [KTO_OK_IBAN_NOT, retval]
|
|
714
|
+
when 3 then [OK, retval]
|
|
715
|
+
when 4 then [OK_IBAN_WITHOUT_KC_TEST, OK_NO_CHK]
|
|
716
|
+
else [FALSE, retval]
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# BIC for a German IBAN: [bic, retval, blz, kto]
|
|
721
|
+
def iban2bic(iban)
|
|
722
|
+
s = iban.to_s.gsub(/[^A-Za-z0-9]/, "")
|
|
723
|
+
return ["", IBAN2BIC_ONLY_GERMAN, "", ""] unless s[0, 2].to_s.casecmp("DE").zero?
|
|
724
|
+
return ["", INVALID_IBAN_LENGTH, nil, nil] if s.length != 22
|
|
725
|
+
digits = s[4..].gsub(/\D/, "")
|
|
726
|
+
blz2 = digits[0, 8].to_s
|
|
727
|
+
kto2 = digits[8, 10].to_s
|
|
728
|
+
retval = OK
|
|
729
|
+
j = lut_index(blz2)
|
|
730
|
+
return ["", j, blz2, kto2] if j < 0
|
|
731
|
+
if j > 0
|
|
732
|
+
uk = CheckMethods::UK_PZ_METHODEN.include?(@data.pz[j])
|
|
733
|
+
regel = @data.iban_regel ? @data.iban_regel[@data.startidx[j]] : 0
|
|
734
|
+
if uk || regel != 0
|
|
735
|
+
ret, papier, bic, = iban_bic_gen(blz2, kto2)
|
|
736
|
+
retval = ret
|
|
737
|
+
return [bic, OK, blz2, kto2] if ret == NO_IBAN_CALCULATION
|
|
738
|
+
if papier
|
|
739
|
+
iban2 = papier.delete(" ")
|
|
740
|
+
if s.casecmp(iban2) != 0
|
|
741
|
+
retval = regel > 0 ? IBAN_CHKSUM_OK_RULE_IGNORED : IBAN_CHKSUM_OK_UNTERKTO_MISSING
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
return [bic, retval, blz2, kto2]
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
bic, retval = lut_bic(blz2, 0)
|
|
748
|
+
bic = "" if bic.nil? || bic.start_with?(" ")
|
|
749
|
+
[bic, retval, blz2, kto2]
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
# Tests whether a German BIC exists: [retval, count]
|
|
753
|
+
def bic_check(bic)
|
|
754
|
+
s = bic.to_s
|
|
755
|
+
return [BIC_ONLY_GERMAN, 0] unless s[4, 2].to_s.casecmp("DE").zero?
|
|
756
|
+
return [INVALID_BIC_LENGTH, 0] unless s.length == 8 || s.length == 11
|
|
757
|
+
code, hits = lut_suche_bic(s)
|
|
758
|
+
return [FALSE, 0] if code == KEY_NOT_FOUND
|
|
759
|
+
return [code, 0] if code < 0
|
|
760
|
+
[OK, hits.size]
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
# Checks a SEPA creditor identifier (Gläubiger-Identifikationsnummer)
|
|
764
|
+
def ci_check(ci)
|
|
765
|
+
return MISSING_PARAMETER if ci.nil?
|
|
766
|
+
s = ci.to_s.gsub(/[^A-Za-z0-9]/, "")
|
|
767
|
+
return FALSE if s.length < 7
|
|
768
|
+
numeric = to_numeric(s[7..].to_s) + to_numeric(s[0, 2]) + s[2, 2].to_s
|
|
769
|
+
numeric.to_i % 97 == 1 ? OK : FALSE
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
# Generates a structured remittance information (IPI): [retval, ipi, ipi_papier]
|
|
773
|
+
def ipi_gen(zweck)
|
|
774
|
+
z = zweck.to_s
|
|
775
|
+
return [IPI_INVALID_LENGTH, nil, nil] if z.length > 18
|
|
776
|
+
return [IPI_INVALID_CHARACTER, nil, nil] unless z =~ /\A[0-9A-Za-z]*\z/
|
|
777
|
+
body = z.upcase.rjust(18, "0")
|
|
778
|
+
rest = (to_numeric(body) + "00").to_i % 97
|
|
779
|
+
pz = 98 - rest
|
|
780
|
+
dst = format("%02d%s", pz, body)
|
|
781
|
+
[OK, dst, dst.scan(/.{1,4}/).join(" ")]
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
def ipi_check(zweck)
|
|
785
|
+
s = zweck.to_s.delete(" \t")
|
|
786
|
+
return IPI_CHECK_INVALID_LENGTH if s.length != 20
|
|
787
|
+
numeric = to_numeric(s[2..]) + s[0, 2]
|
|
788
|
+
numeric.to_i % 97 == 1 ? OK : FALSE
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
# IBAN length per country (ISO 3166 code)
|
|
792
|
+
IBAN_LENGTHS = {
|
|
793
|
+
"AL" => 28, "AD" => 24, "AZ" => 28, "BH" => 22, "BR" => 29, "BE" => 16, "BA" => 20, "BG" => 22,
|
|
794
|
+
"CR" => 21, "DK" => 18, "DE" => 22, "DO" => 28, "EE" => 20, "FO" => 18, "FI" => 18, "FR" => 27,
|
|
795
|
+
"GF" => 27, "PF" => 27, "TF" => 27, "GE" => 22, "GI" => 23, "GR" => 27, "GL" => 18, "GP" => 27,
|
|
796
|
+
"GT" => 28, "HK" => 16, "IE" => 22, "IS" => 26, "IL" => 23, "IT" => 27, "VG" => 24, "KZ" => 20,
|
|
797
|
+
"QA" => 29, "HR" => 21, "KW" => 30, "LV" => 21, "LB" => 28, "LI" => 21, "LT" => 20, "LU" => 20,
|
|
798
|
+
"MT" => 31, "MA" => 24, "MQ" => 27, "MR" => 27, "MU" => 30, "YT" => 27, "MK" => 19, "MD" => 24,
|
|
799
|
+
"MC" => 27, "ME" => 22, "NC" => 27, "NL" => 18, "NO" => 15, "AT" => 20, "PK" => 24, "PS" => 29,
|
|
800
|
+
"PL" => 28, "PT" => 25, "RE" => 27, "RO" => 24, "BL" => 27, "MF" => 27, "SM" => 27, "SA" => 24,
|
|
801
|
+
"SE" => 24, "CH" => 21, "RS" => 22, "SK" => 24, "SI" => 19, "ES" => 24, "PM" => 27, "CZ" => 24,
|
|
802
|
+
"TN" => 24, "TR" => 26, "HU" => 28, "AE" => 23, "GB" => 22, "WF" => 27, "CY" => 28
|
|
803
|
+
}.freeze
|
|
804
|
+
|
|
805
|
+
# ------------------------------------------------------------- encoding
|
|
806
|
+
|
|
807
|
+
# Output encoding of the descriptive texts (retval2txt) and of the fields
|
|
808
|
+
# name, name_kurz, ort: 1 = ISO-8859-1, 2 = UTF-8 (default), 3 = HTML
|
|
809
|
+
# entities, 4 = DOS CP850, 51..54 = short macro names for retval2txt with
|
|
810
|
+
# the field encoding 1..4. mode 0 returns the current encoding.
|
|
811
|
+
def encoding(mode = 0)
|
|
812
|
+
case mode
|
|
813
|
+
when 0 then return @encoding
|
|
814
|
+
when 1, "i", "I" then @encoding = 1
|
|
815
|
+
when 2, "u", "U" then @encoding = 2
|
|
816
|
+
when 3, "h", "H" then @encoding = 3
|
|
817
|
+
when 4, "d", "D" then @encoding = 4
|
|
818
|
+
when 51, 52, 53, 54, "m", "M" then @encoding = mode.is_a?(Integer) ? mode : 51
|
|
819
|
+
end
|
|
820
|
+
@encoding
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def encoding_str(mode = 0)
|
|
824
|
+
case encoding(mode)
|
|
825
|
+
when 1 then "ISO-8859-1"
|
|
826
|
+
when 2 then "UTF-8"
|
|
827
|
+
when 3 then "HTML entities"
|
|
828
|
+
when 4 then "DOS CP-850"
|
|
829
|
+
when 51 then "Makro/ISO-8859-1"
|
|
830
|
+
when 52 then "Makro/UTF-8"
|
|
831
|
+
when 53 then "Makro/HTML"
|
|
832
|
+
when 54 then "Makro/DOS CP-850"
|
|
833
|
+
else "Unbekannte Kodierung"
|
|
834
|
+
end
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
# Converts a (UTF-8) string to the current output encoding
|
|
838
|
+
def encode_out(str, enc = @encoding)
|
|
839
|
+
return str if str.nil?
|
|
840
|
+
case enc % 10
|
|
841
|
+
when 1 then str.encode("ISO-8859-1", undef: :replace)
|
|
842
|
+
when 3 then str.gsub(/[äöüÄÖÜß]/, HTML_ENTITIES)
|
|
843
|
+
when 4 then str.encode("CP850", undef: :replace)
|
|
844
|
+
else str
|
|
845
|
+
end
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
HTML_ENTITIES = { "ä" => "ä", "ö" => "ö", "ü" => "ü", "Ä" => "Ä",
|
|
849
|
+
"Ö" => "Ö", "Ü" => "Ü", "ß" => "ß" }.freeze
|
|
850
|
+
|
|
851
|
+
def retval2txt(retval)
|
|
852
|
+
return retval2txt_short(retval) if @encoding >= 50
|
|
853
|
+
encode_out(retval2utf8(retval))
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
def retval2utf8(retval)
|
|
857
|
+
RETVAL_TEXT.fetch(retval, RETVAL_UNKNOWN_TEXT)
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def retval2iso(retval)
|
|
861
|
+
retval2utf8(retval).encode("ISO-8859-1", undef: :replace)
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
def retval2dos(retval)
|
|
865
|
+
retval2utf8(retval).encode("CP850", undef: :replace)
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
def retval2html(retval)
|
|
869
|
+
RETVAL_HTML.fetch(retval) { retval2utf8(retval).gsub(/[äöüÄÖÜß]/, HTML_ENTITIES) }
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
def retval2txt_short(retval)
|
|
873
|
+
RETVAL_SHORT.fetch(retval, RETVAL_UNKNOWN_SHORT)
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
# Enables/disables the check method changes of 2019-12-09 (only affects
|
|
877
|
+
# the version string; the methods themselves are implemented).
|
|
878
|
+
def pz_aenderungen_enable(set = -1)
|
|
879
|
+
@pz_aenderungen_2019_12 = (set == 1) if set == 0 || set == 1
|
|
880
|
+
@pz_aenderungen_2019_12 ? 1 : 0
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def version(mode = 0)
|
|
884
|
+
case mode
|
|
885
|
+
when 1 then VERSION
|
|
886
|
+
when 2 then VERSION_DATE
|
|
887
|
+
when 3 then "#{VERSION_DATE}, 00:00:00"
|
|
888
|
+
when 4 then @pz_aenderungen_2019_12 ? PZ_METHODS_DATE : "09.09.2019 (Aenderungen vom 09.12.2019 enthalten aber noch nicht aktiviert)"
|
|
889
|
+
when 5 then IBAN_RULES_DATE
|
|
890
|
+
when 6 then C_LIBRARY_DATE
|
|
891
|
+
when 7 then "final"
|
|
892
|
+
when 8 then VERSION.split(".")[0]
|
|
893
|
+
when 9 then VERSION.split(".")[1]
|
|
894
|
+
else "konto_check_ruby Version #{VERSION} vom #{VERSION_DATE}, Copyright (C) 2026 tickettoaster GmbH (Ruby-Port von konto_check #{C_LIBRARY_VERSION} vom #{C_LIBRARY_DATE}, Copyright (C) 2002-2023 Michael Plugge)"
|
|
895
|
+
end
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
# ------------------------------------------------------------- internals
|
|
899
|
+
|
|
900
|
+
# Loads the blocks needed for the IBAN functions if necessary (iban_init in C)
|
|
901
|
+
def iban_init
|
|
902
|
+
return LUT2_NOT_INITIALIZED unless @data
|
|
903
|
+
return LUT2_NOT_ALL_IBAN_BLOCKS_LOADED if @extra_init_done < 0
|
|
904
|
+
return OK if @extra_init_done > 0
|
|
905
|
+
d = @data
|
|
906
|
+
if d.loeschung.nil? || d.aenderung.nil? || d.iban_regel.nil? || d.bic.nil? || d.nachfolge_blz.nil?
|
|
907
|
+
@extra_init_done = 1
|
|
908
|
+
code = d.load_blocks(BlzFile::LUT_SET_IBAN + [LUT2_OWN_IBAN])
|
|
909
|
+
reset_search_cache
|
|
910
|
+
# Deviation from the C library: a missing IBAN_REGEL block is not
|
|
911
|
+
# fatal here (the C library refuses all IBAN and BIC functions then);
|
|
912
|
+
# without the block the standard rule 0 is used for all banks.
|
|
913
|
+
if code < 0 && (d.loeschung.nil? || d.aenderung.nil? || d.bic.nil? || d.nachfolge_blz.nil?)
|
|
914
|
+
@extra_init_done = -1
|
|
915
|
+
return LUT2_NOT_ALL_IBAN_BLOCKS_LOADED
|
|
916
|
+
end
|
|
917
|
+
end
|
|
918
|
+
@extra_init_done = 1
|
|
919
|
+
OK
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def today
|
|
923
|
+
@current_date || LutFile.today_int
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
private
|
|
927
|
+
|
|
928
|
+
def required_blocks(level)
|
|
929
|
+
BlzFile::LUT_SETS[level] || BlzFile::LUT_SETS[9]
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
def file_id_of(lut_name, set)
|
|
933
|
+
code, i1, i2, v1, v2 = LutFile.info(lut_name, today)
|
|
934
|
+
return nil unless code == OK
|
|
935
|
+
info = case set
|
|
936
|
+
when 1 then i1
|
|
937
|
+
when 2 then i2
|
|
938
|
+
else
|
|
939
|
+
if v1 == LUT2_VALID then i1
|
|
940
|
+
elsif v2 == LUT2_VALID then i2
|
|
941
|
+
elsif v1 == LUT2_NO_LONGER_VALID_BETTER then i1
|
|
942
|
+
elsif v2 == LUT2_NO_LONGER_VALID_BETTER then i2
|
|
943
|
+
else i1
|
|
944
|
+
end
|
|
945
|
+
end
|
|
946
|
+
LutFile.file_id(info)
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
def branch_ok?(idx, zweigstelle)
|
|
950
|
+
return false if zweigstelle < 0
|
|
951
|
+
if @data.filialen
|
|
952
|
+
zweigstelle < @data.filialen[idx]
|
|
953
|
+
else
|
|
954
|
+
zweigstelle == 0
|
|
955
|
+
end
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
def field_s(field, error, blz, zweigstelle)
|
|
959
|
+
return [nil, LUT2_BLZ_NOT_INITIALIZED] unless @data
|
|
960
|
+
arr = @data.public_send(field)
|
|
961
|
+
return [nil, error] unless arr
|
|
962
|
+
idx = lut_index(blz)
|
|
963
|
+
return [nil, idx] if idx < 0
|
|
964
|
+
return [nil, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
|
|
965
|
+
[arr[@data.startidx[idx] + zweigstelle], OK]
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
def field_i(field, error, blz, zweigstelle)
|
|
969
|
+
return [0, LUT2_BLZ_NOT_INITIALIZED] unless @data
|
|
970
|
+
arr = @data.public_send(field)
|
|
971
|
+
return [0, error] unless arr
|
|
972
|
+
idx = lut_index(blz)
|
|
973
|
+
return [0, idx] if idx < 0
|
|
974
|
+
return [0, LUT2_INDEX_OUT_OF_RANGE] unless branch_ok?(idx, zweigstelle)
|
|
975
|
+
[arr[@data.startidx[idx] + zweigstelle], OK]
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# kto_check_int prolog + dispatch
|
|
979
|
+
def check_int(blz, pz_methode, kto, um, rv)
|
|
980
|
+
digits = CheckMethods.normalize_kto(kto)
|
|
981
|
+
return INVALID_KTO_LENGTH unless digits
|
|
982
|
+
return INVALID_KTO if digits.any? { |d| d < 0 || d > 9 }
|
|
983
|
+
CheckMethods.check_digits(pz_methode, digits, blz, um, rv)
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
# letters -> two digits (A=10 ... Z=35), digits unchanged, others dropped
|
|
987
|
+
def to_numeric(str)
|
|
988
|
+
str.each_char.map do |c|
|
|
989
|
+
if c =~ /\d/ then c
|
|
990
|
+
elsif c =~ /[A-Za-z]/ then (c.upcase.ord - 55).to_s
|
|
991
|
+
else ""
|
|
992
|
+
end
|
|
993
|
+
end.join
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
def iban_checksum(country, bban)
|
|
997
|
+
rest = (to_numeric(bban) + to_numeric(country) + "00").to_i % 97
|
|
998
|
+
format("%02d", 98 - rest)
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
def iban_checksum_ok?(iban)
|
|
1002
|
+
return false if iban.length < 5
|
|
1003
|
+
(to_numeric(iban[4..]) + to_numeric(iban[0, 2]) + iban[2, 2]).to_i % 97 == 1
|
|
1004
|
+
end
|
|
1005
|
+
end
|
|
1006
|
+
end
|