syrup 0.0.3 → 0.0.4

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.
@@ -1,149 +1,149 @@
1
- module Syrup
2
- module Institutions
3
- class InstitutionBase
4
-
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.
9
- def inherited(subclass)
10
- @subclasses ||= []
11
- @subclasses << subclass
12
- end
13
-
14
- # Returns an array of all classes that inherit from this class. Or, in other words,
15
- # an array of all supported institutions
16
- def subclasses
17
- @subclasses
18
- end
19
- end
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
- #
37
- attr_accessor :username, :password, :secret_questions
38
-
39
- def initialize
40
- @accounts = []
41
- end
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
49
- def setup
50
- yield self
51
- self
52
- end
53
-
54
- def populated?
55
- @populated
56
- end
57
-
58
- def populated=(value)
59
- @populated = value
60
- end
61
-
62
- def accounts
63
- populate_accounts
64
- @accounts
65
- end
1
+ module Syrup
2
+ module Institutions
3
+ class InstitutionBase
4
+
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.
9
+ def inherited(subclass)
10
+ @subclasses ||= []
11
+ @subclasses << subclass
12
+ end
13
+
14
+ # Returns an array of all classes that inherit from this class. Or, in other words,
15
+ # an array of all supported institutions
16
+ def subclasses
17
+ @subclasses
18
+ end
19
+ end
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
+ #
37
+ attr_accessor :username, :password, :secret_questions
38
+
39
+ def initialize
40
+ @accounts = []
41
+ end
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
49
+ def setup
50
+ yield self
51
+ self
52
+ end
53
+
54
+ def populated?
55
+ @populated
56
+ end
57
+
58
+ def populated=(value)
59
+ @populated = value
60
+ end
61
+
62
+ def accounts
63
+ populate_accounts
64
+ @accounts
65
+ end
66
66
 
67
67
  # Returns an account with the specified +account_id+. Always use this method to
68
68
  # create a new `Account` object. If you do, it will get populated correctly whenever
69
- # the population occurs.
70
- def find_account_by_id(account_id)
71
- account = @accounts.find { |a| a.id == account_id }
72
- unless account || populated?
73
- account = Account.new(:id => account_id)
74
- @accounts << account
75
- end
76
- account
77
- end
69
+ # the population occurs.
70
+ def find_account_by_id(account_id)
71
+ account = @accounts.find { |a| a.id == account_id }
72
+ unless account || populated?
73
+ account = Account.new(:id => account_id, :institution => self)
74
+ @accounts << account
75
+ end
76
+ account
77
+ end
78
78
 
79
79
  # Populates an account given an `account_id`. The implementing institution may populate
80
80
  # all accounts when this is called if there isn't a way to only request one account's
81
- # information.
82
- def populate_account(account_id)
83
- unless populated?
84
- result = fetch_account(account_id)
85
- return nil if result.nil?
86
-
87
- if result.respond_to?(:each)
88
- populate_accounts(result)
89
- find_account_by_id(account_id)
90
- else
91
- result.populated = true
92
- account = find_account_by_id(account_id)
93
- account.merge! result if account
94
- end
95
- end
96
- end
97
-
98
- # Populates all of the user's accounts at this institution.
99
- def populate_accounts(populated_accounts = nil)
100
- unless populated?
101
- all_accounts = populated_accounts || fetch_accounts
102
-
103
- # Remove any accounts that were added, that don't actually exist
104
- @accounts.keep_if do |a|
105
- if all_accounts.include?(a)
106
- true
107
- else
108
- a.valid = false
109
- false
110
- end
111
- end
112
-
113
- # Add any additional account information
114
- new_accounts = []
115
- all_accounts.each do |filled_account|
116
- account = @accounts.find { |a| a.id == filled_account.id }
117
-
118
- filled_account.populated = true
119
-
120
- # If we already had an account with this id, fill it with data
121
- if account
122
- account.merge! filled_account
123
- else
124
- new_accounts << filled_account
125
- end
126
- end
127
- @accounts |= new_accounts # Uses set union
128
-
129
- self.populated = true
130
- end
131
- end
132
-
133
- protected
134
-
135
- def agent
136
- @agent ||= Mechanize.new
137
- end
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
143
- def parse_currency(currency)
144
- currency.scan(/[0-9.]/).join.to_f
145
- end
146
-
147
- end
148
- end
81
+ # information.
82
+ def populate_account(account_id)
83
+ unless populated?
84
+ result = fetch_account(account_id)
85
+ return nil if result.nil?
86
+
87
+ if result.respond_to?(:each)
88
+ populate_accounts(result)
89
+ find_account_by_id(account_id)
90
+ else
91
+ result.populated = true
92
+ account = find_account_by_id(account_id)
93
+ account.merge! result if account
94
+ end
95
+ end
96
+ end
97
+
98
+ # Populates all of the user's accounts at this institution.
99
+ def populate_accounts(populated_accounts = nil)
100
+ unless populated?
101
+ all_accounts = populated_accounts || fetch_accounts
102
+
103
+ # Remove any accounts that were added, that don't actually exist
104
+ @accounts.keep_if do |a|
105
+ if all_accounts.include?(a)
106
+ true
107
+ else
108
+ a.valid = false
109
+ false
110
+ end
111
+ end
112
+
113
+ # Add any additional account information
114
+ new_accounts = []
115
+ all_accounts.each do |filled_account|
116
+ account = @accounts.find { |a| a.id == filled_account.id }
117
+
118
+ filled_account.populated = true
119
+
120
+ # If we already had an account with this id, fill it with data
121
+ if account
122
+ account.merge! filled_account
123
+ else
124
+ new_accounts << filled_account
125
+ end
126
+ end
127
+ @accounts |= new_accounts # Uses set union
128
+
129
+ self.populated = true
130
+ end
131
+ end
132
+
133
+ protected
134
+
135
+ def agent
136
+ @agent ||= Mechanize.new
137
+ end
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
143
+ def parse_currency(currency)
144
+ currency.scan(/[0-9.]/).join.to_f
145
+ end
146
+
147
+ end
148
+ end
149
149
  end
@@ -0,0 +1,135 @@
1
+ require 'date'
2
+
3
+ module Syrup
4
+ module Institutions
5
+ class Uccu < InstitutionBase
6
+
7
+ class << self
8
+ def name
9
+ "UCCU"
10
+ end
11
+
12
+ def id
13
+ "uccu"
14
+ end
15
+ end
16
+
17
+ def fetch_account(account_id)
18
+ fetch_accounts
19
+ end
20
+
21
+ def fetch_accounts
22
+ ensure_authenticated
23
+
24
+ # List accounts
25
+ page = agent.post('https://pb.uccu.com/UCCU/Ajax/RpcHandler.ashx',
26
+ '{"id":0,"method":"accounts.getBalances","params":[false]}',
27
+ 'X-JSON-RPC' => 'accounts.getBalances')
28
+
29
+ json = MultiJson.decode(page.body)
30
+
31
+ accounts = []
32
+ json['result'].each do |account|
33
+ next if account['accountIndex'] == -1
34
+
35
+ new_account = Account.new(:id => account['accountIndex'], :institution => self)
36
+ new_account.name = account['displayName'][/^[^(]*/, 0].strip
37
+ new_account.account_number = account['displayName'][/\(([*0-9-]+)\)/, 1]
38
+ new_account.current_balance = account['current'].to_f
39
+ new_account.available_balance = account['available'].to_f
40
+ # new_account.type = :deposit # :credit
41
+
42
+ accounts << new_account
43
+ end
44
+
45
+ accounts
46
+ end
47
+
48
+ def fetch_transactions(account_id, starting_at, ending_at)
49
+ ensure_authenticated
50
+
51
+ transactions = []
52
+
53
+ page = agent.get("https://pb.uccu.com/UCCU/Accounts/Activity.aspx?index=#{account_id}")
54
+ form = page.form("MAINFORM")
55
+ form.ddlAccounts = account_id
56
+ form.ddlType = 0 # 0 = All types of transactions
57
+ form.field_with(:id => 'txtFromDate_textBox').value = starting_at.month.to_s + starting_at.strftime('/%e/%Y')
58
+ form.field_with(:id => 'txtToDate_textBox').value = ending_at.month.to_s + ending_at.strftime('/%e/%Y')
59
+ submit_button = form.button_with(:name => 'btnSubmitHistoryRequest')
60
+ page = form.submit(submit_button)
61
+
62
+ # Look for the account balance
63
+ account = find_account_by_id(account_id)
64
+ page.search('.summaryTable tr').each do |row_element|
65
+ first_cell_text = ''
66
+ row_element.children.each do |cell_element|
67
+ if first_cell_text.empty?
68
+ first_cell_text = cell_element.content.strip if cell_element.respond_to? :name
69
+ else
70
+ case first_cell_text
71
+ when "Available Balance:"
72
+ account.available_balance = parse_currency(cell_element.content.strip)
73
+ when "Current Balance:"
74
+ account.current_balance = parse_currency(cell_element.content.strip)
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ # Get all the transactions
81
+ page.search('#ctlAccountActivityChecking tr').each do |row_element|
82
+ next if row_element['class'] == 'header'
83
+
84
+ data = row_element.css('td').map {|element| element.content.strip }
85
+
86
+ transaction = Transaction.new
87
+ transaction.posted_at = Date.strptime(data[0], '%m/%d/%Y')
88
+ transaction.payee = data[3]
89
+ transaction.status = :posted # :pending
90
+ transaction.amount = -parse_currency(data[4]) if data[4].size > 1
91
+ transaction.amount = parse_currency(data[5]) if data[5].size > 1
92
+
93
+ transactions << transaction
94
+ end
95
+
96
+ transactions
97
+ end
98
+
99
+ private
100
+
101
+ def ensure_authenticated
102
+
103
+ # Check to see if already authenticated
104
+ page = agent.get('https://pb.uccu.com/UCCU/Accounts/Activity.aspx')
105
+ if page.body.include?("Please enter your User ID and Password below.") || page.body.include?("Your Online Banking session has expired.")
106
+
107
+ raise InformationMissingError, "Please supply a username" unless self.username
108
+ raise InformationMissingError, "Please supply a password" unless self.password
109
+
110
+ @agent = Mechanize.new
111
+
112
+ # Enter the username
113
+ page = agent.get('https://pb.uccu.com/UCCU/Login.aspx')
114
+ form = page.form('MAINFORM')
115
+ form.field_with(:id => 'ctlSignon_txtUserID').value = username
116
+ form.field_with(:id => 'ctlSignon_txtPassword').value = password
117
+ form.field_with(:id => 'ctlSignon_ddlSignonDestination').value = 'Accounts.Overview'
118
+ form.TestJavaScript = 'OK'
119
+ login_button = form.button_with(:name => 'ctlSignon:btnLogin')
120
+ page = form.submit(login_button)
121
+
122
+ # If the supplied username/password is incorrect, raise an exception
123
+ raise InformationMissingError, "Invalid username or password" if page.body.include?("Login not accepted.") || page.body.include?("Please enter a valid signon ID.") || page.body.include?("Please enter a valid Password.")
124
+
125
+ # Secret questions???
126
+
127
+ raise "Unknown URL reached. Try logging in manually through a browser." if page.uri.to_s != "https://pb.uccu.com/UCCU/Accounts/Overview.aspx"
128
+ end
129
+
130
+ true
131
+ end
132
+
133
+ end
134
+ end
135
+ end