ynap 1.0.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.
@@ -0,0 +1,305 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Plaid Quickstart OAuth Response Page Example</title>
6
+ <link rel="stylesheet" href="https://threads.plaid.com/threads.css">
7
+
8
+ <meta name="viewport" content="width=device-width, initial-scale=1">
9
+ <style>
10
+ /*
11
+
12
+ Base
13
+
14
+ */
15
+
16
+ body {
17
+ background: #ffffff;
18
+ }
19
+
20
+ .main {
21
+ max-width: 960px;
22
+ margin-right: auto;
23
+ margin-left: auto;
24
+ padding: 80px 40px;
25
+ }
26
+
27
+
28
+ /*
29
+
30
+ Content at the top of the view
31
+
32
+ */
33
+ .loading-indicator {
34
+ -webkit-animation: rot 1200ms infinite cubic-bezier(0.23, 1.2, 0.32, 1);
35
+ animation: rot 1200ms infinite cubic-bezier(0.23, 1.2, 0.32, 1);
36
+ border-bottom: 2px solid #e3e3e3;
37
+ border-left: 2px solid #e3e3e3;
38
+ border-radius: 100%;
39
+ border-right: 2px solid #e3e3e3;
40
+ border-top: 2px solid #7e7e7e;
41
+ font-size: 100%;
42
+ font: inherit;
43
+ height: 45px;
44
+ left: calc(50% - (45px / 2));
45
+ margin: 0;
46
+ padding: 0;
47
+ position: absolute;
48
+ top: 80%;
49
+ vertical-align: baseline;
50
+ width: 45px;
51
+ box-sizing: border-box
52
+ }
53
+ @keyframes rot {
54
+ from {
55
+ -webkit-transform: rotate(0deg);
56
+ transform: rotate(0deg);
57
+ }
58
+ to {
59
+ -webkit-transform: rotate(359deg);
60
+ transform: rotate(359deg);
61
+ }
62
+ }
63
+ @-webkit-keyframes rot {
64
+ from {
65
+ -webkit-transform: rotate(0deg);
66
+ transform: rotate(0deg);
67
+ }
68
+ to {
69
+ -webkit-transform: rotate(359deg);
70
+ transform: rotate(359deg);
71
+ }
72
+ }
73
+
74
+ .everpresent-content {
75
+ border-bottom-width: 1px;
76
+ border-bottom-color: #E3E9EF;
77
+ border-bottom-style: solid;
78
+ margin-bottom: 16px;
79
+ padding-bottom: 16px;
80
+ }
81
+
82
+ .everpresent-content__heading {
83
+ margin-bottom: 16px;
84
+ }
85
+
86
+ .everpresent-content__subheading {
87
+ font-size: 24px;
88
+ font-weight: 300;
89
+ color: #527084;
90
+ line-height: 32px;
91
+ margin-bottom: 0;
92
+ }
93
+
94
+ /*
95
+
96
+ Item overview
97
+
98
+ */
99
+
100
+ .item-overview {
101
+ padding-bottom: 16px;
102
+ }
103
+
104
+ .item-overview__column {
105
+ width: 50%;
106
+ float: left;
107
+ }
108
+
109
+ .item-overview__column:nth-child(1) {
110
+ padding-right: 8px;
111
+ }
112
+
113
+ .item-overview__column:nth-child(2) {
114
+ padding-left: 8px;
115
+ }
116
+
117
+ .item-overview__heading {
118
+ font-weight: bold;
119
+ text-transform: uppercase;
120
+ font-size: 12px;
121
+ margin-bottom: 0;
122
+ }
123
+
124
+ .item-overview__id {
125
+ text-overflow: ellipsis;
126
+ overflow-x: hidden;
127
+ }
128
+
129
+ /*
130
+
131
+ One off tweaks to the layout for tables
132
+
133
+ */
134
+
135
+ .response-row {
136
+ }
137
+
138
+ .response-row--is-identity td {
139
+ width: 25%;
140
+ display: inline-block;
141
+ overflow-wrap: break-word;
142
+ vertical-align: top;
143
+ }
144
+
145
+ /*
146
+
147
+ Display content inside of box
148
+
149
+ */
150
+
151
+ .box {
152
+ border-radius: 4px;
153
+ border-width: 1px;
154
+ border-color: #E3E9EF;
155
+ border-style: solid;
156
+ border-radius: 4px;
157
+ margin-bottom: 24px;
158
+ overflow: hidden;
159
+ box-shadow: 0 1px 2px rgba(3,49,86,0.2);
160
+ }
161
+
162
+ .box__heading {
163
+ padding: 16px 24px;
164
+ border-bottom-width: 1px;
165
+ border-bottom-color: #E3E9EF;
166
+ border-bottom-style: solid;
167
+ background-color: #FAFCFD;
168
+ margin-bottom: 0;
169
+ }
170
+
171
+
172
+ /*
173
+
174
+ Item row
175
+
176
+ */
177
+ .item-data-row {
178
+ border-bottom-width: 1px;
179
+ border-bottom-color: #E3E9EF;
180
+ border-bottom-style: solid;
181
+ padding-top: 24px;
182
+ padding-bottom: 24px;
183
+ padding-left: 24px;
184
+ padding-right: 24px;
185
+ }
186
+
187
+ .item-data-row:after {
188
+ clear: both;
189
+ content: '';
190
+ display: table;
191
+ }
192
+
193
+ .item-data-row:last-child {
194
+ margin-bottom: 0;
195
+ border-bottom: 0;
196
+ }
197
+
198
+ .item-data-row__request-type {
199
+ background-color
200
+ font-size: 12px;
201
+ color: #02B1F8;
202
+ letter-spacing: 0;
203
+ background: #D9F3FE;
204
+ font-weight: 600;
205
+ text-transform: uppercase;
206
+ border-radius: 4px;
207
+ height: 32px;
208
+ line-height: 32px;
209
+ text-align: center;
210
+ font-size: 14px;
211
+ }
212
+
213
+ .item-data-row__endpoint {
214
+ font-family: "monaco", courier;
215
+ font-size: 14px;
216
+ line-height: 32px;
217
+ display: inline-block;
218
+ }
219
+
220
+ .item-data-row__nicename {
221
+ font-size: 16px;
222
+ line-height: 32px;
223
+ display: inline-block;
224
+ font-weight: 500;
225
+ color: #033156;
226
+ }
227
+
228
+
229
+ .item-data-row__left {
230
+ width: 13%;
231
+ float: left;
232
+ }
233
+
234
+ .item-data-row__center {
235
+ width: 62%;
236
+ float: left;
237
+ padding-left: 16px;
238
+ }
239
+
240
+ .item-data-row__right {
241
+ width: 25%;
242
+ float: left;
243
+ }
244
+
245
+ /*
246
+
247
+ Hide things
248
+
249
+ */
250
+
251
+ #app {
252
+ display: none;
253
+ }
254
+
255
+ #steps {
256
+ display: none;
257
+ }
258
+
259
+ /*
260
+
261
+ Errors
262
+
263
+ */
264
+
265
+ .alert {
266
+ padding: 15px;
267
+ margin-top: 20px;
268
+ margin-bottom: 20px;
269
+ border: 1px solid transparent;
270
+ border-radius: 4px;
271
+ }
272
+
273
+ .alert-danger {
274
+ color: #a94442;
275
+ background-color: #f2dede;
276
+ border-color: #ebccd1;
277
+ }
278
+
279
+ .alert-danger a {
280
+ color: #a94442;
281
+ font-weight: bold;
282
+ }
283
+ </style>
284
+ </head>
285
+ <body>
286
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js"></script>
287
+ <script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
288
+ <script>
289
+ (function($) {
290
+ var linkToken = localStorage.getItem('link_token');
291
+ var handler = Plaid.create({
292
+ token: linkToken,
293
+ receivedRedirectUri: window.location.href,
294
+ onSuccess: function(public_token) {
295
+ $.post('/api/set_access_token', {public_token: public_token}, function(data) {
296
+ location.href = 'http://localhost:8000';
297
+ })
298
+ },
299
+ });
300
+ handler.open();
301
+ })(jQuery);
302
+ </script>
303
+ </body>
304
+ </html>
305
+
@@ -0,0 +1,42 @@
1
+ require "bigdecimal"
2
+ require "date"
3
+ require "yaml"
4
+ require "ynab"
5
+ require "ynap/extensions/plaid/models/transaction"
6
+ require "ynap/extensions/ynab/save_transaction"
7
+ require "ynap/extensions/ynab/transaction_detail"
8
+ require "ynap/extensions/float"
9
+ require "ynap/extensions/integer"
10
+ require "ynap/models/account"
11
+ require "ynap/models/bank"
12
+ require "ynap/values/params_converter"
13
+ require "ynap/payee_parser"
14
+ require "ynap/version"
15
+
16
+ module Ynap
17
+ class Error < StandardError; end
18
+
19
+ def self.config
20
+ @config ||= YAML.load(File.read('ynap.yml'))
21
+ end
22
+
23
+ def self.config=(path)
24
+ @config = YAML.load(File.read(path))
25
+ end
26
+
27
+ def self.bank_config(id)
28
+ config[:banks].find { |bank_params| bank_params[:id] == id }
29
+ end
30
+
31
+ def self.regexp
32
+ @regexp ||= Regexp.union config[:regex].map{ |s| Regexp.new s }
33
+ end
34
+
35
+ def self.plaid_client
36
+ @plaid_client ||= Plaid::Client.new config[:plaid].slice(:env, :client_id, :secret)
37
+ end
38
+
39
+ def self.ynab_client
40
+ @ynab_client ||= YNAB::API.new config.dig(:ynab, :token)
41
+ end
42
+ end
@@ -0,0 +1,113 @@
1
+ require "thor"
2
+ require "ynap"
3
+
4
+ module Ynap
5
+ class CLI < Thor
6
+ DEFAULT_CONFIG_PATH = 'ynap.yml'
7
+
8
+ class_option :verbose, :type => :boolean, :aliases => "-v"
9
+
10
+ #
11
+ # System commands
12
+ #
13
+
14
+ desc "console", "Start a YNAP console"
15
+ def console
16
+ system("ruby bin/console")
17
+ end
18
+
19
+ desc "plaid", "Start the Plaid web server, used during setup to retrieve access tokens"
20
+ method_option :config, type: :string, aliases: '-c', default: DEFAULT_CONFIG_PATH
21
+ def plaid
22
+ system("ruby bin/plaid #{options.config}")
23
+ end
24
+
25
+ #
26
+ # Queries
27
+ #
28
+
29
+ desc "plaid_ids [BANK_KEY]", "Print Plaid accounts details for setup"
30
+ method_option :config, type: :string, aliases: '-c', default: DEFAULT_CONFIG_PATH
31
+ def plaid_ids(bank_id)
32
+ load_config options.config
33
+ puts Bank.find(bank_id).all_plaid_ids
34
+ end
35
+
36
+ desc "balances", "Print balances from Plaid"
37
+ method_option :config, type: :string, aliases: '-c', default: DEFAULT_CONFIG_PATH
38
+ method_option :bank, type: :string, aliases: '-b'
39
+ def balances
40
+ load_config options.config
41
+ if options.bank.nil?
42
+ puts Bank.accounts_descriptions
43
+ else
44
+ puts Bank.find(options.bank).accounts_descriptions
45
+ end
46
+ end
47
+
48
+ desc "diff [BANK_KEY]", "Print the latest transactions for YNAB and Plaid for comparison."
49
+ method_option :config, type: :string, aliases: '-c', default: DEFAULT_CONFIG_PATH
50
+ method_option :limit, type: :numeric, aliases: '-l', default: 10
51
+ def diff(bank_id)
52
+ load_config options.config
53
+ bank = Bank.find(bank_id)
54
+ puts "**************\n* Last #{options.limit} transactions for each account @ #{bank.name}\n**************"
55
+ bank.accounts.each do |account|
56
+ puts "\nAccount: #{account.description}\n"
57
+ puts "\nPlaid\n-------"
58
+ puts account.plaid_transactions.first(options.limit).map(&:description).join("\n")
59
+ puts "\nYNAB\n-------"
60
+ puts account.ynab_transactions.first(options.limit).map(&:description).join("\n")
61
+ puts "\n*******"
62
+ end
63
+ end
64
+
65
+ desc "import", "Import available transactions from Plaid to YNAB"
66
+ method_option :config, type: :string, aliases: '-c', default: DEFAULT_CONFIG_PATH
67
+ method_option :bank, type: :string, aliases: '-b'
68
+ method_option :reconcile, type: :boolean, aliases: '-r', default: true, desc: "Fetches balances after import (slower)"
69
+ def import
70
+ load_config options.config
71
+ puts "* Fetching transactions and preparing import\n"
72
+ if options.bank.nil?
73
+ Bank.all.each(&:import)
74
+ else
75
+ Bank.find(options.bank).import
76
+ end
77
+ if options.reconcile
78
+ puts "\n* Fetching balances\n"
79
+ balances
80
+ end
81
+ end
82
+
83
+ desc "payees", "List Payees Names for available transactions"
84
+ method_option :config, type: :string, aliases: '-c', default: DEFAULT_CONFIG_PATH
85
+ method_option :bank, type: :string, aliases: '-b'
86
+ method_option :memos, type: :boolean, aliases: '-m', default: false
87
+ def payees
88
+ load_config options.config
89
+ if options.bank.nil?
90
+ puts Bank.payees(with_memos: options.memos).join("\n")
91
+ else
92
+
93
+ puts Bank.find(options.bank).send(options.memos ? :payees_memos : :payees).join("\n")
94
+ end
95
+ end
96
+
97
+ desc "transactions [BANK_KEY]", "Print transactions"
98
+ method_option :config, type: :string, aliases: '-c', default: DEFAULT_CONFIG_PATH
99
+ method_option :side, type: :string, aliases: '-s', default: 'plaid'
100
+ def transactions(bank_id)
101
+ load_config options.config
102
+ bank = Bank.find bank_id
103
+ scope = options.side == 'ynab' ? :ynab_transactions : :plaid_transactions
104
+ puts bank.send(scope).map(&:description).join("\n")
105
+ end
106
+
107
+ no_commands{
108
+ def load_config(path)
109
+ Ynap.config = path unless path.nil?
110
+ end
111
+ }
112
+ end
113
+ end