ruby_fints 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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