ynap 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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