egg2ofx 0.1.1

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