figo 1.0 → 1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/figo.gemspec +1 -1
- data/lib/cacert.pem +41 -0
- data/lib/figo.rb +151 -32
- data/lib/models.rb +169 -4
- data/test/test_figo.rb +54 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9bbbd2adbe88269274fcff1ddcd8f342bf56e1a
|
4
|
+
data.tar.gz: 7267a93b66bc7aea0e1ae909ebf7e6bf9a0878c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 02cc595570b661a625333d589e7eff74632dbb289d3f20a6fd5ed1e67520234eac7d68e66325471a0e9cd616bba8ba34200fa207d695fac43f737f20a5a2af27
|
7
|
+
data.tar.gz: 85d7e1e88a2ead733cdfd1cefe03a978d1b2065701e483f94bdff50cc588f7704fe1dfdd3ec393ffd14aa2dd997f77ac6e11b04a4d36a818998e69c18bd22f6c
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
ruby-figo [![Build Status](https://secure.travis-ci.org/figo-connect/ruby-figo.png)](https://travis-ci.org/figo-connect/ruby-figo)
|
2
2
|
=========
|
3
3
|
|
4
|
-
Ruby bindings for the figo
|
4
|
+
Ruby bindings for the figo Connect API: http://developer.figo.me
|
5
5
|
|
6
6
|
Usage
|
7
7
|
=====
|
data/figo.gemspec
CHANGED
data/lib/cacert.pem
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290
|
3
|
+
IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB
|
4
|
+
IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA
|
5
|
+
Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO
|
6
|
+
BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi
|
7
|
+
MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ
|
8
|
+
ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
|
9
|
+
CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ
|
10
|
+
8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6
|
11
|
+
zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y
|
12
|
+
fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7
|
13
|
+
w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc
|
14
|
+
G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k
|
15
|
+
epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q
|
16
|
+
laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ
|
17
|
+
QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU
|
18
|
+
fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826
|
19
|
+
YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w
|
20
|
+
ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY
|
21
|
+
gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe
|
22
|
+
MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0
|
23
|
+
IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy
|
24
|
+
dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw
|
25
|
+
czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0
|
26
|
+
dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl
|
27
|
+
aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC
|
28
|
+
AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg
|
29
|
+
b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB
|
30
|
+
ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc
|
31
|
+
nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg
|
32
|
+
18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c
|
33
|
+
gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl
|
34
|
+
Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY
|
35
|
+
sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T
|
36
|
+
SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF
|
37
|
+
CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum
|
38
|
+
GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk
|
39
|
+
zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW
|
40
|
+
omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD
|
41
|
+
-----END CERTIFICATE-----
|
data/lib/figo.rb
CHANGED
@@ -24,48 +24,65 @@ require "json"
|
|
24
24
|
require "logger"
|
25
25
|
require 'net/http/persistent'
|
26
26
|
require "digest/sha1"
|
27
|
-
|
27
|
+
require_relative "models.rb"
|
28
28
|
|
29
|
-
$logger = Logger.new(STDOUT)
|
30
29
|
|
30
|
+
# Ruby bindings for the figo Connect API: http://developer.figo.me
|
31
31
|
module Figo
|
32
32
|
|
33
|
-
|
33
|
+
$api_endpoint = "api.leanbank.com"
|
34
34
|
|
35
|
-
|
36
|
-
|
35
|
+
$valid_fingerprints = ["A6:FE:08:F4:A8:86:F9:C1:BF:4E:70:0A:BD:72:AE:B8:8E:B7:78:52",
|
36
|
+
"AD:A0:E3:2B:1F:CE:E8:44:F2:83:BA:AE:E4:7D:F2:AD:44:48:7F:1E"]
|
37
37
|
|
38
|
+
$logger = Logger.new(STDOUT)
|
39
|
+
|
40
|
+
# Base class for all errors transported via the figo Connect API.
|
38
41
|
class Error < RuntimeError
|
39
42
|
|
40
|
-
|
43
|
+
# Initialize error object.
|
44
|
+
#
|
45
|
+
# @param error [String] the error code
|
46
|
+
# @param error_description [String] the error description
|
47
|
+
def initialize(error, error_description)
|
41
48
|
@error = error
|
42
49
|
@error_description = error_description
|
43
50
|
end
|
44
51
|
|
45
|
-
|
52
|
+
# Convert error object to string.
|
53
|
+
#
|
54
|
+
# @return [String] the error description
|
55
|
+
def to_s
|
46
56
|
return @error_description
|
47
57
|
end
|
48
58
|
|
49
59
|
end
|
50
60
|
|
51
|
-
|
61
|
+
# HTTPS class with certificate authentication and enhanced error handling.
|
62
|
+
class HTTPS < Net::HTTP::Persistent
|
52
63
|
|
64
|
+
# Overwrite `initialize` method from `Net::HTTP::Persistent`.
|
65
|
+
#
|
66
|
+
# Verify fingerprints of server SSL/TLS certificates.
|
53
67
|
def initialize(name = nil, proxy = nil)
|
54
68
|
super(name, proxy)
|
55
69
|
|
56
70
|
# Attribute ca_file must be set, otherwise verify_callback would never be called.
|
57
|
-
@ca_file = ""
|
71
|
+
@ca_file = "lib/cacert.pem"
|
58
72
|
@verify_callback = proc do |preverify_ok, store_context|
|
59
73
|
if preverify_ok and store_context.error == 0
|
60
74
|
certificate = OpenSSL::X509::Certificate.new(store_context.chain[0])
|
61
75
|
fingerprint = Digest::SHA1.hexdigest(certificate.to_der).upcase.scan(/../).join(":")
|
62
|
-
|
76
|
+
$valid_fingerprints.include?(fingerprint)
|
63
77
|
else
|
64
78
|
false
|
65
79
|
end
|
66
80
|
end
|
67
81
|
end
|
68
82
|
|
83
|
+
# Overwrite `request` method from `Net::HTTP::Persistent`.
|
84
|
+
#
|
85
|
+
# Raise error when a REST API error is returned.
|
69
86
|
def request(uri, req = nil, &block)
|
70
87
|
response = super(uri, req, &block)
|
71
88
|
|
@@ -81,7 +98,7 @@ module Figo
|
|
81
98
|
when Net::HTTPForbidden
|
82
99
|
raise Error.new("forbidden", "Insufficient permission.")
|
83
100
|
when Net::HTTPNotFound
|
84
|
-
|
101
|
+
return nil
|
85
102
|
when Net::HTTPMethodNotAllowed
|
86
103
|
raise Error.new("method_not_allowed", "Unexpected request method.")
|
87
104
|
when Net::HTTPServiceUnavailable
|
@@ -95,9 +112,15 @@ module Figo
|
|
95
112
|
end
|
96
113
|
|
97
114
|
# Represents a non user-bound connection to the figo Connect API.
|
115
|
+
#
|
116
|
+
# It's main purpose is to let user login via OAuth 2.0.
|
98
117
|
class Connection
|
99
118
|
|
100
|
-
# Create connection object with
|
119
|
+
# Create connection object with client credentials.
|
120
|
+
#
|
121
|
+
# @param client_id [String] the client ID
|
122
|
+
# @param client_secret [String] the client secret
|
123
|
+
# @param redirect_uri [String] optional redirect URI
|
101
124
|
def initialize(client_id, client_secret, redirect_uri = nil)
|
102
125
|
@client_id = client_id
|
103
126
|
@client_secret = client_secret
|
@@ -105,33 +128,58 @@ module Figo
|
|
105
128
|
@https = HTTPS.new("figo-#{client_id}")
|
106
129
|
end
|
107
130
|
|
108
|
-
|
109
|
-
|
110
|
-
|
131
|
+
# Helper method for making a OAuth 2.0 request.
|
132
|
+
#
|
133
|
+
# @param path [String] the URL path on the server
|
134
|
+
# @param data [Hash] this optional object will be used as url-encoded POST content.
|
135
|
+
# @return [Hash] JSON response
|
136
|
+
def query_api(path, data = nil)
|
137
|
+
uri = URI("https://#{$api_endpoint}#{path}")
|
111
138
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
139
|
+
# Setup HTTP request.
|
140
|
+
request = Net::HTTP::Post.new(path)
|
141
|
+
request.basic_auth(@client_id, @client_secret)
|
142
|
+
request["Accept"] = "application/json"
|
143
|
+
request["Content-Type"] = "application/x-www-form-urlencoded"
|
144
|
+
request['User-Agent'] = "ruby-figo"
|
145
|
+
request.body = URI.encode_www_form(data) unless data.nil?
|
118
146
|
|
119
|
-
|
120
|
-
|
147
|
+
# Send HTTP request.
|
148
|
+
response = @https.request(uri, request)
|
121
149
|
|
122
|
-
|
123
|
-
|
150
|
+
# Evaluate HTTP response.
|
151
|
+
return response.body == "" ? {} : JSON.parse(response.body)
|
124
152
|
end
|
125
153
|
|
154
|
+
|
126
155
|
# Get the URL a user should open in the web browser to start the login process.
|
156
|
+
#
|
157
|
+
# When the process is completed, the user is redirected to the URL provided to
|
158
|
+
# the constructor and passes on an authentication code. This code can be converted
|
159
|
+
# into an access token for data access.
|
160
|
+
#
|
161
|
+
# @param state [String] this string will be passed on through the complete login
|
162
|
+
# process and to the redirect target at the end. It should be used to
|
163
|
+
# validated the authenticity of the call to the redirect URL
|
164
|
+
# @param scope [String] optional scope of data access to ask the user for,
|
165
|
+
# e.g. `accounts=ro`
|
127
166
|
def login_url(state, scope = nil)
|
128
167
|
data = { "response_type" => "code", "client_id" => @client_id, "state" => state }
|
129
168
|
data["redirect_uri"] = @redirect_uri unless @redirect_uri.nil?
|
130
169
|
data["scope"] = scope unless scope.nil?
|
131
|
-
return "https://#{
|
170
|
+
return "https://#{$api_endpoint}/auth/code?" + URI.encode_www_form(data)
|
132
171
|
end
|
133
172
|
|
173
|
+
|
134
174
|
# Exchange authorization code or refresh token for access token.
|
175
|
+
#
|
176
|
+
# @param authorization_code_or_refresh_token [String] either the authorization
|
177
|
+
# code received as part of the call to the redirect URL at the end of the
|
178
|
+
# logon process, or a refresh token
|
179
|
+
# @param scope [String] optional scope of data access to ask the user for,
|
180
|
+
# e.g. `accounts=ro`
|
181
|
+
# @return [Hash] object with the keys `access_token`, `refresh_token` and
|
182
|
+
# `expires,` as documented in the figo Connect API specification.
|
135
183
|
def obtain_access_token(authorization_code_or_refresh_token, scope = nil)
|
136
184
|
# Authorization codes always start with "O" and refresh tokens always start with "R".
|
137
185
|
if authorization_code_or_refresh_token[0] == "O"
|
@@ -145,6 +193,11 @@ module Figo
|
|
145
193
|
end
|
146
194
|
|
147
195
|
# Revoke refresh token or access token.
|
196
|
+
#
|
197
|
+
# @note this action has immediate effect, i.e. you will not be able use that token anymore after this call.
|
198
|
+
#
|
199
|
+
# @param token [String] access or refresh token to be revoked
|
200
|
+
# @return [nil]
|
148
201
|
def revoke_token(refresh_token_or_access_token)
|
149
202
|
data = { "token" => refresh_token_or_access_token }
|
150
203
|
query_api("/auth/revoke?" + URI.encode_www_form(data))
|
@@ -157,13 +210,20 @@ module Figo
|
|
157
210
|
class Session
|
158
211
|
|
159
212
|
# Create session object with access token.
|
213
|
+
#
|
214
|
+
# @param access_token [String] the access token
|
160
215
|
def initialize(access_token)
|
161
216
|
@access_token = access_token
|
162
217
|
@https = HTTPS.new("figo-#{access_token}")
|
163
218
|
end
|
164
219
|
|
220
|
+
# Helper method for making a REST request.
|
221
|
+
#
|
222
|
+
# @param path [String] the URL path on the server
|
223
|
+
# @param data [hash] this optional object will be used as JSON-encoded POST content.
|
224
|
+
# @return [Hash] JSON response
|
165
225
|
def query_api(path, data=nil, method="GET") # :nodoc:
|
166
|
-
uri = URI("https://#{
|
226
|
+
uri = URI("https://#{$api_endpoint}#{path}")
|
167
227
|
|
168
228
|
# Setup HTTP request.
|
169
229
|
request = case method
|
@@ -178,6 +238,7 @@ module Figo
|
|
178
238
|
end
|
179
239
|
|
180
240
|
request["Authorization"] = "Bearer #{@access_token}"
|
241
|
+
request["Accept"] = "application/json"
|
181
242
|
request["Content-Type"] = "application/json"
|
182
243
|
request['User-Agent'] = "ruby-figo"
|
183
244
|
request.body = JSON.generate(data) unless data.nil?
|
@@ -186,22 +247,41 @@ module Figo
|
|
186
247
|
response = @https.request(uri, request)
|
187
248
|
|
188
249
|
# Evaluate HTTP response.
|
189
|
-
|
250
|
+
if response.nil?
|
251
|
+
return nil
|
252
|
+
elsif response.body.nil?
|
253
|
+
return nil
|
254
|
+
else
|
255
|
+
return response.body == "" ? nil : JSON.parse(response.body)
|
256
|
+
end
|
190
257
|
end
|
191
258
|
|
192
259
|
# Request list of accounts.
|
260
|
+
#
|
261
|
+
# @return [Array] an array of `Account` objects, one for each account the user has granted the app access
|
193
262
|
def accounts
|
194
263
|
response = query_api("/rest/accounts")
|
195
264
|
return response["accounts"].map {|account| Account.new(self, account)}
|
196
265
|
end
|
197
266
|
|
198
267
|
# Request specific account.
|
268
|
+
#
|
269
|
+
# @param account_id [String] ID of the account to be retrieved.
|
270
|
+
# @return [Account] account object
|
199
271
|
def get_account(account_id)
|
200
272
|
response = query_api("/rest/accounts/#{account_id}")
|
201
273
|
return Account.new(self, response)
|
202
274
|
end
|
203
275
|
|
204
276
|
# Request list of transactions.
|
277
|
+
#
|
278
|
+
# @param since [String] this parameter can either be a transaction ID or a date
|
279
|
+
# @param start_id [String] do only return transactions which were booked after the start transaction ID
|
280
|
+
# @param count [Intger] limit the number of returned transactions
|
281
|
+
# @param include_pending [Boolean] this flag indicates whether pending transactions should be included
|
282
|
+
# in the response; pending transactions are always included as a complete set, regardless of
|
283
|
+
# the `since` parameter
|
284
|
+
# @return [Array] an array of `Transaction` objects, one for each transaction of the user
|
205
285
|
def transactions(since = nil, start_id = nil, count = 1000, include_pending = false)
|
206
286
|
data = {}
|
207
287
|
data["since"] = (since.is_a?(Date) ? since.to_s : since) unless since.nil?
|
@@ -213,28 +293,67 @@ module Figo
|
|
213
293
|
end
|
214
294
|
|
215
295
|
# Request the URL a user should open in the web browser to start the synchronization process.
|
296
|
+
#
|
297
|
+
# @param redirect_uri [String] URI the user is redirected to after the process completes
|
298
|
+
# @param state [String] this string will be passed on through the complete synchronization process
|
299
|
+
# and to the redirect target at the end. It should be used to validated the authenticity of
|
300
|
+
# the call to the redirect URL
|
301
|
+
# @param disable_notifications [Booleon] this flag indicates whether notifications should be sent
|
302
|
+
# @param if_not_synced_since [Integer] if this parameter is set, only those accounts will be
|
303
|
+
# synchronized, which have not been synchronized within the specified number of minutes.
|
304
|
+
# @return [String] the URL to be opened by the user.
|
216
305
|
def sync_url(redirect_uri, state, disable_notifications = false, if_not_synced_since = 0)
|
217
306
|
data = { "redirect_uri" => redirect_uri, "state" => state, "disable_notifications" => disable_notifications, "if_not_synced_since" => if_not_synced_since }
|
218
307
|
response = query_api("/rest/sync", data, "POST")
|
219
|
-
return "https://#{
|
308
|
+
return "https://#{$api_endpoint}/task/start?id=#{response["task_token"]}"
|
220
309
|
end
|
221
310
|
|
222
311
|
# Request list of registered notifications.
|
312
|
+
#
|
313
|
+
# @return [Notification] an array of `Notification` objects, one for each registered notification
|
223
314
|
def notifications
|
224
315
|
response = query_api("/rest/notifications")
|
225
316
|
return response["notifications"].map {|notification| Notification.new(self, notification)}
|
226
317
|
end
|
227
318
|
|
319
|
+
# Request specific notification.
|
320
|
+
#
|
321
|
+
# @param notification_id [String] ID of the notification to be retrieved
|
322
|
+
# @return [Notification] `Notification` object for the respective notification
|
323
|
+
def get_notification(notification_id)
|
324
|
+
response = query_api("/rest/notifications/#{notification_id}")
|
325
|
+
return response.nil? ? nil : Notification.new(self, response)
|
326
|
+
end
|
327
|
+
|
228
328
|
# Register notification.
|
329
|
+
#
|
330
|
+
# @param observe_key [String] one of the notification keys specified in the figo Connect API
|
331
|
+
# specification
|
332
|
+
# @param notify_uri [String] notification messages will be sent to this URL
|
333
|
+
# @param state [String] any kind of string that will be forwarded in the notification message
|
334
|
+
# @return [Notification] newly created `Notification` object
|
229
335
|
def add_notification(observe_key, notify_uri, state)
|
230
336
|
data = { "observe_key" => observe_key, "notify_uri" => notify_uri, "state" => state }
|
231
337
|
response = query_api("/rest/notifications", data, "POST")
|
232
|
-
return response
|
338
|
+
return Notification.new(self, response)
|
339
|
+
end
|
340
|
+
|
341
|
+
# Modify a notification.
|
342
|
+
#
|
343
|
+
# @param notification [Notification] modified notification object
|
344
|
+
# @return [nil]
|
345
|
+
def modify_notification(notification)
|
346
|
+
data = { "observe_key" => notification.observe_key, "notify_uri" => notification.notify_uri, "state" => notification.state }
|
347
|
+
response = query_api("/rest/notifications/#{notification.notification_id}", data, "PUT")
|
348
|
+
return nil
|
233
349
|
end
|
234
350
|
|
235
351
|
# Unregister notification.
|
236
|
-
|
237
|
-
|
352
|
+
#
|
353
|
+
# @param notification [Notification] notification object which should be deleted
|
354
|
+
# @return [nil]
|
355
|
+
def remove_notification(notification)
|
356
|
+
query_api("/rest/notifications/#{notification.notification_id}", nil, "DELETE")
|
238
357
|
return nil
|
239
358
|
end
|
240
359
|
|
data/lib/models.rb
CHANGED
@@ -23,11 +23,13 @@
|
|
23
23
|
require "date"
|
24
24
|
require "flt"
|
25
25
|
|
26
|
+
|
26
27
|
module Figo
|
27
28
|
|
28
29
|
# Set decimal precision to two digits.
|
29
30
|
Flt::DecNum.context.precision = 2
|
30
31
|
|
32
|
+
# Account type enumeration.
|
31
33
|
class AccountType
|
32
34
|
GIRO = "Giro account"
|
33
35
|
SAVINGS = "Savings account"
|
@@ -37,6 +39,7 @@ module Figo
|
|
37
39
|
UNKNOWN = "Unknown"
|
38
40
|
end
|
39
41
|
|
42
|
+
# Transaction type enumeration.
|
40
43
|
class TransactionType
|
41
44
|
TRANSFER = "Transfer"
|
42
45
|
STANDING_ORDER = "Standing order"
|
@@ -49,15 +52,20 @@ module Figo
|
|
49
52
|
UNKNOWN = "Unknown"
|
50
53
|
end
|
51
54
|
|
52
|
-
class
|
55
|
+
# Abstract base class for model objects.
|
56
|
+
class Base
|
53
57
|
|
58
|
+
# Instantiate model object from hash.
|
59
|
+
#
|
60
|
+
# @param session [Session] figo `Session` object
|
61
|
+
# @param hash [Hash] use keys of this hash for model object creation
|
54
62
|
def initialize(session, hash)
|
55
63
|
@session = session
|
56
64
|
|
57
65
|
hash.each do |key, value|
|
58
66
|
if key == "status"
|
59
67
|
value = SynchronizationStatus.new(session, value)
|
60
|
-
elsif key == "amount" or key == "balance"
|
68
|
+
elsif key == "amount" or key == "balance" or key == "credit_line" or key == "monthly_spending_limit"
|
61
69
|
value = Flt::DecNum(value.to_s)
|
62
70
|
elsif key.end_with?("_date")
|
63
71
|
value = DateTime.iso8601(value)
|
@@ -70,32 +78,90 @@ module Figo
|
|
70
78
|
|
71
79
|
end
|
72
80
|
|
81
|
+
# Object representing one bank account of the user.
|
73
82
|
class Account < Base
|
74
83
|
|
84
|
+
# Internal figo Connect account ID.
|
85
|
+
# @return [String]
|
75
86
|
attr_accessor :account_id
|
87
|
+
|
88
|
+
# Internal figo Connect bank ID.
|
89
|
+
# @return [String]
|
76
90
|
attr_accessor :bank_id
|
91
|
+
|
92
|
+
# Account name.
|
93
|
+
# @return [String]
|
77
94
|
attr_accessor :name
|
95
|
+
|
96
|
+
# Account owner.
|
97
|
+
# @return [String]
|
78
98
|
attr_accessor :owner
|
99
|
+
|
100
|
+
# This flag indicates whether the account will be automatically synchronized.
|
101
|
+
# @return [Boolean]
|
79
102
|
attr_accessor :auto_sync
|
103
|
+
|
104
|
+
# Account number.
|
105
|
+
# @return [String]
|
80
106
|
attr_accessor :account_number
|
107
|
+
|
108
|
+
# Bank code.
|
109
|
+
# @return [String]
|
81
110
|
attr_accessor :bank_code
|
111
|
+
|
112
|
+
# Bank name.
|
113
|
+
# @return [String]
|
82
114
|
attr_accessor :bank_name
|
115
|
+
|
116
|
+
# Three-character currency code.
|
117
|
+
# @return [String]
|
83
118
|
attr_accessor :currency
|
119
|
+
|
120
|
+
# IBAN.
|
121
|
+
# @return [String]
|
84
122
|
attr_accessor :iban
|
123
|
+
|
124
|
+
# BIC.
|
125
|
+
# @return [String]
|
85
126
|
attr_accessor :bic
|
127
|
+
|
128
|
+
# Account type. One of the constants of the `AccountType` object.
|
129
|
+
# @return [String]
|
86
130
|
attr_accessor :type
|
131
|
+
|
132
|
+
# Account icon URL.
|
133
|
+
# @return [String]
|
87
134
|
attr_accessor :icon
|
135
|
+
|
136
|
+
# This flag indicates whether the balance of this account is added to the total balance of accounts.
|
137
|
+
# @return [Boolean]
|
88
138
|
attr_accessor :in_total_balance
|
139
|
+
|
140
|
+
# This flag indicates whether this account is only shown as preview for an unpaid premium plan.
|
141
|
+
# @return [Boolean]
|
89
142
|
attr_accessor :preview
|
143
|
+
|
144
|
+
# Synchronization status object.
|
145
|
+
# @return [SynchronizationStatus]
|
90
146
|
attr_accessor :status
|
91
147
|
|
92
|
-
# Request balance.
|
148
|
+
# Request balance of this account.
|
149
|
+
#
|
150
|
+
# @return [AccountBalance] account balance object
|
93
151
|
def balance
|
94
152
|
response = @session.query_api("/rest/accounts/#{@account_id}/balance")
|
95
153
|
return AccountBalance.new(@session, response)
|
96
154
|
end
|
97
155
|
|
98
|
-
# Request list of transactions.
|
156
|
+
# Request list of transactions of this account.
|
157
|
+
#
|
158
|
+
# @param since [String] this parameter can either be a transaction ID or a date
|
159
|
+
# @param start_id [String] do only return transactions which were booked after the start transaction ID
|
160
|
+
# @param count [Intger] limit the number of returned transactions
|
161
|
+
# @param include_pending [Boolean] this flag indicates whether pending transactions should be included
|
162
|
+
# in the response; pending transactions are always included as a complete set, regardless of
|
163
|
+
# the `since` parameter
|
164
|
+
# @return [Array] an array of `Transaction` objects, one for each transaction of the user
|
99
165
|
def transactions(since = nil, start_id = nil, count = 1000, include_pending = false)
|
100
166
|
data = {}
|
101
167
|
data["since"] = (since.is_a?(Date) ? since.to_s : since) unless since.nil?
|
@@ -106,54 +172,153 @@ module Figo
|
|
106
172
|
return response["transactions"].map {|transaction| Transaction.new(@session, transaction)}
|
107
173
|
end
|
108
174
|
|
175
|
+
# Request a specific transaction.
|
176
|
+
#
|
177
|
+
# @param transaction_id [String] ID of the transaction to be retrieved
|
178
|
+
# @return [Transaction] transaction object
|
179
|
+
def transaction(transaction_id)
|
180
|
+
response = @session.query_api("/rest/accounts/#{@account_id}/transactions/#{transaction_id}")
|
181
|
+
return Transaction.new(@session, response)
|
182
|
+
end
|
183
|
+
|
109
184
|
end
|
110
185
|
|
186
|
+
# Object representing the balance of a certain bank account of the user.
|
111
187
|
class AccountBalance < Base
|
112
188
|
|
189
|
+
# Account balance or `nil` if the balance is not yet known.
|
190
|
+
# @return [DecNum]
|
113
191
|
attr_accessor :balance
|
192
|
+
|
193
|
+
# Bank server timestamp of balance or `nil` if the balance is not yet known.
|
194
|
+
# @return [Date]
|
114
195
|
attr_accessor :balance_date
|
196
|
+
|
197
|
+
# Credit line.
|
198
|
+
# @return [DecNum]
|
115
199
|
attr_accessor :credit_line
|
200
|
+
|
201
|
+
# User-defined spending limit.
|
202
|
+
# @return [DecNum]
|
116
203
|
attr_accessor :monthly_spending_limit
|
204
|
+
|
205
|
+
# Synchronization status object.
|
206
|
+
# @return [SynchronizationStatus]
|
117
207
|
attr_accessor :status
|
118
208
|
|
119
209
|
end
|
120
210
|
|
211
|
+
# Object representing one bank transaction on a certain bank account of the user.
|
121
212
|
class Transaction < Base
|
122
213
|
|
214
|
+
# Internal figo Connect transaction ID.
|
215
|
+
# @return [String]
|
123
216
|
attr_accessor :transaction_id
|
217
|
+
|
218
|
+
# Internal figo Connect account ID.
|
219
|
+
# @return [String]
|
124
220
|
attr_accessor :account_id
|
221
|
+
|
222
|
+
# Name of originator or recipient.
|
223
|
+
# @return [String]
|
125
224
|
attr_accessor :name
|
225
|
+
|
226
|
+
# Account number of originator or recipient.
|
227
|
+
# @return [String]
|
126
228
|
attr_accessor :account_number
|
229
|
+
|
230
|
+
# Bank code of originator or recipien.
|
231
|
+
# @return [String]
|
127
232
|
attr_accessor :bank_code
|
233
|
+
|
234
|
+
# Bank name of originator or recipient.
|
235
|
+
# @return [String]
|
128
236
|
attr_accessor :bank_name
|
237
|
+
|
238
|
+
# Transaction amount.
|
239
|
+
# @return [DecNum]
|
129
240
|
attr_accessor :amount
|
241
|
+
|
242
|
+
# Three-character currency code.
|
243
|
+
# @return [String]
|
130
244
|
attr_accessor :currency
|
245
|
+
|
246
|
+
# Booking date.
|
247
|
+
# @return [Date]
|
131
248
|
attr_accessor :booking_date
|
249
|
+
|
250
|
+
# Value date.
|
251
|
+
# @return [Date]
|
132
252
|
attr_accessor :value_date
|
253
|
+
|
254
|
+
# Purpose text.
|
255
|
+
# @return [String]
|
133
256
|
attr_accessor :purpose
|
257
|
+
|
258
|
+
# Transaction type. One of the constants of the `TransactionType` object.
|
259
|
+
# @return [String]
|
134
260
|
attr_accessor :type
|
261
|
+
|
262
|
+
# Booking text.
|
263
|
+
# @return [String]
|
135
264
|
attr_accessor :booking_text
|
265
|
+
|
266
|
+
# This flag indicates whether the transaction is booked or pending.
|
267
|
+
# @return [Boolean]
|
136
268
|
attr_accessor :booked
|
269
|
+
|
270
|
+
# Internal creation timestamp on the figo Connect server.
|
271
|
+
# @return [DateTime]
|
137
272
|
attr_accessor :creation_timestamp
|
273
|
+
|
274
|
+
# Internal modification timestamp on the figo Connect server.
|
275
|
+
# @return [DateTime]
|
138
276
|
attr_accessor :modification_timestamp
|
277
|
+
|
278
|
+
# This flag indicates whether the transaction has already been marked as visited by the user.
|
279
|
+
# @return [Boolean]
|
139
280
|
attr_accessor :visited
|
140
281
|
|
141
282
|
end
|
142
283
|
|
284
|
+
# Object representing the bank server synchronization status.
|
143
285
|
class SynchronizationStatus < Base
|
144
286
|
|
287
|
+
# Internal figo Connect status code.
|
288
|
+
# @return [Integer]
|
145
289
|
attr_accessor :code
|
290
|
+
|
291
|
+
# Human-readable error message.
|
292
|
+
# @return [String]
|
146
293
|
attr_accessor :message
|
294
|
+
|
295
|
+
# Timestamp of last synchronization.
|
296
|
+
# @return [DateTime]
|
147
297
|
attr_accessor :sync_timestamp
|
298
|
+
|
299
|
+
# Timestamp of last successful synchronization.
|
300
|
+
# @return [DateTime]
|
148
301
|
attr_accessor :success_timestamp
|
149
302
|
|
150
303
|
end
|
151
304
|
|
305
|
+
# Object representing a configured notification, e.g. a webhook or email hook.
|
152
306
|
class Notification < Base
|
153
307
|
|
308
|
+
# Internal figo Connect notification ID from the notification registration response.
|
309
|
+
# @return [String]
|
154
310
|
attr_accessor :notification_id
|
311
|
+
|
312
|
+
# One of the notification keys specified in the figo Connect API specification.
|
313
|
+
# @return [String]
|
155
314
|
attr_accessor :observe_key
|
315
|
+
|
316
|
+
# Notification messages will be sent to this URL.
|
317
|
+
# @return [String]
|
156
318
|
attr_accessor :notify_uri
|
319
|
+
|
320
|
+
# State similiar to sync and logon process. It will passed as POST payload for webhooks.
|
321
|
+
# @return [String]
|
157
322
|
attr_accessor :state
|
158
323
|
|
159
324
|
end
|
data/test/test_figo.rb
CHANGED
@@ -21,8 +21,61 @@
|
|
21
21
|
#
|
22
22
|
|
23
23
|
require "test/unit"
|
24
|
-
|
24
|
+
require_relative "../lib/figo"
|
25
|
+
|
25
26
|
|
26
27
|
class FigoTest < Test::Unit::TestCase
|
27
28
|
|
29
|
+
def setup
|
30
|
+
$api_endpoint = "api.staging.figo.me"
|
31
|
+
$valid_fingerprints = ["AF:FF:C3:2A:45:13:86:FB:28:57:55:80:0A:58:23:C7:7A:70:B6:2D"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_accounts
|
35
|
+
sut = Figo::Session.new("ASHWLIkouP2O6_bgA2wWReRhletgWKHYjLqDaqb0LFfamim9RjexTo22ujRIP_cjLiRiSyQXyt2kM1eXU2XLFZQ0Hro15HikJQT_eNeT_9XQ")
|
36
|
+
|
37
|
+
sut.accounts
|
38
|
+
sut.get_account "A1.1"
|
39
|
+
account = sut.get_account "A1.2"
|
40
|
+
assert_equal account.account_id, "A1.2"
|
41
|
+
|
42
|
+
# account sub-resources
|
43
|
+
balance = sut.get_account("A1.2").balance
|
44
|
+
assert balance.balance
|
45
|
+
assert balance.balance_date
|
46
|
+
|
47
|
+
transactions = sut.get_account("A1.2").transactions
|
48
|
+
assert transactions.length > 0
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_global_transactions
|
52
|
+
sut = Figo::Session.new("ASHWLIkouP2O6_bgA2wWReRhletgWKHYjLqDaqb0LFfamim9RjexTo22ujRIP_cjLiRiSyQXyt2kM1eXU2XLFZQ0Hro15HikJQT_eNeT_9XQ")
|
53
|
+
transactions = sut.transactions
|
54
|
+
self.assert transactions.length > 0
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_sync_uri
|
58
|
+
sut = Figo::Session.new("ASHWLIkouP2O6_bgA2wWReRhletgWKHYjLqDaqb0LFfamim9RjexTo22ujRIP_cjLiRiSyQXyt2kM1eXU2XLFZQ0Hro15HikJQT_eNeT_9XQ")
|
59
|
+
sut.sync_url("qwe", "qew")
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_create_update_delete_notification
|
63
|
+
sut = Figo::Session.new("ASHWLIkouP2O6_bgA2wWReRhletgWKHYjLqDaqb0LFfamim9RjexTo22ujRIP_cjLiRiSyQXyt2kM1eXU2XLFZQ0Hro15HikJQT_eNeT_9XQ")
|
64
|
+
|
65
|
+
notification = sut.add_notification(observe_key="/rest/transactions", notify_uri="http://figo.me/test", state="qwe")
|
66
|
+
assert_equal notification.observe_key, "/rest/transactions"
|
67
|
+
assert_equal notification.notify_uri, "http://figo.me/test"
|
68
|
+
assert_equal notification.state, "qwe"
|
69
|
+
|
70
|
+
notification.state = "asd"
|
71
|
+
sut.modify_notification(notification)
|
72
|
+
|
73
|
+
notification = sut.get_notification(notification.notification_id)
|
74
|
+
assert_equal notification.observe_key, "/rest/transactions"
|
75
|
+
assert_equal notification.notify_uri, "http://figo.me/test"
|
76
|
+
assert_equal notification.state, "asd"
|
77
|
+
|
78
|
+
sut.remove_notification(notification)
|
79
|
+
assert_equal sut.get_notification(notification.notification_id), nil
|
80
|
+
end
|
28
81
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: figo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: "1.
|
4
|
+
version: "1.1"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefan Richter
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2013-06-
|
13
|
+
date: 2013-06-07 00:00:00 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: flt
|
@@ -48,6 +48,7 @@ files:
|
|
48
48
|
- README.md
|
49
49
|
- Rakefile
|
50
50
|
- figo.gemspec
|
51
|
+
- lib/cacert.pem
|
51
52
|
- lib/figo.rb
|
52
53
|
- lib/models.rb
|
53
54
|
- test/test_figo.rb
|