burglar 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ac080f5a8dd80fc3e143c80017fb735cd27511d686a088fe257e8e604658a9e
4
- data.tar.gz: 386b942a22b29cfc4601ab20cbf36953dc0b1d0123227fe1aeb205cedb3af1d1
3
+ metadata.gz: 6cbc26fc77ea4427b7ae6cbfa3d53fb441e0897beafba7797b63a7cd853b4a45
4
+ data.tar.gz: 45e247c5fa8886e65d70ebc35e61227f9c64b5012247922b7373ed20aa76a8fd
5
5
  SHA512:
6
- metadata.gz: de433fd0719a368466ba29e9de9346e8027c0a4bfda545d32947b9a0747e75976d94b919de967fbd85c35f8bc0ef3af2530dc094ec5dbcf7192bfb53ea7d045f
7
- data.tar.gz: 8315d152306e918330142da70d9beb943e5adbcd44485e8ccf7b857a21839e6522caad6677c0ad7ce873892cce46c08c2b2b7bd85f3c3800bce5561c6cfe35f5
6
+ metadata.gz: 7d33eb6451240ee1300b6ecdb2a39792f859aec122084d31470b1ba2fbae0ed3ab50f84ae5d986b4584358c0b8d1d2090811da5de8dcfb749872796a64b42daa
7
+ data.tar.gz: 20af4b002fc0e12ae83d54a70a22db7b9580816920fdf6ea0eb02d9392e5c2c3104a289a6ede64423cea94cc12fc1d119d905d9dc0f9f8dc2c7f6bc38306168f
data/.travis.yml CHANGED
@@ -1,4 +1,4 @@
1
- dist: trusty
1
+ dist: xenial
2
2
  install:
3
3
  - for i in $(cat .circle-ruby) ; do rvm install $i || exit 1 ; done
4
4
  - for i in $(cat .circle-ruby) ; do rvm-exec $i bundle install || exit 1 ; done
data/bin/burglar CHANGED
@@ -32,7 +32,7 @@ Mercenary.program(:burglar) do |p|
32
32
 
33
33
  p.action do |_, options|
34
34
  options[:end] = date_parse_or_default(options[:end], Date.today)
35
- options[:begin] = date_parse_or_default(options[:begin], options[:end])
35
+ options[:begin] = date_parse_or_default(options[:begin], options[:end] - 30)
36
36
  config = load_config(options[:config]).merge(options)
37
37
  puts Burglar.new(config).transactions
38
38
  end
data/burglar.gemspec CHANGED
@@ -25,8 +25,8 @@ Gem::Specification.new do |s|
25
25
 
26
26
  s.add_development_dependency 'codecov', '~> 0.1.1'
27
27
  s.add_development_dependency 'fuubar', '~> 2.3.0'
28
- s.add_development_dependency 'goodcop', '~> 0.6.0'
28
+ s.add_development_dependency 'goodcop', '~> 0.7.0'
29
29
  s.add_development_dependency 'rake', '~> 12.3.0'
30
30
  s.add_development_dependency 'rspec', '~> 3.8.0'
31
- s.add_development_dependency 'rubocop', '~> 0.65.0'
31
+ s.add_development_dependency 'rubocop', '~> 0.67.2'
32
32
  end
@@ -0,0 +1,109 @@
1
+ Burglar.extra_dep('plaid', 'plaid')
2
+
3
+ module LogCabin
4
+ module Modules
5
+ ##
6
+ # Plaid
7
+ module Plaid
8
+ include Burglar.helpers.find(:creds)
9
+ include Burglar.helpers.find(:ledger)
10
+
11
+ PLAID_DOMAIN = 'https://plaid.com'.freeze
12
+
13
+ def raw_transactions # rubocop:disable Metrics/MethodLength
14
+ @raw_transactions ||= all_transactions.map do |row|
15
+ amount = "$#{row.amount}"
16
+ name = row.name.downcase
17
+ action = guess_action(name)
18
+ state = row.pending ? :pending : :cleared
19
+
20
+ ::Ledger::Entry.new(
21
+ name: name,
22
+ state: state,
23
+ date: row.date,
24
+ actions: [
25
+ { name: action, amount: amount },
26
+ { name: account_name }
27
+ ]
28
+ )
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def api_client
35
+ @api_client ||= ::Plaid::Client.new(
36
+ env: 'development',
37
+ client_id: client_id,
38
+ secret: secret_key,
39
+ public_key: public_key
40
+ )
41
+ end
42
+
43
+ def get_transactions_page(offset)
44
+ resp = api_client.transactions.get(
45
+ access_token,
46
+ begin_date_str,
47
+ end_date_str,
48
+ account_ids: [account_id],
49
+ offset: offset
50
+ )
51
+ [resp.transactions, resp.total_transactions]
52
+ end
53
+
54
+ def all_transactions
55
+ return @all_transactions if @all_transactions
56
+ list, total = get_transactions_page(0)
57
+ while list.length < total
58
+ new, total = get_transactions_page(list.length)
59
+ list += new
60
+ end
61
+ @all_transactions = list
62
+ end
63
+
64
+ def begin_date_str
65
+ @begin_date_str ||= date_str(begin_date)
66
+ end
67
+
68
+ def end_date_str
69
+ @end_date_str ||= date_str(end_date)
70
+ end
71
+
72
+ def date_str(date)
73
+ date.strftime('%Y-%m-%d')
74
+ end
75
+
76
+ def accounts
77
+ @accounts ||= api_client.accounts.get(access_token)['accounts']
78
+ end
79
+
80
+ def account_id
81
+ @account_id ||= accounts.find do |x|
82
+ x['name'] == account_clean_name
83
+ end['account_id']
84
+ end
85
+
86
+ def account_clean_name
87
+ @account_clean_name ||= @options[:name] || raise(
88
+ 'No account name provided'
89
+ )
90
+ end
91
+
92
+ def client_id
93
+ @client_id ||= creds(PLAID_DOMAIN, 'client_id')
94
+ end
95
+
96
+ def secret_key
97
+ @secret_key ||= creds(PLAID_DOMAIN, 'secret_key')
98
+ end
99
+
100
+ def public_key
101
+ @public_key ||= creds(PLAID_DOMAIN, 'public_key')
102
+ end
103
+
104
+ def access_token
105
+ @access_token ||= creds(PLAID_DOMAIN, account_name)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -3,5 +3,5 @@
3
3
  ##
4
4
  # Set the version (needed for Mercenary -v)
5
5
  module Burglar
6
- VERSION = '0.0.3'.freeze
6
+ VERSION = '0.1.0'.freeze
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: burglar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Les Aker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-03 00:00:00.000000000 Z
11
+ date: 2019-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cymbal
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.6.0
103
+ version: 0.7.0
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 0.6.0
110
+ version: 0.7.0
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rake
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -142,14 +142,14 @@ dependencies:
142
142
  requirements:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: 0.65.0
145
+ version: 0.67.2
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: 0.65.0
152
+ version: 0.67.2
153
153
  description: Tool for parsing data from bank websites
154
154
  email: me@lesaker.org
155
155
  executables:
@@ -175,8 +175,7 @@ files:
175
175
  - lib/burglar/helpers/creds.rb
176
176
  - lib/burglar/helpers/ledger.rb
177
177
  - lib/burglar/helpers/mechanize.rb
178
- - lib/burglar/modules/ally.rb
179
- - lib/burglar/modules/american_express.rb
178
+ - lib/burglar/modules/plaid.rb
180
179
  - lib/burglar/version.rb
181
180
  - spec/burglar_spec.rb
182
181
  - spec/spec_helper.rb
@@ -1,156 +0,0 @@
1
- require 'json'
2
- require 'csv'
3
- require 'date'
4
- require 'digest/sha1'
5
-
6
- module LogCabin
7
- module Modules
8
- ##
9
- # Ally
10
- module Ally # rubocop:disable Metrics/ModuleLength
11
- include Burglar.helpers.find(:creds)
12
- include Burglar.helpers.find(:mechanize)
13
- include Burglar.helpers.find(:ledger)
14
-
15
- ALLY_DOMAIN = 'https://secure.ally.com'.freeze
16
- ALLY_CSRF_URL = ALLY_DOMAIN + '/capi-gw/session/status/olbWeb'
17
- ALLY_AUTH_URL = ALLY_DOMAIN + '/capi-gw/customer/authentication'
18
- ALLY_AUTH_PATCH_URL = ALLY_AUTH_URL + '?_method=PATCH'
19
- ALLY_MFA_URL = ALLY_DOMAIN + '/capi-gw/notification'
20
- ALLY_DEVICE_URL = ALLY_DOMAIN + '/capi-gw/customer/device'
21
- ALLY_DEVICE_PATCH_URL = ALLY_DEVICE_URL + '?_method=PATCH'
22
- ALLY_ACCOUNT_URL = ALLY_DOMAIN + '/capi-gw/accounts'
23
-
24
- def raw_transactions
25
- csv.map do |x|
26
- amount = format('$%.2f', x[:amount] * -1)
27
- simple_ledger(x[:date], x[:description], amount)
28
- end
29
- end
30
-
31
- private
32
-
33
- def default_account_name
34
- 'Assets:' + @options[:name].gsub(/\s/, '')
35
- end
36
-
37
- def raw_csv_url
38
- ALLY_DOMAIN + "/capi-gw/accounts/#{account_id}/transactions.csv"
39
- end
40
-
41
- def raw_csv
42
- csv_headers = headers('text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8') # rubocop:disable Metrics/LineLength
43
- @raw_csv ||= mech.get(raw_csv_url, csv_data, nil, csv_headers)
44
- end
45
-
46
- def csv
47
- @csv ||= CSV.new(
48
- raw_csv.body,
49
- headers: true,
50
- header_converters: :symbol,
51
- converters: %i[date float]
52
- ).map(&:to_h)
53
- end
54
-
55
- def csv_data
56
- @csv_data ||= {
57
- 'fromDate' => begin_date.strftime('%Y-%m-%d'),
58
- 'toDate' => end_date.strftime('%Y-%m-%d'),
59
- 'status' => 'Posted'
60
- }
61
- end
62
-
63
- def user
64
- @user ||= @options[:user]
65
- end
66
-
67
- def password
68
- @password ||= creds(ALLY_DOMAIN, user)
69
- end
70
-
71
- def csrf_token
72
- mech
73
- @csrf_token ||= mech.get(ALLY_CSRF_URL).response['csrfchallengetoken']
74
- end
75
-
76
- def device_token
77
- return @device_token if @device_token
78
- res = `system_profiler SPHardwareDataType`
79
- raw_token = res.lines.grep(/Serial Number/).first.split.last
80
- @device_token = Digest::SHA1.hexdigest raw_token
81
- end
82
-
83
- def headers(accept, spname = nil)
84
- {
85
- 'CSRFChallengeToken' => csrf_token,
86
- 'ApplicationName' => 'AOB',
87
- 'ApplicationId' => 'ALLYUSBOLB',
88
- 'ApplicationVersion' => '1.0',
89
- 'Accept' => accept,
90
- 'spname' => spname
91
- }.reject { |_, v| v.nil? }
92
- end
93
-
94
- def auth_headers
95
- @auth_headers ||= headers('application/v1+json', 'auth')
96
- end
97
-
98
- def common_data
99
- @common_data = {
100
- 'channelType' => 'OLB',
101
- 'devicePrintRSA' => device_token
102
- }
103
- end
104
-
105
- def auth_data
106
- @auth_data = common_data.merge(
107
- 'userNamePvtEncrypt' => user,
108
- 'passwordPvtBlock' => password,
109
- 'rememberMeFlag' => 'false'
110
- )
111
- end
112
-
113
- def setup_mech
114
- auth = mech.post(ALLY_AUTH_URL, auth_data, auth_headers)
115
- auth_res = JSON.parse(auth.body)
116
- mfa(auth_res) if auth_res['authentication']['mfa']
117
- end
118
-
119
- def mfa_data(payload)
120
- mfa_methods = payload['authentication']['mfa']['mfaDeliveryMethods']
121
- mfa_id = mfa_methods.first['deliveryMethodId']
122
- common_data.merge('deliveryMethodId' => mfa_id)
123
- end
124
-
125
- def mfa_token
126
- UserInput.new(message: 'MFA token', validation: /^\d+$/).ask
127
- end
128
-
129
- def mfa(payload)
130
- mech.post(ALLY_MFA_URL, mfa_data(payload), auth_headers)
131
- data = common_data.merge('otpCodePvtBlock' => mfa_token)
132
- mech.post(ALLY_AUTH_PATCH_URL, data, auth_headers)
133
- mech.post(ALLY_DEVICE_PATCH_URL, common_data, auth_headers)
134
- end
135
-
136
- def accounts
137
- account_headers = headers('application/vnd.api+json', 'common-api')
138
- @accounts ||= JSON.parse(
139
- mech.get(ALLY_ACCOUNT_URL, [], nil, account_headers).body
140
- )
141
- end
142
-
143
- def account
144
- return @account if @account
145
- list = accounts['accounts']['deposit']['accountSummary']
146
- match = list.find { |x| x['accountNickname'].match @options[:name] }
147
- raise('No matching account found') unless match
148
- @account = match
149
- end
150
-
151
- def account_id
152
- @account_id ||= account['accountId']
153
- end
154
- end
155
- end
156
- end
@@ -1,79 +0,0 @@
1
- require 'csv'
2
- require 'date'
3
-
4
- module LogCabin
5
- module Modules
6
- ##
7
- # American Express
8
- module AmericanExpress
9
- include Burglar.helpers.find(:creds)
10
- include Burglar.helpers.find(:ledger)
11
- include Burglar.helpers.find(:mechanize)
12
-
13
- # rubocop:disable Metrics/LineLength
14
- AMEX_DOMAIN = 'https://online.americanexpress.com'.freeze
15
- AMEX_LOGIN_PATH = '/myca/logon/us/action/LogonHandler?request_type=LogonHandler&Face=en_US'.freeze
16
- AMEX_LOGIN_FORM = 'lilo_formLogon'.freeze
17
- AMEX_CSV_PATH = '/myca/estmt/us/downloadTxn.do'.freeze
18
- # rubocop:enable Metrics/LineLength
19
-
20
- def raw_transactions
21
- @raw_transactions ||= csv.map do |row|
22
- raw_date, raw_amount, raw_name = row.values_at(0, 7, 11)
23
- date = Date.strptime(raw_date, '%m/%d/%Y %a')
24
- amount = format('$%.2f', raw_amount)
25
- name = raw_name.empty? ? 'Amex Payment' : raw_name.downcase
26
- simple_ledger(date, name, amount)
27
- end
28
- end
29
-
30
- private
31
-
32
- def default_account_name
33
- 'Liabilities:Credit:american_express'.freeze
34
- end
35
-
36
- def user
37
- @user ||= @options[:user]
38
- end
39
-
40
- def password
41
- @password ||= creds(AMEX_DOMAIN, user)
42
- end
43
-
44
- def setup_mech
45
- mech.user_agent = 'chrome'
46
- page = mech.get(AMEX_DOMAIN + AMEX_LOGIN_PATH)
47
- form = page.form_with(id: AMEX_LOGIN_FORM) do |f|
48
- f.UserID = user
49
- f.Password = password
50
- end
51
- form.submit
52
- end
53
-
54
- def csv
55
- CSV.parse(csv_page.body)
56
- end
57
-
58
- def csv_page
59
- params = static_fields.merge(
60
- 'startDate' => begin_date.strftime('%m%d%Y'),
61
- 'endDate' => end_date.strftime('%m%d%Y')
62
- )
63
- mech.post(AMEX_DOMAIN + AMEX_CSV_PATH, params)
64
- end
65
-
66
- def static_fields
67
- @static_fields ||= {
68
- 'request_type' => 'authreg_Statement',
69
- 'downloadType' => 'C',
70
- 'downloadView' => 'C',
71
- 'downloadWithETDTool' => 'true',
72
- 'viewType' => 'L',
73
- 'reportType' => '1',
74
- 'BPIndex' => '-99'
75
- }.freeze
76
- end
77
- end
78
- end
79
- end