syrup 0.0.8 → 0.0.9

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,186 +1,194 @@
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
-
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.
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
-
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.
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.delete_if do |a|
105
- if all_accounts.include?(a)
106
- false
107
- else
108
- a.valid = false
109
- true
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
- # A helper method that replaces a few HTML entities with their actual characters
148
- #
149
- # unescape_html("You &amp; I") #=> "You & I"
150
- def unescape_html(str)
151
- str.gsub(/&(.*?);/n) do
152
- match = $1.dup
153
- case match
154
- when /\Aamp\z/ni then '&'
155
- when /\Aquot\z/ni then '"'
156
- when /\Agt\z/ni then '>'
157
- when /\Alt\z/ni then '<'
158
- when /\A#0*(\d+)\z/n then
159
- if Integer($1) < 256
160
- Integer($1).chr
161
- else
162
- if Integer($1) < 65536 and ($KCODE[0] == ?u or $KCODE[0] == ?U)
163
- [Integer($1)].pack("U")
164
- else
165
- "&##{$1};"
166
- end
167
- end
168
- when /\A#x([0-9a-f]+)\z/ni then
169
- if $1.hex < 256
170
- $1.hex.chr
171
- else
172
- if $1.hex < 65536 and ($KCODE[0] == ?u or $KCODE[0] == ?U)
173
- [$1.hex].pack("U")
174
- else
175
- "&#x#{$1};"
176
- end
177
- end
178
- else
179
- "&#{match};"
180
- end
181
- end
182
- end
183
-
184
- end
185
- end
186
- 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
+
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.
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
+
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.
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.delete_if do |a|
105
+ if all_accounts.include?(a)
106
+ false
107
+ else
108
+ a.valid = false
109
+ true
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
+ unless @agent
137
+ @agent = Mechanize.new
138
+
139
+ # Provide path to cert bundle for Windows
140
+ # Downloaded from http://curl.haxx.se/ca/
141
+ @agent.agent.http.ca_file = File.expand_path(File.dirname(__FILE__) + "/cacert.pem") if RUBY_PLATFORM =~ /mingw|mswin/i
142
+ end
143
+
144
+ @agent
145
+ end
146
+
147
+ # This is just a helper method that simplifies the common process of extracting a number
148
+ # from a string representing a currency.
149
+ #
150
+ # parse_currency('$ 1,234.56') #=> 1234.56
151
+ def parse_currency(currency)
152
+ currency.scan(/[0-9.]/).join.to_f
153
+ end
154
+
155
+ # A helper method that replaces a few HTML entities with their actual characters
156
+ #
157
+ # unescape_html("You &amp; I") #=> "You & I"
158
+ def unescape_html(str)
159
+ str.gsub(/&(.*?);/n) do
160
+ match = $1.dup
161
+ case match
162
+ when /\Aamp\z/ni then '&'
163
+ when /\Aquot\z/ni then '"'
164
+ when /\Agt\z/ni then '>'
165
+ when /\Alt\z/ni then '<'
166
+ when /\A#0*(\d+)\z/n then
167
+ if Integer($1) < 256
168
+ Integer($1).chr
169
+ else
170
+ if Integer($1) < 65536 and ($KCODE[0] == ?u or $KCODE[0] == ?U)
171
+ [Integer($1)].pack("U")
172
+ else
173
+ "&##{$1};"
174
+ end
175
+ end
176
+ when /\A#x([0-9a-f]+)\z/ni then
177
+ if $1.hex < 256
178
+ $1.hex.chr
179
+ else
180
+ if $1.hex < 65536 and ($KCODE[0] == ?u or $KCODE[0] == ?U)
181
+ [$1.hex].pack("U")
182
+ else
183
+ "&#x#{$1};"
184
+ end
185
+ end
186
+ else
187
+ "&#{match};"
188
+ end
189
+ end
190
+ end
191
+
192
+ end
193
+ end
194
+ end
@@ -1,136 +1,136 @@
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 = unescape_html(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').strip
58
- form.field_with(:id => 'txtToDate_textBox').value = ending_at.month.to_s + '/' + ending_at.strftime('%e/%Y').strip
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
- content = cell_element.content.strip
71
- case first_cell_text
72
- when "Available Balance:"
73
- account.available_balance = parse_currency(content) if content.match(/\d+/)
74
- when "Current Balance:"
75
- account.current_balance = parse_currency(content) if content.match(/\d+/)
76
- end
77
- end
78
- end
79
- end
80
-
81
- # Get all the transactions
82
- page.search('#ctlAccountActivityChecking tr').each do |row_element|
83
- next if row_element['class'] == 'header'
84
-
85
- data = row_element.css('td').map {|element| element.content.strip }
86
-
87
- transaction = Transaction.new
88
- transaction.posted_at = Date.strptime(data[0], '%m/%d/%Y')
89
- transaction.payee = unescape_html(data[3])
90
- transaction.status = :posted # :pending
91
- transaction.amount = -parse_currency(data[4]) if data[4].match(/\d+/)
92
- transaction.amount = parse_currency(data[5]) if data[5].match(/\d+/)
93
-
94
- transactions << transaction
95
- end
96
-
97
- transactions
98
- end
99
-
100
- private
101
-
102
- def ensure_authenticated
103
-
104
- # Check to see if already authenticated
105
- page = agent.get('https://pb.uccu.com/UCCU/Accounts/Activity.aspx')
106
- if page.body.include?("Please enter your User ID and Password below.") || page.body.include?("Your Online Banking session has expired.")
107
-
108
- raise InformationMissingError, "Please supply a username" unless self.username
109
- raise InformationMissingError, "Please supply a password" unless self.password
110
-
111
- @agent = Mechanize.new
112
-
113
- # Enter the username
114
- page = agent.get('https://pb.uccu.com/UCCU/Login.aspx')
115
- form = page.form('MAINFORM')
116
- form.field_with(:id => 'ctlSignon_txtUserID').value = username
117
- form.field_with(:id => 'ctlSignon_txtPassword').value = password
118
- form.field_with(:id => 'ctlSignon_ddlSignonDestination').value = 'Accounts.Overview'
119
- form.TestJavaScript = 'OK'
120
- login_button = form.button_with(:name => 'ctlSignon:btnLogin')
121
- page = form.submit(login_button)
122
-
123
- # If the supplied username/password is incorrect, raise an exception
124
- 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.")
125
-
126
- # Secret questions???
127
-
128
- raise "Unknown URL reached. Try logging in manually through a browser." if page.uri.to_s != "https://pb.uccu.com/UCCU/Accounts/Overview.aspx"
129
- end
130
-
131
- true
132
- end
133
-
134
- end
135
- end
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 = unescape_html(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').strip
58
+ form.field_with(:id => 'txtToDate_textBox').value = ending_at.month.to_s + '/' + ending_at.strftime('%e/%Y').strip
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
+ content = cell_element.content.strip
71
+ case first_cell_text
72
+ when "Available Balance:"
73
+ account.available_balance = parse_currency(content) if content.match(/\d+/)
74
+ when "Current Balance:"
75
+ account.current_balance = parse_currency(content) if content.match(/\d+/)
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ # Get all the transactions
82
+ page.search('#ctlAccountActivityChecking tr').each do |row_element|
83
+ next if row_element['class'] == 'header'
84
+
85
+ data = row_element.css('td').map {|element| element.content.strip }
86
+
87
+ transaction = Transaction.new
88
+ transaction.posted_at = Date.strptime(data[0], '%m/%d/%Y')
89
+ transaction.payee = unescape_html(data[3])
90
+ transaction.status = :posted # :pending
91
+ transaction.amount = -parse_currency(data[4]) if data[4].match(/\d+/)
92
+ transaction.amount = parse_currency(data[5]) if data[5].match(/\d+/)
93
+
94
+ transactions << transaction
95
+ end
96
+
97
+ transactions
98
+ end
99
+
100
+ private
101
+
102
+ def ensure_authenticated
103
+
104
+ # Check to see if already authenticated
105
+ page = agent.get('https://pb.uccu.com/UCCU/Accounts/Activity.aspx')
106
+ if page.body.include?("Please enter your User ID and Password below.") || page.body.include?("Your Online Banking session has expired.")
107
+
108
+ raise InformationMissingError, "Please supply a username" unless self.username
109
+ raise InformationMissingError, "Please supply a password" unless self.password
110
+
111
+ @agent = Mechanize.new
112
+
113
+ # Enter the username
114
+ page = agent.get('https://pb.uccu.com/UCCU/Login.aspx')
115
+ form = page.form('MAINFORM')
116
+ form.field_with(:id => 'ctlSignon_txtUserID').value = username
117
+ form.field_with(:id => 'ctlSignon_txtPassword').value = password
118
+ form.field_with(:id => 'ctlSignon_ddlSignonDestination').value = 'Accounts.Overview'
119
+ form.TestJavaScript = 'OK'
120
+ login_button = form.button_with(:name => 'ctlSignon:btnLogin')
121
+ page = form.submit(login_button)
122
+
123
+ # If the supplied username/password is incorrect, raise an exception
124
+ 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.")
125
+
126
+ # Secret questions???
127
+
128
+ raise "Unknown URL reached. Try logging in manually through a browser." if page.uri.to_s != "https://pb.uccu.com/UCCU/Accounts/Overview.aspx"
129
+ end
130
+
131
+ true
132
+ end
133
+
134
+ end
135
+ end
136
136
  end