banker-ofx 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ coverage
6
+ *.un~
7
+ *.log
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rvmrc ADDED
@@ -0,0 +1,2 @@
1
+ rvm use ruby-1.9.2@banker-ofx --create
2
+ rvm use ruby-1.9.3@banker-ofx --create
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ script: rspec
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in banker.gemspec
4
+ gemspec
5
+
6
+ gem 'guard-rspec'
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ guard 'rspec', :notification => true, :version => 2, :cli => "--colour", :run_all => {:cli => '--profile'} do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec/" }
5
+ end
6
+
7
+ guard 'bundler' do
8
+ watch('Gemfile')
9
+ watch(/^.+\.gemspec/)
10
+ end
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Banker [![CI Build Status](https://secure.travis-ci.org/BritRuby/Banker-OFX.png?branch=master)][travis] [![Dependency Status](https://gemnasium.com/BritRuby/Banker-OFX.png?travis)][gemnasium]
2
+
3
+ [travis]:http://travis-ci.org/BritRuby/Banker-OFX
4
+ [gemnasium]:https://gemnasium.com/BritRuby/Banker-OFX
5
+
6
+ A simple OFX (Open Financial Exchange) parser for Bank Accounts and Credit Cards. Supports multiple accounts.
7
+
8
+ ### Usage
9
+
10
+ require 'ofx'
11
+
12
+ ofx = OFX("statement.ofx")
13
+
14
+ ofx.bank_accounts.each do |bank_account|
15
+ puts bank_account.id # => "492108"
16
+ puts bank_account.bank_id # => "1837"
17
+ puts bank_account.currency # => "GBP"
18
+ puts bank_account.type # => :checking
19
+ puts bank_account.balance.amount # => "100.00"
20
+ puts bank_account.balance.amount_in_pennies # => "10000"
21
+ puts bank_account.transactions # => [#<OFX::Transaction:0x007ff3bb8cf418>]
22
+ end
23
+
24
+ ofx.credit_cards.each do |credit_card|
25
+ puts credit_card.id # => "81728918309730"
26
+ puts credit_card.currency # => "GBP"
27
+ puts bank_account.balance.amount # => "-100.00"
28
+ puts bank_account.balance.amount_in_pennies # => "-10000"
29
+ puts credit_card.transactions # => [#<OFX::Transaction:0x007ff3bb8cf418>]
30
+ end
31
+
32
+ ### Supports
33
+ #### Ruby
34
+ * 1.9.2
35
+ * 1.9.3
36
+
37
+ #### OFX
38
+ * 1.0.2
39
+ * 2.1.1
40
+
41
+ ### ToDo
42
+ 1. Support other OFX Versions
43
+
44
+ ### Original Fork
45
+
46
+ * Nando Vieira - https://github.com/fnando/ofx
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler"
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require "rspec/core/rake_task"
5
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,10 @@
1
+ module OFX
2
+ class Account < Foundation
3
+ attr_accessor :balance
4
+ attr_accessor :bank_id
5
+ attr_accessor :currency
6
+ attr_accessor :id
7
+ attr_accessor :transactions
8
+ attr_accessor :type
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module OFX
2
+ class Balance < Foundation
3
+ attr_accessor :amount
4
+ attr_accessor :amount_in_pennies
5
+ attr_accessor :posted_at
6
+ end
7
+ end
data/lib/ofx/errors.rb ADDED
@@ -0,0 +1,3 @@
1
+ module OFX
2
+ class UnsupportedFileError < StandardError; end
3
+ end
@@ -0,0 +1,9 @@
1
+ module OFX
2
+ class Foundation
3
+ def initialize(attrs)
4
+ attrs.each do |key, value|
5
+ send("#{key}=", value)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,134 @@
1
+ module OFX
2
+ module Parser
3
+ class OFX102
4
+ VERSION = "1.0.2"
5
+
6
+ ACCOUNT_TYPES = {
7
+ "CHECKING" => :checking
8
+ }
9
+
10
+ TRANSACTION_TYPES = [
11
+ 'ATM', 'CASH', 'CHECK', 'CREDIT', 'DEBIT', 'DEP', 'DIRECTDEBIT', 'DIRECTDEP', 'DIV',
12
+ 'FEE', 'INT', 'OTHER', 'PAYMENT', 'POS', 'REPEATPMT', 'SRVCHG', 'XFER'
13
+ ].inject({}) { |hash, tran_type| hash[tran_type] = tran_type.downcase.to_sym; hash }
14
+
15
+ attr_reader :headers
16
+ attr_reader :body
17
+ attr_reader :html
18
+
19
+ def initialize(options = {})
20
+ @headers = options[:headers]
21
+ @body = options[:body]
22
+ @html = Nokogiri::HTML.parse(body)
23
+ end
24
+
25
+ def bank_accounts
26
+ @bank_accounts ||= build_bank_account
27
+ end
28
+
29
+ def credit_cards
30
+ @credit_cards ||= build_credit_card
31
+ end
32
+
33
+ def self.parse_headers(header_text)
34
+ # Change single CR's to LF's to avoid issues with some banks
35
+ header_text.gsub!(/\r(?!\n)/, "\n")
36
+
37
+ # Parse headers. When value is NONE, convert it to nil.
38
+ headers = header_text.to_enum(:each_line).inject({}) do |memo, line|
39
+ _, key, value = *line.match(/^(.*?):(.*?)\s*(\r?\n)*$/)
40
+
41
+ unless key.nil?
42
+ memo[key] = value == "NONE" ? nil : value
43
+ end
44
+
45
+ memo
46
+ end
47
+
48
+ return headers unless headers.empty?
49
+ end
50
+
51
+ private
52
+
53
+ def build_bank_account
54
+ html.search("stmttrnrs").each_with_object([]) do |account, accounts|
55
+ args = {
56
+ :bank_id => account.search("bankacctfrom > bankid").inner_text,
57
+ :id => account.search("bankacctfrom > acctid").inner_text,
58
+ :type => ACCOUNT_TYPES[account.search("bankacctfrom > accttype").inner_text.to_s.upcase],
59
+ :transactions => build_transactions(account.search("banktranlist > stmttrn")),
60
+ :balance => build_balance(account),
61
+ :currency => account.search("stmtrs > curdef").inner_text
62
+ }
63
+
64
+ accounts << OFX::Account.new(args)
65
+ end
66
+ end
67
+
68
+ def build_credit_card
69
+ html.search("ccstmttrnrs").each_with_object([]) do |account, accounts|
70
+ args = {
71
+ :id => account.search("ccstmtrs > ccacctfrom > acctid").inner_text,
72
+ :transactions => build_transactions(account.search("banktranlist > stmttrn")),
73
+ :balance => build_balance(account),
74
+ :currency => account.search("ccstmtrs > curdef").inner_text
75
+ }
76
+
77
+ accounts << OFX::Account.new(args)
78
+ end
79
+ end
80
+
81
+ def build_transactions(transactions)
82
+ transactions.each_with_object([]) do |transaction, transactions|
83
+ transactions << build_transaction(transaction)
84
+ end
85
+ end
86
+
87
+ def build_transaction(transaction)
88
+ args = {
89
+ :amount => build_amount(transaction),
90
+ :amount_in_pennies => (build_amount(transaction) * 100).to_i,
91
+ :fit_id => transaction.search("fitid").inner_text,
92
+ :memo => transaction.search("memo").inner_text,
93
+ :name => transaction.search("name").inner_text,
94
+ :payee => transaction.search("payee").inner_text,
95
+ :check_number => transaction.search("checknum").inner_text,
96
+ :ref_number => transaction.search("refnum").inner_text,
97
+ :posted_at => build_date(transaction.search("dtposted").inner_text),
98
+ :type => build_type(transaction)
99
+ }
100
+
101
+ OFX::Transaction.new(args)
102
+ end
103
+
104
+ def build_type(transaction)
105
+ TRANSACTION_TYPES[transaction.search("trntype").inner_text.to_s.upcase]
106
+ end
107
+
108
+ def build_amount(transaction)
109
+ BigDecimal.new(transaction.search("trnamt").inner_text)
110
+ end
111
+
112
+ def build_date(date)
113
+ _, year, month, day, hour, minutes, seconds = *date.match(/(\d{4})(\d{2})(\d{2})(?:(\d{2})(\d{2})(\d{2}))?/)
114
+
115
+ date = "#{year}-#{month}-#{day} "
116
+ date << "#{hour}:#{minutes}:#{seconds}" if hour && minutes && seconds
117
+
118
+ Time.parse(date)
119
+ end
120
+
121
+ def build_balance(account)
122
+ amount = account.search("ledgerbal > balamt").inner_text.to_f
123
+
124
+ args = {
125
+ :amount => amount,
126
+ :amount_in_pennies => (amount * 100).to_i,
127
+ :posted_at => build_date(account.search("ledgerbal > dtasof").inner_text)
128
+ }
129
+
130
+ OFX::Balance.new(args)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,40 @@
1
+ module OFX
2
+ module Parser
3
+ class OFX211 < OFX102
4
+ VERSION = "2.1.1"
5
+
6
+ def self.parse_headers(header_text)
7
+ doc = Nokogiri::XML(header_text)
8
+
9
+ # Nokogiri can't search for processing instructions, so we
10
+ # need to do this manually.
11
+ doc.children.each do |e|
12
+ if e.type == Nokogiri::XML::Node::PI_NODE && e.name == "OFX"
13
+ # Getting the attributes from the element doesn't seem to
14
+ # work either.
15
+ return extract_headers(e.text)
16
+ end
17
+ end
18
+
19
+ nil
20
+ end
21
+
22
+ private
23
+ def self.extract_headers(text)
24
+ headers = {}
25
+ text.split(/\s+/).each do |attr_text|
26
+ match = /(.+)="(.+)"/.match(attr_text)
27
+ next unless match
28
+ k, v = match[1], match[2]
29
+ headers[k] = v
30
+ end
31
+ headers
32
+ end
33
+
34
+ def self.strip_quotes(s)
35
+ return unless s
36
+ s.sub(/^"(.*)"$/, '\1')
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/ofx/parser.rb ADDED
@@ -0,0 +1,67 @@
1
+ module OFX
2
+ module Parser
3
+ class Base
4
+ attr_reader :headers
5
+ attr_reader :body
6
+ attr_reader :content
7
+ attr_reader :parser
8
+
9
+ def initialize(resource)
10
+ @content = open_resource(resource).read
11
+ prepare(content)
12
+ end
13
+
14
+ private
15
+
16
+ def open_resource(document)
17
+ return document if document.respond_to?(:read)
18
+ open(document, "r:iso-8859-1:utf-8")
19
+ rescue
20
+ StringIO.new(document.encode('utf-8'))
21
+ end
22
+
23
+ def prepare(content)
24
+ header, body = content.dup.split(/<OFX>/, 2)
25
+ raise OFX::UnsupportedFileError unless body
26
+
27
+ begin
28
+ @headers = condition_headers(header)
29
+ @body = condition_body(body)
30
+ rescue Exception
31
+ raise OFX::UnsupportedFileError
32
+ end
33
+
34
+ instantiate_parser
35
+ end
36
+
37
+ def instantiate_parser
38
+ case headers["VERSION"]
39
+ when /102/ then
40
+ @parser = OFX102.new(:headers => headers, :body => body)
41
+ when /200|211/ then
42
+ @parser = OFX211.new(:headers => headers, :body => body)
43
+ else
44
+ raise OFX::UnsupportedFileError
45
+ end
46
+ end
47
+
48
+ def condition_headers(header)
49
+ headers = nil
50
+
51
+ OFX::Parser.constants.grep(/OFX/).each do |name|
52
+ headers = OFX::Parser.const_get(name).parse_headers(header)
53
+ break if headers
54
+ end
55
+
56
+ headers
57
+ end
58
+
59
+ def condition_body(body)
60
+ body.gsub!(/>\s+</m, "><").
61
+ gsub!(/\s+</m, "<").
62
+ gsub!(/>\s+/m, ">").
63
+ gsub!(/<(\w+?)>([^<]+)/m, '<\1>\2</\1>')
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,14 @@
1
+ module OFX
2
+ class Transaction < Foundation
3
+ attr_accessor :amount
4
+ attr_accessor :amount_in_pennies
5
+ attr_accessor :check_number
6
+ attr_accessor :fit_id
7
+ attr_accessor :memo
8
+ attr_accessor :name
9
+ attr_accessor :payee
10
+ attr_accessor :posted_at
11
+ attr_accessor :ref_number
12
+ attr_accessor :type
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ module OFX
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 4
5
+ PATCH = 0
6
+ STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
7
+ end
8
+ end
data/lib/ofx.rb ADDED
@@ -0,0 +1,27 @@
1
+ require "open-uri"
2
+ require "nokogiri"
3
+ require "bigdecimal"
4
+
5
+ require "ofx/errors"
6
+ require "ofx/parser"
7
+ require "ofx/parser/ofx102"
8
+ require "ofx/parser/ofx211"
9
+ require "ofx/foundation"
10
+ require "ofx/balance"
11
+ require "ofx/account"
12
+ require "ofx/transaction"
13
+ require "ofx/version"
14
+
15
+ def OFX(resource, &block)
16
+ parser = OFX::Parser::Base.new(resource).parser
17
+
18
+ if block_given?
19
+ if block.arity == 1
20
+ yield parser
21
+ else
22
+ parser.instance_eval(&block)
23
+ end
24
+ end
25
+
26
+ parser
27
+ end
data/ofx.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/ofx/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ['Kyle Welsby', 'Chuck Hardy', "Nando Vieira"]
6
+ gem.email = ['app@britishruby.com', "fnando.vieira@gmail.com"]
7
+ gem.homepage = 'https://github.com/BritRuby/Banker-OFX'
8
+ gem.description = %q{A simple OFX (Open Financial Exchange) parser built on top of Nokogiri. Currently supports OFX 102, 200 and 211.}
9
+ gem.summary = gem.description
10
+
11
+ gem.add_runtime_dependency 'nokogiri'
12
+
13
+ gem.add_development_dependency "gem-release"
14
+ gem.add_development_dependency 'rspec'
15
+ gem.add_development_dependency 'simplecov'
16
+
17
+ gem.files = `git ls-files`.split("\n")
18
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ gem.name = "banker-ofx"
21
+ gem.require_paths = ['lib']
22
+ gem.platform = Gem::Platform::RUBY
23
+ gem.version = OFX::Version::STRING
24
+ end
Binary file