egg2ofx 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +1 -0
- data/Rakefile +118 -0
- data/bin/egg2ofx +13 -0
- data/lib/egg/account.rb +8 -0
- data/lib/egg/date.rb +10 -0
- data/lib/egg/description.rb +14 -0
- data/lib/egg/money.rb +13 -0
- data/lib/egg/parser.rb +32 -0
- data/lib/egg/parsers/document_parser.rb +42 -0
- data/lib/egg/parsers/recent_transactions_document_parser.rb +19 -0
- data/lib/egg/parsers/recent_transactions_transaction_parser.rb +12 -0
- data/lib/egg/parsers/statement_document_parser.rb +17 -0
- data/lib/egg/parsers/statement_transaction_parser.rb +19 -0
- data/lib/egg/parsers/transaction_parser.rb +29 -0
- data/lib/egg/statement.rb +24 -0
- data/lib/egg/transaction.rb +33 -0
- data/lib/egg.rb +15 -0
- data/lib/ofx.rb +49 -0
- data/test/fixtures/recent_transactions.html +299 -0
- data/test/fixtures/statement.html +429 -0
- data/test/integration/recent_transactions_integration_test.rb +92 -0
- data/test/integration/statement_integration_test.rb +103 -0
- data/test/test_helper.rb +7 -0
- data/test/unit/description_test.rb +27 -0
- data/test/unit/ofx_test.rb +15 -0
- data/test/unit/parser_test.rb +24 -0
- data/test/unit/parsers/recent_transactions_transaction_parser_test.rb +23 -0
- data/test/unit/parsers/statement_transaction_parser_test.rb +23 -0
- metadata +102 -0
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
|
data/lib/egg/account.rb
ADDED
data/lib/egg/date.rb
ADDED
@@ -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,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
|