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.
@@ -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