fintoc 0.1.0 → 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 +4 -4
- data/.github/pull_request_template.md +47 -0
- data/.github/workflows/ci.yml +46 -0
- data/.rubocop.yml +14 -7
- data/CHANGELOG.md +40 -0
- data/Gemfile +20 -1
- data/Gemfile.lock +237 -61
- data/README.md +286 -42
- data/Rakefile +3 -3
- data/fintoc.gemspec +3 -7
- data/lib/config/initializers/money.rb +5 -0
- data/lib/fintoc/base_client.rb +150 -0
- data/lib/fintoc/client.rb +14 -123
- data/lib/fintoc/constants.rb +4 -3
- data/lib/fintoc/errors.rb +139 -15
- data/lib/fintoc/jws.rb +83 -0
- data/lib/fintoc/utils.rb +2 -2
- data/lib/fintoc/v1/client/client.rb +12 -0
- data/lib/fintoc/v1/managers/links_manager.rb +46 -0
- data/lib/fintoc/v1/resources/account.rb +95 -0
- data/lib/fintoc/v1/resources/balance.rb +27 -0
- data/lib/fintoc/v1/resources/institution.rb +21 -0
- data/lib/fintoc/v1/resources/link.rb +85 -0
- data/lib/fintoc/v1/resources/movement.rb +62 -0
- data/lib/fintoc/v1/resources/transfer_account.rb +24 -0
- data/lib/fintoc/v2/client/client.rb +37 -0
- data/lib/fintoc/v2/managers/account_numbers_manager.rb +59 -0
- data/lib/fintoc/v2/managers/account_verifications_manager.rb +45 -0
- data/lib/fintoc/v2/managers/accounts_manager.rb +54 -0
- data/lib/fintoc/v2/managers/entities_manager.rb +36 -0
- data/lib/fintoc/v2/managers/simulate_manager.rb +30 -0
- data/lib/fintoc/v2/managers/transfers_manager.rb +56 -0
- data/lib/fintoc/v2/resources/account.rb +105 -0
- data/lib/fintoc/v2/resources/account_number.rb +105 -0
- data/lib/fintoc/v2/resources/account_verification.rb +73 -0
- data/lib/fintoc/v2/resources/entity.rb +51 -0
- data/lib/fintoc/v2/resources/transfer.rb +131 -0
- data/lib/fintoc/version.rb +1 -1
- data/lib/fintoc/webhook_signature.rb +73 -0
- data/lib/fintoc.rb +3 -0
- data/lib/tasks/simplecov_config.rb +19 -0
- metadata +35 -83
- data/lib/fintoc/resources/account.rb +0 -84
- data/lib/fintoc/resources/balance.rb +0 -24
- data/lib/fintoc/resources/institution.rb +0 -18
- data/lib/fintoc/resources/link.rb +0 -83
- data/lib/fintoc/resources/movement.rb +0 -55
- data/lib/fintoc/resources/transfer_account.rb +0 -22
data/lib/fintoc/client.rb
CHANGED
@@ -1,149 +1,40 @@
|
|
1
|
-
require '
|
2
|
-
require 'fintoc/
|
3
|
-
require 'fintoc/errors'
|
4
|
-
require 'fintoc/resources/link'
|
5
|
-
require 'fintoc/constants'
|
6
|
-
require 'fintoc/version'
|
7
|
-
require 'json'
|
1
|
+
require 'fintoc/v1/client/client'
|
2
|
+
require 'fintoc/v2/client/client'
|
8
3
|
|
9
4
|
module Fintoc
|
10
5
|
class Client
|
11
|
-
|
12
|
-
def initialize(api_key)
|
6
|
+
def initialize(api_key, jws_private_key: nil)
|
13
7
|
@api_key = api_key
|
14
|
-
@
|
15
|
-
@headers = { "Authorization": @api_key, "User-Agent": @user_agent }
|
16
|
-
@link_headers = nil
|
17
|
-
@link_header_pattern = '<(?<url>.*)>;\s*rel="(?<rel>.*)"'
|
18
|
-
@default_params = {}
|
8
|
+
@jws_private_key = jws_private_key
|
19
9
|
end
|
20
10
|
|
21
|
-
def
|
22
|
-
|
11
|
+
def v1
|
12
|
+
@v1 ||= Fintoc::V1::Client.new(@api_key)
|
23
13
|
end
|
24
14
|
|
25
|
-
def
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
def request(method)
|
30
|
-
proc do |resource, **kwargs|
|
31
|
-
parameters = params(method, **kwargs)
|
32
|
-
response = make_request(method, resource, parameters)
|
33
|
-
content = JSON.parse(response.body, symbolize_names: true)
|
34
|
-
|
35
|
-
if response.status.client_error? || response.status.server_error?
|
36
|
-
raise_custom_error(content[:error])
|
37
|
-
end
|
38
|
-
|
39
|
-
@link_headers = response.headers.get('link')
|
40
|
-
content
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def fetch_next
|
45
|
-
next_ = link_headers['next']
|
46
|
-
Enumerator.new do |yielder|
|
47
|
-
while next_
|
48
|
-
yielder << get.call(next_)
|
49
|
-
next_ = link_headers['next']
|
50
|
-
end
|
51
|
-
end
|
15
|
+
def v2
|
16
|
+
@v2 ||= Fintoc::V2::Client.new(@api_key, jws_private_key: @jws_private_key)
|
52
17
|
end
|
53
18
|
|
19
|
+
# These methods are kept for backward compatibility
|
54
20
|
def get_link(link_token)
|
55
|
-
|
56
|
-
build_link(data)
|
21
|
+
@v1.links.get(link_token)
|
57
22
|
end
|
58
23
|
|
59
24
|
def get_links
|
60
|
-
|
25
|
+
@v1.links.list
|
61
26
|
end
|
62
27
|
|
63
28
|
def delete_link(link_id)
|
64
|
-
delete
|
29
|
+
@v1.links.delete(link_id)
|
65
30
|
end
|
66
31
|
|
67
32
|
def get_account(link_token, account_id)
|
68
|
-
|
33
|
+
@v1.links.get(link_token).find(id: account_id)
|
69
34
|
end
|
70
35
|
|
71
36
|
def to_s
|
72
|
-
|
73
|
-
hidden_part = '*' * (@api_key.size - visible_chars)
|
74
|
-
visible_key = @api_key.slice(0, visible_chars)
|
75
|
-
"Client(🔑=#{hidden_part + visible_key}"
|
76
|
-
end
|
77
|
-
|
78
|
-
private
|
79
|
-
|
80
|
-
def client
|
81
|
-
@client ||= HTTP.headers(@headers)
|
82
|
-
end
|
83
|
-
|
84
|
-
def parse_headers(dict, link)
|
85
|
-
matches = link.strip.match(@link_header_pattern)
|
86
|
-
dict[matches[:rel]] = matches[:url]
|
87
|
-
dict
|
88
|
-
end
|
89
|
-
|
90
|
-
def _get_link(link_token)
|
91
|
-
get.call("links/#{link_token}")
|
92
|
-
end
|
93
|
-
|
94
|
-
def _get_links
|
95
|
-
get.call('links')
|
96
|
-
end
|
97
|
-
|
98
|
-
def build_link(data)
|
99
|
-
param = Utils.pick(data, 'link_token')
|
100
|
-
@default_params.update(param)
|
101
|
-
Link.new(**data, client: self)
|
102
|
-
end
|
103
|
-
|
104
|
-
def make_request(method, resource, parameters)
|
105
|
-
# this is to handle url returned in the link headers
|
106
|
-
# I'm sure there is a better and more clever way to solve this
|
107
|
-
if resource.start_with? 'https'
|
108
|
-
client.send(method, resource)
|
109
|
-
else
|
110
|
-
url = "#{Fintoc::Constants::SCHEME}#{Fintoc::Constants::BASE_URL}#{resource}"
|
111
|
-
client.send(method, url, parameters)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def params(method, **kwargs)
|
116
|
-
if method == 'get'
|
117
|
-
{ params: { **@default_params, **kwargs } }
|
118
|
-
else
|
119
|
-
{ json: { **@default_params, **kwargs } }
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def raise_custom_error(error)
|
124
|
-
raise error_class(error[:code]).new(error[:message], error[:doc_url])
|
125
|
-
end
|
126
|
-
|
127
|
-
def error_class(snake_code)
|
128
|
-
pascal_klass_name = Utils.snake_to_pascal(snake_code)
|
129
|
-
# this conditional klass_name is to handle InternalServerError custom error class
|
130
|
-
# without this the error class name would be like InternalServerErrorError (^-^)
|
131
|
-
klass = pascal_klass_name.end_with?('Error') ? pascal_klass_name : "#{pascal_klass_name}Error"
|
132
|
-
Module.const_get("Fintoc::Errors::#{klass}")
|
133
|
-
end
|
134
|
-
|
135
|
-
# This attribute getter parses the link headers using some regex 24K magic in the air...
|
136
|
-
# Ex.
|
137
|
-
# <https://api.fintoc.com/v1/links?page=1>; rel="first", <https://api.fintoc.com/v1/links?page=1>; rel="last"
|
138
|
-
# this helps to handle pagination see: https://fintoc.com/docs#paginacion
|
139
|
-
# return a hash like { first:"https://api.fintoc.com/v1/links?page=1" }
|
140
|
-
#
|
141
|
-
# @param link_headers [String]
|
142
|
-
# @return [Hash]
|
143
|
-
def link_headers
|
144
|
-
return if @link_headers.nil?
|
145
|
-
|
146
|
-
@link_headers[0].split(',').reduce({}) { |dict, link| parse_headers(dict, link) }
|
37
|
+
"Fintoc::Client(v1: #{@v1}, v2: #{@v2})"
|
147
38
|
end
|
148
39
|
end
|
149
40
|
end
|
data/lib/fintoc/constants.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
module Fintoc
|
2
2
|
module Constants
|
3
3
|
FIELDSUBS = [%w[id id_], %w[type type_]].freeze
|
4
|
-
GENERAL_DOC_URL =
|
5
|
-
SCHEME =
|
6
|
-
BASE_URL =
|
4
|
+
GENERAL_DOC_URL = 'https://docs.fintoc.com/reference/errors'
|
5
|
+
SCHEME = 'https://'
|
6
|
+
BASE_URL = 'api.fintoc.com/v1/'
|
7
|
+
BASE_URL_V2 = 'api.fintoc.com/v2/'
|
7
8
|
end
|
8
9
|
end
|
data/lib/fintoc/errors.rb
CHANGED
@@ -4,6 +4,8 @@ module Fintoc
|
|
4
4
|
module Errors
|
5
5
|
class FintocError < StandardError
|
6
6
|
def initialize(message, doc_url = Fintoc::Constants::GENERAL_DOC_URL)
|
7
|
+
super(message)
|
8
|
+
|
7
9
|
@message = message
|
8
10
|
@doc_url = doc_url
|
9
11
|
end
|
@@ -11,28 +13,150 @@ module Fintoc
|
|
11
13
|
def message
|
12
14
|
"\n#{@message}\n Please check the docs at: #{@doc_url}"
|
13
15
|
end
|
14
|
-
|
15
|
-
def to_s
|
16
|
-
message
|
17
|
-
end
|
18
16
|
end
|
17
|
+
|
18
|
+
# 400 Bad Request Errors
|
19
19
|
class InvalidRequestError < FintocError; end
|
20
|
-
class
|
21
|
-
class
|
22
|
-
class
|
23
|
-
class
|
24
|
-
class
|
25
|
-
class
|
26
|
-
class
|
27
|
-
class
|
20
|
+
class InvalidCurrencyError < FintocError; end
|
21
|
+
class InvalidAmountError < FintocError; end
|
22
|
+
class InvalidAccountTypeError < FintocError; end
|
23
|
+
class InvalidAccountNumberError < FintocError; end
|
24
|
+
class InvalidAccountStatusError < FintocError; end
|
25
|
+
class InvalidAccountBalanceError < FintocError; end
|
26
|
+
class InvalidInstitutionIdError < FintocError; end
|
27
|
+
class CurrencyMismatchError < FintocError; end
|
28
|
+
class InvalidCommentSizeError < FintocError; end
|
29
|
+
class InvalidReferenceIdSizeError < FintocError; end
|
28
30
|
class MissingParameterError < FintocError; end
|
31
|
+
class InvalidPositiveIntegerError < FintocError; end
|
29
32
|
class EmptyStringError < FintocError; end
|
30
|
-
class
|
33
|
+
class InvalidStringSizeError < FintocError; end
|
34
|
+
class InvalidHashError < FintocError; end
|
35
|
+
class InvalidBooleanError < FintocError; end
|
36
|
+
class InvalidArrayError < FintocError; end
|
37
|
+
class InvalidIntegerError < FintocError; end
|
38
|
+
class InvalidJsonError < FintocError; end
|
39
|
+
class InvalidParamsError < FintocError; end
|
40
|
+
class MissingCursorError < FintocError; end
|
41
|
+
class InvalidEnumError < FintocError; end
|
42
|
+
class InvalidStringError < FintocError; end
|
43
|
+
class InvalidUsernameError < FintocError; end
|
44
|
+
class InvalidLinkTokenError < FintocError; end
|
31
45
|
class InvalidDateError < FintocError; end
|
32
|
-
class
|
33
|
-
class
|
46
|
+
class InvalidHolderIdError < FintocError; end
|
47
|
+
class InvalidCardNumberError < FintocError; end
|
48
|
+
class InvalidProductError < FintocError; end
|
49
|
+
class InvalidWebhookSubscriptionError < FintocError; end
|
50
|
+
class InvalidIssueTypeError < FintocError; end
|
51
|
+
class InvalidRefreshTypeError < FintocError; end
|
52
|
+
class InvalidBusinessProfileTaxIdError < FintocError; end
|
53
|
+
class InvalidSessionHolderIdError < FintocError; end
|
54
|
+
class InvalidPaymentRecipientAccountError < FintocError; end
|
55
|
+
class InvalidPayoutRecipientAccountError < FintocError; end
|
56
|
+
class InvalidWidgetTokenError < FintocError; end
|
57
|
+
class InvalidPaymentReferenceNumberError < FintocError; end
|
58
|
+
class InvalidOnDemandLinkError < FintocError; end
|
59
|
+
class InvalidHolderTypeError < FintocError; end
|
60
|
+
class InvalidVoucherDownloadError < FintocError; end
|
61
|
+
class InvalidModeError < FintocError; end
|
62
|
+
class InvalidRsaKeyError < FintocError; end
|
63
|
+
class ExpectedPublicRsaKeyError < FintocError; end
|
64
|
+
class InvalidCidrBlockError < FintocError; end
|
65
|
+
class InvalidExpiresAtError < FintocError; end
|
66
|
+
class InvalidInstallmentsCurrencyError < FintocError; end
|
67
|
+
class InvalidClabeError < FintocError; end
|
68
|
+
class MismatchTransferAccountCurrencyError < FintocError; end
|
69
|
+
|
70
|
+
# 401 Unauthorized Errors
|
71
|
+
class AuthenticationError < FintocError; end
|
34
72
|
class InvalidApiKeyError < FintocError; end
|
73
|
+
class ExpiredApiKeyError < FintocError; end
|
74
|
+
class InvalidApiKeyModeError < FintocError; end
|
75
|
+
class ExpiredExchangeTokenError < FintocError; end
|
76
|
+
class InvalidExchangeTokenError < FintocError; end
|
77
|
+
class MissingActiveJwsPublicKeyError < FintocError; end
|
78
|
+
class InvalidJwsSignatureAlgorithmError < FintocError; end
|
79
|
+
class InvalidJwsSignatureHeaderError < FintocError; end
|
80
|
+
class InvalidJwsSignatureNonceError < FintocError; end
|
81
|
+
class InvalidJwsSignatureTimestampError < FintocError; end
|
82
|
+
class InvalidJwsSignatureTimestampFormatError < FintocError; end
|
83
|
+
class InvalidJwsSignatureTimestampValueError < FintocError; end
|
84
|
+
class MissingJwsSignatureHeaderError < FintocError; end
|
85
|
+
class JwsNonceAlreadyUsedError < FintocError; end
|
86
|
+
class InvalidJwsTsError < FintocError; end
|
87
|
+
|
88
|
+
# 402 Payment Required Errors
|
89
|
+
class PaymentRequiredError < FintocError; end
|
90
|
+
|
91
|
+
# 403 Forbidden Errors
|
92
|
+
class InvalidAccountError < FintocError; end
|
93
|
+
class InvalidRecipientAccountError < FintocError; end
|
94
|
+
class AccountNotActiveError < FintocError; end
|
95
|
+
class EntityNotOperationalError < FintocError; end
|
96
|
+
class ForbiddenEntityError < FintocError; end
|
97
|
+
class ForbiddenAccountError < FintocError; end
|
98
|
+
class ForbiddenAccountNumberError < FintocError; end
|
99
|
+
class ForbiddenAccountVerificationError < FintocError; end
|
100
|
+
class InvalidApiVersionError < FintocError; end
|
101
|
+
class ProductAccessRequiredError < FintocError; end
|
102
|
+
class ForbiddenRequestError < FintocError; end
|
103
|
+
class MissingAllowedCidrBlocksError < FintocError; end
|
104
|
+
class AllowedCidrBlocksDoesNotContainIpError < FintocError; end
|
105
|
+
class RecipientBlockedAccountError < FintocError; end
|
106
|
+
|
107
|
+
# 404 Not Found Errors
|
108
|
+
class MissingResourceError < FintocError; end
|
109
|
+
class InvalidUrlError < FintocError; end
|
110
|
+
class OrganizationWithoutEntitiesError < FintocError; end
|
111
|
+
|
112
|
+
# 405 Method Not Allowed Errors
|
113
|
+
class OperationNotAllowedError < FintocError; end
|
114
|
+
|
115
|
+
# 406 Not Acceptable Errors
|
116
|
+
class InstitutionCredentialsInvalidError < FintocError; end
|
117
|
+
class LockedCredentialsError < FintocError; end
|
35
118
|
class UnavailableInstitutionError < FintocError; end
|
119
|
+
|
120
|
+
# 409 Conflict Errors
|
121
|
+
class InsufficientBalanceError < FintocError; end
|
122
|
+
class InvalidDuplicatedTransferError < FintocError; end
|
123
|
+
class InvalidTransferStatusError < FintocError; end
|
124
|
+
class InvalidTransferDirectionError < FintocError; end
|
125
|
+
class AccountNumberLimitReachedError < FintocError; end
|
126
|
+
class AccountCannotBeBlockedError < FintocError; end
|
127
|
+
|
128
|
+
# 422 Unprocessable Entity Errors
|
129
|
+
class InvalidOtpCodeError < FintocError; end
|
130
|
+
class OtpNotFoundError < FintocError; end
|
131
|
+
class OtpBlockedError < FintocError; end
|
132
|
+
class OtpVerificationFailedError < FintocError; end
|
133
|
+
class OtpAlreadyExistsError < FintocError; end
|
134
|
+
class SubscriptionInProgressError < FintocError; end
|
135
|
+
class OnDemandPolicyRequiredError < FintocError; end
|
136
|
+
class OnDemandRefreshUnavailableError < FintocError; end
|
137
|
+
class NotSupportedCountryError < FintocError; end
|
138
|
+
class NotSupportedCurrencyError < FintocError; end
|
139
|
+
class NotSupportedModeError < FintocError; end
|
140
|
+
class NotSupportedProductError < FintocError; end
|
141
|
+
class RefreshIntentInProgressError < FintocError; end
|
142
|
+
class RejectedRefreshIntentError < FintocError; end
|
143
|
+
class SenderBlockedAccountError < FintocError; end
|
144
|
+
|
145
|
+
# 429 Too Many Requests Errors
|
146
|
+
class RateLimitExceededError < FintocError; end
|
147
|
+
|
148
|
+
# 500 Internal Server Errors
|
36
149
|
class InternalServerError < FintocError; end
|
150
|
+
class UnrecognizedRequestError < FintocError; end
|
151
|
+
class CoreResponseError < FintocError; end
|
152
|
+
|
153
|
+
# Webhook Errors
|
154
|
+
class WebhookSignatureError < FintocError; end
|
155
|
+
|
156
|
+
# Legacy Errors (keeping existing ones for backward compatibility and just in case)
|
157
|
+
class LinkError < FintocError; end
|
158
|
+
class InstitutionError < FintocError; end
|
159
|
+
class ApiError < FintocError; end
|
160
|
+
class InvalidCredentialsError < FintocError; end
|
37
161
|
end
|
38
162
|
end
|
data/lib/fintoc/jws.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'json'
|
3
|
+
require 'base64'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module Fintoc
|
7
|
+
class JWS
|
8
|
+
def initialize(private_key)
|
9
|
+
unless private_key.is_a?(OpenSSL::PKey::RSA)
|
10
|
+
raise ArgumentError, 'private_key must be an OpenSSL::PKey::RSA instance'
|
11
|
+
end
|
12
|
+
|
13
|
+
@private_key = private_key
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_signature(raw_body)
|
17
|
+
body_string = raw_body.is_a?(Hash) ? raw_body.to_json : raw_body.to_s
|
18
|
+
|
19
|
+
headers = {
|
20
|
+
alg: 'RS256',
|
21
|
+
nonce: SecureRandom.hex(16),
|
22
|
+
ts: Time.now.to_i,
|
23
|
+
crit: %w[ts nonce]
|
24
|
+
}
|
25
|
+
|
26
|
+
protected_base64 = base64url_encode(headers.to_json)
|
27
|
+
payload_base64 = base64url_encode(body_string)
|
28
|
+
signing_input = "#{protected_base64}.#{payload_base64}"
|
29
|
+
|
30
|
+
signature = @private_key.sign(OpenSSL::Digest.new('SHA256'), signing_input)
|
31
|
+
signature_base64 = base64url_encode(signature)
|
32
|
+
|
33
|
+
"#{protected_base64}.#{signature_base64}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_signature(signature)
|
37
|
+
protected_b64, signature_b64 = signature.split('.')
|
38
|
+
|
39
|
+
{
|
40
|
+
protected_headers: decode_protected_headers(protected_b64),
|
41
|
+
signature_bytes: decode_signature(signature_b64),
|
42
|
+
protected_b64: protected_b64,
|
43
|
+
signature_b64: signature_b64
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def verify_signature(signature, payload)
|
48
|
+
parsed = parse_signature(signature)
|
49
|
+
|
50
|
+
# Reconstruct the signing input
|
51
|
+
payload_json = payload.is_a?(Hash) ? payload.to_json : payload.to_s
|
52
|
+
payload_b64 = base64url_encode(payload_json)
|
53
|
+
signing_input = "#{parsed[:protected_b64]}.#{payload_b64}"
|
54
|
+
|
55
|
+
# Verify with public key
|
56
|
+
public_key = @private_key.public_key
|
57
|
+
public_key.verify(OpenSSL::Digest.new('SHA256'), parsed[:signature_bytes], signing_input)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def decode_protected_headers(protected_b64)
|
63
|
+
padded = add_padding(protected_b64)
|
64
|
+
|
65
|
+
protected_json = Base64.urlsafe_decode64(padded)
|
66
|
+
JSON.parse(protected_json, symbolize_names: true)
|
67
|
+
end
|
68
|
+
|
69
|
+
def decode_signature(signature_b64)
|
70
|
+
padded = add_padding(signature_b64)
|
71
|
+
|
72
|
+
Base64.urlsafe_decode64(padded)
|
73
|
+
end
|
74
|
+
|
75
|
+
def add_padding(b64)
|
76
|
+
(b64.length % 4).zero? ? b64 : (b64 + ('=' * (4 - (b64.length % 4))))
|
77
|
+
end
|
78
|
+
|
79
|
+
def base64url_encode(data)
|
80
|
+
Base64.urlsafe_encode64(data).tr('=', '')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/fintoc/utils.rb
CHANGED
@@ -32,7 +32,7 @@ module Fintoc
|
|
32
32
|
# @param suffix [String]
|
33
33
|
# @return [String]
|
34
34
|
def pluralize(amount, noun, suffix = 's')
|
35
|
-
quantifier = amount
|
35
|
+
quantifier = amount || 'no'
|
36
36
|
"#{quantifier} #{amount == 1 ? noun : noun + suffix}"
|
37
37
|
end
|
38
38
|
|
@@ -41,7 +41,7 @@ module Fintoc
|
|
41
41
|
# @param name [String]
|
42
42
|
# @return [String]
|
43
43
|
def snake_to_pascal(name)
|
44
|
-
name.split('_').map(&:capitalize).join
|
44
|
+
name.split('_').map(&:capitalize).join
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'fintoc/v1/resources/link'
|
2
|
+
|
3
|
+
module Fintoc
|
4
|
+
module V1
|
5
|
+
module Managers
|
6
|
+
class LinksManager
|
7
|
+
def initialize(client)
|
8
|
+
@client = client
|
9
|
+
end
|
10
|
+
|
11
|
+
def get(link_token)
|
12
|
+
data = { **_get_link(link_token), link_token: link_token }
|
13
|
+
build_link(data)
|
14
|
+
end
|
15
|
+
|
16
|
+
def list
|
17
|
+
_get_links.map { |data| build_link(data) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete(link_id)
|
21
|
+
_delete_link(link_id)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def _get_link(link_token)
|
27
|
+
@client.get(version: :v1).call("links/#{link_token}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def _get_links
|
31
|
+
@client.get(version: :v1).call('links')
|
32
|
+
end
|
33
|
+
|
34
|
+
def _delete_link(link_id)
|
35
|
+
@client.delete(version: :v1).call("links/#{link_id}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_link(data)
|
39
|
+
param = Utils.pick(data, 'link_token')
|
40
|
+
@client.default_params.update(param)
|
41
|
+
Fintoc::V1::Link.new(**data, client: @client)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'tabulate'
|
2
|
+
require 'fintoc/utils'
|
3
|
+
require 'fintoc/v1/resources/movement'
|
4
|
+
require 'fintoc/v1/resources/balance'
|
5
|
+
|
6
|
+
module Fintoc
|
7
|
+
module V1
|
8
|
+
class Account
|
9
|
+
include Utils
|
10
|
+
|
11
|
+
attr_reader :id, :name, :holder_name, :currency, :type, :refreshed_at,
|
12
|
+
:official_name, :number, :holder_id, :balance, :movements
|
13
|
+
|
14
|
+
HEADERS = ['#', 'Amount', 'Currency', 'Description', 'Date'].freeze
|
15
|
+
|
16
|
+
def initialize(
|
17
|
+
id:,
|
18
|
+
name:,
|
19
|
+
official_name:,
|
20
|
+
number:,
|
21
|
+
holder_id:,
|
22
|
+
holder_name:,
|
23
|
+
type:,
|
24
|
+
currency:,
|
25
|
+
refreshed_at: nil,
|
26
|
+
balance: nil,
|
27
|
+
movements: nil,
|
28
|
+
client: nil,
|
29
|
+
**
|
30
|
+
)
|
31
|
+
@id = id
|
32
|
+
@name = name
|
33
|
+
@official_name = official_name
|
34
|
+
@number = number
|
35
|
+
@holder_id = holder_id
|
36
|
+
@holder_name = holder_name
|
37
|
+
@type = type
|
38
|
+
@currency = currency
|
39
|
+
@refreshed_at = DateTime.iso8601(refreshed_at) if refreshed_at
|
40
|
+
@balance = Fintoc::V1::Balance.new(**balance)
|
41
|
+
@movements = movements || []
|
42
|
+
@client = client
|
43
|
+
end
|
44
|
+
|
45
|
+
def update_balance
|
46
|
+
@balance = Fintoc::V1::Balance.new(**get_account[:balance])
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_movements(**params)
|
50
|
+
_get_movements(**params).lazy.map do
|
51
|
+
|movement| Fintoc::V1::Movement.new(**movement, client: @client)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def update_movements(**params)
|
56
|
+
@movements += get_movements(**params).to_a
|
57
|
+
@movements = @movements.uniq.sort_by(&:post_date)
|
58
|
+
end
|
59
|
+
|
60
|
+
def show_movements(rows = 5)
|
61
|
+
puts("This account has #{Utils.pluralize(@movements.size, 'movement')}.")
|
62
|
+
|
63
|
+
return unless @movements.any?
|
64
|
+
|
65
|
+
movements =
|
66
|
+
@movements
|
67
|
+
.to_a
|
68
|
+
.slice(0, rows)
|
69
|
+
.map.with_index do |mov, index|
|
70
|
+
[index + 1, mov.amount, mov.currency, mov.description, mov.locale_date]
|
71
|
+
end
|
72
|
+
|
73
|
+
puts
|
74
|
+
puts tabulate(HEADERS, movements, indent: 4, style: 'fancy')
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
"💰 #{@holder_name}’s #{@name} #{@balance}"
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def get_account
|
84
|
+
@client.get(version: :v1).call("accounts/#{@id}")
|
85
|
+
end
|
86
|
+
|
87
|
+
def _get_movements(**params)
|
88
|
+
first = @client.get(version: :v1).call("accounts/#{@id}/movements", **params)
|
89
|
+
return first if params.empty?
|
90
|
+
|
91
|
+
first + Utils.flatten(@client.fetch_next)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fintoc
|
4
|
+
module V1
|
5
|
+
class Balance
|
6
|
+
attr_reader :available, :current, :limit
|
7
|
+
|
8
|
+
def initialize(available:, current:, limit:)
|
9
|
+
@available = available
|
10
|
+
@current = current
|
11
|
+
@limit = limit
|
12
|
+
end
|
13
|
+
|
14
|
+
def id
|
15
|
+
object_id
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"#{@available} (#{@current})"
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect
|
23
|
+
"<Fintoc::V1::Balance #{@available} (#{@current})>"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Fintoc
|
2
|
+
module V1
|
3
|
+
class Institution
|
4
|
+
attr_reader :id, :name, :country
|
5
|
+
|
6
|
+
def initialize(id:, name:, country:, **)
|
7
|
+
@id = id
|
8
|
+
@name = name
|
9
|
+
@country = country
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"🏦 #{@name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
"<Fintoc::V1::Institution #{@name}>"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|