syrup 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -4,3 +4,4 @@
4
4
  .bundle
5
5
  Gemfile.lock
6
6
  pkg/*
7
+ doc/*
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
- make sure that mechanize validates SSL certificates
2
+ Make sure that mechanize validates SSL certificates
3
3
 
4
- pass in username, password, secret questions
4
+ When getting transactions
5
+ * populate as many variables on Account as you can (eg. current_balance, etc.)
5
6
 
6
- zions.fetch_accounts
7
- should I store things?
8
- List accounts
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
- account.transactions OR account.fetch_transactions
12
- zions.
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
- # known types are :deposit and :credit
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 = ActiveSupport::JSON.decode(page.body)
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
@@ -1,8 +1,14 @@
1
1
  module Syrup
2
2
  class Transaction
3
- # known statuses are :posted and :pending
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
@@ -1,3 +1,3 @@
1
1
  module Syrup
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
data/lib/syrup.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'date'
2
2
  require 'mechanize'
3
- require 'active_support/json'
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
- i.setup { |config| yield config }
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
- @bank.fetch_transactions
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 "activesupport"
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.2
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-27 00:00:00.000000000Z
12
+ date: 2011-06-29 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mechanize
16
- requirement: &22805304 !ruby/object:Gem::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: '0'
21
+ version: 1.0.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *22805304
24
+ version_requirements: *25418244
25
25
  - !ruby/object:Gem::Dependency
26
- name: activesupport
27
- requirement: &22805052 !ruby/object:Gem::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: '0'
32
+ version: 1.0.3
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *22805052
35
+ version_requirements: *25419708
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rspec
38
- requirement: &22804800 !ruby/object:Gem::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: '0'
43
+ version: 2.6.0
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *22804800
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