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