banker-ofx 0.4.0

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