lib/figo.rb in figo-1.1.1 vs lib/figo.rb in figo-1.2.0
- old
+ new
@@ -1,43 +1,42 @@
#
# Copyright (c) 2013 figo GmbH
-#
+#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
-#
+#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
-#
+#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-#
+#
require "json"
require "net/http/persistent"
require "digest/sha1"
require_relative "models.rb"
# Ruby bindings for the figo Connect API: http://developer.figo.me
module Figo
-
$api_endpoint = "api.figo.me"
- $valid_fingerprints = ["3A:62:54:4D:86:B4:34:38:EA:34:64:4E:95:10:A9:FF:37:27:69:C0"]
+ $valid_fingerprints = ["3A:62:54:4D:86:B4:34:38:EA:34:64:4E:95:10:A9:FF:37:27:69:C0",
+ "CF:C1:BC:7F:6A:16:09:2B:10:83:8A:B0:22:4F:3A:65:D2:70:D7:3E"]
# Base class for all errors transported via the figo Connect API.
class Error < RuntimeError
-
# Initialize error object.
#
# @param error [String] the error code
# @param error_description [String] the error description
def initialize(error, error_description)
@@ -49,16 +48,14 @@
#
# @return [String] the error description
def to_s
return @error_description
end
-
end
# HTTPS class with certificate authentication and enhanced error handling.
class HTTPS < Net::HTTP::Persistent
-
# Overwrite `initialize` method from `Net::HTTP::Persistent`.
#
# Verify fingerprints of server SSL/TLS certificates.
def initialize(name = nil, proxy = nil)
super(name, proxy)
@@ -108,11 +105,10 @@
# Represents a non user-bound connection to the figo Connect API.
#
# It's main purpose is to let user login via OAuth 2.0.
class Connection
-
# Create connection object with client credentials.
#
# @param client_id [String] the client ID
# @param client_secret [String] the client secret
# @param redirect_uri [String] optional redirect URI
@@ -147,36 +143,36 @@
end
# Get the URL a user should open in the web browser to start the login process.
#
- # When the process is completed, the user is redirected to the URL provided to
- # the constructor and passes on an authentication code. This code can be converted
+ # When the process is completed, the user is redirected to the URL provided to
+ # the constructor and passes on an authentication code. This code can be converted
# into an access token for data access.
#
- # @param state [String] this string will be passed on through the complete login
- # process and to the redirect target at the end. It should be used to
+ # @param state [String] this string will be passed on through the complete login
+ # process and to the redirect target at the end. It should be used to
# validated the authenticity of the call to the redirect URL
- # @param scope [String] optional scope of data access to ask the user for,
+ # @param scope [String] optional scope of data access to ask the user for,
# e.g. `accounts=ro`
# @return [String] the URL to be opened by the user.
def login_url(state, scope = nil)
data = { "response_type" => "code", "client_id" => @client_id, "state" => state }
data["redirect_uri"] = @redirect_uri unless @redirect_uri.nil?
data["scope"] = scope unless scope.nil?
- return "https://#{$api_endpoint}/auth/code?" + URI.encode_www_form(data)
+ return "https://#{$api_endpoint}/auth/code?" + URI.encode_www_form(data)
end
# Exchange authorization code or refresh token for access token.
#
- # @param authorization_code_or_refresh_token [String] either the authorization
- # code received as part of the call to the redirect URL at the end of the
+ # @param authorization_code_or_refresh_token [String] either the authorization
+ # code received as part of the call to the redirect URL at the end of the
# logon process, or a refresh token
- # @param scope [String] optional scope of data access to ask the user for,
+ # @param scope [String] optional scope of data access to ask the user for,
# e.g. `accounts=ro`
- # @return [Hash] object with the keys `access_token`, `refresh_token` and
+ # @return [Hash] object with the keys `access_token`, `refresh_token` and
# `expires`, as documented in the figo Connect API specification.
def obtain_access_token(authorization_code_or_refresh_token, scope = nil)
# Authorization codes always start with "O" and refresh tokens always start with "R".
if authorization_code_or_refresh_token[0] == "O"
data = { "grant_type" => "authorization_code", "code" => authorization_code_or_refresh_token }
@@ -198,10 +194,22 @@
data = { "token" => refresh_token_or_access_token }
query_api("/auth/revoke?" + URI.encode_www_form(data))
return nil
end
+ # Create a new figo Account
+ #
+ # @param name [String] First and last name
+ # @param email [String] Email address; It must obey the figo username & password policy
+ # @param password [String] New figo Account password; It must obey the figo username & password policy
+ # @param language [String] Two-letter code of preferred language
+ # @param send_newsletter [Boolean] This flag indicates whether the user has agreed to be contacted by email
+ # @return [Hash] object with the key `recovery_password` as documented in the figo Connect API specification
+ def create_user(name, email, password, language='de', send_newsletter=True)
+ data = { 'name' => name, 'email' => email, 'password' => password, 'language' => language, 'send_newsletter' => send_newsletter, 'affiliate_client_id' => @client_id}
+ return query_api("/auth/user", data)
+ end
end
# Represents a user-bound connection to the figo Connect API and allows access to the user's data.
class Session
@@ -242,118 +250,241 @@
# Send HTTP request.
response = @https.request(uri, request)
# Evaluate HTTP response.
- if response.nil?
- return nil
- elsif response.body.nil?
- return nil
- else
- return response.body == "" ? nil : JSON.parse(response.body)
- end
+ return nil if response.nil?
+ return nil if response.body.nil?
+ return response.body == "" ? nil : JSON.parse(response.body)
end
- # Request list of accounts.
+ def query_api_object(type, path, data=nil, method="GET", array_name=nil) # :nodoc:
+ response = query_api path, data, method
+ return nil if response.nil?
+ return type.new(self, response) if array_name.nil?
+ return response[array_name].map {|entry| type.new(self, entry)}
+ end
+
+ # Retrieve current User
#
+ # @return [User] the current user
+ def user
+ query_api_object User, "/rest/user"
+ end
+
+ # Modify the current user
+ #
+ # @param user [User] the modified user object to be saved
+ # @return [User] the modified user returned
+ def modify_user(user)
+ query_api_object User, "/rest/user", user.dump(), "PUT"
+ end
+
+ # Remove the current user
+ # Note: this has immidiate effect and you wont be able to interact with the user after this call
+ def remove_user
+ query_api "/rest/user", nil, "DELETE"
+ end
+
+ # Retrieve all accounts
+ #
# @return [Array] an array of `Account` objects, one for each account the user has granted the app access
def accounts
- response = query_api("/rest/accounts")
- return response["accounts"].map {|account| Account.new(self, account)}
+ query_api_object Account, "/rest/accounts", nil, "GET", "accounts"
end
- # Request specific account.
+ # Retrieve specific account.
#
# @param account_id [String] ID of the account to be retrieved.
# @return [Account] account object
def get_account(account_id)
- response = query_api("/rest/accounts/#{account_id}")
- return response.nil? ? nil : Account.new(self, response)
+ query_api_object Account, "/rest/accounts/#{account_id}"
end
- # Request list of transactions.
+ # Modify specific account
#
+ # @param account [Account] modified account to be saved
+ # @return [Account] modified account returned by the server
+ def modify_account(account)
+ query_api_object Account, "/rest/accounts/#{account.account_id}", account.dump(), "PUT"
+ end
+
+ # Remove specific account
+ #
+ # @param account [Account, String] the account to be removed or its ID
+ def remove_account(account)
+ query_api account.is_a?(String) ? "/rest/accounts/#{account}" : "/rest/accounts/#{account.account_id}", nil, "DELETE"
+ end
+
+ # Retrieve balance of an account.
+ #
+ # @return [AccountBalance] account balance object
+ def get_account_balance(account_id)
+ query_api_object AccountBalance, "/rest/accounts/#{account_id}/balance"
+ end
+
+ # Modify balance or account limits
+ #
+ # @param account_id [String] ID of the account which balance should be modified
+ # @param account_balance [AccountBalance] modified AccountBalance to be saved
+ # @return [AccountBalance] modified AccountBalance returned by server
+ def modify_account_balance(account_id, account_balance)
+ query_api_object AccountBalance, "/rest/accounts/#{account_id}/balance", account_balance.dump(), "PUT"
+ end
+
+ # Retrieve specific bank
+ #
+ # @return [Bank] bank object
+ def get_bank(bank_id)
+ query_api_object Bank, "/rest/banks/#{bank_id}"
+ end
+
+ # Modify bank
+ #
+ # @param bank [Bank] modified bank object
+ # @return [Bank] modified bank object returned by server
+ def modify_bank(bank)
+ query_api_object Bank, "/rest/banks/#{bank.bank_id}", bank.dump(), "PUT"
+ end
+
+ # Remove stored PIN from bank
+ #
+ # @param bank [Bank, String] the bank whose stored PIN should be removed or its ID
+ # @return [nil]
+ def remove_bank_pin(bank)
+ query_app bank.is_a?(String) ? "/rest/banks/#{bank}/submit": "/rest/banks/#{bank.bank_id}/submit", nil, "POST"
+ end
+
+ # Retrieve list of transactions (on all or a specific account)
+ #
+ # @param account_id [String] ID of the account for which to list the transactions
# @param since [String, Date] this parameter can either be a transaction ID or a date
# @param start_id [String] do only return transactions which were booked after the start transaction ID
# @param count [Integer] limit the number of returned transactions
- # @param include_pending [Boolean] this flag indicates whether pending transactions should be included
- # in the response; pending transactions are always included as a complete set, regardless of
+ # @param include_pending [Boolean] this flag indicates whether pending transactions should be included
+ # in the response; pending transactions are always included as a complete set, regardless of
# the `since` parameter
# @return [Array] an array of `Transaction` objects, one for each transaction of the user
- def transactions(since = nil, start_id = nil, count = 1000, include_pending = false)
- data = {}
- data["since"] = (since.is_a?(Date) ? since.to_s : since) unless since.nil?
+ def transactions(account_id = nil, since = nil, start_id = nil, count = 1000, include_pending = false)
+ data = {"count" => count.to_s, "include_pending" => include_pending ? "1" : "0"}
+ data["since"] = ((since.is_a?(Date) ? since.to_s : since) unless since.nil?)
data["start_id"] = start_id unless start_id.nil?
- data["count"] = count.to_s
- data["include_pending"] = include_pending ? "1" : "0"
- response = query_api("/rest/transactions?" + URI.encode_www_form(data))
- return response["transactions"].map {|transaction| Transaction.new(self, transaction)}
+
+ query_api_object Transaction, (account_id.nil? ? "/rest/transactions?" : "/rest/accounts/#{account_id}/transactions?") + URI.encode_www_form(data), nil, "GET", "transactions"
end
- # Request the URL a user should open in the web browser to start the synchronization process.
+ # Retrieve a specific transaction
#
+ # @param account_id [String] ID of the account on which the transaction occured
+ # @param transaction_id [String] ID of the transaction to be retrieved
+ # @return [Transaction] transaction object
+ def get_transaction(account_id, transaction_id)
+ query_api_object Transaction, "/rest/accounts/#{account_id}/transactions/#{transaction_id}"
+ end
+
+ # Retrieve the URL a user should open in the web browser to start the synchronization process.
+ #
# @param redirect_uri [String] the user will be redirected to this URL after the process completes
- # @param state [String] this string will be passed on through the complete synchronization process
- # and to the redirect target at the end. It should be used to validated the authenticity of
+ # @param state [String] this string will be passed on through the complete synchronization process
+ # and to the redirect target at the end. It should be used to validated the authenticity of
# the call to the redirect URL
- # @param disable_notifications [Booleon] this flag indicates whether notifications should be sent
- # @param if_not_synced_since [Integer] if this parameter is set, only those accounts will be
+ # @param if_not_synced_since [Integer] if this parameter is set, only those accounts will be
# synchronized, which have not been synchronized within the specified number of minutes.
# @return [String] the URL to be opened by the user.
- def sync_url(redirect_uri, state, disable_notifications = false, if_not_synced_since = 0)
- data = { "redirect_uri" => redirect_uri, "state" => state, "disable_notifications" => disable_notifications, "if_not_synced_since" => if_not_synced_since }
- response = query_api("/rest/sync", data, "POST")
+ def sync_url(redirect_uri, state, if_not_synced_since = 0)
+ response = query_api "/rest/sync", {"redirect_uri" => redirect_uri, "state" => state, "if_not_synced_since" => if_not_synced_since}, "POST"
return "https://#{$api_endpoint}/task/start?id=#{response["task_token"]}"
end
- # Request list of registered notifications.
+ # Retrieve list of registered notifications.
#
# @return [Notification] an array of `Notification` objects, one for each registered notification
def notifications
- response = query_api("/rest/notifications")
- return response["notifications"].map {|notification| Notification.new(self, notification)}
+ query_api_object Notification, "/rest/notifications", nil, "GET", "notifications"
end
- # Request specific notification.
+ # Retrieve specific notification.
#
# @param notification_id [String] ID of the notification to be retrieved
# @return [Notification] `Notification` object for the respective notification
def get_notification(notification_id)
- response = query_api("/rest/notifications/#{notification_id}")
- return response.nil? ? nil : Notification.new(self, response)
+ query_api_object Notification, "/rest/notifications/#{notification_id}"
end
- # Register notification.
+ # Register a new notification.
#
- # @param observe_key [String] one of the notification keys specified in the figo Connect API
- # specification
- # @param notify_uri [String] notification messages will be sent to this URL
- # @param state [String] any kind of string that will be forwarded in the notification message
+ # @param notification [Notification] notification to be crated. It should not have a notification_id set.
# @return [Notification] newly created `Notification` object
- def add_notification(observe_key, notify_uri, state)
- data = { "observe_key" => observe_key, "notify_uri" => notify_uri, "state" => state }
- response = query_api("/rest/notifications", data, "POST")
- return Notification.new(self, response)
+ def add_notification(notification)
+ query_api_object Notification, "/rest/notifications", notification.dump(), "POST"
end
# Modify notification.
#
# @param notification [Notification] modified notification object
- # @return [nil]
+ # @return [Notification] modified notification returned by server
def modify_notification(notification)
- data = { "observe_key" => notification.observe_key, "notify_uri" => notification.notify_uri, "state" => notification.state }
- query_api("/rest/notifications/#{notification.notification_id}", data, "PUT")
- return nil
+ query_api_object Notification, "/rest/notifications/#{notification.notification_id}", notification.dump(), "PUT"
end
# Unregister notification.
#
- # @param notification [Notification] notification object which should be deleted
- # @return [nil]
+ # @param notification [Notification, String] notification object which should be deleted or its ID
def remove_notification(notification)
- query_api("/rest/notifications/#{notification.notification_id}", nil, "DELETE")
- return nil
+ query_api notification.is_a?(String) ? "/rest/notifications/#{notification}" : "/rest/notifications/#{notification.notification_id}", nil, "DELETE"
end
- end
+ # Retrieve list of all payments (on all accounts or one)
+ #
+ # @param account_id [String] ID of the account for whicht to list the payments
+ # @return [Payment] an array of `Payment` objects, one for each payment
+ def payments(account_id = nil)
+ query_api_object Payment, account_id.nil? ? "/rest/payments" : "/rest/accounts/#{account_id}/payments", nil, "GET", "payments"
+ end
+ # Retrieve specific payment.
+ #
+ # @param account_id [String] ID for the account on which the payment to be retrieved was created
+ # @param payment_id [String] ID of the notification to be retrieved
+ # @return [Payment] `Payment` object for the respective payment
+ def get_payment(account_id, payment_id)
+ query_api_object Payment, "/rest/accounts/#{account_id}/payments/#{payment_id}"
+ end
+
+ # Create new payment
+ #
+ # @param payment [Payment] payment object to be created. It should not have a payment_id set.
+ # @return [Payment] newly created `Payment` object
+ def add_payment(payment)
+ query_api_object Payment, "/rest/accounts/#{payment.account_id}/payments", payment.dump(), "POST"
+ end
+
+ # Modify payment
+ #
+ # @param payment [Payment] modified payment object
+ # @return [Payment] modified payment object
+ def modify_payment(payment)
+ query_api_object Payment, "/rest/accounts/#{payment.account_id}/payments/#{payment.payment_id}", payment.dump(), "PUT"
+ end
+
+ # Submit payment
+ #
+ # @param tan_scheme_id [String] TAN scheme ID of user-selected TAN scheme
+ # @param state [String] Any kind of string that will be forwarded in the callback response message
+ # @param redirect_uri [String] At the end of the submission process a response will be sent to this callback URL
+ # @return [String] the URL to be opened by the user for the TAN process
+ def submit_payment(payment, tan_scheme_id, state, redirect_uri = nil)
+ params = {"tan_scheme_id" => tan_scheme_id, "state" => state}
+ params['redirect_uri'] = redirect_uri unless redirect_uri.nil?
+
+ response = query_api "/rest/accounts/#{payment.account_id}/payments/#{payment.payment_id}/submit", params, "POST"
+ return "https://#{$api_endpoint}/task/start?id=#{response["task_token"]}"
+ end
+
+ # Remove payment
+ #
+ # @param payment [Payment, String] payment object which should be removed
+ def remove_payment(payment)
+ query_api "/rest/accounts/#{payment.account_id}/payments/#{payment.payment_id}", nil, "DELETE"
+ end
+ end
end