salt-parser 0.0.1
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 +11 -0
- data/.rspec +1 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +88 -0
- data/README.rdoc +39 -0
- data/Rakefile +7 -0
- data/lib/ofx/account.rb +28 -0
- data/lib/ofx/accounts.rb +9 -0
- data/lib/ofx/balance.rb +15 -0
- data/lib/ofx/builder.rb +61 -0
- data/lib/ofx/dependencies.rb +13 -0
- data/lib/ofx/parser/base.rb +366 -0
- data/lib/ofx/parser/ofx102.rb +28 -0
- data/lib/ofx/parser/ofx211.rb +21 -0
- data/lib/ofx/sign_on.rb +18 -0
- data/lib/ofx/transaction.rb +28 -0
- data/lib/qif/account.rb +19 -0
- data/lib/qif/accounts.rb +54 -0
- data/lib/qif/builder.rb +37 -0
- data/lib/qif/dependencies.rb +8 -0
- data/lib/qif/parser.rb +68 -0
- data/lib/qif/transaction.rb +28 -0
- data/lib/salt-parser/accounts.rb +11 -0
- data/lib/salt-parser/base.rb +19 -0
- data/lib/salt-parser/builder.rb +24 -0
- data/lib/salt-parser/errors.rb +14 -0
- data/lib/salt-parser/version.rb +8 -0
- data/salt-parser.gemspec +24 -0
- data/spec/ofx/account_spec.rb +97 -0
- data/spec/ofx/accounts_response_spec.rb +45 -0
- data/spec/ofx/accounts_spec.rb +34 -0
- data/spec/ofx/balance_spec.rb +32 -0
- data/spec/ofx/builder_spec.rb +136 -0
- data/spec/ofx/error_request_spec.rb +37 -0
- data/spec/ofx/fixtures/accounts_partial.ofx +52 -0
- data/spec/ofx/fixtures/accounts_request.ofx +11 -0
- data/spec/ofx/fixtures/accounts_response.ofx +109 -0
- data/spec/ofx/fixtures/avatar.gif +0 -0
- data/spec/ofx/fixtures/bb.ofx +700 -0
- data/spec/ofx/fixtures/credit_card_response.ofx +52 -0
- data/spec/ofx/fixtures/creditcard.ofx +79 -0
- data/spec/ofx/fixtures/creditcard_transactions_request.ofx +11 -0
- data/spec/ofx/fixtures/creditcards_partial.ofx +85 -0
- data/spec/ofx/fixtures/date_missing.ofx +73 -0
- data/spec/ofx/fixtures/empty_balance.ofx +44 -0
- data/spec/ofx/fixtures/invalid_version.ofx +308 -0
- data/spec/ofx/fixtures/investment_transactions_response.ofx +108 -0
- data/spec/ofx/fixtures/investment_transactions_response2.ofx +200 -0
- data/spec/ofx/fixtures/investments_with_mkval.ofx +99 -0
- data/spec/ofx/fixtures/missing_headers.ofx +47 -0
- data/spec/ofx/fixtures/mixed_accountinfo_response.ofx +58 -0
- data/spec/ofx/fixtures/ms_money.ofx +52 -0
- data/spec/ofx/fixtures/request_error.ofx +39 -0
- data/spec/ofx/fixtures/request_error2.ofx +39 -0
- data/spec/ofx/fixtures/request_error3.ofx +36 -0
- data/spec/ofx/fixtures/sample_examples/sample_401K_loan.qfx +651 -0
- data/spec/ofx/fixtures/sample_examples/sample_banking.qbo +258 -0
- data/spec/ofx/fixtures/sample_examples/sample_banking.qfx +258 -0
- data/spec/ofx/fixtures/sample_examples/sample_banking_multiacct.qfx +284 -0
- data/spec/ofx/fixtures/sample_examples/sample_credit_card.qfx +257 -0
- data/spec/ofx/fixtures/sample_examples/sample_investment.qfx +654 -0
- data/spec/ofx/fixtures/transactions_empty.ofx +60 -0
- data/spec/ofx/fixtures/utf8.ofx +65 -0
- data/spec/ofx/fixtures/v102.ofx +314 -0
- data/spec/ofx/fixtures/v202.ofx +22 -0
- data/spec/ofx/fixtures/v211.ofx +85 -0
- data/spec/ofx/investment_accounts_spec.rb +70 -0
- data/spec/ofx/ofx102_spec.rb +44 -0
- data/spec/ofx/ofx211_spec.rb +68 -0
- data/spec/ofx/ofx_parser_spec.rb +100 -0
- data/spec/ofx/sign_on_spec.rb +49 -0
- data/spec/ofx/transaction_spec.rb +157 -0
- data/spec/qif/account_spec.rb +42 -0
- data/spec/qif/fixtures/3_records_ddmmyy.qif +19 -0
- data/spec/qif/fixtures/3_records_ddmmyyyy.qif +19 -0
- data/spec/qif/fixtures/3_records_dmyy.qif +19 -0
- data/spec/qif/fixtures/3_records_invalid_header.qif +20 -0
- data/spec/qif/fixtures/3_records_mdyy.qif +19 -0
- data/spec/qif/fixtures/3_records_mmddyy.qif +19 -0
- data/spec/qif/fixtures/3_records_mmddyyyy.qif +19 -0
- data/spec/qif/fixtures/3_records_spaced.qif +19 -0
- data/spec/qif/fixtures/bank_account.qif +19 -0
- data/spec/qif/fixtures/empty_body.qif +1 -0
- data/spec/qif/fixtures/empty_header.qif +18 -0
- data/spec/qif/fixtures/incompatible_date_formats.qif +13 -0
- data/spec/qif/fixtures/invalid_date_format.qif +7 -0
- data/spec/qif/fixtures/not_a_QIF_file.txt +3 -0
- data/spec/qif/fixtures/quicken_non_investement_account.qif +30 -0
- data/spec/qif/fixtures/unknown_account.qif +20 -0
- data/spec/qif/fixtures/various_date_format.qif +19 -0
- data/spec/qif/fixtures/with_categories_list.qif +8669 -0
- data/spec/qif/parser_spec.rb +156 -0
- data/spec/qif/transaction_spec.rb +18 -0
- data/spec/spec_helper.rb +37 -0
- data/spec/support/fixture.rb +9 -0
- metadata +208 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ba5f5e0a409feb645962c2649710323a47534b8c
|
4
|
+
data.tar.gz: 43f111a2394f816e7b6905931eef337a95dd8875
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 595e2860944ab5083e02b8146eaf6d1be427ad3a061f28e3ad2350eb858eb2b9fcc4dd22732888a99adadf4d5f39a8d38481b50d0fd18b419eb184e299907983
|
7
|
+
data.tar.gz: f909cf1b0a524d9fe9904b77c6f78baef50f3f13fb81d6cba3fe044ca5c782778be7598055acb2199d026a8f1285351801ad3067b955cf568a4a8584e3498860
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
group :test do
|
4
|
+
gem 'timecop'
|
5
|
+
gem 'rspec'
|
6
|
+
gem 'simplecov', require: false
|
7
|
+
end
|
8
|
+
|
9
|
+
gem 'activesupport'
|
10
|
+
gem 'chronic'
|
11
|
+
gem 'rest-client'
|
12
|
+
gem 'rack'
|
13
|
+
gem 'yajl-ruby', require: 'yajl'
|
14
|
+
gem 'nokogiri'
|
15
|
+
gem 'pry-byebug', "~> 1.3.2"
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activesupport (4.2.3)
|
5
|
+
i18n (~> 0.7)
|
6
|
+
json (~> 1.7, >= 1.7.7)
|
7
|
+
minitest (~> 5.1)
|
8
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
9
|
+
tzinfo (~> 1.1)
|
10
|
+
byebug (2.7.0)
|
11
|
+
columnize (~> 0.3)
|
12
|
+
debugger-linecache (~> 1.2)
|
13
|
+
chronic (0.10.2)
|
14
|
+
coderay (1.1.0)
|
15
|
+
columnize (0.9.0)
|
16
|
+
debugger-linecache (1.2.0)
|
17
|
+
diff-lcs (1.2.5)
|
18
|
+
docile (1.1.5)
|
19
|
+
domain_name (0.5.24)
|
20
|
+
unf (>= 0.0.5, < 1.0.0)
|
21
|
+
http-cookie (1.0.2)
|
22
|
+
domain_name (~> 0.5)
|
23
|
+
i18n (0.7.0)
|
24
|
+
json (1.8.3)
|
25
|
+
method_source (0.8.2)
|
26
|
+
mime-types (2.6.1)
|
27
|
+
mini_portile (0.6.2)
|
28
|
+
minitest (5.8.0)
|
29
|
+
netrc (0.10.3)
|
30
|
+
nokogiri (1.6.6.2)
|
31
|
+
mini_portile (~> 0.6.0)
|
32
|
+
pry (0.10.1)
|
33
|
+
coderay (~> 1.1.0)
|
34
|
+
method_source (~> 0.8.1)
|
35
|
+
slop (~> 3.4)
|
36
|
+
pry-byebug (1.3.3)
|
37
|
+
byebug (~> 2.7)
|
38
|
+
pry (~> 0.10)
|
39
|
+
rack (1.6.4)
|
40
|
+
rest-client (1.8.0)
|
41
|
+
http-cookie (>= 1.0.2, < 2.0)
|
42
|
+
mime-types (>= 1.16, < 3.0)
|
43
|
+
netrc (~> 0.7)
|
44
|
+
rspec (3.3.0)
|
45
|
+
rspec-core (~> 3.3.0)
|
46
|
+
rspec-expectations (~> 3.3.0)
|
47
|
+
rspec-mocks (~> 3.3.0)
|
48
|
+
rspec-core (3.3.2)
|
49
|
+
rspec-support (~> 3.3.0)
|
50
|
+
rspec-expectations (3.3.1)
|
51
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
52
|
+
rspec-support (~> 3.3.0)
|
53
|
+
rspec-mocks (3.3.2)
|
54
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
55
|
+
rspec-support (~> 3.3.0)
|
56
|
+
rspec-support (3.3.0)
|
57
|
+
simplecov (0.10.0)
|
58
|
+
docile (~> 1.1.0)
|
59
|
+
json (~> 1.8)
|
60
|
+
simplecov-html (~> 0.10.0)
|
61
|
+
simplecov-html (0.10.0)
|
62
|
+
slop (3.6.0)
|
63
|
+
thread_safe (0.3.5)
|
64
|
+
timecop (0.8.0)
|
65
|
+
tzinfo (1.2.2)
|
66
|
+
thread_safe (~> 0.1)
|
67
|
+
unf (0.1.4)
|
68
|
+
unf_ext
|
69
|
+
unf_ext (0.0.7.1)
|
70
|
+
yajl-ruby (1.2.1)
|
71
|
+
|
72
|
+
PLATFORMS
|
73
|
+
ruby
|
74
|
+
|
75
|
+
DEPENDENCIES
|
76
|
+
activesupport
|
77
|
+
chronic
|
78
|
+
nokogiri
|
79
|
+
pry-byebug (~> 1.3.2)
|
80
|
+
rack
|
81
|
+
rest-client
|
82
|
+
rspec
|
83
|
+
simplecov
|
84
|
+
timecop
|
85
|
+
yajl-ruby
|
86
|
+
|
87
|
+
BUNDLED WITH
|
88
|
+
1.10.6
|
data/README.rdoc
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
= Salt Parser
|
2
|
+
|
3
|
+
Library for parsing OFX, HBCI, QIF and SWIFT formats.
|
4
|
+
|
5
|
+
TODOS:
|
6
|
+
- Add SWIFT parser
|
7
|
+
- Add HBCI parser
|
8
|
+
- Add code examples
|
9
|
+
|
10
|
+
== Credits
|
11
|
+
|
12
|
+
Special thanks to:
|
13
|
+
|
14
|
+
- @annacruz (ofx)[https://github.com/annacruz/ofx]
|
15
|
+
- @jemmyw (qif)[https://github.com/jemmyw/Qif]
|
16
|
+
|
17
|
+
|
18
|
+
== License
|
19
|
+
|
20
|
+
(The MIT License)
|
21
|
+
|
22
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
23
|
+
a copy of this software and associated documentation files (the
|
24
|
+
'Software'), to deal in the Software without restriction, including
|
25
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
26
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
27
|
+
permit persons to whom the Software is furnished to do so, subject to
|
28
|
+
the following conditions:
|
29
|
+
|
30
|
+
The above copyright notice and this permission notice shall be
|
31
|
+
included in all copies or substantial portions of the Software.
|
32
|
+
|
33
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
34
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
35
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
36
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
37
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
38
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
39
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/lib/ofx/account.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module SaltParser
|
2
|
+
module Ofx
|
3
|
+
class Account < SaltParser::Base
|
4
|
+
attr_accessor :balance, :bank_id, :broker_id, :currency, :id, :name,
|
5
|
+
:transactions, :type, :units, :unit_price, :available_balance
|
6
|
+
|
7
|
+
def identifier
|
8
|
+
id
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_hash
|
12
|
+
{
|
13
|
+
:balance => balance ? balance.to_hash : nil,
|
14
|
+
:bank_id => bank_id,
|
15
|
+
:broker_id => broker_id,
|
16
|
+
:currency => currency,
|
17
|
+
:id => id,
|
18
|
+
:name => name,
|
19
|
+
:transactions => transactions.map(&:to_hash),
|
20
|
+
:type => type,
|
21
|
+
:units => units,
|
22
|
+
:unit_price => unit_price,
|
23
|
+
:available_balance => available_balance ? available_balance.to_hash : nil
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/ofx/accounts.rb
ADDED
data/lib/ofx/balance.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module SaltParser
|
2
|
+
module Ofx
|
3
|
+
class Balance < SaltParser::Base
|
4
|
+
attr_accessor :amount, :amount_in_pennies, :posted_at
|
5
|
+
|
6
|
+
def to_hash
|
7
|
+
{
|
8
|
+
:amount => amount,
|
9
|
+
:amount_in_pennies => amount_in_pennies,
|
10
|
+
:posted_at => posted_at
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/ofx/builder.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
module SaltParser
|
2
|
+
module Ofx
|
3
|
+
class Builder < SaltParser::Builder
|
4
|
+
|
5
|
+
def initialize(resource)
|
6
|
+
resource = open_resource(resource)
|
7
|
+
resource.rewind
|
8
|
+
begin
|
9
|
+
@content = convert_to_utf8(resource.read)
|
10
|
+
prepare(content)
|
11
|
+
rescue Exception
|
12
|
+
raise SaltParser::Error::UnsupportedFileError
|
13
|
+
end
|
14
|
+
|
15
|
+
@parser = case headers["VERSION"]
|
16
|
+
when /102|103/ then
|
17
|
+
SaltParser::Ofx::Parser::OFX102.new(:headers => headers, :body => body)
|
18
|
+
when /200|202|211/ then
|
19
|
+
SaltParser::Ofx::Parser::OFX211.new(:headers => headers, :body => body)
|
20
|
+
else
|
21
|
+
raise SaltParser::Error::UnsupportedFileError
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def prepare(content)
|
28
|
+
# split headers & body
|
29
|
+
header_text, body_text = content.dup.split(/<OFX>/, 2)
|
30
|
+
header_text.gsub!("encoding=\"USASCII\"", "encoding=\"US-ASCII\"") if header_text.include?("encoding=\"USASCII\"")
|
31
|
+
|
32
|
+
raise SaltParser::Error::UnsupportedFileError unless body_text
|
33
|
+
|
34
|
+
@headers = extract_headers(header_text)
|
35
|
+
@body = extract_body(body_text)
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract_headers(header_text)
|
39
|
+
# Header format is different among versions. Give each
|
40
|
+
# parser a chance to parse the headers.
|
41
|
+
headers = nil
|
42
|
+
|
43
|
+
SaltParser::Ofx::Parser.constants.grep(/OFX/).each do |name|
|
44
|
+
headers = SaltParser::Ofx::Parser.const_get(name).parse_headers(header_text)
|
45
|
+
break if headers
|
46
|
+
end
|
47
|
+
|
48
|
+
raise SaltParser::Error::UnsupportedFileError if headers.empty?
|
49
|
+
headers
|
50
|
+
end
|
51
|
+
|
52
|
+
def extract_body(body_text)
|
53
|
+
# Replace body tags to parse it with Nokogiri
|
54
|
+
body_text.gsub!(/>\s+</m, "><")
|
55
|
+
body_text.gsub!(/\s+</m, "<")
|
56
|
+
body_text.gsub!(/>\s+/m, ">")
|
57
|
+
body_text.gsub!(/<(\w+?)>([^<]+)/m, '<\1>\2</\1>')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
require "bigdecimal"
|
3
|
+
require "kconv"
|
4
|
+
|
5
|
+
require_relative "builder"
|
6
|
+
require_relative "parser/base"
|
7
|
+
require_relative "parser/ofx102"
|
8
|
+
require_relative "parser/ofx211"
|
9
|
+
require_relative "balance"
|
10
|
+
require_relative "account"
|
11
|
+
require_relative "accounts"
|
12
|
+
require_relative "sign_on"
|
13
|
+
require_relative "transaction"
|
@@ -0,0 +1,366 @@
|
|
1
|
+
module SaltParser
|
2
|
+
module Ofx
|
3
|
+
module Parser
|
4
|
+
class Base
|
5
|
+
ACCOUNT_TYPES = {
|
6
|
+
"CHECKING" => :checking,
|
7
|
+
"SAVINGS" => :savings,
|
8
|
+
"CREDITCARD" => :credit_card,
|
9
|
+
"CREDITLINE" => :credit,
|
10
|
+
"INVESTMENT" => :investment,
|
11
|
+
"MONEYMRKT" => :savings
|
12
|
+
}
|
13
|
+
|
14
|
+
TRANSACTION_TYPES = [
|
15
|
+
'ATM', 'CASH', 'CHECK', 'CREDIT', 'DEBIT', 'DEP', 'DIRECTDEBIT', 'DIRECTDEP', 'DIV',
|
16
|
+
'FEE', 'INT', 'OTHER', 'PAYMENT', 'POS', 'REPEATPMT', 'SRVCHG', 'XFER'
|
17
|
+
].inject({}) { |hash, tran_type| hash[tran_type] = tran_type.downcase.to_sym; hash }
|
18
|
+
|
19
|
+
attr_reader :headers, :body, :html, :errors, :accounts, :sign_on
|
20
|
+
|
21
|
+
def initialize(options = {})
|
22
|
+
@headers = options[:headers]
|
23
|
+
@body = options[:body]
|
24
|
+
@html = Nokogiri::HTML.parse(body)
|
25
|
+
@errors = []
|
26
|
+
build_sign_on
|
27
|
+
build_accounts
|
28
|
+
check_for_errors
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_hash
|
32
|
+
{
|
33
|
+
:errors => errors,
|
34
|
+
:sign_on => sign_on.to_hash,
|
35
|
+
:accounts => accounts.to_hash
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_accounts
|
40
|
+
@accounts = Ofx::Accounts.new
|
41
|
+
build_bank_account
|
42
|
+
build_credit_card_account
|
43
|
+
build_investments_account
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def check_for_errors
|
49
|
+
statuses = html.search("status").reverse
|
50
|
+
statuses.each do |status|
|
51
|
+
if status.search("severity").inner_text == "ERROR"
|
52
|
+
errors << SaltParser::Error::RequestError.new(["[#{status.search("code").inner_text}]", status.search("message").inner_text].join(" ").strip)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def build_bank_account
|
58
|
+
html.search("stmttrnrs", "bankacctinfo").each do |account|
|
59
|
+
begin
|
60
|
+
account_id = account.search("bankacctfrom > acctid").inner_text
|
61
|
+
|
62
|
+
@accounts << SaltParser::Ofx::Account.new(
|
63
|
+
:bank_id => account.search("bankacctfrom > bankid").inner_text,
|
64
|
+
:id => account_id,
|
65
|
+
:name => account.parent.search("desc").inner_text,
|
66
|
+
:type => ACCOUNT_TYPES[account.search("bankacctfrom > accttype").inner_text.to_s.upcase],
|
67
|
+
:transactions => build_transactions(account.search("banktranlist > stmttrn"), account_id),
|
68
|
+
:balance => build_balance(account),
|
69
|
+
:available_balance => build_available_balance(account),
|
70
|
+
:currency => account.search("stmtrs > curdef").inner_text
|
71
|
+
)
|
72
|
+
rescue SaltParser::Error::ParseError => error
|
73
|
+
errors << error
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def build_credit_card_account
|
79
|
+
html.search("ccstmttrnrs", "acctinfo").each do |account|
|
80
|
+
begin
|
81
|
+
account_id = account.search("ccacctfrom > acctid").inner_text
|
82
|
+
next if account_id.blank?
|
83
|
+
@accounts << SaltParser::Ofx::Account.new(
|
84
|
+
:id => account_id,
|
85
|
+
:name => account.search("desc").inner_text,
|
86
|
+
:type => ACCOUNT_TYPES["CREDITCARD"],
|
87
|
+
:transactions => build_transactions(account.search("banktranlist > stmttrn"), account_id),
|
88
|
+
:balance => build_balance(account),
|
89
|
+
:currency => account.search("curdef").inner_text
|
90
|
+
)
|
91
|
+
rescue SaltParser::Error::ParseError => error
|
92
|
+
errors << error
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_investments_account
|
98
|
+
html.search("invstmttrnrs", "acctinfo").each do |account|
|
99
|
+
begin
|
100
|
+
account_id = account.search("invacctfrom > acctid").inner_text
|
101
|
+
broker_id = account.search("invacctfrom > brokerid").inner_text
|
102
|
+
|
103
|
+
next if broker_id.blank? or account_id.blank?
|
104
|
+
@accounts << SaltParser::Ofx::Account.new(
|
105
|
+
:id => account_id,
|
106
|
+
:broker_id => broker_id,
|
107
|
+
:type => ACCOUNT_TYPES["INVESTMENT"],
|
108
|
+
:transactions => build_investment_transactions(account.search("invtranlist"), account_id),
|
109
|
+
:balance => build_investment_balance(account),
|
110
|
+
:currency => account.search("curdef").inner_text,
|
111
|
+
:units => compute_investment_units(account),
|
112
|
+
:unit_price => compute_investment_unit_price(account)
|
113
|
+
)
|
114
|
+
rescue SaltParser::Error::ParseError => error
|
115
|
+
errors << error
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def build_transactions(transactions, account_id)
|
121
|
+
transactions.each_with_object([]) do |transaction, transactions|
|
122
|
+
begin
|
123
|
+
transactions << build_transaction(transaction, account_id)
|
124
|
+
rescue SaltParser::Error::ParseError => error
|
125
|
+
errors << error
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def build_transaction(transaction, account_id)
|
131
|
+
SaltParser::Ofx::Transaction.new(
|
132
|
+
:amount => build_amount(transaction),
|
133
|
+
:amount_in_pennies => ((build_amount(transaction) * 100).round 2).to_i,
|
134
|
+
:fit_id => transaction.search("fitid").inner_text,
|
135
|
+
:memo => transaction.search("memo").inner_text,
|
136
|
+
:name => transaction.search("name").inner_text,
|
137
|
+
:payee => transaction.search("payee").inner_text,
|
138
|
+
:check_number => transaction.search("checknum").inner_text,
|
139
|
+
:ref_number => transaction.search("refnum").inner_text,
|
140
|
+
:posted_at => build_date(transaction.search("dtposted").inner_text),
|
141
|
+
:type => build_type(transaction),
|
142
|
+
:sic => transaction.search("sic").inner_text,
|
143
|
+
:account_id => account_id
|
144
|
+
)
|
145
|
+
end
|
146
|
+
|
147
|
+
def build_investment_transactions(transactions_xml, account_id)
|
148
|
+
transactions = transactions_xml.search("stmttrn", "invtran")
|
149
|
+
transactions.each_with_object([]) do |transaction, transactions|
|
150
|
+
begin
|
151
|
+
if transaction.name.include?("stmttrn")
|
152
|
+
transactions << build_investment_transaction(transaction, account_id)
|
153
|
+
else
|
154
|
+
transactions << build_investment_transaction(transaction.parent, account_id)
|
155
|
+
end
|
156
|
+
rescue SaltParser::Error::ParseError => error
|
157
|
+
errors << error
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def build_investment_transaction(transaction, account_id)
|
163
|
+
SaltParser::Ofx::Transaction.new(
|
164
|
+
:amount => build_investment_amount(transaction),
|
165
|
+
:amount_in_pennies => ((build_investment_amount(transaction) * 100).round 2).to_i,
|
166
|
+
:fit_id => transaction.search("fitid").inner_text,
|
167
|
+
:memo => transaction.search("memo").inner_text,
|
168
|
+
:name => transaction.search("name").inner_text,
|
169
|
+
:posted_at => build_date(transaction.search("dtposted", "dttrade").inner_text),
|
170
|
+
:type => build_type(transaction),
|
171
|
+
:ref_number => transaction.search("refnum", "uniqueid").empty? ? "N/A" : transaction.search("refnum", "uniqueid").inner_text,
|
172
|
+
:account_id => account_id,
|
173
|
+
:units => parse_float(transaction.search("units").inner_text),
|
174
|
+
:unit_price => parse_float(transaction.search("unitprice").inner_text)
|
175
|
+
)
|
176
|
+
end
|
177
|
+
|
178
|
+
def build_sign_on
|
179
|
+
@sign_on = SaltParser::Ofx::SignOn.new(
|
180
|
+
:language => html.search("signonmsgsrsv1 > sonrs > language").inner_text,
|
181
|
+
:fi_id => html.search("signonmsgsrsv1 > sonrs > fi > fid").inner_text,
|
182
|
+
:fi_name => html.search("signonmsgsrsv1 > sonrs > fi > org").inner_text,
|
183
|
+
:code => html.search("signonmsgsrsv1 > sonrs > status > code").inner_text,
|
184
|
+
:severity => html.search("signonmsgsrsv1 > sonrs > status > severity").inner_text,
|
185
|
+
:message => html.search("signonmsgsrsv1 > sonrs > status > message").inner_text
|
186
|
+
)
|
187
|
+
end
|
188
|
+
|
189
|
+
def build_balance(account)
|
190
|
+
return nil unless account.search("ledgerbal > balamt").size > 0
|
191
|
+
|
192
|
+
if account.search("ledgerbal > balamt").inner_text.match(/[\d]{14}\.[\d]+/)
|
193
|
+
SaltParser::Ofx::Balance.new(
|
194
|
+
:amount => 0.0,
|
195
|
+
:amount_in_pennies => 0,
|
196
|
+
:posted_at => build_date(account.search("ledgerbal > balamt").inner_text)
|
197
|
+
)
|
198
|
+
else
|
199
|
+
amount = parse_float(account.search("ledgerbal > balamt").inner_text)
|
200
|
+
|
201
|
+
SaltParser::Ofx::Balance.new(
|
202
|
+
:amount => amount,
|
203
|
+
:amount_in_pennies => ((amount * 100).round 2).to_i,
|
204
|
+
:posted_at => build_date(account.search("ledgerbal > dtasof").inner_text)
|
205
|
+
)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def build_available_balance(account)
|
210
|
+
return nil unless account.search("availbal").size > 0
|
211
|
+
|
212
|
+
if account.search("availbal > balamt").inner_text.match(/[\d]{14}\.[\d]+/)
|
213
|
+
SaltParser::Ofx::Balance.new(
|
214
|
+
:amount => 0.0,
|
215
|
+
:amount_in_pennies => 0,
|
216
|
+
:posted_at => build_date(account.search("availbal > balamt").inner_text)
|
217
|
+
)
|
218
|
+
else
|
219
|
+
amount = parse_float(account.search("availbal > balamt").inner_text)
|
220
|
+
|
221
|
+
SaltParser::Ofx::Balance.new(
|
222
|
+
:amount => amount,
|
223
|
+
:amount_in_pennies => ((amount * 100).round 2).to_i,
|
224
|
+
:posted_at => build_date(account.search("availbal > dtasof").inner_text)
|
225
|
+
)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def build_investment_balance(account)
|
230
|
+
if account.search("invbal > availcash").size > 0 && account.search("invpos > mktval").size < 1
|
231
|
+
amount = parse_float(account.search("invbal > availcash").inner_text)
|
232
|
+
|
233
|
+
elsif account.search("invbal > availcash").size < 1 && account.search("invpos > mktval").size > 0
|
234
|
+
amount = 0
|
235
|
+
account.search("invpos > mktval").map do |mktval|
|
236
|
+
amount += parse_float(mktval.inner_text)
|
237
|
+
end
|
238
|
+
amount
|
239
|
+
|
240
|
+
elsif account.search("invbal > availcash").size > 0 && account.search("invpos > mktval").size > 0
|
241
|
+
amount = 0
|
242
|
+
account.search("invbal > availcash").map do |availcash|
|
243
|
+
amount += parse_float(availcash.inner_text)
|
244
|
+
end
|
245
|
+
account.search("invpos > mktval").map do |mktval|
|
246
|
+
amount += parse_float(mktval.inner_text)
|
247
|
+
end
|
248
|
+
amount
|
249
|
+
|
250
|
+
else
|
251
|
+
return nil
|
252
|
+
end
|
253
|
+
|
254
|
+
SaltParser::Ofx::Balance.new(
|
255
|
+
:amount => amount,
|
256
|
+
:amount_in_pennies => ((amount * 100).round 2).to_i
|
257
|
+
)
|
258
|
+
end
|
259
|
+
|
260
|
+
def compute_investment_units(account)
|
261
|
+
if account.search("invpos > units").size == 1
|
262
|
+
parse_float(account.search("invpos > units").inner_text)
|
263
|
+
else
|
264
|
+
0.0
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def compute_investment_unit_price(account)
|
269
|
+
if account.search("invpos > unitprice").size == 1
|
270
|
+
parse_float(account.search("invpos > unitprice").inner_text)
|
271
|
+
else
|
272
|
+
0.0
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def build_type(element)
|
277
|
+
TRANSACTION_TYPES[element.search("trntype", "incometype").inner_text.to_s.upcase]
|
278
|
+
end
|
279
|
+
|
280
|
+
def build_amount(element)
|
281
|
+
parse_float(element.search("trnamt", "total").inner_text)
|
282
|
+
rescue TypeError => error
|
283
|
+
raise SaltParser::Ofx::ParseError.new(SaltParser::Ofx::ParseError::AMOUNT)
|
284
|
+
end
|
285
|
+
|
286
|
+
def build_investment_amount(element)
|
287
|
+
if element.parent.search("invbuy", "invsell").size > 0
|
288
|
+
-1 * parse_float(element.search("total").inner_text)
|
289
|
+
else
|
290
|
+
build_amount(element)
|
291
|
+
end
|
292
|
+
rescue TypeError => error
|
293
|
+
raise SaltParser::Error::ParseError.new(SaltParser::Error::ParseError::AMOUNT)
|
294
|
+
end
|
295
|
+
|
296
|
+
def build_date(date)
|
297
|
+
_, year, month, day, hour, minutes, seconds = *date.match(/(\d{4})(\d{2})(\d{2})(?:(\d{2})(\d{2})(\d{2}))?/)
|
298
|
+
|
299
|
+
date = "#{year}-#{month}-#{day} "
|
300
|
+
date << "#{hour}:#{minutes}:#{seconds}" if hour && minutes && seconds
|
301
|
+
|
302
|
+
Time.parse(date)
|
303
|
+
rescue TypeError, ArgumentError => error
|
304
|
+
raise SaltParser::Error::ParseError.new(SaltParser::Error::ParseError::TIME)
|
305
|
+
end
|
306
|
+
|
307
|
+
def parse_float(incoming, options={})
|
308
|
+
return incoming if incoming.is_a?(Float)
|
309
|
+
string = incoming.dup
|
310
|
+
sanitize_float_string!(string)
|
311
|
+
|
312
|
+
if options[:integral]
|
313
|
+
string.gsub!(",", "")
|
314
|
+
string.gsub!(".", "")
|
315
|
+
return string.to_f
|
316
|
+
end
|
317
|
+
|
318
|
+
indexes = {
|
319
|
+
"," => string.rindex(","),
|
320
|
+
"." => string.rindex(".")
|
321
|
+
}
|
322
|
+
|
323
|
+
return string.to_f if indexes["."].nil? && indexes[","].nil?
|
324
|
+
|
325
|
+
if indexes["."] == nil
|
326
|
+
if string.scan(/,/).size > 1
|
327
|
+
string.gsub!(",", "") # 123,123,123
|
328
|
+
else
|
329
|
+
string.gsub!(",", ".") # 123,123
|
330
|
+
end
|
331
|
+
return string.to_f
|
332
|
+
end
|
333
|
+
|
334
|
+
if indexes[","] == nil
|
335
|
+
string.gsub!(".", "") if string.scan(/\./).size > 1 # 123.123.123
|
336
|
+
return string.to_f
|
337
|
+
end
|
338
|
+
|
339
|
+
if indexes[","] > indexes["."]
|
340
|
+
# comma is decimal separator
|
341
|
+
string.gsub!(".", "")
|
342
|
+
string.gsub!(",", ".")
|
343
|
+
else
|
344
|
+
# dot is decimal separator
|
345
|
+
string.gsub!(",", "")
|
346
|
+
end
|
347
|
+
|
348
|
+
string.to_f
|
349
|
+
rescue => error
|
350
|
+
raise SaltParser::Error::ParseError.new(SaltParser::Error::ParseError::FLOAT)
|
351
|
+
end
|
352
|
+
|
353
|
+
def sanitize_float_string!(string)
|
354
|
+
# replace weird minus sign with proper minus
|
355
|
+
string.gsub!(8211.chr(Encoding::UTF_8), "-")
|
356
|
+
# replace an even weirder minus sign with proper minus
|
357
|
+
string.gsub!(8722.chr(Encoding::UTF_8), "-")
|
358
|
+
# remove everything except digits, dots, commas, '+', '-'
|
359
|
+
string.gsub!(/[^0-9\-+.,]/, "")
|
360
|
+
# remove trailing non digits
|
361
|
+
string.gsub!(/[-+.,]+$/, "")
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|