syrup 0.0.2 → 0.0.3
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/.gitignore +1 -0
- data/README.rdoc +1 -1
- data/TODO.rdoc +8 -11
- data/lib/syrup/account.rb +56 -8
- data/lib/syrup/information_missing_error.rb +5 -0
- data/lib/syrup/institutions/institution_base.rb +42 -4
- data/lib/syrup/institutions/zions_bank.rb +18 -16
- data/lib/syrup/transaction.rb +7 -1
- data/lib/syrup/version.rb +1 -1
- data/lib/syrup.rb +21 -2
- data/spec/syrup/institutions/zions_bank_spec.rb +16 -1
- data/syrup.gemspec +3 -3
- metadata +12 -12
data/.gitignore
CHANGED
data/README.rdoc
CHANGED
@@ -43,4 +43,4 @@ like support for a different bank, you have two options:
|
|
43
43
|
|
44
44
|
1. Get me the credentials to log into an account with that bank (you'd have to
|
45
45
|
trust me).
|
46
|
-
2. Implement it yourself and submit a pull request.
|
46
|
+
2. Implement it yourself and submit a pull request. See {Adding Support For Another Institution}[https://github.com/dontangg/syrup/wiki/Adding-Support-For-Another-Institution]
|
data/TODO.rdoc
CHANGED
@@ -1,15 +1,12 @@
|
|
1
1
|
|
2
|
-
|
2
|
+
Make sure that mechanize validates SSL certificates
|
3
3
|
|
4
|
-
|
4
|
+
When getting transactions
|
5
|
+
* populate as many variables on Account as you can (eg. current_balance, etc.)
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
* create an array of Account objects
|
7
|
+
Tests
|
8
|
+
* Add generic tests to test institution implementations
|
9
|
+
* Add tests to test Zions Bank without storing username, password, etc.
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
When getting transactions
|
14
|
-
* create an array of Transaction objects
|
15
|
-
* populate as many variables on Account as you can (eg. current_balance, etc.)
|
11
|
+
Documentation
|
12
|
+
* Improve GitHub wiki documentation
|
data/lib/syrup/account.rb
CHANGED
@@ -1,41 +1,82 @@
|
|
1
1
|
require 'date'
|
2
2
|
|
3
3
|
module Syrup
|
4
|
+
# An account contains all the information related to the account. Information
|
5
|
+
# is loaded lazily so that you can use an account to get transactions without
|
6
|
+
# incurring the cost of getting any account information.
|
4
7
|
class Account
|
5
|
-
|
8
|
+
##
|
9
|
+
# :attr_reader: name
|
10
|
+
# Gets the name of the account (eg. "Don's Checking").
|
11
|
+
|
12
|
+
##
|
13
|
+
# :attr_reader: type
|
14
|
+
# Gets the type of account. Currently, the only valid types are :deposit and :credit.
|
15
|
+
|
16
|
+
##
|
17
|
+
# :attr_reader: account_number
|
18
|
+
|
19
|
+
##
|
20
|
+
# :attr_reader: available_balance
|
21
|
+
|
22
|
+
##
|
23
|
+
# :attr_reader: current_balance
|
24
|
+
|
25
|
+
##
|
26
|
+
# :attr_reader: prior_day_balance
|
27
|
+
|
28
|
+
##
|
29
|
+
# :attr_reader: populated?
|
30
|
+
|
31
|
+
##
|
32
|
+
# :attr_writer: populated
|
33
|
+
|
34
|
+
##
|
35
|
+
# :attr_reader: valid?
|
36
|
+
# Since account information is lazily loaded, the validity of this account isn't immediately
|
37
|
+
# known. Once this account has been populated, this will return true if the account is a
|
38
|
+
# valid account, nil otherwise. Calling this method causes the account to attempt to be populated.
|
39
|
+
|
40
|
+
##
|
41
|
+
# :attr_writer: valid
|
42
|
+
|
43
|
+
#
|
6
44
|
attr_accessor :id
|
7
45
|
attr_writer :name, :type, :account_number, :current_balance, :available_balance, :prior_day_balance
|
8
46
|
|
47
|
+
|
9
48
|
def name
|
10
|
-
populate
|
49
|
+
populate unless @name
|
11
50
|
@name
|
12
51
|
end
|
13
52
|
|
14
53
|
def type
|
15
|
-
populate
|
54
|
+
populate unless @type
|
16
55
|
@type
|
17
56
|
end
|
18
57
|
|
19
58
|
def account_number
|
20
|
-
populate
|
59
|
+
populate unless @account_number
|
21
60
|
@account_number
|
22
61
|
end
|
23
62
|
|
24
63
|
def current_balance
|
25
|
-
populate
|
64
|
+
populate unless @current_balance
|
26
65
|
@current_balance
|
27
66
|
end
|
28
67
|
|
29
68
|
def available_balance
|
30
|
-
populate
|
69
|
+
populate unless @available_balance
|
31
70
|
@available_balance
|
32
71
|
end
|
33
72
|
|
34
73
|
def prior_day_balance
|
35
|
-
populate
|
74
|
+
populate unless @prior_day_balance
|
36
75
|
@prior_day_balance
|
37
76
|
end
|
38
77
|
|
78
|
+
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
|
79
|
+
# attributes (pass a hash with key names matching the associated attribute names).
|
39
80
|
def initialize(attr_hash = nil)
|
40
81
|
if attr_hash
|
41
82
|
attr_hash.each do |k, v|
|
@@ -45,7 +86,7 @@ module Syrup
|
|
45
86
|
|
46
87
|
@cached_transactions = []
|
47
88
|
end
|
48
|
-
|
89
|
+
|
49
90
|
def populated?
|
50
91
|
@populated
|
51
92
|
end
|
@@ -54,23 +95,30 @@ module Syrup
|
|
54
95
|
@populated = value
|
55
96
|
end
|
56
97
|
|
98
|
+
# Populates this account with all of its information
|
57
99
|
def populate
|
100
|
+
puts "populate called"
|
58
101
|
unless populated? || @institution.nil?
|
59
102
|
raise "The account id must not be nil when populating an account" if id.nil?
|
60
103
|
@institution.populate_account(id)
|
61
104
|
end
|
62
105
|
end
|
63
106
|
|
107
|
+
# Tests equality of this account with another account. Accounts are considered equal
|
108
|
+
# if they have the same id.
|
64
109
|
def ==(other_account)
|
65
110
|
other_account.id == self.id && other_account.is_a?(Account)
|
66
111
|
end
|
67
112
|
|
113
|
+
# Returns an array of transactions from this account for the supplied date range.
|
68
114
|
def find_transactions(starting_at, ending_at = Date.today)
|
69
115
|
return [] if starting_at > ending_at
|
70
116
|
|
71
117
|
@institution.fetch_transactions(self.id, starting_at, ending_at)
|
72
118
|
end
|
73
119
|
|
120
|
+
# Merges this account information with another account. The other account's information
|
121
|
+
# overrides this account's.
|
74
122
|
def merge!(account_with_info)
|
75
123
|
if account_with_info
|
76
124
|
account_with_info.instance_variables.each do |filled_var|
|
@@ -1,4 +1,9 @@
|
|
1
1
|
module Syrup
|
2
|
+
# This error is raised when the information supplied was invalid or incorrect.
|
3
|
+
# Here are some example situations:
|
4
|
+
#
|
5
|
+
# * A username/password wasn't supplied.
|
6
|
+
# * The password supplied didn't work.
|
2
7
|
class InformationMissingError < StandardError
|
3
8
|
end
|
4
9
|
end
|
@@ -3,22 +3,49 @@ module Syrup
|
|
3
3
|
class InstitutionBase
|
4
4
|
|
5
5
|
class << self
|
6
|
+
# This method is called whenever a class inherits from this class. We keep track of
|
7
|
+
# all of them because they should all be institutions. This way we can provide a
|
8
|
+
# list of supported institutions via code.
|
6
9
|
def inherited(subclass)
|
7
10
|
@subclasses ||= []
|
8
11
|
@subclasses << subclass
|
9
12
|
end
|
10
13
|
|
14
|
+
# Returns an array of all classes that inherit from this class. Or, in other words,
|
15
|
+
# an array of all supported institutions
|
11
16
|
def subclasses
|
12
17
|
@subclasses
|
13
18
|
end
|
14
19
|
end
|
15
20
|
|
21
|
+
##
|
22
|
+
# :attr_writer: populated
|
23
|
+
|
24
|
+
##
|
25
|
+
# :attr_reader: populated?
|
26
|
+
|
27
|
+
##
|
28
|
+
# :attr_reader: agent
|
29
|
+
# Gets an instance of Mechanize for use by any subclasses.
|
30
|
+
|
31
|
+
##
|
32
|
+
# :attr_reader: accounts
|
33
|
+
# Returns an array of all of the user's accounts at this institution.
|
34
|
+
# If accounts hasn't been populated, it populates accounts and then returns them.
|
35
|
+
|
36
|
+
#
|
16
37
|
attr_accessor :username, :password, :secret_questions
|
17
38
|
|
18
39
|
def initialize
|
19
40
|
@accounts = []
|
20
41
|
end
|
21
42
|
|
43
|
+
# This method allows you to setup an institution with block syntax
|
44
|
+
#
|
45
|
+
# InstitutionBase.setup do |config|
|
46
|
+
# config.username = 'my_user"
|
47
|
+
# ...
|
48
|
+
# end
|
22
49
|
def setup
|
23
50
|
yield self
|
24
51
|
self
|
@@ -36,7 +63,10 @@ module Syrup
|
|
36
63
|
populate_accounts
|
37
64
|
@accounts
|
38
65
|
end
|
39
|
-
|
66
|
+
|
67
|
+
# Returns an account with the specified +account_id+. Always use this method to
|
68
|
+
# create a new `Account` object. If you do, it will get populated correctly whenever
|
69
|
+
# the population occurs.
|
40
70
|
def find_account_by_id(account_id)
|
41
71
|
account = @accounts.find { |a| a.id == account_id }
|
42
72
|
unless account || populated?
|
@@ -45,7 +75,10 @@ module Syrup
|
|
45
75
|
end
|
46
76
|
account
|
47
77
|
end
|
48
|
-
|
78
|
+
|
79
|
+
# Populates an account given an `account_id`. The implementing institution may populate
|
80
|
+
# all accounts when this is called if there isn't a way to only request one account's
|
81
|
+
# information.
|
49
82
|
def populate_account(account_id)
|
50
83
|
unless populated?
|
51
84
|
result = fetch_account(account_id)
|
@@ -61,7 +94,8 @@ module Syrup
|
|
61
94
|
end
|
62
95
|
end
|
63
96
|
end
|
64
|
-
|
97
|
+
|
98
|
+
# Populates all of the user's accounts at this institution.
|
65
99
|
def populate_accounts(populated_accounts = nil)
|
66
100
|
unless populated?
|
67
101
|
all_accounts = populated_accounts || fetch_accounts
|
@@ -101,7 +135,11 @@ module Syrup
|
|
101
135
|
def agent
|
102
136
|
@agent ||= Mechanize.new
|
103
137
|
end
|
104
|
-
|
138
|
+
|
139
|
+
# This is just a helper method that simplifies the common process of extracting a number
|
140
|
+
# from a string representing a currency.
|
141
|
+
#
|
142
|
+
# parse_currency('$ 1,234.56') #=> 1234.56
|
105
143
|
def parse_currency(currency)
|
106
144
|
currency.scan(/[0-9.]/).join.to_f
|
107
145
|
end
|
@@ -23,7 +23,7 @@ module Syrup
|
|
23
23
|
|
24
24
|
# List accounts
|
25
25
|
page = agent.get('https://banking.zionsbank.com/ibuir/displayAccountBalance.htm')
|
26
|
-
json =
|
26
|
+
json = MultiJson.decode(page.body)
|
27
27
|
|
28
28
|
accounts = []
|
29
29
|
json['accountBalance']['depositAccountList'].each do |account|
|
@@ -58,7 +58,23 @@ module Syrup
|
|
58
58
|
|
59
59
|
page = agent.post("https://banking.zionsbank.com/zfnb/userServlet/app/bank/user/register_view_main?reSort=false&actAcct=#{account_id}", post_vars)
|
60
60
|
|
61
|
+
# Get all the transactions
|
61
62
|
page.search('tr').each do |row_element|
|
63
|
+
# Look for the account information first
|
64
|
+
account = find_account_by_id(account_id)
|
65
|
+
datapart = row_element.css('.acct')
|
66
|
+
if datapart
|
67
|
+
/Prior Day Balance:\s*([^<]+)/.match(datapart.inner_html) do |match|
|
68
|
+
account.prior_day_balance = parse_currency(match[1])
|
69
|
+
end
|
70
|
+
/Current Balance:\s*([^<]+)/.match(datapart.inner_html) do |match|
|
71
|
+
account.current_balance = parse_currency(match[1])
|
72
|
+
end
|
73
|
+
/Available Balance:\s*([^<]+)/.match(datapart.inner_html) do |match|
|
74
|
+
account.available_balance = parse_currency(match[1])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
62
78
|
data = []
|
63
79
|
datapart = row_element.css('.data')
|
64
80
|
if datapart
|
@@ -93,20 +109,6 @@ module Syrup
|
|
93
109
|
end
|
94
110
|
end
|
95
111
|
|
96
|
-
# https://banking.zionsbank.com/zfnb/userServlet/app/bank/user/register_view_main?actAcct=498282&sortBy=Default&sortOrder=Default
|
97
|
-
# actAcct is the accountId (498282)
|
98
|
-
# dayRange.startDate, dayRange.endDate
|
99
|
-
# dayRange.searchType (dates or days, dates uses dayRange.startDate and dayRange.endDate, days uses dayRange.numberOfDays)
|
100
|
-
|
101
|
-
# The transactions table is messy. Cells we want either have data, curr, datagrey, or currgrey css class
|
102
|
-
# 1. The date (initiated or cleared? they're generally the same)
|
103
|
-
# 2. The type of transaction (Debit, Transfer Debit, ATM Debit, Deposit 3785596). This may be irrelevant because of the position of the transaction amount.)
|
104
|
-
# 3. The payee
|
105
|
-
# 4. The transaction status (Posted or ... Pending?)
|
106
|
-
# 5. The debit amount
|
107
|
-
# 6. The deposit amount
|
108
|
-
# 7. The then-current account balance
|
109
|
-
|
110
112
|
transactions
|
111
113
|
end
|
112
114
|
|
@@ -130,7 +132,7 @@ module Syrup
|
|
130
132
|
page = form.submit
|
131
133
|
|
132
134
|
# If the supplied username is incorrect, raise an exception
|
133
|
-
raise "Invalid username" if page.title == "Error Page"
|
135
|
+
raise InformationMissingError, "Invalid username" if page.title == "Error Page"
|
134
136
|
|
135
137
|
# Go on to the next page
|
136
138
|
page = page.links.first.click
|
data/lib/syrup/transaction.rb
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
module Syrup
|
2
2
|
class Transaction
|
3
|
-
|
3
|
+
##
|
4
|
+
# :attr_accessor: status
|
5
|
+
# Currently, the only valid types are :posted and :pending
|
6
|
+
|
7
|
+
#
|
4
8
|
attr_accessor :id, :payee, :amount, :posted_at, :status
|
5
9
|
|
10
|
+
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
|
11
|
+
# attributes (pass a hash with key names matching the associated attribute names).
|
6
12
|
def initialize(attr_hash = nil)
|
7
13
|
if attr_hash
|
8
14
|
attr_hash.each do |k, v|
|
data/lib/syrup/version.rb
CHANGED
data/lib/syrup.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'date'
|
2
2
|
require 'mechanize'
|
3
|
-
require '
|
3
|
+
require 'multi_json'
|
4
4
|
require 'syrup/information_missing_error'
|
5
5
|
require 'syrup/account'
|
6
6
|
require 'syrup/transaction'
|
@@ -12,16 +12,35 @@ Dir[File.dirname(__FILE__) + '/syrup/institutions/*.rb'].each {|file| require fi
|
|
12
12
|
module Syrup
|
13
13
|
extend self
|
14
14
|
|
15
|
+
# Returns an array of institutions.
|
16
|
+
#
|
17
|
+
# Syrup.institutions.each do |institution|
|
18
|
+
# puts "name: #{institution.name}, id: #{institution.id}"
|
19
|
+
# end
|
15
20
|
def institutions
|
16
21
|
Institutions::InstitutionBase.subclasses
|
17
22
|
end
|
18
23
|
|
24
|
+
# Returns a new institution object with the specified +institution_id+.
|
25
|
+
# If you pass in a block, you can use it to setup the username, password, and secret_questions.
|
26
|
+
#
|
27
|
+
# Syrup.setup_institution('zions_bank') do |config|
|
28
|
+
# config.username = "my_user"
|
29
|
+
# config.password = "my_password"
|
30
|
+
# config.secret_questions = {
|
31
|
+
# 'How long is your beard?' => '6in'
|
32
|
+
# }
|
33
|
+
# end
|
19
34
|
def setup_institution(institution_id)
|
20
35
|
institution = institutions.find { |i| i.id == institution_id }
|
21
36
|
|
22
37
|
if institution
|
23
38
|
i = institution.new
|
24
|
-
|
39
|
+
if block_given?
|
40
|
+
i.setup { |config| yield config }
|
41
|
+
else
|
42
|
+
i
|
43
|
+
end
|
25
44
|
end
|
26
45
|
end
|
27
46
|
end
|
@@ -21,6 +21,21 @@ describe ZionsBank, :bank_integration => true do
|
|
21
21
|
end
|
22
22
|
|
23
23
|
it "fetches transactions given a date range" do
|
24
|
-
|
24
|
+
account_id = 1
|
25
|
+
|
26
|
+
account = @bank.find_account_by_id(account_id)
|
27
|
+
account.instance_variable_get(:@prior_day_balance).should be_nil
|
28
|
+
account.instance_variable_get(:@current_balance).should be_nil
|
29
|
+
account.instance_variable_get(:@available_balance).should be_nil
|
30
|
+
|
31
|
+
@bank.fetch_transactions(account_id, Date.today - 30, Date.today)
|
32
|
+
|
33
|
+
puts account.prior_day_balance
|
34
|
+
puts account.current_balance
|
35
|
+
puts account.available_balance
|
36
|
+
|
37
|
+
account.prior_day_balance.should_not be_nil
|
38
|
+
account.current_balance.should_not be_nil
|
39
|
+
account.available_balance.should_not be_nil
|
25
40
|
end
|
26
41
|
end
|
data/syrup.gemspec
CHANGED
@@ -12,10 +12,10 @@ Gem::Specification.new do |s|
|
|
12
12
|
s.summary = %q{Simple account balance and transactions extractor.}
|
13
13
|
s.description = %q{Simple account balance and transactions extractor by scraping bank websites.}
|
14
14
|
|
15
|
-
s.add_dependency "mechanize"
|
16
|
-
s.add_dependency "
|
15
|
+
s.add_dependency "mechanize", ">= 1.0.0"
|
16
|
+
s.add_dependency "multi_json", ">= 1.0.3"
|
17
17
|
|
18
|
-
s.add_development_dependency "rspec"
|
18
|
+
s.add_development_dependency "rspec", ">= 2.6.0"
|
19
19
|
|
20
20
|
s.rubyforge_project = s.name
|
21
21
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: syrup
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,41 +9,41 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-06-
|
12
|
+
date: 2011-06-29 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mechanize
|
16
|
-
requirement: &
|
16
|
+
requirement: &25418244 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version:
|
21
|
+
version: 1.0.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *25418244
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
|
-
name:
|
27
|
-
requirement: &
|
26
|
+
name: multi_json
|
27
|
+
requirement: &25419708 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version:
|
32
|
+
version: 1.0.3
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *25419708
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rspec
|
38
|
-
requirement: &
|
38
|
+
requirement: &25421004 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
42
42
|
- !ruby/object:Gem::Version
|
43
|
-
version:
|
43
|
+
version: 2.6.0
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *25421004
|
47
47
|
description: Simple account balance and transactions extractor by scraping bank websites.
|
48
48
|
email:
|
49
49
|
- dontangg@gmail.com
|