ruby_fints 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +58 -0
- data/.ruby-gemset +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +71 -0
- data/Rakefile +10 -0
- data/lib/fints/client.rb +144 -0
- data/lib/fints/connection_error.rb +3 -0
- data/lib/fints/dialog.rb +104 -0
- data/lib/fints/helper.rb +17 -0
- data/lib/fints/https_connection.rb +20 -0
- data/lib/fints/message.rb +67 -0
- data/lib/fints/mt535_miniparser.rb +107 -0
- data/lib/fints/pin_tan_client.rb +22 -0
- data/lib/fints/response.rb +160 -0
- data/lib/fints/segment/base_segment.rb +30 -0
- data/lib/fints/segment/hkend.rb +22 -0
- data/lib/fints/segment/hkidn.rb +22 -0
- data/lib/fints/segment/hkkaz.rb +31 -0
- data/lib/fints/segment/hkspa.rb +26 -0
- data/lib/fints/segment/hksyn.rb +26 -0
- data/lib/fints/segment/hkvvb.rb +26 -0
- data/lib/fints/segment/hkwpd.rb +30 -0
- data/lib/fints/segment/hnhbk.rb +27 -0
- data/lib/fints/segment/hnhbs.rb +22 -0
- data/lib/fints/segment/hnsha.rb +27 -0
- data/lib/fints/segment/hnshk.rb +38 -0
- data/lib/fints/segment/hnvsd.rb +30 -0
- data/lib/fints/segment/hnvsk.rb +34 -0
- data/lib/fints/segment_not_found_error.rb +3 -0
- data/lib/fints/version.rb +4 -0
- data/lib/ruby_fints.rb +29 -0
- data/ruby_fints.gemspec +27 -0
- data/test/fixtures/bpd-allowedgv.txt +1 -0
- data/test/pin_tan_client_test.rb +24 -0
- data/test/ruby_fints_test.rb +7 -0
- data/test/segment_test.rb +90 -0
- data/test/test_helper.rb +10 -0
- metadata +196 -0
data/lib/fints/helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module FinTS
|
2
|
+
class Helper
|
3
|
+
def self.fints_escape(content)
|
4
|
+
content.gsub('?', '??').gsub('+', '?+').gsub(':', '?:').gsub("'", "?'")
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.fints_unescape(content)
|
8
|
+
content.gsub('??', '?').gsub("?'", "'").gsub('?+', '+').gsub('?:', ':')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.mt940_to_array(data)
|
12
|
+
processed_data = data.gsub('@@', '\r\n').gsub('-0000', '+0000')
|
13
|
+
mt940 = Cmxl.parse(processed_data, encoding: 'ISO-8859-1')
|
14
|
+
mt940.flat_map(&:transactions)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module FinTS
|
2
|
+
class HTTPSConnection
|
3
|
+
def initialize(url)
|
4
|
+
@url = url
|
5
|
+
end
|
6
|
+
|
7
|
+
def send_msg(msg)
|
8
|
+
message_string = msg.to_s.encode('iso-8859-1')
|
9
|
+
FinTS::Client.logger.debug("<< #{message_string}")
|
10
|
+
data = Base64.encode64(message_string)
|
11
|
+
response = HTTParty.post(@url, body: data, headers: {'Content-Type' => 'text/plain', })
|
12
|
+
if response.code < 200 || response.code > 299
|
13
|
+
raise ConnectionError, "Bad status code #{response.code}"
|
14
|
+
end
|
15
|
+
res = Base64.decode64(response.body).force_encoding('iso-8859-1').encode('utf-8')
|
16
|
+
FinTS::Client.logger.debug(">> #{res}")
|
17
|
+
res
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module FinTS
|
2
|
+
class Message
|
3
|
+
attr_accessor :msg_no
|
4
|
+
attr_accessor :dialog_id
|
5
|
+
attr_accessor :encrypted_segments
|
6
|
+
|
7
|
+
def initialize(blz, username, pin, system_id, dialog_id, msg_no, encrypted_segments, tan_mechs=nil)
|
8
|
+
@blz = blz
|
9
|
+
@username = username
|
10
|
+
@pin = pin
|
11
|
+
@system_id = system_id
|
12
|
+
@dialog_id = dialog_id
|
13
|
+
@msg_no = msg_no
|
14
|
+
@segments = []
|
15
|
+
@encrypted_segments = []
|
16
|
+
|
17
|
+
if tan_mechs && !tan_mechs.include?('999')
|
18
|
+
@profile_version = 2
|
19
|
+
@security_function = tan_mechs[0]
|
20
|
+
else
|
21
|
+
@profile_version = 1
|
22
|
+
@security_function = '999'
|
23
|
+
end
|
24
|
+
|
25
|
+
sig_head = build_signature_head
|
26
|
+
enc_head = build_encryption_head
|
27
|
+
@segments << enc_head
|
28
|
+
|
29
|
+
@enc_envelop = Segment::HNVSD.new(999, '')
|
30
|
+
@segments << @enc_envelop
|
31
|
+
|
32
|
+
append_enc_segment(sig_head)
|
33
|
+
encrypted_segments.each do |segment|
|
34
|
+
append_enc_segment(segment)
|
35
|
+
end
|
36
|
+
|
37
|
+
cur_count = encrypted_segments.length + 3
|
38
|
+
|
39
|
+
sig_end = Segment::HNSHA.new(cur_count, @secref, @pin)
|
40
|
+
append_enc_segment(sig_end)
|
41
|
+
@segments << Segment::HNHBS.new(cur_count + 1, msg_no)
|
42
|
+
end
|
43
|
+
|
44
|
+
def append_enc_segment(seg)
|
45
|
+
@encrypted_segments << seg
|
46
|
+
@enc_envelop.set_data(@enc_envelop.encoded_data + seg.to_s)
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_signature_head
|
50
|
+
@secref = Kernel.rand(1000000..9999999)
|
51
|
+
Segment::HNSHK.new(2, @secref, @blz, @username, @system_id, @profile_version, @security_function)
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_encryption_head
|
55
|
+
Segment::HNVSK.new(998, @blz, @username, @system_id, @profile_version)
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_header
|
59
|
+
length = @segments.map(&:to_s).inject(0) { |sum, segment| sum + segment.length }
|
60
|
+
Segment::HNHBK.new(length, @dialog_id, @msg_no)
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
build_header.to_s + @segments.map(&:to_s).join('')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module FinTS
|
2
|
+
class MT535Miniparser
|
3
|
+
RE_IDENTIFICATION = /^:35B:ISIN\s(.*)\|(.*)\|(.*)$/
|
4
|
+
RE_MARKETPRICE = /^:90B::MRKT\/\/ACTU\/([A-Z]{3})(\d*),{1}(\d*)$/
|
5
|
+
RE_PRICEDATE = /^:98A::PRIC\/\/(\d*)$/
|
6
|
+
RE_PIECES = /^:93B::AGGR\/\/UNIT\/(\d*),(\d*)$/
|
7
|
+
RE_TOTALVALUE = /^:19A::HOLD\/\/([A-Z]{3})(\d*),{1}(\d*)$/
|
8
|
+
|
9
|
+
def parse(lines)
|
10
|
+
retval = []
|
11
|
+
# First: Collapse multiline clauses into one clause
|
12
|
+
clauses = collapse_multilines(lines)
|
13
|
+
# Second: Scan sequence of clauses for financial instrument
|
14
|
+
# sections
|
15
|
+
finsegs = grab_financial_instrument_segments(clauses)
|
16
|
+
# Third: Extract financial instrument data
|
17
|
+
finsegs.each do |finseg|
|
18
|
+
isin = name = market_price = price_symbol = price_date = pieces = total_value = nil
|
19
|
+
finseg.each do |clause|
|
20
|
+
# identification of instrument
|
21
|
+
# e.g. ':35B:ISIN LU0635178014|/DE/ETF127|COMS.-MSCI EM.M.T.U.ETF I'
|
22
|
+
m = RE_IDENTIFICATION.match(clause)
|
23
|
+
if m
|
24
|
+
isin = m[1]
|
25
|
+
name = m[3]
|
26
|
+
end
|
27
|
+
# current market price
|
28
|
+
# e.g. ':90B::MRKT//ACTU/EUR38,82'
|
29
|
+
m = RE_MARKETPRICE.match(clause)
|
30
|
+
if m
|
31
|
+
price_symbol = m[1]
|
32
|
+
market_price = (m[2] + '.' + m[3]).to_f
|
33
|
+
end
|
34
|
+
# date of market price
|
35
|
+
# e.g. ':98A::PRIC//20170428'
|
36
|
+
m = RE_PRICEDATE.match(clause)
|
37
|
+
if m
|
38
|
+
price_date = Time.strptime(m[1], '%Y%m%d').date()
|
39
|
+
end
|
40
|
+
# number of pieces
|
41
|
+
# e.g. ':93B::AGGR//UNIT/16,8211'
|
42
|
+
m = RE_PIECES.match(clause)
|
43
|
+
if m
|
44
|
+
pieces = (m[1] + '.' + m[2]).to_s
|
45
|
+
end
|
46
|
+
# total value of holding
|
47
|
+
# e.g. ':19A::HOLD//EUR970,17'
|
48
|
+
m = RE_TOTALVALUE.match(clause)
|
49
|
+
if m
|
50
|
+
total_value = (m[2] + '.' + m[3]).to_f
|
51
|
+
end
|
52
|
+
end
|
53
|
+
# processed all clauses
|
54
|
+
retval << {
|
55
|
+
ISIN: isin,
|
56
|
+
name: name,
|
57
|
+
market_value: market_price,
|
58
|
+
value_symbol: price_symbol,
|
59
|
+
valuation_date: price_date,
|
60
|
+
pieces: pieces,
|
61
|
+
total_value: total_value
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
retval
|
66
|
+
end
|
67
|
+
|
68
|
+
def collapse_multilines(lines)
|
69
|
+
clauses = []
|
70
|
+
prevline = ''
|
71
|
+
lines.each do |line|
|
72
|
+
if line.start_with?(':')
|
73
|
+
clauses << prevline if prevline != ''
|
74
|
+
prevline = line
|
75
|
+
elsif line.startswith("-")
|
76
|
+
# last line
|
77
|
+
clauses << prevline
|
78
|
+
clauses << line
|
79
|
+
else
|
80
|
+
prevline += "|#{line}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
clauses
|
84
|
+
end
|
85
|
+
|
86
|
+
def grab_financial_instrument_segments(clauses)
|
87
|
+
retval = []
|
88
|
+
stack = []
|
89
|
+
within_financial_instrument = false
|
90
|
+
clauses.each do |clause|
|
91
|
+
if clause.start_with?(':16R:FIN')
|
92
|
+
# start of financial instrument
|
93
|
+
within_financial_instrument = true
|
94
|
+
elsif clause.startswith(':16S:FIN')
|
95
|
+
# end of financial instrument - move stack over to
|
96
|
+
# return value
|
97
|
+
retval << stack
|
98
|
+
stack = []
|
99
|
+
within_financial_instrument = false
|
100
|
+
elsif within_financial_instrument
|
101
|
+
stack << clause
|
102
|
+
end
|
103
|
+
end
|
104
|
+
retval
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module FinTS
|
2
|
+
class PinTanClient < Client
|
3
|
+
def initialize(blz, username, pin, server)
|
4
|
+
@blz = blz
|
5
|
+
@username = username
|
6
|
+
@pin = pin
|
7
|
+
@connection = HTTPSConnection.new(server)
|
8
|
+
@system_id = 0
|
9
|
+
super()
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def new_dialog
|
15
|
+
Dialog.new(@blz, @username, @pin, @system_id, @connection)
|
16
|
+
end
|
17
|
+
|
18
|
+
def new_message(dialog, segments)
|
19
|
+
Message.new(@blz, @username, @pin, dialog.system_id, dialog.dialog_id, dialog.msg_no, segments, dialog.tan_mechs)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module FinTS
|
2
|
+
class Response
|
3
|
+
RE_SEGMENTS = /'(?=[A-Z]{4,}:\d|')/
|
4
|
+
RE_UNWRAP = /HNVSD:\d+:\d+\+@\d+@(.+)''/
|
5
|
+
RE_SYSTEMID = /HISYN:\d+:\d+:\d+\+(.+)/
|
6
|
+
RE_TANMECH = /\d{3}/
|
7
|
+
|
8
|
+
def initialize(data)
|
9
|
+
@response = unwrap(data)
|
10
|
+
@segments = data.split(RE_SEGMENTS)
|
11
|
+
end
|
12
|
+
|
13
|
+
def split_for_data_groups(seg)
|
14
|
+
seg.split(/\+(?<!\?\+)/)
|
15
|
+
end
|
16
|
+
|
17
|
+
def split_for_data_elements(deg)
|
18
|
+
deg.split(/:(?<!\?:)/)
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_summary_by_segment(name)
|
22
|
+
if !['HIRMS', 'HIRMG'].include?(name)
|
23
|
+
raise ArgumentError, 'Unsupported segment for message summary'
|
24
|
+
end
|
25
|
+
|
26
|
+
seg = find_segment(name)
|
27
|
+
return {} if seg.nil?
|
28
|
+
res = {}
|
29
|
+
parts = split_for_data_groups(seg).drop(1)
|
30
|
+
parts.each do |de|
|
31
|
+
de = split_for_data_elements(de)
|
32
|
+
res[de[0]] = de[2]
|
33
|
+
end
|
34
|
+
res
|
35
|
+
end
|
36
|
+
|
37
|
+
def successful?
|
38
|
+
summary = get_summary_by_segment('HIRMG')
|
39
|
+
summary.each do |code, msg|
|
40
|
+
if code[0] == '9'
|
41
|
+
return false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
return true
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_dialog_id
|
48
|
+
seg = self.find_segment('HNHBK')
|
49
|
+
unless seg
|
50
|
+
raise ArgumentError, 'Invalid response, no HNHBK segment'
|
51
|
+
end
|
52
|
+
get_segment_index(4, seg)
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_system_id
|
56
|
+
seg = find_segment('HISYN')
|
57
|
+
match = RE_SYSTEMID.match(seg)
|
58
|
+
raise ArgumentError, 'Could not find system_id' if match.nil?
|
59
|
+
match[1]
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_bank_name
|
63
|
+
seg = find_segment('HIBPA')
|
64
|
+
return nil if seg.nil?
|
65
|
+
parts = split_for_data_groups(seg)
|
66
|
+
return nil if parts.length <= 3
|
67
|
+
parts[3]
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_hkkaz_max_version
|
71
|
+
get_segment_max_version('HIKAZS')
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_hksal_max_version
|
75
|
+
get_segment_max_version('HISALS')
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_segment_index(idx, seg)
|
79
|
+
seg = split_for_data_groups(seg)
|
80
|
+
return seg[idx - 1] if seg.length > idx - 1
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_segment_max_version(name)
|
85
|
+
ret = 3
|
86
|
+
segs = find_segments(name)
|
87
|
+
segs.each do |s|
|
88
|
+
parts = split_for_data_groups(s)
|
89
|
+
segheader = split_for_data_elements(parts[0])
|
90
|
+
current_version = segheader[2].to_i
|
91
|
+
if current_version > ret
|
92
|
+
ret = current_version
|
93
|
+
end
|
94
|
+
end
|
95
|
+
ret
|
96
|
+
end
|
97
|
+
|
98
|
+
def get_supported_tan_mechanisms
|
99
|
+
segs = self.find_segments('HIRMS')
|
100
|
+
segs.each do |s|
|
101
|
+
seg = split_for_data_groups(s).drop(1)
|
102
|
+
seg.each do |segment_data_group|
|
103
|
+
id, msg = segment_data_group.split('::', 2)
|
104
|
+
if id == '3920'
|
105
|
+
match = RE_TANMECH.match(msg)
|
106
|
+
return [match[0]] if match
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
return false
|
111
|
+
end
|
112
|
+
|
113
|
+
def unwrap(data)
|
114
|
+
match = RE_UNWRAP.match(data)
|
115
|
+
match ? match[1] : data
|
116
|
+
end
|
117
|
+
|
118
|
+
def find_segment(name)
|
119
|
+
find_segments(name, one: true)
|
120
|
+
end
|
121
|
+
|
122
|
+
def find_segments(name, one: false)
|
123
|
+
found = one ? nil : []
|
124
|
+
@segments.each do |segment|
|
125
|
+
spl = segment.split(':', 2)
|
126
|
+
if spl[0] == name
|
127
|
+
return segment if one
|
128
|
+
found << segment
|
129
|
+
end
|
130
|
+
end
|
131
|
+
found
|
132
|
+
end
|
133
|
+
|
134
|
+
def find_segment_for_reference(name, ref)
|
135
|
+
segs = find_segments(name)
|
136
|
+
segs.each do |seg|
|
137
|
+
segsplit = split_for_data_elements(split_for_data_groups(seg)[0])
|
138
|
+
return seg if segsplit[3] == ref.segmentno.to_s
|
139
|
+
end
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
|
143
|
+
def get_touchdowns(msg)
|
144
|
+
touchdown = {}
|
145
|
+
msg.encrypted_segments.each do |msgseg|
|
146
|
+
seg = find_segment_for_reference('HIRMS', msgseg)
|
147
|
+
next unless seg
|
148
|
+
parts = split_for_data_groups(seg).drop(1)
|
149
|
+
parts.each do |p|
|
150
|
+
psplit = split_for_data_elements(p)
|
151
|
+
next if psplit[0] != '3040'
|
152
|
+
td = psplit[3]
|
153
|
+
next if td.nil?
|
154
|
+
touchdown[msgseg.class] = Helper.fints_unescape(td)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
touchdown
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class BaseSegment
|
2
|
+
attr_accessor :segmentno
|
3
|
+
|
4
|
+
def initialize(segmentno, data)
|
5
|
+
@segmentno = segmentno
|
6
|
+
@data = data
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
res = [type, @segmentno, version].join(':')
|
11
|
+
@data.each do |d|
|
12
|
+
res += "+#{d}"
|
13
|
+
end
|
14
|
+
res + "'"
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def type
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def version
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def country_code
|
28
|
+
280
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module FinTS
|
2
|
+
module Segment
|
3
|
+
# HKEND (Dialogende)
|
4
|
+
# Section C.4.1.2
|
5
|
+
class HKEND < BaseSegment
|
6
|
+
def initialize(segno, dialog_id)
|
7
|
+
data = [dialog_id]
|
8
|
+
super(segno, data)
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def type
|
14
|
+
'HKEND'
|
15
|
+
end
|
16
|
+
|
17
|
+
def version
|
18
|
+
1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|