ofxparser 1.0.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/lib/ofx.rb ADDED
@@ -0,0 +1,189 @@
1
+ module OfxParser
2
+ module MonetarySupport
3
+ # @@monies = []
4
+ # @@monies ||= []
5
+
6
+ # class_extension do
7
+ # def monetary_vars(*methods) #:nodoc:
8
+ # @@monies += methods
9
+ # end
10
+ # end
11
+
12
+ # Returns pennies for a given string amount, i.e:
13
+ # '-123.45' => -12345
14
+ # '123' => 12300
15
+ def pennies_for(amount)
16
+ return nil if amount == ""
17
+ int, fraction = amount.scan(/\d+/)
18
+ i = (fraction.to_s.strip =~ /[1-9]/) ? "#{int}#{fraction[0,2]}".to_i : int.to_i * 100
19
+ amount =~ /^\s*-\s*\d+/ ? -i : i
20
+ end
21
+
22
+ def original_method(meth) #:nodoc:
23
+ meth.to_s.sub('_in_pennies','').to_sym rescue nil
24
+ end
25
+
26
+ def monetary_method_call?(meth) #:nodoc:
27
+ orig = original_method(meth)
28
+ @@monies.include?(orig) && meth.to_s == "#{orig}_in_pennies"
29
+ end
30
+
31
+ def method_missing(meth, *args) #:nodoc:
32
+ if (monetary_method_call?(meth))
33
+ pennies_for(send(original_method(meth)))
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def respond_to?(meth) #:nodoc:
40
+ monetary_method_call?(meth) ? true : super
41
+ end
42
+
43
+ end
44
+
45
+ # This class is returned when a parse is successful.
46
+ # == General Notes
47
+ # * currency symbols are an iso4217 3-letter code
48
+ # * language is defined by iso639 3-letter code
49
+ class Ofx
50
+ attr_accessor :header, :sign_on, :signup_account_info,
51
+ :bank_account, :credit_card, :investment
52
+
53
+ def accounts
54
+ accounts = []
55
+ [:bank_account, :credit_card, :investment].each do |method|
56
+ val = send(method)
57
+ accounts << val if val
58
+ end
59
+ accounts
60
+ end
61
+ end
62
+
63
+ class SignOn
64
+ attr_accessor :status, :date, :language, :institute
65
+ end
66
+
67
+ class AccountInfo
68
+ attr_accessor :desc, :number
69
+ end
70
+
71
+ class Account
72
+ attr_accessor :number, :statement, :transaction_uid
73
+ end
74
+
75
+ class BankAccount < Account
76
+ TYPE = [:CHECKING, :SAVINGS, :MONEYMRKT, :CREDITLINE]
77
+ attr_accessor :routing_number, :type, :balance, :balance_date
78
+
79
+ include MonetarySupport
80
+ #monetary_vars :balance
81
+
82
+ undef type
83
+ def type
84
+ @type.to_s.upcase.to_sym
85
+ end
86
+ end
87
+
88
+ class CreditAccount < Account
89
+ attr_accessor :remaining_credit, :remaining_credit_date, :balance, :balance_date
90
+
91
+ include MonetarySupport
92
+ # monetary_vars :remaining_credit, :balance
93
+ end
94
+
95
+ class InvestmentAccount < Account
96
+ attr_accessor :broker_id, :positions, :margin_balance, :short_balance, :cash_balance
97
+
98
+ # include MonetarySupport
99
+ # monetary_vars :margin_balance, :short_balance, :cash_balance
100
+ end
101
+
102
+
103
+ class Statement
104
+ attr_accessor :currency, :transactions, :start_date, :end_date, :stock_positions, :opt_positions
105
+ end
106
+
107
+ class Transaction
108
+ attr_accessor :type, :date, :amount, :fit_id, :check_number, :sic, :memo, :payee
109
+
110
+ include MonetarySupport
111
+ # monetary_vars :amount
112
+
113
+ TYPE = {
114
+ :CREDIT => "Generic credit",
115
+ :DEBIT => "Generic debit",
116
+ :INT => "Interest earned or paid ",
117
+ :DIV => "Dividend",
118
+ :FEE => "FI fee",
119
+ :SRVCHG => "Service charge",
120
+ :DEP => "Deposit",
121
+ :ATM => "ATM debit or credit",
122
+ :POS => "Point of sale debit or credit ",
123
+ :XFER => "Transfer",
124
+ :CHECK => "Check",
125
+ :PAYMENT => "Electronic payment",
126
+ :CASH => "Cash withdrawal",
127
+ :DIRECTDEP => "Direct deposit",
128
+ :DIRECTDEBIT => "Merchant initiated debit",
129
+ :REPEATPMT => "Repeating payment/standing order",
130
+ :OTHER => "Other"
131
+ }
132
+
133
+ def type_desc
134
+ TYPE[type]
135
+ end
136
+
137
+ undef type
138
+ def type
139
+ @type.to_s.strip.upcase.to_sym
140
+ end
141
+
142
+ undef sic
143
+ def sic
144
+ @sic == "" ? nil : @sic
145
+ end
146
+
147
+ def sic_desc
148
+ Mcc::CODES[sic]
149
+ end
150
+ end
151
+
152
+ class Stock_Position
153
+ attr_accessor :uniqueid, :uniqueid_type, :heldinacct, :type, :units, :unitprice, :pricedate, :memo
154
+ end
155
+
156
+ class Opt_Position
157
+ attr_accessor :uniqueid, :uniqueid_type, :heldinacct, :type, :units, :unitprice, :pricedate, :memo, :mktval
158
+ end
159
+
160
+
161
+ # Status of a sign on
162
+ class Status
163
+ attr_accessor :code, :severity, :message
164
+
165
+ CODES = {
166
+ '0' => 'Success',
167
+ '2000' => 'General error',
168
+ '15000' => 'Must change USERPASS',
169
+ '15500' => 'Signon invalid',
170
+ '15501' => 'Customer account already in use',
171
+ '15502' => 'USERPASS Lockout'
172
+ }
173
+
174
+ def code_desc
175
+ CODES[code]
176
+ end
177
+
178
+ undef code
179
+ def code
180
+ @code.to_s.strip
181
+ end
182
+
183
+ end
184
+
185
+ class Institute
186
+ attr_accessor :name, :id
187
+ end
188
+
189
+ end
data/lib/ofxparser.rb ADDED
@@ -0,0 +1,236 @@
1
+ require 'hpricot'
2
+ require 'time'
3
+ require 'date'
4
+
5
+ %w(class-extension ofx mcc).each do |fn|
6
+ require File.dirname(__FILE__) + "/#{fn}"
7
+ end
8
+
9
+ module OfxParser
10
+ VERSION = '1.0.0'
11
+
12
+ class OfxParser
13
+
14
+ # Creates and returns an Ofx instance when given a well-formed OFX document,
15
+ # complete with the mandatory key:pair header.
16
+ def self.parse(ofx)
17
+ ofx = ofx.respond_to?(:read) ? ofx.read.to_s : ofx.to_s
18
+
19
+ return Ofx.new if ofx == ""
20
+
21
+ header, body = pre_process(ofx)
22
+
23
+ ofx_out = parse_body(body)
24
+ ofx_out.header = header
25
+ ofx_out
26
+ end
27
+
28
+ # Designed to make the main OFX body parsable. This means adding closing tags
29
+ # to the SGML to make it parsable by hpricot.
30
+ #
31
+ # Returns an array of 2 elements:
32
+ # * header as a hash,
33
+ # * body as an evily pre-processed string ready for parsing by hpricot.
34
+ def self.pre_process(ofx)
35
+ header, body = ofx.split(/\n{2,}|:?<OFX>/, 2)
36
+
37
+ header = Hash[*header.gsub(/^\r?\n+/,'').split(/\r\n/).collect do |e|
38
+ e.split(/:/,2)
39
+ end.flatten]
40
+
41
+ body.gsub!(/>\s+</m, '><')
42
+ body.gsub!(/\s+</m, '<')
43
+ body.gsub!(/>\s+/m, '>')
44
+ body.gsub!(/<(\w+?)>([^<]+)/m, '<\1>\2</\1>')
45
+
46
+ [header, body]
47
+ end
48
+
49
+ # Takes an OFX datetime string of the format:
50
+ # * YYYYMMDDHHMMSS.XXX[gmt offset:tz name]
51
+ # * YYYYMMDD
52
+ # * YYYYMMDDHHMMSS
53
+ # * YYYYMMDDHHMMSS.XXX
54
+ #
55
+ # Returns a DateTime object. Milliseconds (XXX) are ignored.
56
+ def self.parse_datetime(date)
57
+ if /\A\s*
58
+ (\d{4})(\d{2})(\d{2}) ?# YYYYMMDD 1,2,3
59
+ (?:(\d{2})(\d{2})(\d{2}))? ?# HHMMSS - optional 4,5,6
60
+ (?:\.(\d{3}))? ?# .XXX - optional 7
61
+ (?:\[(-?\d+)\:\w{3}\])? ?# [-n:TZ] - optional 8,9
62
+ \s*\z/ix =~ date
63
+ year = $1.to_i
64
+ mon = $2.to_i
65
+ day = $3.to_i
66
+ hour = $4.to_i
67
+ min = $5.to_i
68
+ sec = $6.to_i
69
+ # DateTime does not support usecs.
70
+ # usec = 0
71
+ # usec = $7.to_f * 1000000 if $7
72
+ off = Rational($8.to_i, 24) # offset as a fraction of day. :|
73
+ DateTime.civil(year, mon, day, hour, min, sec, off)
74
+ end
75
+ end
76
+
77
+ private
78
+ def self.parse_body(body)
79
+ doc = Hpricot.XML(body)
80
+
81
+ ofx = Ofx.new
82
+
83
+ ofx.sign_on = build_signon((doc/"SIGNONMSGSRSV1/SONRS"))
84
+ ofx.signup_account_info = build_info((doc/"SIGNUPMSGSRSV1/ACCTINFOTRNRS"))
85
+ ofx.bank_account = build_bank((doc/"BANKMSGSRSV1/STMTTRNRS")) unless (doc/"BANKMSGSRSV1").empty?
86
+ ofx.credit_card = build_credit((doc/"CREDITCARDMSGSRSV1/CCSTMTTRNRS")) unless (doc/"CREDITCARDMSGSRSV1").empty?
87
+ ofx.investment=build_investment((doc/"INVSTMTMSGSRSV1/INVSTMTTRNRS"))
88
+
89
+
90
+ #build_investment((doc/"SIGNONMSGSRQV1"))
91
+
92
+ ofx
93
+ end
94
+
95
+ def self.build_signon(doc)
96
+ sign_on = SignOn.new
97
+ sign_on.status = build_status((doc/"STATUS"))
98
+ sign_on.date = parse_datetime((doc/"DTSERVER").inner_text)
99
+ sign_on.language = (doc/"LANGUAGE").inner_text
100
+
101
+ sign_on.institute = Institute.new
102
+ sign_on.institute.name = ((doc/"FI")/"ORG").inner_text
103
+ sign_on.institute.id = ((doc/"FI")/"FID").inner_text
104
+ sign_on
105
+ end
106
+
107
+ def self.build_info(doc)
108
+ account_infos = []
109
+
110
+ (doc/"ACCTINFO").each do |info_doc|
111
+ acc_info = AccountInfo.new
112
+ acc_info.desc = (info_doc/"DESC").inner_text
113
+ acc_info.number = (info_doc/"ACCTID").inner_text
114
+ account_infos << acc_info
115
+ end
116
+
117
+ account_infos
118
+ end
119
+
120
+ def self.build_bank(doc)
121
+ acct = BankAccount.new
122
+
123
+ acct.transaction_uid = (doc/"TRNUID").inner_text.strip
124
+ acct.number = (doc/"STMTRS/BANKACCTFROM/ACCTID").inner_text
125
+ acct.routing_number = (doc/"STMTRS/BANKACCTFROM/BANKID").inner_text
126
+ acct.type = (doc/"STMTRS/BANKACCTFROM/ACCTTYPE").inner_text.strip
127
+ acct.balance = (doc/"STMTRS/LEDGERBAL/BALAMT").inner_text
128
+ acct.balance_date = parse_datetime((doc/"STMTRS/LEDGERBAL/DTASOF").inner_text)
129
+
130
+ statement = Statement.new
131
+ statement.currency = (doc/"STMTRS/CURDEF").inner_text
132
+ statement.start_date = parse_datetime((doc/"STMTRS/BANKTRANLIST/DTSTART").inner_text)
133
+ statement.end_date = parse_datetime((doc/"STMTRS/BANKTRANLIST/DTEND").inner_text)
134
+ acct.statement = statement
135
+
136
+ statement.transactions = (doc/"STMTRS/BANKTRANLIST/STMTTRN").collect do |t|
137
+ build_transaction(t)
138
+ end
139
+
140
+ acct
141
+ end
142
+
143
+ def self.build_credit(doc)
144
+ acct = CreditAccount.new
145
+
146
+ acct.number = (doc/"CCSTMTRS/CCACCTFROM/ACCTID").inner_text
147
+ acct.transaction_uid = (doc/"TRNUID").inner_text.strip
148
+ acct.balance = (doc/"CCSTMTRS/LEDGERBAL/BALAMT").inner_text
149
+ acct.balance_date = parse_datetime((doc/"CCSTMTRS/LEDGERBAL/DTASOF").inner_text)
150
+ acct.remaining_credit = (doc/"CCSTMTRS/AVAILBAL/BALAMT").inner_text
151
+ acct.remaining_credit_date = parse_datetime((doc/"CCSTMTRS/AVAILBAL/DTASOF").inner_text)
152
+
153
+ statement = Statement.new
154
+ statement.currency = (doc/"CCSTMTRS/CURDEF").inner_text
155
+ statement.start_date = parse_datetime((doc/"CCSTMTRS/BANKTRANLIST/DTSTART").inner_text)
156
+ statement.end_date = parse_datetime((doc/"CCSTMTRS/BANKTRANLIST/DTEND").inner_text)
157
+ acct.statement = statement
158
+
159
+ statement.transactions = (doc/"CCSTMTRS/BANKTRANLIST/STMTTRN").collect do |t|
160
+ build_transaction(t)
161
+ end
162
+
163
+ acct
164
+ end
165
+
166
+ # for credit and bank transactions.
167
+ def self.build_transaction(t)
168
+ transaction = Transaction.new
169
+ transaction.type = (t/"TRNTYPE").inner_text
170
+ transaction.date = parse_datetime((t/"DTPOSTED").inner_text)
171
+ transaction.amount = (t/"TRNAMT").inner_text
172
+ transaction.fit_id = (t/"FITID").inner_text
173
+ transaction.payee = (t/"PAYEE").inner_text + (t/"NAME").inner_text
174
+ transaction.memo = (t/"MEMO").inner_text
175
+ transaction.sic = (t/"SIC").inner_text
176
+ transaction.check_number = (t/"CHECKNUM").inner_text if transaction.type == :CHECK
177
+ transaction
178
+ end
179
+
180
+
181
+ def self.build_investment(doc)
182
+ acct = InvestmentAccount.new
183
+ acct.broker_id=(doc/"INVSTMTRS/INVACCTFROM/BROKERID").inner_text
184
+ acct.cash_balance=(doc/"INVSTMTRS/INVBAL/AVAILCASH").inner_text
185
+
186
+ statement = Statement.new
187
+ acct.statement=statement
188
+
189
+ statement.stock_positions = (doc/"INVSTMTRS/INVPOSLIST/POSSTOCK").collect do |p|
190
+ build_stock_position(p)
191
+ end
192
+
193
+ statement.opt_positions = (doc/"INVSTMTRS/INVPOSLIST/POSOPT").collect do |p|
194
+ build_opt_position(p)
195
+ end
196
+
197
+ acct
198
+ end
199
+
200
+ def self.build_stock_position(p)
201
+ stock_position = Stock_Position.new
202
+ stock_position.uniqueid = (p/"INVPOS/SECID/UNIQUEID").inner_text
203
+ stock_position.uniqueid_type = (p/"INVPOS/SECID/UNIQUEIDTYPE").inner_text
204
+ stock_position.heldinacct = (p/"INVPOS/HELDINACCT").inner_text
205
+ stock_position.type = (p/"INVPOS/POSTYPE").inner_text
206
+ stock_position.units = (p/"INVPOS/UNITS").inner_text
207
+ stock_position.unitprice = (p/"INVPOS/UNITPRICE").inner_text
208
+ stock_position.pricedate = parse_datetime((p/"INVPOS/DTPRICEASOF").inner_text)
209
+ stock_position.memo = (p/"INVPOS/MEMO").inner_text
210
+ stock_position
211
+ end
212
+
213
+ def self.build_opt_position(p)
214
+ opt_position = Opt_Position.new
215
+ opt_position.uniqueid = (p/"INVPOS/SECID/UNIQUEID").inner_text
216
+ opt_position.uniqueid_type = (p/"INVPOS/SECID/UNIQUEIDTYPE").inner_text
217
+ opt_position.heldinacct = (p/"INVPOS/HELDINACCT").inner_text
218
+ opt_position.type = (p/"INVPOS/POSTYPE").inner_text
219
+ opt_position.units = (p/"INVPOS/UNITS").inner_text
220
+ opt_position.unitprice = (p/"INVPOS/UNITPRICE").inner_text
221
+ opt_position.mktval = (p/"INVPOS/MKTVAL").inner_text
222
+ opt_position.pricedate = parse_datetime((p/"INVPOS/DTPRICEASOF").inner_text)
223
+ opt_position.memo = (p/"INVPOS/MEMO").inner_text
224
+ opt_position
225
+ end
226
+
227
+ def self.build_status(doc)
228
+ status = Status.new
229
+ status.code = (doc/"CODE").inner_text
230
+ status.severity = (doc/"SEVERITY").inner_text
231
+ status.message = (doc/"MESSAGE").inner_text
232
+ status
233
+ end
234
+
235
+ end
236
+ end
@@ -0,0 +1,8 @@
1
+ require "test/unit"
2
+ require "ofxparser"
3
+
4
+ class TestOfxparser < Test::Unit::TestCase
5
+ def test_sanity
6
+ flunk "write tests or I will kneecap you"
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ofxparser
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Pavit Masson (forked from Andrew A. Smith)
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-05-27 00:00:00.000000000 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hpricot
17
+ requirement: &9298020 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0.6'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *9298020
26
+ - !ruby/object:Gem::Dependency
27
+ name: hoe
28
+ requirement: &9297684 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.5.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *9297684
37
+ description: ! '== DESCRIPTION: My fork of aasmith''s ofx-parser v1.0.2 and attempt
38
+ at building the investment acct methods. OfxParser is a ruby library to parse a
39
+ realistic subset of the lengthy OFX 1.x specification. == FEATURES/PROBLEMS: *
40
+ Reads OFX responses - i.e. those downloaded from financial institutions and puts
41
+ it into a usable object graph. * Supports the 3 main message sets: banking, credit
42
+ card and investment accounts, as well as the required ''sign on'' set. * Knows about
43
+ SIC codes - if your institution provides them. See http://www.eeoc.gov/stats/jobpat/siccodes.html
44
+ * Monetary amounts can be retrieved either as a raw string, or in pennies. * Supports
45
+ OFX timestamps.'
46
+ email: pavitm@gmail.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files:
50
+ - History.txt
51
+ - Manifest.txt
52
+ - README.txt
53
+ files:
54
+ - History.txt
55
+ - Manifest.txt
56
+ - README.txt
57
+ - Rakefile
58
+ - lib/class-extension.rb
59
+ - lib/mcc.rb
60
+ - lib/ofxparser.rb
61
+ - lib/ofx.rb
62
+ - test/test_ofxparser.rb
63
+ has_rdoc: true
64
+ homepage: http://ofxparser.rubyforge.org/
65
+ licenses: []
66
+ post_install_message:
67
+ rdoc_options:
68
+ - --main
69
+ - README.txt
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project: ofxparser
86
+ rubygems_version: 1.5.2
87
+ signing_key:
88
+ specification_version: 2
89
+ summary: ofx-parser is a ruby library for parsing OFX 1.x data.
90
+ test_files:
91
+ - test/test_ofxparser.rb