ruby_fints 0.0.2
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/.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
|