ofx-parser-bp 1.0.2

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-parser.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.2'
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
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
@@ -0,0 +1,87 @@
1
+ OFXHEADER:100
2
+ DATA:OFXSGML
3
+ VERSION:102
4
+ SECURITY:NONE
5
+ ENCODING:USASCII
6
+ CHARSET:1252
7
+ COMPRESSION:NONE
8
+ OLDFILEUID:
9
+ NEWFILEUID:NONE
10
+
11
+ <OFX>
12
+ <SIGNONMSGSRSV1>
13
+ <SONRS>
14
+ <STATUS>
15
+ <CODE>0</CODE>
16
+ <SEVERITY>INFO</SEVERITY>
17
+ <MESSAGE>The user is authentic; operation succeeded.</MESSAGE>
18
+ </STATUS>
19
+ <DTSERVER>20070623142635.169[-5:CDT]</DTSERVER>
20
+ <LANGUAGE>ENG</LANGUAGE>
21
+ <FI>
22
+ <ORG>U.S. Bank</ORG>
23
+ <FID>1402</FID>
24
+ </FI>
25
+ <INTU.BID>1402</INTU.BID>
26
+ </SONRS>
27
+ </SIGNONMSGSRSV1>
28
+ <BANKMSGSRSV1>
29
+ <STMTTRNRS>
30
+ <TRNUID>9C24229A0077EAA50000011353C9E00743FC</TRNUID>
31
+ <STATUS>
32
+ <CODE>0</CODE>
33
+ <SEVERITY>INFO</SEVERITY>
34
+ </STATUS>
35
+ <STMTRS>
36
+ <CURDEF>USD</CURDEF>
37
+ <BANKACCTFROM>
38
+ <BANKID>033000033</BANKID>
39
+ <ACCTID>103333333333</ACCTID>
40
+ <ACCTTYPE>CHECKING</ACCTTYPE>
41
+ </BANKACCTFROM>
42
+ <BANKTRANLIST>
43
+ <DTSTART>20070604190000.000[-5:CDT]</DTSTART>
44
+ <DTEND>20070622190000.000[-5:CDT]</DTEND>
45
+ <STMTTRN>
46
+ <TRNTYPE>PAYMENT</TRNTYPE>
47
+ <DTPOSTED>20070606120000.000</DTPOSTED>
48
+ <TRNAMT>-11.11</TRNAMT>
49
+ <FITID>11111111 22</FITID>
50
+ <NAME>WEB AUTHORIZED PMT FOO INC</NAME>
51
+ <MEMO>Download from usbank.com. FOO INC</MEMO>
52
+ </STMTTRN>
53
+ <STMTTRN>
54
+ <TRNTYPE>CHECK</TRNTYPE>
55
+ <DTPOSTED>20070607120000.000</DTPOSTED>
56
+ <TRNAMT>-111.11</TRNAMT>
57
+ <FITID>22222A</FITID>
58
+ <CHECKNUM>0000009611</CHECKNUM>
59
+ <NAME>CHECK</NAME>
60
+ <MEMO>Download from usbank.com.</MEMO>
61
+ </STMTTRN>
62
+ <STMTTRN>
63
+ <TRNTYPE>DIRECTDEP</TRNTYPE>
64
+ <DTPOSTED>20070614120000.000</DTPOSTED>
65
+ <TRNAMT>1111.11</TRNAMT>
66
+ <FITID>X34AE33</FITID>
67
+ <NAME>ELECTRONIC DEPOSIT BAR INC</NAME>
68
+ <MEMO>Download from usbank.com. BAR INC</MEMO>
69
+ </STMTTRN>
70
+ <STMTTRN>
71
+ <TRNTYPE>CREDIT</TRNTYPE>
72
+ <DTPOSTED>20070619120000.000</DTPOSTED>
73
+ <TRNAMT>11.11</TRNAMT>
74
+ <FITID>8 8 9089743</FITID>
75
+ <NAME>ATM DEPOSIT US BANK ANYTOWNAS</NAME>
76
+ <MEMO>Download from usbank.com. US BANK ANYTOWN ASUS1</MEMO>
77
+ </STMTTRN>
78
+ </BANKTRANLIST>
79
+ <LEDGERBAL>
80
+ <BALAMT>1234.09</BALAMT>
81
+ <DTASOF>20070623142635.369[-5:CDT]</DTASOF>
82
+ </LEDGERBAL>
83
+ </STMTRS>
84
+ </STMTTRNRS>
85
+ </BANKMSGSRSV1>
86
+ </OFX>
87
+