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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +266 -0
- data/bin/console +14 -0
- data/bin/plaid +349 -0
- data/bin/setup +8 -0
- data/bin/ynap +5 -0
- data/config/ynap.yml.example +29 -0
- data/exe/ynap +6 -0
- data/html/index.html +950 -0
- data/html/oauth-response.html +305 -0
- data/lib/ynap.rb +42 -0
- data/lib/ynap/cli.rb +113 -0
- data/lib/ynap/extensions/float.rb +5 -0
- data/lib/ynap/extensions/integer.rb +5 -0
- data/lib/ynap/extensions/plaid/models/transaction.rb +8 -0
- data/lib/ynap/extensions/ynab/save_transaction.rb +8 -0
- data/lib/ynap/extensions/ynab/transaction_detail.rb +8 -0
- data/lib/ynap/models/account.rb +62 -0
- data/lib/ynap/models/bank.rb +145 -0
- data/lib/ynap/models/bridge_record.rb +9 -0
- data/lib/ynap/payee_parser.rb +12 -0
- data/lib/ynap/values/params_converter.rb +52 -0
- data/lib/ynap/version.rb +3 -0
- metadata +197 -0
@@ -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
|
+
|
data/lib/ynap.rb
ADDED
@@ -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
|
data/lib/ynap/cli.rb
ADDED
@@ -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
|