burglar 0.0.3 → 0.1.0

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