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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/pull_request_template.md +47 -0
  3. data/.github/workflows/ci.yml +46 -0
  4. data/.rubocop.yml +14 -7
  5. data/CHANGELOG.md +40 -0
  6. data/Gemfile +20 -1
  7. data/Gemfile.lock +237 -61
  8. data/README.md +286 -42
  9. data/Rakefile +3 -3
  10. data/fintoc.gemspec +3 -7
  11. data/lib/config/initializers/money.rb +5 -0
  12. data/lib/fintoc/base_client.rb +150 -0
  13. data/lib/fintoc/client.rb +14 -123
  14. data/lib/fintoc/constants.rb +4 -3
  15. data/lib/fintoc/errors.rb +139 -15
  16. data/lib/fintoc/jws.rb +83 -0
  17. data/lib/fintoc/utils.rb +2 -2
  18. data/lib/fintoc/v1/client/client.rb +12 -0
  19. data/lib/fintoc/v1/managers/links_manager.rb +46 -0
  20. data/lib/fintoc/v1/resources/account.rb +95 -0
  21. data/lib/fintoc/v1/resources/balance.rb +27 -0
  22. data/lib/fintoc/v1/resources/institution.rb +21 -0
  23. data/lib/fintoc/v1/resources/link.rb +85 -0
  24. data/lib/fintoc/v1/resources/movement.rb +62 -0
  25. data/lib/fintoc/v1/resources/transfer_account.rb +24 -0
  26. data/lib/fintoc/v2/client/client.rb +37 -0
  27. data/lib/fintoc/v2/managers/account_numbers_manager.rb +59 -0
  28. data/lib/fintoc/v2/managers/account_verifications_manager.rb +45 -0
  29. data/lib/fintoc/v2/managers/accounts_manager.rb +54 -0
  30. data/lib/fintoc/v2/managers/entities_manager.rb +36 -0
  31. data/lib/fintoc/v2/managers/simulate_manager.rb +30 -0
  32. data/lib/fintoc/v2/managers/transfers_manager.rb +56 -0
  33. data/lib/fintoc/v2/resources/account.rb +105 -0
  34. data/lib/fintoc/v2/resources/account_number.rb +105 -0
  35. data/lib/fintoc/v2/resources/account_verification.rb +73 -0
  36. data/lib/fintoc/v2/resources/entity.rb +51 -0
  37. data/lib/fintoc/v2/resources/transfer.rb +131 -0
  38. data/lib/fintoc/version.rb +1 -1
  39. data/lib/fintoc/webhook_signature.rb +73 -0
  40. data/lib/fintoc.rb +3 -0
  41. data/lib/tasks/simplecov_config.rb +19 -0
  42. metadata +35 -83
  43. data/lib/fintoc/resources/account.rb +0 -84
  44. data/lib/fintoc/resources/balance.rb +0 -24
  45. data/lib/fintoc/resources/institution.rb +0 -18
  46. data/lib/fintoc/resources/link.rb +0 -83
  47. data/lib/fintoc/resources/movement.rb +0 -55
  48. data/lib/fintoc/resources/transfer_account.rb +0 -22
data/lib/fintoc/client.rb CHANGED
@@ -1,149 +1,40 @@
1
- require 'http'
2
- require 'fintoc/utils'
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
- include Utils
12
- def initialize(api_key)
6
+ def initialize(api_key, jws_private_key: nil)
13
7
  @api_key = api_key
14
- @user_agent = "fintoc-ruby/#{Fintoc::VERSION}"
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 get
22
- request('get')
11
+ def v1
12
+ @v1 ||= Fintoc::V1::Client.new(@api_key)
23
13
  end
24
14
 
25
- def delete
26
- request('delete')
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
- data = { **_get_link(link_token), "link_token": link_token }
56
- build_link(data)
21
+ @v1.links.get(link_token)
57
22
  end
58
23
 
59
24
  def get_links
60
- _get_links.map { |data| build_link(data) }
25
+ @v1.links.list
61
26
  end
62
27
 
63
28
  def delete_link(link_id)
64
- delete.call("links/#{link_id}")
29
+ @v1.links.delete(link_id)
65
30
  end
66
31
 
67
32
  def get_account(link_token, account_id)
68
- get_link(link_token).find(id: account_id)
33
+ @v1.links.get(link_token).find(id: account_id)
69
34
  end
70
35
 
71
36
  def to_s
72
- visible_chars = 4
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
@@ -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 = "https://fintoc.com/docs"
5
- SCHEME = "https://"
6
- BASE_URL = "api.fintoc.com/v1/"
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 LinkError < FintocError; end
21
- class AuthenticationError < FintocError; end
22
- class InstitutionError < FintocError; end
23
- class ApiError < FintocError; end
24
- class MissingResourceError < FintocError; end
25
- class InvalidLinkTokenError < FintocError; end
26
- class InvalidUsernameError < FintocError; end
27
- class InvalidHolderTypeError < FintocError; end
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 UnrecognizedRequestError < FintocError; end
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 InvalidCredentialsError < FintocError; end
33
- class LockedCredentialsError < FintocError; end
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 or 'no'
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,12 @@
1
+ require 'fintoc/base_client'
2
+ require 'fintoc/v1/managers/links_manager'
3
+
4
+ module Fintoc
5
+ module V1
6
+ class Client < BaseClient
7
+ def links
8
+ @links ||= Managers::LinksManager.new(self)
9
+ end
10
+ end
11
+ end
12
+ 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