egg2ofx 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1 @@
1
+ This appears to be in a bit of a mess. I seem to remember starting with the egg.html -> egg.ofx conversion and then wanting to do the same with ing-direct. I'm not sure how much of it works anymore.
data/Rakefile ADDED
@@ -0,0 +1,118 @@
1
+ require "rubygems"
2
+ require "rake/gempackagetask"
3
+ require "rake/rdoctask"
4
+
5
+ task :default => :test
6
+
7
+ require "rake/testtask"
8
+ Rake::TestTask.new do |t|
9
+ t.libs << "test"
10
+ t.test_files = FileList["test/**/*_test.rb"]
11
+ t.verbose = true
12
+ end
13
+
14
+ # This builds the actual gem. For details of what all these options
15
+ # mean, and other ones you can add, check the documentation here:
16
+ #
17
+ # http://rubygems.org/read/chapter/20
18
+ #
19
+ spec = Gem::Specification.new do |s|
20
+
21
+ # Change these as appropriate
22
+ s.name = "egg2ofx"
23
+ s.version = "0.1.1"
24
+ s.summary = "Converts the html statements and recent transactions from the egg site to ofx"
25
+ s.author = "Chris Roos"
26
+ s.email = "chris@seagul.co.uk"
27
+ s.homepage = "http://chrisroos.co.uk"
28
+
29
+ s.has_rdoc = true
30
+ s.extra_rdoc_files = %w(README)
31
+ s.rdoc_options = %w(--main README)
32
+
33
+ # Add any extra files to include in the gem
34
+ s.files = %w(Rakefile README) + Dir.glob("{bin,test,lib}/**/*")
35
+ s.executables = FileList["bin/**"].map { |f| File.basename(f) }
36
+
37
+ s.require_paths = ["lib"]
38
+
39
+ # If you want to depend on other gems, add them here, along with any
40
+ # relevant versions
41
+ # s.add_dependency("some_other_gem", "~> 0.1.0")
42
+ s.add_dependency 'hpricot'
43
+
44
+ # If your tests use any gems, include them here
45
+ # s.add_development_dependency("mocha")
46
+ s.add_development_dependency 'mocha'
47
+
48
+ # If you want to publish automatically to rubyforge, you'll may need
49
+ # to tweak this, and the publishing task below too.
50
+ s.rubyforge_project = "egg2ofx"
51
+ end
52
+
53
+ # This task actually builds the gem. We also regenerate a static
54
+ # .gemspec file, which is useful if something (i.e. GitHub) will
55
+ # be automatically building a gem for this project. If you're not
56
+ # using GitHub, edit as appropriate.
57
+ Rake::GemPackageTask.new(spec) do |pkg|
58
+ pkg.gem_spec = spec
59
+
60
+ # Generate the gemspec file for github.
61
+ file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
62
+ File.open(file, "w") {|f| f << spec.to_ruby }
63
+ end
64
+
65
+ # Generate documentation
66
+ Rake::RDocTask.new do |rd|
67
+ rd.main = "README"
68
+ rd.rdoc_files.include("README", "lib/**/*.rb")
69
+ rd.rdoc_dir = "rdoc"
70
+ end
71
+
72
+ desc 'Clear out RDoc and generated packages'
73
+ task :clean => [:clobber_rdoc, :clobber_package] do
74
+ rm "#{spec.name}.gemspec"
75
+ end
76
+
77
+ # If you want to publish to RubyForge automatically, here's a simple
78
+ # task to help do that. If you don't, just get rid of this.
79
+ # Be sure to set up your Rubyforge account details with the Rubyforge
80
+ # gem; you'll need to run `rubyforge setup` and `rubyforge config` at
81
+ # the very least.
82
+ begin
83
+ require "rake/contrib/sshpublisher"
84
+ namespace :rubyforge do
85
+
86
+ desc "Release gem and RDoc documentation to RubyForge"
87
+ task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
88
+
89
+ namespace :release do
90
+ desc "Release a new version of this gem"
91
+ task :gem => [:package] do
92
+ require 'rubyforge'
93
+ rubyforge = RubyForge.new
94
+ rubyforge.configure
95
+ rubyforge.login
96
+ rubyforge.userconfig['release_notes'] = spec.summary
97
+ path_to_gem = File.join(File.dirname(__FILE__), "pkg", "#{spec.name}-#{spec.version}.gem")
98
+ puts "Publishing #{spec.name}-#{spec.version.to_s} to Rubyforge..."
99
+ rubyforge.add_release(spec.rubyforge_project, spec.name, spec.version.to_s, path_to_gem)
100
+ end
101
+
102
+ desc "Publish RDoc to RubyForge."
103
+ task :docs => [:rdoc] do
104
+ config = YAML.load(
105
+ File.read(File.expand_path('~/.rubyforge/user-config.yml'))
106
+ )
107
+
108
+ host = "#{config['username']}@rubyforge.org"
109
+ remote_dir = "/var/www/gforge-projects/egg2ofx/" # Should be the same as the rubyforge project name
110
+ local_dir = 'rdoc'
111
+
112
+ Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
113
+ end
114
+ end
115
+ end
116
+ rescue LoadError
117
+ puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
118
+ end
data/bin/egg2ofx ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'egg')
4
+
5
+ html_file = ARGV.shift
6
+ unless html_file and File.exists?(html_file)
7
+ raise "Usage: egg2ofx path/to/egg-html-file"
8
+ end
9
+
10
+ html = File.open(html_file) { |f| f.read }
11
+ parser = Egg::Parser.new(html)
12
+
13
+ puts parser.to_ofx
@@ -0,0 +1,8 @@
1
+ module Egg
2
+ class Account
3
+ def initialize(currency, number)
4
+ @currency, @number = currency, number
5
+ end
6
+ attr_reader :currency, :number
7
+ end
8
+ end
data/lib/egg/date.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'date'
2
+
3
+ module Egg
4
+ class Date
5
+ def self.build(date)
6
+ return nil if date == ''
7
+ ::Date.parse(date).strftime('%Y%m%d')
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ module Egg
2
+ class Description
3
+ attr_reader :payee, :note, :territory
4
+ def initialize(description)
5
+ if m = description.match(/(.{23}) (.{13}) (.{2})?/) # territory (last two chars) is not in recent transactions
6
+ @payee = m[1].strip.squeeze(' ')
7
+ @note = m[2].strip
8
+ @territory = m[3].strip if m[3]
9
+ else
10
+ @payee = description
11
+ end
12
+ end
13
+ end
14
+ end
data/lib/egg/money.rb ADDED
@@ -0,0 +1,13 @@
1
+ module Egg
2
+ class Money
3
+ def initialize(money)
4
+ money = money.sub(/£/, '').gsub(/,/, '')
5
+ md = money.match(/(\d+\.\d+) ([A-Z]+)/)
6
+ @money = md ? Float(md[1]) : 0
7
+ @money = -@money if md && md[2] == 'DR'
8
+ end
9
+ def to_f
10
+ @money
11
+ end
12
+ end
13
+ end
data/lib/egg/parser.rb ADDED
@@ -0,0 +1,32 @@
1
+ $: << File.dirname(__FILE__)
2
+
3
+ require 'parsers/document_parser'
4
+ require 'parsers/transaction_parser'
5
+ require 'parsers/statement_document_parser'
6
+ require 'parsers/statement_transaction_parser'
7
+ require 'parsers/recent_transactions_document_parser'
8
+ require 'parsers/recent_transactions_transaction_parser'
9
+
10
+ module Egg
11
+
12
+ class Parser
13
+
14
+ class UnparsableHtmlError < StandardError; end
15
+
16
+ def initialize(html)
17
+ if html =~ /your egg card statement/i
18
+ @parser = StatementDocumentParser.new(html)
19
+ elsif html =~ /egg card recent transactions/i
20
+ @parser = RecentTransactionsDocumentParser.new(html)
21
+ else
22
+ raise UnparsableHtmlError
23
+ end
24
+ end
25
+
26
+ def to_ofx
27
+ @parser.to_ofx
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,42 @@
1
+ module Egg
2
+
3
+ class DocumentParser
4
+
5
+ attr_reader :doc
6
+
7
+ def initialize(html)
8
+ @doc = Hpricot(html)
9
+ end
10
+
11
+ def to_ofx
12
+ account = Egg::Account.new('GBP', card_number)
13
+ statement = Egg::Statement.new(statement_date, closing_balance, account)
14
+
15
+ each_transaction do |transaction_row|
16
+ next if transaction_row.skip?
17
+ transaction = Egg::Transaction.new(transaction_row.date, transaction_row.description, transaction_row.money)
18
+ statement.add_transaction(transaction)
19
+ end
20
+
21
+ Ofx::Statement.new(statement).to_xml
22
+ end
23
+
24
+ private
25
+
26
+ def each_transaction
27
+ (doc/"table#tblTransactionsTable"/"tbody"/"tr").each do |row|
28
+ yield transaction_parser_class.new(row)
29
+ end
30
+ end
31
+
32
+ def card_number
33
+ (doc/"span#lblCardNumber").inner_html
34
+ end
35
+
36
+ def closing_balance
37
+ ((doc/"table#tblTransactionsTable"/"tfoot"/"tr").first/"td").inner_html
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,19 @@
1
+ module Egg
2
+
3
+ class RecentTransactionsDocumentParser < DocumentParser
4
+
5
+ private
6
+
7
+ def transaction_parser_class
8
+ RecentTransactionsTransactionParser
9
+ end
10
+
11
+ def statement_date
12
+ first_transaction_date = (doc/'#tblTransactionsTable'/'tbody'/'tr'/'td.date').first.inner_text
13
+ last_transaction_date = (doc/'#tblTransactionsTable'/'tbody'/'tr'/'td.date').last.inner_text
14
+ "#{first_transaction_date} to #{last_transaction_date}"
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,12 @@
1
+ module Egg
2
+
3
+ class RecentTransactionsTransactionParser < TransactionParser
4
+
5
+ def money
6
+ money = super
7
+ money =~ /^\-/ ? money + ' CR' : money + ' DR'
8
+ end
9
+
10
+ end
11
+
12
+ end
@@ -0,0 +1,17 @@
1
+ module Egg
2
+
3
+ class StatementDocumentParser < DocumentParser
4
+
5
+ private
6
+
7
+ def transaction_parser_class
8
+ StatementTransactionParser
9
+ end
10
+
11
+ def statement_date
12
+ (doc/'#ctl00_content_eggCardStatements_lstPreviousStatements'/'option[@selected=selected]').first.attributes['value']
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,19 @@
1
+ module Egg
2
+
3
+ class StatementTransactionParser < TransactionParser
4
+
5
+ def description
6
+ description = super
7
+ if row.next_sibling && (row.next_sibling/"td.date").inner_text == '' && (row.next_sibling/"td.description").inner_text != ''
8
+ description += ' / ' + (row.next_sibling/"td.description").inner_text
9
+ end
10
+ description
11
+ end
12
+
13
+ def skip?
14
+ date == '' or description == 'OPENING BALANCE'
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,29 @@
1
+ module Egg
2
+
3
+ class TransactionParser
4
+
5
+ attr_reader :row
6
+
7
+ def initialize(row)
8
+ @row = row
9
+ end
10
+
11
+ def skip?
12
+ false
13
+ end
14
+
15
+ def money
16
+ (row/"td.money").inner_text.sub(/^\?/, '').sub(/^£/, '')
17
+ end
18
+
19
+ def date
20
+ (row/"td.date").inner_text
21
+ end
22
+
23
+ def description
24
+ (row/"td.description").inner_text
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,24 @@
1
+ module Egg
2
+ class Statement
3
+ def initialize(statement_date, closing_balance, account)
4
+ from_date, to_date = statement_date.split(' to ')
5
+ @from_date = Date.build(from_date)
6
+ @to_date = Date.build(to_date)
7
+ @closing_balance = Money.new(closing_balance).to_f
8
+ @account = account
9
+ @transactions = []
10
+ end
11
+ attr_reader :from_date, :to_date, :closing_balance, :transactions
12
+ def add_transaction(transaction)
13
+ transaction.date = from_date unless transaction.date
14
+ @transactions << transaction
15
+ end
16
+ def method_missing(sym, *args, &blk)
17
+ if account_method = sym.to_s[/^account_(.+)/, 1]
18
+ @account.__send__(account_method, *args, &blk)
19
+ else
20
+ super(sym, *args, &blk)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ require 'digest/md5'
2
+
3
+ module Egg
4
+ class Transaction
5
+ def initialize(date, description, money)
6
+ @date = Date.build(date)
7
+ @raw_description = description
8
+ @description = Description.new(description)
9
+ @amount = Money.new(money).to_f
10
+ @ofx_id = nil
11
+ end
12
+ attr_reader :date, :amount
13
+ def payee
14
+ @description.payee
15
+ end
16
+ def note
17
+ @description.note
18
+ end
19
+ def ofx_id
20
+ @ofx_id ||= Digest::MD5.hexdigest(to_s)
21
+ end
22
+ def to_s
23
+ [date, @raw_description, amount].join(', ')
24
+ end
25
+ def type
26
+ if payee =~ /INTEREST/
27
+ 'INT'
28
+ else
29
+ (amount < 0) ? 'DEBIT' : 'CREDIT'
30
+ end
31
+ end
32
+ end
33
+ end
data/lib/egg.rb ADDED
@@ -0,0 +1,15 @@
1
+ $: << File.dirname(__FILE__)
2
+
3
+ require 'hpricot'
4
+
5
+ require 'egg/date'
6
+
7
+ require 'egg/statement'
8
+ require 'egg/account'
9
+ require 'egg/transaction'
10
+ require 'egg/money'
11
+ require 'egg/description'
12
+
13
+ require 'egg/parser'
14
+
15
+ require 'ofx'
data/lib/ofx.rb ADDED
@@ -0,0 +1,49 @@
1
+ require 'builder'
2
+
3
+ module Ofx
4
+ class Statement
5
+ def initialize(statement)
6
+ @statement = statement
7
+ @xml = nil
8
+ end
9
+ def to_xml
10
+ @xml ||= (
11
+ buffer = ''
12
+ builder = Builder::XmlMarkup.new(:target => buffer, :indent => 2)
13
+ builder.instruct!
14
+ builder << "<?OFX OFXHEADER=\"200\" VERSION=\"200\" SECURITY=\"NONE\" OLDFILEUID=\"NONE\" NEWFILEUID=\"NONE\"?>\n"
15
+ builder.OFX do
16
+ builder.CREDITCARDMSGSRSV1 do
17
+ builder.CCSTMTTRNRS do
18
+ builder.CCSTMTRS do
19
+ builder.CURDEF @statement.account_currency
20
+ builder.CCACCTFROM do
21
+ builder.ACCTID @statement.account_number
22
+ end
23
+ builder.BANKTRANLIST do
24
+ builder.DTSTART @statement.from_date
25
+ builder.DTEND @statement.to_date
26
+ @statement.transactions.each do |transaction|
27
+ builder.STMTTRN do
28
+ builder.TRNTYPE transaction.type
29
+ builder.DTPOSTED transaction.date
30
+ builder.NAME transaction.payee
31
+ builder.MEMO transaction.note
32
+ builder.TRNAMT transaction.amount
33
+ builder.FITID transaction.ofx_id
34
+ end
35
+ end
36
+ end
37
+ builder.LEDGERBAL do
38
+ builder.BALAMT @statement.closing_balance
39
+ builder.DTASOF @statement.to_date
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ buffer
46
+ )
47
+ end
48
+ end
49
+ end