ofx_for_ruby 0.1.4 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: f5ff318155e5d3faeab49209d7492b3f9ba36400
4
- data.tar.gz: 049c7b99734a9da7a0fcedf0f34dd2b051c63d29
2
+ SHA256:
3
+ metadata.gz: e96380301ef72d29b924fba5825d9bad25e46ce40c200697dbd4a21bd284105b
4
+ data.tar.gz: f787102b4cea5960e2efe2467e31f082a7c82dccc1f19dfd3183bd938a47d95f
5
5
  SHA512:
6
- metadata.gz: 0574c03c81ba22a20babfbbba79a4d821ed7f6a5e0a131ed6d4a87f0bbfa6f31d5e3e7ee6fdb8e255fa8d9a9fa4c02755b4412ab08b94d75e905933662ea3231
7
- data.tar.gz: 81a52f06bfc3bcd1280b2057302e0e0882c6b9a468ffcd7e73485a8ae8ad776285c4fa46e7c587fa665059d2961bc12a60c2fff8a82f05aa09b7bb085ee78da6
6
+ metadata.gz: 9a7ed0297f15bd693ebce58e792b5928daa5ef87155a20972e52daf24985a0a77553a8d2ea2619331fdecdac338a1f892c0b819e2a46b809425987566b26113a
7
+ data.tar.gz: 4588782f3013fede00ba5dd29759dd6b181ceac8dfa7d96da51cfa76958cfc35f2b051e0d5600c71ab3bec75bb6735e0d4a5eb4917ea65eef9daa70175e505c6
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # DO NOT MODIFY!!!!
3
- # This file is automatically generated by Racc 1.4.6
4
- # from Racc grammer file "".
3
+ # This file is automatically generated by Racc 1.8.1
4
+ # from Racc grammar file "ofx_102.racc".
5
5
  #
6
6
 
7
7
  require 'racc/parser.rb'
@@ -62,16 +62,16 @@ end
62
62
  ##### State transition tables begin ###
63
63
 
64
64
  racc_action_table = [
65
- 4, 16, 4, 10, 12, 10, 12, 8, 5, 7,
66
- 17, 19, 20, 4 ]
65
+ 4, 4, 4, 10, 12, 10, 12, 5, 7, 8,
66
+ 16, 17, 19, 20 ]
67
67
 
68
68
  racc_action_check = [
69
- 18, 7, 6, 18, 18, 6, 6, 5, 1, 4,
70
- 10, 15, 17, 0 ]
69
+ 6, 0, 18, 6, 6, 18, 18, 1, 4, 5,
70
+ 7, 10, 15, 17 ]
71
71
 
72
72
  racc_action_pointer = [
73
- 11, 8, nil, nil, 6, 7, 0, -3, nil, nil,
74
- 7, nil, nil, nil, nil, 5, nil, 8, -2, nil,
73
+ -1, 7, nil, nil, 5, 9, -2, 6, nil, nil,
74
+ 8, nil, nil, nil, nil, 6, nil, 9, 0, nil,
75
75
  nil, nil ]
76
76
 
77
77
  racc_action_default = [
@@ -80,15 +80,15 @@ racc_action_default = [
80
80
  -5, -11 ]
81
81
 
82
82
  racc_goto_table = [
83
- 9, 6, 3, 2, 1, nil, nil, nil, nil, nil,
84
- nil, nil, 21, 18 ]
83
+ 6, 9, 1, 2, 3, nil, nil, nil, nil, nil,
84
+ nil, nil, 18, 21 ]
85
85
 
86
86
  racc_goto_check = [
87
- 5, 4, 3, 2, 1, nil, nil, nil, nil, nil,
88
- nil, nil, 5, 4 ]
87
+ 4, 5, 1, 2, 3, nil, nil, nil, nil, nil,
88
+ nil, nil, 4, 5 ]
89
89
 
90
90
  racc_goto_pointer = [
91
- nil, 4, 3, 2, -2, -6, nil, nil, nil ]
91
+ nil, 2, 3, 4, -3, -5, nil, nil, nil ]
92
92
 
93
93
  racc_goto_default = [
94
94
  nil, nil, nil, 15, nil, nil, 11, 13, 14 ]
@@ -140,6 +140,7 @@ Racc_arg = [
140
140
  racc_shift_n,
141
141
  racc_reduce_n,
142
142
  racc_use_result_var ]
143
+ Ractor.make_shareable(Racc_arg) if defined?(Ractor)
143
144
 
144
145
  Racc_token_to_s_table = [
145
146
  "$end",
@@ -158,6 +159,7 @@ Racc_token_to_s_table = [
158
159
  "content",
159
160
  "aggregate",
160
161
  "value" ]
162
+ Ractor.make_shareable(Racc_token_to_s_table) if defined?(Ractor)
161
163
 
162
164
  Racc_debug_parser = false
163
165
 
@@ -173,14 +175,14 @@ Racc_debug_parser = false
173
175
 
174
176
  module_eval(<<'.,.,', 'ofx_102.racc', 25)
175
177
  def _reduce_4(val, _values, result)
176
- start_tag(val[1])
178
+ start_tag(val[1])
177
179
  result
178
180
  end
179
181
  .,.,
180
182
 
181
183
  module_eval(<<'.,.,', 'ofx_102.racc', 26)
182
184
  def _reduce_5(val, _values, result)
183
- end_tag(val[1])
185
+ end_tag(val[1])
184
186
  result
185
187
  end
186
188
  .,.,
@@ -199,7 +201,7 @@ module_eval(<<'.,.,', 'ofx_102.racc', 26)
199
201
 
200
202
  module_eval(<<'.,.,', 'ofx_102.racc', 36)
201
203
  def _reduce_12(val, _values, result)
202
- data_value(val[1])
204
+ data_value(val[1])
203
205
  result
204
206
  end
205
207
  .,.,
@@ -209,7 +211,7 @@ def _reduce_none(val, _values, result)
209
211
  end
210
212
 
211
213
  end # class Parser
212
- end # module OFX102
213
- end # module OFX
214
+ end # module OFX102
215
+ end # module OFX
214
216
 
215
217
 
@@ -1,17 +1,17 @@
1
1
  # Copyright © 2007 Chris Guidry <chrisguidry@gmail.com>
2
2
  #
3
3
  # This file is part of OFX for Ruby.
4
- #
4
+ #
5
5
  # OFX for Ruby is free software; you can redistribute it and/or modify
6
6
  # it under the terms of the GNU General Public License as published by
7
7
  # the Free Software Foundation; either version 3 of the License, or
8
8
  # (at your option) any later version.
9
- #
9
+ #
10
10
  # OFX for Ruby is distributed in the hope that it will be useful,
11
11
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
12
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
13
  # GNU General Public License for more details.
14
- #
14
+ #
15
15
  # You should have received a copy of the GNU General Public License
16
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
 
@@ -48,37 +48,35 @@ module OFX
48
48
  body += message_set.to_ofx_102_s
49
49
  end
50
50
  body += "</OFX>\n"
51
-
52
- # puts body
53
51
 
54
52
  body
55
53
  end
56
54
 
57
55
  def from_http_response_body(body)
58
- # puts "Raw response:\n#{body}"
56
+ body = body.lstrip
57
+
58
+ end_of_header_index = body.index("<OFX>")
59
+
60
+ header_str = body[0...end_of_header_index].strip
61
+ body = body[end_of_header_index..-1]
59
62
 
60
- #header_pattern = /(\w+\:.*\n)+\n/
61
- header_pattern = /(\w+\:.*\r*\n)+\r*\n/
62
- header_match = header_pattern.match(body)
63
- if header_match.nil?
63
+ if header_str.nil? || header_str == ""
64
64
  raise NotImplementedError, "OFX server returned unmatched ASCII"
65
- return body
66
65
  end
67
66
 
68
- body = header_match.post_match
69
- header = Header.from_ofx_102_s(header_match[0].strip)
70
-
67
+ header = Header.from_ofx_102_s(header_str)
68
+
71
69
  parser = OFX::OFX102::Parser.new
72
70
 
73
71
  parser.scan_str body
74
-
72
+
75
73
  if parser.documents.length > 1
76
74
  raise NotImplementedError, "Multiple response documents"
77
75
  end
78
-
76
+
79
77
  # require 'pp'
80
78
  # pp parser.ofx_hashes[0]
81
-
79
+
82
80
  document = parser.documents[0]
83
81
  document.header = header
84
82
  document
@@ -0,0 +1,25 @@
1
+ require File.dirname(__FILE__) + '/../1.0.2/serializer'
2
+ require File.dirname(__FILE__) + '/xml_parser'
3
+
4
+ module OFX
5
+ module OFX200
6
+ class Serializer
7
+ def self.xml_ofx?(body)
8
+ OFX::OFX200::XMLParser.xml_ofx?(body)
9
+ end
10
+
11
+ def from_http_response_body(body)
12
+ body = body.to_s.lstrip
13
+ unless self.class.xml_ofx?(body)
14
+ raise NotImplementedError, 'OFX 2.x serializer only supports XML OFX documents'
15
+ end
16
+
17
+ OFX::OFX200::XMLParser.new(body).to_document
18
+ end
19
+
20
+ def to_http_post_body(document)
21
+ raise NotImplementedError
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,78 @@
1
+ require 'rexml/document'
2
+
3
+ module OFX
4
+ module OFX200
5
+ class XMLParser
6
+ def self.xml_ofx?(body)
7
+ body.to_s.lstrip =~ /\A(?:<\?xml\b[^?]*\?>\s*)?<\?OFX\b/i ? true : false
8
+ end
9
+
10
+ def initialize(body)
11
+ @body = body.to_s.lstrip
12
+ end
13
+
14
+ def header
15
+ header = OFX::Header.new
16
+ processing_instruction_attributes.each do |key, value|
17
+ header[key] = value
18
+ end
19
+ header['DATA'] ||= 'OFXXML'
20
+ header
21
+ end
22
+
23
+ def to_document
24
+ document = OFX::Document.from_ofx_hash(to_ofx_hash)
25
+ document.header = header
26
+ document
27
+ end
28
+
29
+ def to_ofx_hash
30
+ { document.root.name => element_to_hash_value(document.root) }
31
+ end
32
+
33
+ private
34
+
35
+ def document
36
+ @document ||= REXML::Document.new(@body)
37
+ end
38
+
39
+ def processing_instruction_attributes
40
+ return {} unless ofx_processing_instruction
41
+
42
+ attributes = {}
43
+ ofx_processing_instruction.content.scan(/([A-Za-z0-9_.-]+)\s*=\s*("([^"]*)"|'([^']*)')/) do |match|
44
+ attributes[match[0]] = match[2] || match[3]
45
+ end
46
+ attributes
47
+ end
48
+
49
+ def ofx_processing_instruction
50
+ document.children.find do |child|
51
+ child.is_a?(REXML::Instruction) && child.target.upcase == 'OFX'
52
+ end
53
+ end
54
+
55
+ def element_to_hash_value(element)
56
+ children = element.elements.to_a
57
+ return element.text.to_s.strip if children.empty?
58
+
59
+ hash = {}
60
+ children.each do |child|
61
+ append_child(hash, child.name, element_to_hash_value(child))
62
+ end
63
+ hash
64
+ end
65
+
66
+ def append_child(hash, name, value)
67
+ previous = hash[name]
68
+ if previous.nil?
69
+ hash[name] = value
70
+ elsif previous.kind_of?(Array)
71
+ previous << value
72
+ else
73
+ hash[name] = [previous, value]
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
data/lib/ofx/document.rb CHANGED
@@ -1,17 +1,17 @@
1
1
  # Copyright © 2007 Chris Guidry <chrisguidry@gmail.com>
2
2
  #
3
3
  # This file is part of OFX for Ruby.
4
- #
4
+ #
5
5
  # OFX for Ruby is free software; you can redistribute it and/or modify
6
6
  # it under the terms of the GNU General Public License as published by
7
7
  # the Free Software Foundation; either version 3 of the License, or
8
8
  # (at your option) any later version.
9
- #
9
+ #
10
10
  # OFX for Ruby is distributed in the hope that it will be useful,
11
11
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
12
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
13
  # GNU General Public License for more details.
14
- #
14
+ #
15
15
  # You should have received a copy of the GNU General Public License
16
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
 
@@ -26,5 +26,9 @@ module OFX
26
26
  @message_sets = []
27
27
  end
28
28
 
29
+ def self.from_ofx_hash(document_hash)
30
+ from_ofx_102_hash(document_hash)
31
+ end
32
+
29
33
  end
30
34
  end
@@ -1,30 +1,44 @@
1
1
  # Copyright © 2007 Chris Guidry <chrisguidry@gmail.com>
2
2
  #
3
3
  # This file is part of OFX for Ruby.
4
- #
4
+ #
5
5
  # OFX for Ruby is free software; you can redistribute it and/or modify
6
6
  # it under the terms of the GNU General Public License as published by
7
7
  # the Free Software Foundation; either version 3 of the License, or
8
8
  # (at your option) any later version.
9
- #
9
+ #
10
10
  # OFX for Ruby is distributed in the hope that it will be useful,
11
11
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
12
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
13
  # GNU General Public License for more details.
14
- #
14
+ #
15
15
  # You should have received a copy of the GNU General Public License
16
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
 
18
18
  require File.dirname(__FILE__) + '/1.0.2/serializer'
19
+ require File.dirname(__FILE__) + '/2.0.0/serializer'
19
20
 
20
21
  module OFX
21
22
  class Serializer
22
23
  def self.get(version)
23
- case version
24
- when OFX::Version.new("1.0.2"), OFX::Version.new("1.0.3")
25
- return OFX::OFX102::Serializer.new
26
- else
27
- raise NotImplementedError
24
+ version = OFX::Version.new(version)
25
+
26
+ if version == OFX::Version.new("1.0.2") || version == OFX::Version.new("1.0.3")
27
+ return OFX::OFX102::Serializer.new
28
+ elsif version.major == 2
29
+ return OFX::OFX200::Serializer.new
30
+ else
31
+ raise NotImplementedError
32
+ end
33
+ end
34
+
35
+ def self.from_http_response_body(body)
36
+ body = body.to_s.lstrip
37
+
38
+ if OFX::OFX200::Serializer.xml_ofx?(body)
39
+ OFX::OFX200::Serializer.new.from_http_response_body(body)
40
+ else
41
+ OFX::OFX102::Serializer.new.from_http_response_body(body)
28
42
  end
29
43
  end
30
44
 
@@ -35,5 +49,9 @@ module OFX
35
49
  def from_http_request_body(document)
36
50
  raise NotImplementedError
37
51
  end
52
+
53
+ def from_http_response_body(body)
54
+ self.class.from_http_response_body(body)
55
+ end
38
56
  end
39
57
  end
data/lib/ofx/version.rb CHANGED
@@ -82,5 +82,5 @@ module OFX
82
82
  end
83
83
  end
84
84
 
85
- VERSION = Version.new('0.1.4')
85
+ VERSION = Version.new('0.1.6')
86
86
  end
data/ofx_for_ruby.gemspec CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
6
6
  s.name = "ofx_for_ruby"
7
7
  s.version = OFX::VERSION.to_dotted_s
8
8
  s.platform = Gem::Platform::RUBY
9
-
9
+
10
10
  s.authors = ["Chris Guidry", "Patrick Bacon"]
11
11
  s.description = "OFX for Ruby is a pure Ruby implementation of Open Financial Exchange specifications (1.0.2 through 2.1.1) for building both financial clients and servers, providing parsers/serializers for each version, and a uniform object model across all versions."
12
12
  s.email = ["bacon@atomicobject.com"]
@@ -16,11 +16,17 @@ Gem::Specification.new do |s|
16
16
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
17
  s.require_paths = ["lib"]
18
18
  s.homepage = "http://github.com/baconpat/ofx_for_ruby"
19
-
19
+
20
20
  s.add_dependency "activesupport"
21
-
22
- s.add_development_dependency "racc", "1.4.6"
21
+ s.add_dependency "bigdecimal"
22
+ s.add_dependency "logger"
23
+ s.add_dependency "rexml"
24
+
25
+ s.add_development_dependency "racc", "~> 1.8"
23
26
  s.add_development_dependency "rexical", "1.0.5"
27
+ s.add_development_dependency "getoptlong"
28
+ s.add_development_dependency "ostruct"
24
29
  s.add_development_dependency "pry"
25
30
  s.add_development_dependency "rake"
31
+ s.add_development_dependency "minitest", "~> 5.27"
26
32
  end
@@ -0,0 +1,43 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+
3
+ class OFXXMLParserTest < Minitest::Test
4
+ XML_OFX = '<?xml version="1.0" encoding="utf-8"?><?OFX OFXHEADER="200" VERSION="220" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?><OFX><SIGNONMSGSRSV1><SONRS><STATUS><CODE>0</CODE><SEVERITY>INFO</SEVERITY></STATUS><DTSERVER>20260627111438.091[-4:EDT]</DTSERVER><LANGUAGE>ENG</LANGUAGE><FI><ORG>Capital One Bank</ORG><FID>1001</FID></FI><INTU.BID>1236</INTU.BID></SONRS></SIGNONMSGSRSV1><BANKMSGSRSV1><STMTTRNRS><STMTRS><BANKTRANLIST><STMTTRN><FITID>1</FITID></STMTTRN><STMTTRN><FITID>2</FITID></STMTTRN></BANKTRANLIST></STMTRS></STMTTRNRS></BANKMSGSRSV1></OFX>'
5
+
6
+ def test_detects_xml_ofx_documents
7
+ assert OFX::OFX200::XMLParser.xml_ofx?(XML_OFX)
8
+ refute OFX::OFX200::XMLParser.xml_ofx?("OFXHEADER:100\nDATA:OFXSGML\n<OFX></OFX>")
9
+ end
10
+
11
+ def test_parses_processing_instruction_header
12
+ header = OFX::OFX200::XMLParser.new(XML_OFX).header
13
+
14
+ assert_equal OFX::Version.new('2.0.0'), header.header_version
15
+ assert_equal 'OFXXML', header.content_type
16
+ assert_equal OFX::Version.new('2.2.0'), header.document_version
17
+ assert_equal 'NONE', header.security
18
+ assert_equal 'NONE', header.previous_unique_identifier
19
+ assert_equal 'NONE', header.unique_identifier
20
+ end
21
+
22
+ def test_converts_xml_tree_to_ofx_hash
23
+ ofx_hash = OFX::OFX200::XMLParser.new(XML_OFX).to_ofx_hash
24
+
25
+ sonrs = ofx_hash['OFX']['SIGNONMSGSRSV1']['SONRS']
26
+ assert_equal '0', sonrs['STATUS']['CODE']
27
+ assert_equal 'INFO', sonrs['STATUS']['SEVERITY']
28
+ assert_equal '20260627111438.091[-4:EDT]', sonrs['DTSERVER']
29
+ assert_equal 'Capital One Bank', sonrs['FI']['ORG']
30
+ assert_equal '1236', sonrs['INTU.BID']
31
+ end
32
+
33
+ def test_repeated_xml_tags_become_arrays
34
+ ofx_hash = OFX::OFX200::XMLParser.new(XML_OFX).to_ofx_hash
35
+ transactions = ofx_hash['OFX']['BANKMSGSRSV1']['STMTTRNRS']['STMTRS']['BANKTRANLIST']['STMTTRN']
36
+
37
+ assert_equal [{'FITID' => '1'}, {'FITID' => '2'}], transactions
38
+ end
39
+
40
+ def test_top_level_serializer_get_returns_ofx200_serializer_for_ofx2_versions
41
+ assert_instance_of OFX::OFX200::Serializer, OFX::Serializer.get(OFX::Version.new('2.2.0'))
42
+ end
43
+ end
@@ -0,0 +1,244 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+
3
+ class OFXParserTest < Minitest::Test
4
+ def test_parse_with_no_newline_after_header
5
+ body = <<-EOS
6
+
7
+ OFXHEADER:100
8
+ DATA:OFXSGML
9
+ VERSION:102
10
+ SECURITY:NONE
11
+ ENCODING:USASCII
12
+ CHARSET:1252
13
+ COMPRESSION:NONE
14
+ OLDFILEUID:NONE
15
+ NEWFILEUID:NONE
16
+ <OFX>
17
+ <SIGNONMSGSRSV1>
18
+ <SONRS>
19
+ <STATUS>
20
+ <CODE>0
21
+ <SEVERITY>INFO
22
+ </STATUS>
23
+ <DTSERVER>20221013120000[0:GMT]
24
+ <LANGUAGE>ENG
25
+ <FI>
26
+ <ORG>B1
27
+ <FID>10898
28
+ </FI>
29
+ <INTU.BID>10898
30
+ </SONRS>
31
+ </SIGNONMSGSRSV1>
32
+ <BANKMSGSRSV1>
33
+ <STMTTRNRS>
34
+ <TRNUID>1
35
+ <STATUS>
36
+ <CODE>0
37
+ <SEVERITY>INFO
38
+ <MESSAGE>Success
39
+ </STATUS>
40
+ <STMTRS>
41
+ <CURDEF>USD
42
+ <BANKACCTFROM>
43
+ <BANKID>0713456513
44
+ <ACCTID>123456788
45
+ <ACCTTYPE>CHECKING
46
+ </BANKACCTFROM>
47
+ <BANKTRANLIST>
48
+ <DTSTART>20221003120000[0:GMT]
49
+ <DTEND>20221012120000[0:GMT]
50
+ <STMTTRN>
51
+ <TRNTYPE>DEBIT
52
+ <DTPOSTED>20221012120000[0:GMT]
53
+ <TRNAMT>-100.01
54
+ <FITID>202210120
55
+ <NAME>CAPITAL ONE N.A. CAPITALONE 0000
56
+ <MEMO>0234345d WEB ID: 15234563
57
+ </STMTTRN>
58
+ </BANKTRANLIST>
59
+ <LEDGERBAL>
60
+ <BALAMT>29.03
61
+ <DTASOF>20221013120000[0:GMT]
62
+ </LEDGERBAL>
63
+ <AVAILBAL>
64
+ <BALAMT>30.06
65
+ <DTASOF>20221013120000[0:GMT]
66
+ </AVAILBAL>
67
+ </STMTRS>
68
+ </STMTTRNRS>
69
+ </BANKMSGSRSV1>
70
+ </OFX>
71
+ EOS
72
+
73
+ document = OFX::Serializer.from_http_response_body(body)
74
+
75
+ signon_message_set = document.message_sets[0]
76
+ signon_response = signon_message_set.responses[0]
77
+
78
+ banking_statement_message_set = document.message_sets[1]
79
+ banking_statement_response = banking_statement_message_set.responses[0]
80
+
81
+ assert_equal '123456788', banking_statement_response.account.account_identifier
82
+ assert_equal '0713456513', banking_statement_response.account.bank_identifier
83
+ assert_equal '30.06', banking_statement_response.available_balance.amount
84
+ assert_equal '29.03', banking_statement_response.ledger_balance.amount
85
+
86
+ transactions = banking_statement_response.transactions
87
+ assert_equal 1, transactions.size
88
+
89
+ transaction = transactions.first
90
+ assert_equal '-100.01', transaction.amount.to_f.to_s
91
+ assert_equal 'CAPITAL ONE N.A. CAPITALONE 0000', transaction.payee
92
+ assert_equal :debit, transaction.transaction_type
93
+ end
94
+
95
+ def test_with_newline_after_header
96
+ body =
97
+ "OFXHEADER:100
98
+ DATA:OFXSGML
99
+ VERSION:102
100
+ SECURITY:NONE
101
+ ENCODING:USASCII
102
+ CHARSET:1252
103
+ COMPRESSION:NONE
104
+ OLDFILEUID:NONE
105
+ NEWFILEUID:NONE
106
+
107
+ <OFX>
108
+ <SIGNONMSGSRSV1>
109
+ <SONRS>
110
+ <STATUS>
111
+ <CODE>0
112
+ <SEVERITY>INFO
113
+ </STATUS>
114
+ <DTSERVER>20221013120000[0:GMT]
115
+ <LANGUAGE>ENG
116
+ <FI>
117
+ <ORG>B1
118
+ <FID>10898
119
+ </FI>
120
+ <INTU.BID>10898
121
+ </SONRS>
122
+ </SIGNONMSGSRSV1>
123
+ <BANKMSGSRSV1>
124
+ <STMTTRNRS>
125
+ <TRNUID>1
126
+ <STATUS>
127
+ <CODE>0
128
+ <SEVERITY>INFO
129
+ <MESSAGE>Success
130
+ </STATUS>
131
+ <STMTRS>
132
+ <CURDEF>USD
133
+ <BANKACCTFROM>
134
+ <BANKID>0713456513
135
+ <ACCTID>123456788
136
+ <ACCTTYPE>CHECKING
137
+ </BANKACCTFROM>
138
+ <BANKTRANLIST>
139
+ <DTSTART>20221003120000[0:GMT]
140
+ <DTEND>20221012120000[0:GMT]
141
+ <STMTTRN>
142
+ <TRNTYPE>DEBIT
143
+ <DTPOSTED>20221012120000[0:GMT]
144
+ <TRNAMT>-100.01
145
+ <FITID>202210120
146
+ <NAME>CAPITAL ONE N.A. CAPITALONE 0000
147
+ <MEMO>0234345d WEB ID: 15234563
148
+ </STMTTRN>
149
+ </BANKTRANLIST>
150
+ <LEDGERBAL>
151
+ <BALAMT>29.03
152
+ <DTASOF>20221013120000[0:GMT]
153
+ </LEDGERBAL>
154
+ <AVAILBAL>
155
+ <BALAMT>30.06
156
+ <DTASOF>20221013120000[0:GMT]
157
+ </AVAILBAL>
158
+ </STMTRS>
159
+ </STMTTRNRS>
160
+ </BANKMSGSRSV1>
161
+ </OFX>"
162
+
163
+ serializer = OFX::OFX102::Serializer.new
164
+ document = serializer.from_http_response_body(body)
165
+
166
+ # require 'pp'
167
+ # pp document
168
+
169
+ signon_message_set = document.message_sets[0]
170
+ signon_response = signon_message_set.responses[0]
171
+
172
+ signon_message_set = document.message_sets[0]
173
+ signon_response = signon_message_set.responses[0]
174
+
175
+ banking_statement_message_set = document.message_sets[1]
176
+ banking_statement_response = banking_statement_message_set.responses[0]
177
+
178
+ assert_equal '123456788', banking_statement_response.account.account_identifier
179
+ assert_equal '0713456513', banking_statement_response.account.bank_identifier
180
+ assert_equal '30.06', banking_statement_response.available_balance.amount
181
+ assert_equal '29.03', banking_statement_response.ledger_balance.amount
182
+
183
+ transactions = banking_statement_response.transactions
184
+ assert_equal 1, transactions.size
185
+
186
+ transaction = transactions.first
187
+ assert_equal '-100.01', transaction.amount.to_f.to_s
188
+ assert_equal 'CAPITAL ONE N.A. CAPITALONE 0000', transaction.payee
189
+ assert_equal :debit, transaction.transaction_type
190
+ end
191
+
192
+ def test_capitalone_ofx
193
+ body = <<-EOS
194
+ <?xml version="1.0" encoding="utf-8"?><?OFX OFXHEADER="200" VERSION="220" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?><OFX><SIGNONMSGSRSV1><SONRS><STATUS><CODE>0</CODE><SEVERITY>INFO</SEVERITY></STATUS><DTSERVER>20260627111438.091[-4:EDT]</DTSERVER><LANGUAGE>ENG</LANGUAGE><FI><ORG>Capital One Bank</ORG><FID>1001</FID></FI><INTU.BID>1236</INTU.BID></SONRS></SIGNONMSGSRSV1><BANKMSGSRSV1><STMTTRNRS><TRNUID>0</TRNUID><STATUS><CODE>0</CODE><SEVERITY>INFO</SEVERITY></STATUS><STMTRS><CURDEF>USD</CURDEF><BANKACCTFROM><BANKID>0123456789</BANKID><ACCTID>1234</ACCTID><ACCTTYPE>SAVINGS</ACCTTYPE></BANKACCTFROM><BANKTRANLIST><DTSTART>20260528000000.000[-4:EDT]</DTSTART><DTEND>20260627000000.000[-4:EDT]</DTEND><STMTTRN><TRNTYPE>CREDIT</TRNTYPE><DTPOSTED>20260626000000.000[-4:EDT]</DTPOSTED><TRNAMT>115</TRNAMT><FITID>20260626405</FITID><MEMO>Deposit from SOME PAYROLL</MEMO></STMTTRN></BANKTRANLIST><LEDGERBAL><BALAMT>200.31</BALAMT><DTASOF>20260627111438.091[-4:EDT]</DTASOF></LEDGERBAL></STMTRS></STMTTRNRS></BANKMSGSRSV1></OFX>
195
+ EOS
196
+
197
+ document = OFX::Serializer.from_http_response_body(body)
198
+
199
+ assert_equal OFX::Version.new('2.0.0'), document.header.header_version
200
+ assert_equal 'OFXXML', document.header.content_type
201
+ assert_equal OFX::Version.new('2.2.0'), document.header.document_version
202
+ assert_equal 'NONE', document.header.security
203
+ assert_equal 'NONE', document.header.previous_unique_identifier
204
+ assert_equal 'NONE', document.header.unique_identifier
205
+
206
+ signon_message_set = document.message_sets[0]
207
+ signon_response = signon_message_set.responses[0]
208
+
209
+ assert_equal 0, signon_response.status.code
210
+ assert_equal :information, signon_response.status.severity
211
+ assert_equal '20260627111438', signon_response.date.strftime('%Y%m%d%H%M%S')
212
+ assert_equal 'ENG', signon_response.language
213
+ assert_equal 'Capital One Bank', signon_response.financial_institution_identification.organization
214
+ assert_equal '1001', signon_response.financial_institution_identification.financial_institution_identifier
215
+
216
+ banking_statement_message_set = document.message_sets[1]
217
+ banking_statement_response = banking_statement_message_set.responses[0]
218
+
219
+ assert_equal '0', banking_statement_response.transaction_identifier
220
+ assert_equal 0, banking_statement_response.status.code
221
+ assert_equal :information, banking_statement_response.status.severity
222
+ assert_equal 'USD', banking_statement_response.default_currency
223
+ assert_equal '1234', banking_statement_response.account.account_identifier
224
+ assert_equal '0123456789', banking_statement_response.account.bank_identifier
225
+ assert_equal :savings, banking_statement_response.account.account_type
226
+ assert_nil banking_statement_response.available_balance
227
+ assert_equal '200.31', banking_statement_response.ledger_balance.amount
228
+ assert_equal '20260627111438', banking_statement_response.ledger_balance.as_of.strftime('%Y%m%d%H%M%S')
229
+ assert_equal '20260528000000', banking_statement_response.transaction_range.begin.strftime('%Y%m%d%H%M%S')
230
+ assert_equal '20260627000000', banking_statement_response.transaction_range.end.strftime('%Y%m%d%H%M%S')
231
+
232
+ transactions = banking_statement_response.transactions
233
+ assert_equal 1, transactions.size
234
+
235
+ transaction = transactions.first
236
+ assert_equal '115'.to_d, transaction.amount
237
+ assert_equal 'USD', transaction.currency
238
+ assert_equal '20260626000000', transaction.date_posted.strftime('%Y%m%d%H%M%S')
239
+ assert_equal '20260626405', transaction.financial_institution_transaction_identifier
240
+ assert_nil transaction.payee
241
+ assert_equal 'Deposit from SOME PAYROLL', transaction.memo
242
+ assert_equal :credit, transaction.transaction_type
243
+ end
244
+ end
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ofx_for_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Guidry
8
8
  - Patrick Bacon
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2020-12-27 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: activesupport
@@ -25,20 +24,62 @@ dependencies:
25
24
  - - ">="
26
25
  - !ruby/object:Gem::Version
27
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bigdecimal
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: logger
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rexml
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
28
69
  - !ruby/object:Gem::Dependency
29
70
  name: racc
30
71
  requirement: !ruby/object:Gem::Requirement
31
72
  requirements:
32
- - - '='
73
+ - - "~>"
33
74
  - !ruby/object:Gem::Version
34
- version: 1.4.6
75
+ version: '1.8'
35
76
  type: :development
36
77
  prerelease: false
37
78
  version_requirements: !ruby/object:Gem::Requirement
38
79
  requirements:
39
- - - '='
80
+ - - "~>"
40
81
  - !ruby/object:Gem::Version
41
- version: 1.4.6
82
+ version: '1.8'
42
83
  - !ruby/object:Gem::Dependency
43
84
  name: rexical
44
85
  requirement: !ruby/object:Gem::Requirement
@@ -53,6 +94,34 @@ dependencies:
53
94
  - - '='
54
95
  - !ruby/object:Gem::Version
55
96
  version: 1.0.5
97
+ - !ruby/object:Gem::Dependency
98
+ name: getoptlong
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: ostruct
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
56
125
  - !ruby/object:Gem::Dependency
57
126
  name: pry
58
127
  requirement: !ruby/object:Gem::Requirement
@@ -81,6 +150,20 @@ dependencies:
81
150
  - - ">="
82
151
  - !ruby/object:Gem::Version
83
152
  version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: minitest
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '5.27'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '5.27'
84
167
  description: OFX for Ruby is a pure Ruby implementation of Open Financial Exchange
85
168
  specifications (1.0.2 through 2.1.1) for building both financial clients and servers,
86
169
  providing parsers/serializers for each version, and a uniform object model across
@@ -124,6 +207,8 @@ files:
124
207
  - lib/ofx/1.0.2/statements.rb
125
208
  - lib/ofx/1.0.2/status.rb
126
209
  - lib/ofx/1.0.2/wire_funds_transfer_message_set.rb
210
+ - lib/ofx/2.0.0/serializer.rb
211
+ - lib/ofx/2.0.0/xml_parser.rb
127
212
  - lib/ofx/banking_message_set.rb
128
213
  - lib/ofx/credit_card_statement_message_set.rb
129
214
  - lib/ofx/document.rb
@@ -167,11 +252,12 @@ files:
167
252
  - test/citi/test_signup_account_information.rb
168
253
  - test/test_helper.rb
169
254
  - test/test_ofx_version.rb
255
+ - test/test_ofx_xml_parser.rb
170
256
  - test/test_parser.rb
257
+ - test/test_serializer.rb
171
258
  homepage: http://github.com/baconpat/ofx_for_ruby
172
259
  licenses: []
173
260
  metadata: {}
174
- post_install_message:
175
261
  rdoc_options: []
176
262
  require_paths:
177
263
  - lib
@@ -186,9 +272,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
272
  - !ruby/object:Gem::Version
187
273
  version: '0'
188
274
  requirements: []
189
- rubyforge_project:
190
- rubygems_version: 2.6.12
191
- signing_key:
275
+ rubygems_version: 3.6.9
192
276
  specification_version: 4
193
277
  summary: Pure Ruby implementation of Open Financial Exchange specifications
194
278
  test_files: []