diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0046ce0..b0c6692 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,4 +43,4 @@ jobs: bundler-cache: true - name: Run tests - run: bundle exec rspec + run: COVERAGE=true bundle exec rspec diff --git a/CHANGELOG.md b/CHANGELOG.md index 987d892..40f7f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,45 @@ # Changelog -## 0.2.0 - 2025-08-26 +## 1.0.0 - 2025-09-05 -* Update linting rules and fix linting issues -* Update CI flow -* Update gems and support only Ruby versions that haven't reached EOL -* Update README +### πŸš€ New Features + +- **New Client Architecture**: Restructured client to distinguish between API versions + - Movements API now accessible via `Fintoc::V1::Client` + - Transfers API accessible via `Fintoc::V2::Client` + - Unified client interface with `Fintoc::Client` providing access to both versions + - Backward compatibility maintained for existing method signatures + +- **V2 Client - Transfers API Implementation**: Partial implementation of Transfers API endpoints in `Fintoc::V2::Client` + - **Entities**: List and retrieve business entities + - **Transfer Accounts**: Create, read, update, and list transfer accounts + - **Account Numbers**: Manage account numbers/CLABEs + - **Transfers**: Create, retrieve, list, and return transfers + - **Simulation**: Simulate receiving transfers for testing + - **Account Verifications**: Verify account numbers + - **Movements**: TODO! Not yet implemented + +### πŸ§ͺ Testing & Quality + +- **100% Line Coverage**: Achieved full line coverage using SimpleCov gem, increasing the spec coverage and testing all new code. + - Configured with `minimum_coverage 100` and `minimum_coverage_by_file 100` + - Uses `simplecov_text_formatter` and `simplecov_linter_formatter` for reporting + +- **Robust CI Pipeline**: Enhanced GitHub Actions workflow for comprehensive testing + - **Linting**: Dedicated RuboCop job for code quality enforcement + - **Multi-version Testing**: Tests against all currently supported Ruby versions (3.2, 3.3, and 3.4) for version compatibility + - **Coverage Integration**: Automated coverage reporting in CI pipeline + +### Others + +- **Money-Rails Integration**: Added `money-rails` gem for proper currency handling +- **Comprehensive README Update**: Extensively updated documentation with usage examples and development instructions +- **Improved Error Handling**: Better error management across API versions +- **JWS Support**: JSON Web Signature support for secure V2 API operations +- **HMac Signature verification**: Added the `Fintoc::WebhookSignature` class for easing webhook signature verification ## 0.1.0 - 2021-01-18 +Initial version + * Up to date with the [2020-11-17](https://docs.fintoc.com/docs/api-changelog#2020-11-17) API version diff --git a/Gemfile b/Gemfile index 1badaa3..3831ec5 100644 --- a/Gemfile +++ b/Gemfile @@ -4,12 +4,20 @@ source 'https://rubygems.org' gemspec # Development dependencies -gem 'rexml' # Required for webmock in Ruby 3.0+ -gem 'rspec', '~> 3.0' -gem 'rubocop', '~> 1.80' -gem 'rubocop-capybara', '~> 2.22', '>= 2.22.1' -gem 'rubocop-performance', '~> 1.25' -gem 'rubocop-rails', '~> 2.33', '>= 2.33.3' -gem 'rubocop-rspec', '~> 3.6' -gem 'vcr', '~> 6.3' -gem 'webmock' +group :development do + gem 'rubocop', '~> 1.80' + gem 'rubocop-capybara', '~> 2.22', '>= 2.22.1' + gem 'rubocop-performance', '~> 1.25' + gem 'rubocop-rails', '~> 2.33', '>= 2.33.3' + gem 'rubocop-rspec', '~> 3.6' +end + +group :test do + gem 'rexml' # Required for webmock in Ruby 3.0+ + gem 'rspec', '~> 3.0' + gem 'simplecov', '~> 0.21' + gem 'simplecov_linter_formatter' + gem 'simplecov_text_formatter' + gem 'vcr', '~> 6.3' + gem 'webmock' +end diff --git a/Gemfile.lock b/Gemfile.lock index ee60bbe..e0d60c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,30 @@ PATH remote: . specs: - fintoc (0.2.0) + fintoc (1.0.0) http + money-rails tabulate GEM remote: https://rubygems.org/ specs: + actionpack (8.0.2.1) + actionview (= 8.0.2.1) + activesupport (= 8.0.2.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actionview (8.0.2.1) + activesupport (= 8.0.2.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) activesupport (8.0.2.1) base64 benchmark (>= 0.3) @@ -27,22 +44,26 @@ GEM base64 (0.3.0) benchmark (0.4.1) bigdecimal (3.2.2) + builder (3.3.0) concurrent-ruby (1.3.5) connection_pool (2.5.3) crack (1.0.0) bigdecimal rexml + crass (1.0.6) + date (3.4.1) diff-lcs (1.6.2) + docile (1.4.1) domain_name (0.6.20240107) drb (2.2.3) + erb (5.0.2) + erubi (1.13.1) ffi (1.17.2) ffi (1.17.2-aarch64-linux-gnu) ffi (1.17.2-aarch64-linux-musl) ffi (1.17.2-arm-linux-gnu) ffi (1.17.2-arm-linux-musl) ffi (1.17.2-arm64-darwin) - ffi (1.17.2-x86-linux-gnu) - ffi (1.17.2-x86-linux-musl) ffi (1.17.2-x86_64-darwin) ffi (1.17.2-x86_64-linux-gnu) ffi (1.17.2-x86_64-linux-musl) @@ -60,6 +81,11 @@ GEM http-form_data (2.3.0) i18n (1.14.7) concurrent-ruby (~> 1.0) + io-console (0.8.1) + irb (1.15.2) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) json (2.13.2) language_server-protocol (3.17.0.5) lint_roller (1.1.0) @@ -67,18 +93,79 @@ GEM ffi-compiler (~> 1.0) rake (~> 13.0) logger (1.7.0) + loofah (2.24.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) minitest (5.25.5) + monetize (1.13.0) + money (~> 6.12) + money (6.19.0) + i18n (>= 0.6.4, <= 2) + money-rails (1.15.0) + activesupport (>= 3.0) + monetize (~> 1.9) + money (~> 6.13) + railties (>= 3.0) + nokogiri (1.18.9-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.9-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.9-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.9-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.9-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.9-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.9-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.9-x86_64-linux-musl) + racc (~> 1.4) parallel (1.27.0) parser (3.3.9.0) ast (~> 2.4.1) racc + pp (0.6.2) + prettyprint + prettyprint (0.2.0) prism (1.4.0) + psych (5.2.6) + date + stringio public_suffix (6.0.2) racc (1.8.1) rack (3.2.0) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.0.2.1) + actionpack (= 8.0.2.1) + activesupport (= 8.0.2.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.3.0) + rdoc (6.14.2) + erb + psych (>= 4.0.0) regexp_parser (2.11.2) + reline (0.6.2) + io-console (~> 0.5) rexml (3.4.2) rspec (3.13.1) rspec-core (~> 3.13.0) @@ -125,19 +212,32 @@ GEM rubocop (~> 1.72, >= 1.72.1) ruby-progressbar (1.13.0) securerandom (0.4.1) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + simplecov_linter_formatter (0.2.0) + rainbow + simplecov_text_formatter (0.1.0) + stringio (3.1.7) tabulate (0.1.2) + thor (1.4.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (3.1.5) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) uri (1.0.3) + useragent (0.16.11) vcr (6.3.1) base64 webmock (3.25.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) + zeitwerk (2.7.3) PLATFORMS aarch64-linux-gnu @@ -145,9 +245,6 @@ PLATFORMS arm-linux-gnu arm-linux-musl arm64-darwin - ruby - x86-linux-gnu - x86-linux-musl x86_64-darwin x86_64-linux x86_64-linux-gnu @@ -162,6 +259,9 @@ DEPENDENCIES rubocop-performance (~> 1.25) rubocop-rails (~> 2.33, >= 2.33.3) rubocop-rspec (~> 3.6) + simplecov (~> 0.21) + simplecov_linter_formatter + simplecov_text_formatter vcr (~> 6.3) webmock diff --git a/README.md b/README.md index 3da6ca7..d308eb3 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,22 @@ Do yourself a favor: go grab some ice cubes by installing this refreshing librar - [Table of contents](#table-of-contents) - [How to Install](#how-to-install) - [Quickstart](#quickstart) + - [Client Architecture](#client-architecture) + - [**API V1 Client**](#api-v1-client) + - [**API V2 Client**](#api-v2-client) + - [**Backward compatibility**](#backward-compatibility) - [Documentation](#documentation) - [Examples](#examples) - - [Get accounts](#get-accounts) - - [Get movements](#get-movements) + - [Movements API Examples](#movements-api-examples) + - [Get accounts](#get-accounts) + - [Get movements](#get-movements) + - [Transfers API Examples](#transfers-api-examples) + - [Entities](#entities) + - [Transfer Accounts](#transfer-accounts) + - [Account Numbers](#account-numbers) + - [Transfers](#transfers) + - [Simulate](#simulate) + - [Account Verifications](#account-verifications) - [Development](#development) - [Dependencies](#dependencies) - [Setup](#setup) @@ -51,37 +63,121 @@ Or install it yourself as: ```ruby require 'fintoc' -client = Fintoc::Client.new('sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx') -link = client.get_link('6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W') +client_v1 = Fintoc::V1::Client.new('api_key') +link = client_v1.links.get('link_token') account = link.find(type: 'checking_account') -# Get the las 30 movements -movements = account.get_movements +# Get the last 30 movements +movements = account.movements.list # Or get all the movements since a specific date -movements = account.get_movements(since: '2020-08-15') +movements = account.movements.list(since: '2020-08-15') ``` And that’s it! +## Client Architecture + +The Fintoc Ruby client is organized into separate clients that mirror the official API structure: + +### **API V1 Client** + +The API client currently provides access to part of the Movements API: + +```ruby +client = Fintoc::V1::Client.new('api_key') + +# Link management +links = client_v1.links.list +link = client_v1.links.get('link_token') +client_v1.links.delete('link_id') + +# Account access +account = link.find(id: account_id) +``` + +### **API V2 Client** + +The API V2 client currently provides access to part of the Transfers API: + +```ruby +client = Fintoc::V1::Client.new('api_key') + +# Entities +entities = client_v2.entities.list +entity = client_v2.entities.get('entity_id') + +# Transfer Accounts +accounts = client_v2.accounts.list +account = client_v2.accounts.get('account_id') +account = client_v2.accounts.create(entity_id: 'entity_id', description: 'My Account') +client_v2.accounts.update('account_id', description: 'Updated') + +# Account Numbers +account_numbers = client_v2.account_numbers.list +account_number = client_v2.account_numbers.get('account_number_id') +account_number = client_v2.account_numbers.create(account_id: 'account_id', description: 'Main') +client_v2.account_numbers.update('account_number_id', description: 'Updated') + +# Transfers +transfers = client_v2.transfers.list +transfer = client_v2.transfers.get('transfer_id') +transfer = client_v2.transfers.create( + amount: 1000, + currency: 'CLP', + account_id: 'account_id', + counterparty: {...} +) +client_v2.transfers.return('transfer_id') + +# Simulate +simulated_transfer = client_v2.simulate.receive_transfer( + account_number_id: 'account_number_id', + amount: 1000, + currency: 'CLP' +) + +# Account Verifications +account_verifications = client_v2.account_verifications.list +account_verification = client_v2.account_verifications.get('account_verification_id') +account_verification = client_v2.account_verifications.create(account_number: 'account_number') + +# TODO: Movements +``` + +### **Backward compatibility** + +The methods of the previous `Fintoc::Client` class implementation are kept for backward compatibility purposes. + +```ruby +client = Fintoc::V1::Client.new('api_key') + +link = client.get_link('link_token') +links = client.get_links +client.delete_link(link.id) +account = client.get_account('link_token', 'account_id') +``` + ## Documentation -This client supports all Fintoc API endpoints. For complete information about the API, head to the [docs](https://docs.fintoc.com/reference). +This client does not support all Fintoc API endpoints yet. For complete information about the API, head to the [docs](https://docs.fintoc.com/reference). ## Examples -### Get accounts +### Movements API Examples + +#### Get accounts ```ruby require 'fintoc' -client = Fintoc::Client.new('api_key') -link = client.get_link('link_token') +client = Fintoc::V1::Client.new('api_key') +link = client_v1.links.get('link_token') puts link.accounts # Or... you can pretty print all the accounts in a Link -link = client.get_link('link_token') +link = client_v1.links.get('link_token') link.show_accounts ``` @@ -91,8 +187,8 @@ If you want to find a specific account in a link, you can use **find**. You can ```ruby require 'fintoc' -client = Fintoc::Client.new('api_key') -link = client.get_link('link_token') +client = Fintoc::V1::Client.new('api_key') +link = client_v1.links.get('link_token') account = link.find(type: 'checking_account') # Or by number @@ -107,8 +203,8 @@ You can also search for multiple accounts matching a specific criteria with **fi ```ruby require 'fintoc' -client = Fintoc::Client.new('api_key') -link = client.get_link('link_token') +client = Fintoc::V1::Client.new('api_key') +link = client_v1.links.get('link_token') accounts = link.find_all(currency: 'CLP') ``` @@ -117,34 +213,170 @@ To update the account balance you can use **update_balance**: ```ruby require 'fintoc' -client = Fintoc::Client.new('api_key') -link = client.get_link('link_token') +client = Fintoc::V1::Client.new('api_key') +link = client_v1.links.get('link_token') account = link.find(number: '1111111') account.update_balance ``` -### Get movements +#### Get movements ```ruby require 'fintoc' require 'time' -client = Fintoc::Client.new('api_key') -link = client.get_link('link_token') +client = Fintoc::V1::Client.new('api_key') +link = client_v1.links.get('link_token') account = link.find(type: 'checking_account') # You can get the account movements since a specific DateTime yesterday = DateTime.now - 1 -account.get_movements(since: yesterday) +account.movements.list(since: yesterday) # Or you can use an ISO 8601 formatted string representation of the Date -account.get_movements(since: '2020-01-01') +account.movements.list(since: '2020-01-01') # You can also set how many movements you want per_page -account.get_movements(since: '2020-01-01', per_page: 100) +account.movements.list(since: '2020-01-01', per_page: 100) +``` + +Calling **movements.list** without arguments gets the last 30 movements of the account + +### Transfers API Examples + +#### Entities + +```ruby +require 'fintoc' + +client_v2 = Fintoc::V2::Client.new('api_key', 'jws_private_key') + +# Get all entities +entities = client_v2.entities.list + +# Get a specific entity +entity = client_v2.entities.get('entity_id') +``` + +You can also list entities with pagination: + +```ruby +# Get entities with pagination +entities = client_v2.entities.list(limit: 10, starting_after: 'entity_id') ``` -Calling **get_movements** without arguments gets the last 30 movements of the account +#### Transfer Accounts + +```ruby +require 'fintoc' + +client_v2 = Fintoc::V2::Client.new('api_key', 'jws_private_key') + +# Create a transfer account +account = client_v2.accounts.create( + entity_id: 'entity_id', + description: 'My Business Account' +) + +# Get a specific account +account = client_v2.accounts.get('account_id') + +# List all accounts +accounts = client_v2.accounts.list + +# Update an account +updated_account = client_v2.accounts.update('account_id', description: 'Updated Description') +``` + +#### Account Numbers + +```ruby +require 'fintoc' + +client_v2 = Fintoc::V2::Client.new('api_key', 'jws_private_key') + +# Create an account number +account_number = client_v2.account_numbers.create( + account_id: 'account_id', + description: 'Main account number' +) + +# Get a specific account number +account_number = client_v2.account_numbers.get('account_number_id') + +# List all account numbers +account_numbers = client_v2.account_numbers.list + +# Update an account number +updated_account_number = client_v2.account_numbers.update( + 'account_number_id', + description: 'Updated account number' +) +``` + +#### Transfers + +```ruby +require 'fintoc' + +client_v2 = Fintoc::V2::Client.new('api_key', 'jws_private_key') + +# Create a transfer +transfer = client_v2.transfers.create( + amount: 10000, + currency: 'CLP', + account_id: 'account_id', + counterparty: { + name: 'John Doe', + rut: '12345678-9', + email: 'john@example.com', + bank: 'banco_de_chile', + account_type: 'checking_account', + account_number: '1234567890' + } +) + +# Get a specific transfer +transfer = client_v2.transfers.get('transfer_id') + +# List all transfers +transfers = client_v2.transfers.list + +# Return a transfer +returned_transfer = client_v2.transfers.return('transfer_id') +``` + +#### Simulate + +```ruby +require 'fintoc' + +client_v2 = Fintoc::V2::Client.new('api_key', 'jws_private_key') + +# Simulate receiving a transfer +simulated_transfer = client_v2.simulate.receive_transfer( + account_number_id: 'account_number_id', + amount: 5000, + currency: 'CLP' +) +``` + +#### Account Verifications + +```ruby +require 'fintoc' + +client_v2 = Fintoc::V2::Client.new('api_key', 'jws_private_key') + +# Create an account verification +account_verification = client_v2.account_verifications.create(account_number: 'account_number') + +# Get a specific account verification +account_verification = client_v2.account_verifications.get('account_verification_id') + +# List all account verifications +account_verifications = client_v2.account_verifications.list +``` ## Development diff --git a/fintoc.gemspec b/fintoc.gemspec index 28396b0..a0c6afc 100644 --- a/fintoc.gemspec +++ b/fintoc.gemspec @@ -27,5 +27,6 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] spec.add_dependency 'http' + spec.add_dependency 'money-rails' spec.add_dependency 'tabulate' end diff --git a/lib/config/initializers/money.rb b/lib/config/initializers/money.rb new file mode 100644 index 0000000..f541c58 --- /dev/null +++ b/lib/config/initializers/money.rb @@ -0,0 +1,5 @@ +require 'money-rails' + +MoneyRails.configure do |config| + config.locale_backend = :currency +end diff --git a/lib/fintoc.rb b/lib/fintoc.rb index 022b5c8..4743214 100644 --- a/lib/fintoc.rb +++ b/lib/fintoc.rb @@ -1,6 +1,9 @@ require 'fintoc/version' require 'fintoc/errors' require 'fintoc/client' +require 'fintoc/webhook_signature' + +require 'config/initializers/money' module Fintoc end diff --git a/lib/fintoc/base_client.rb b/lib/fintoc/base_client.rb new file mode 100644 index 0000000..1e9640c --- /dev/null +++ b/lib/fintoc/base_client.rb @@ -0,0 +1,150 @@ +require 'http' +require 'json' +require 'fintoc/utils' +require 'fintoc/errors' +require 'fintoc/constants' +require 'fintoc/version' +require 'fintoc/jws' + +module Fintoc + class BaseClient + include Utils + + attr_accessor :default_params + + def initialize(api_key, jws_private_key: nil) + @api_key = api_key + @user_agent = "fintoc-ruby/#{Fintoc::VERSION}" + @headers = { Authorization: "Bearer #{@api_key}", 'User-Agent': @user_agent } + @link_headers = nil + @link_header_pattern = '<(?.*)>;\s*rel="(?.*)"' + @default_params = {} + @jws = jws_private_key ? Fintoc::JWS.new(jws_private_key) : nil + end + + def get(version: :v1) + request('get', version: version) + end + + def delete(version: :v1) + request('delete', version: version) + end + + def post(version: :v1, use_jws: false) + request('post', version: version, use_jws: use_jws) + end + + def patch(version: :v1, use_jws: false) + request('patch', version: version, use_jws: use_jws) + end + + def request(method, version: :v1, use_jws: false) + proc do |resource, **kwargs| + parameters = params(method, **kwargs) + response = make_request(method, resource, parameters, version: version, use_jws: use_jws) + content = JSON.parse(response.body, symbolize_names: true) + + if response.status.client_error? || response.status.server_error? + raise_custom_error(content[:error]) + end + + @link_headers = response.headers.get('link') + content + end + end + + def fetch_next + next_ = link_headers['next'] + Enumerator.new do |yielder| + while next_ + yielder << get.call(next_) + next_ = link_headers['next'] + end + end + end + + def to_s + visible_chars = 8 + visible_key = @api_key.slice(0, visible_chars) + hidden_part = '*' * (@api_key.size - visible_chars) + "#{self.class.name}(πŸ”‘=#{visible_key + hidden_part})" + end + + private + + def client + @client ||= HTTP.headers(@headers) + end + + def parse_headers(dict, link) + matches = link.strip.match(@link_header_pattern) + dict[matches[:rel]] = matches[:url] + dict + end + + def build_url(resource, version: :v1) + base_url = version == :v2 ? Fintoc::Constants::BASE_URL_V2 : Fintoc::Constants::BASE_URL + "#{Fintoc::Constants::SCHEME}#{base_url}#{resource}" + end + + def should_use_jws?(method, use_jws) + use_jws && @jws && %w[post patch put].include?(method.downcase) + end + + def make_request(method, resource, parameters, version: :v1, use_jws: false) + # this is to handle url returned in the link headers + # I'm sure there is a better and more clever way to solve this + if resource.start_with? 'https' + return client.send(method, resource) + end + + url = build_url(resource, version:) + + if should_use_jws?(method, use_jws) + request_body = parameters[:json]&.to_json || '' + jws_signature = @jws.generate_signature(request_body) + + return client.headers('Fintoc-JWS-Signature' => jws_signature).send(method, url, parameters) + end + + client.send(method, url, parameters) + end + + def params(method, **kwargs) + if method == 'get' + { params: { **@default_params, **kwargs } } + else + { json: { **@default_params, **kwargs } } + end + end + + def raise_custom_error(error) + raise error_class(error[:code]).new(error[:message], error[:doc_url]) + end + + def error_class(snake_code) + pascal_klass_name = Utils.snake_to_pascal(snake_code) + # this conditional klass_name is to handle InternalServerError custom error class + # without this the error class name would be like InternalServerErrorError (^-^) + klass = + pascal_klass_name.end_with?('Error') ? pascal_klass_name : "#{pascal_klass_name}Error" + Module.const_get("Fintoc::Errors::#{klass}") + end + + # rubocop:disable Layout/LineLength + # This attribute getter parses the link headers using some regex 24K magic in the air... + # Ex. + # ; rel="first", ; rel="last" + # this helps to handle pagination see: https://fintoc.com/docs#paginacion + # return a hash like { first:"https://api.fintoc.com/v1/links?page=1" } + # + # @param link_headers [String] + # @return [Hash] + # rubocop:enable Layout/LineLength + def link_headers + return if @link_headers.nil? + + @link_headers[0].split(',').reduce({}) { |dict, link| parse_headers(dict, link) } + end + end +end diff --git a/lib/fintoc/client.rb b/lib/fintoc/client.rb index 7576ec0..b24bae7 100644 --- a/lib/fintoc/client.rb +++ b/lib/fintoc/client.rb @@ -1,152 +1,40 @@ -require 'http' -require 'fintoc/utils' -require 'fintoc/errors' -require 'fintoc/resources/link' -require 'fintoc/constants' -require 'fintoc/version' -require 'json' +require 'fintoc/v1/client/client' +require 'fintoc/v2/client/client' module Fintoc class Client - include Utils - - def initialize(api_key) + def initialize(api_key, jws_private_key: nil) @api_key = api_key - @user_agent = "fintoc-ruby/#{Fintoc::VERSION}" - @headers = { Authorization: @api_key, 'User-Agent': @user_agent } - @link_headers = nil - @link_header_pattern = '<(?.*)>;\s*rel="(?.*)"' - @default_params = {} - end - - def get - request('get') - end - - def delete - request('delete') + @jws_private_key = jws_private_key end - def request(method) - proc do |resource, **kwargs| - parameters = params(method, **kwargs) - response = make_request(method, resource, parameters) - content = JSON.parse(response.body, symbolize_names: true) - - if response.status.client_error? || response.status.server_error? - raise_custom_error(content[:error]) - end - - @link_headers = response.headers.get('link') - content - end + def v1 + @v1 ||= Fintoc::V1::Client.new(@api_key) end - def fetch_next - next_ = link_headers['next'] - Enumerator.new do |yielder| - while next_ - yielder << get.call(next_) - next_ = link_headers['next'] - end - end + def v2 + @v2 ||= Fintoc::V2::Client.new(@api_key, jws_private_key: @jws_private_key) end + # These methods are kept for backward compatibility def get_link(link_token) - data = { **_get_link(link_token), link_token: link_token } - build_link(data) + @v1.links.get(link_token) end def get_links - _get_links.map { |data| build_link(data) } + @v1.links.list end def delete_link(link_id) - delete.call("links/#{link_id}") + @v1.links.delete(link_id) end def get_account(link_token, account_id) - get_link(link_token).find(id: account_id) + @v1.links.get(link_token).find(id: account_id) end def to_s - visible_chars = 4 - hidden_part = '*' * (@api_key.size - visible_chars) - visible_key = @api_key.slice(0, visible_chars) - "Client(πŸ”‘=#{hidden_part + visible_key}" - end - - private - - def client - @client ||= HTTP.headers(@headers) - end - - def parse_headers(dict, link) - matches = link.strip.match(@link_header_pattern) - dict[matches[:rel]] = matches[:url] - dict - end - - def _get_link(link_token) - get.call("links/#{link_token}") - end - - def _get_links - get.call('links') - end - - def build_link(data) - param = Utils.pick(data, 'link_token') - @default_params.update(param) - Link.new(**data, client: self) - end - - def make_request(method, resource, parameters) - # this is to handle url returned in the link headers - # I'm sure there is a better and more clever way to solve this - if resource.start_with? 'https' - client.send(method, resource) - else - url = "#{Fintoc::Constants::SCHEME}#{Fintoc::Constants::BASE_URL}#{resource}" - client.send(method, url, parameters) - end - end - - def params(method, **kwargs) - if method == 'get' - { params: { **@default_params, **kwargs } } - else - { json: { **@default_params, **kwargs } } - end - end - - def raise_custom_error(error) - raise error_class(error[:code]).new(error[:message], error[:doc_url]) - end - - def error_class(snake_code) - pascal_klass_name = Utils.snake_to_pascal(snake_code) - # this conditional klass_name is to handle InternalServerError custom error class - # without this the error class name would be like InternalServerErrorError (^-^) - klass = pascal_klass_name.end_with?('Error') ? pascal_klass_name : "#{pascal_klass_name}Error" - Module.const_get("Fintoc::Errors::#{klass}") - end - - # rubocop:disable Layout/LineLength - # This attribute getter parses the link headers using some regex 24K magic in the air... - # Ex. - # ; rel="first", ; rel="last" - # this helps to handle pagination see: https://fintoc.com/docs#paginacion - # return a hash like { first:"https://api.fintoc.com/v1/links?page=1" } - # - # @param link_headers [String] - # @return [Hash] - # rubocop:enable Layout/LineLength - def link_headers - return if @link_headers.nil? - - @link_headers[0].split(',').reduce({}) { |dict, link| parse_headers(dict, link) } + "Fintoc::Client(v1: #{@v1}, v2: #{@v2})" end end end diff --git a/lib/fintoc/constants.rb b/lib/fintoc/constants.rb index 5e27a62..fc7e811 100644 --- a/lib/fintoc/constants.rb +++ b/lib/fintoc/constants.rb @@ -1,8 +1,9 @@ module Fintoc module Constants FIELDSUBS = [%w[id id_], %w[type type_]].freeze - GENERAL_DOC_URL = 'https://fintoc.com/docs' + GENERAL_DOC_URL = 'https://docs.fintoc.com/reference/errors' SCHEME = 'https://' BASE_URL = 'api.fintoc.com/v1/' + BASE_URL_V2 = 'api.fintoc.com/v2/' end end diff --git a/lib/fintoc/errors.rb b/lib/fintoc/errors.rb index b928ddd..9470109 100644 --- a/lib/fintoc/errors.rb +++ b/lib/fintoc/errors.rb @@ -13,29 +13,150 @@ def initialize(message, doc_url = Fintoc::Constants::GENERAL_DOC_URL) def message "\n#{@message}\n Please check the docs at: #{@doc_url}" end - - def to_s - message - end end + # 400 Bad Request Errors class InvalidRequestError < FintocError; end - class LinkError < FintocError; end - class AuthenticationError < FintocError; end - class InstitutionError < FintocError; end - class ApiError < FintocError; end - class MissingResourceError < FintocError; end - class InvalidLinkTokenError < FintocError; end - class InvalidUsernameError < FintocError; end - class InvalidHolderTypeError < FintocError; end + class InvalidCurrencyError < FintocError; end + class InvalidAmountError < FintocError; end + class InvalidAccountTypeError < FintocError; end + class InvalidAccountNumberError < FintocError; end + class InvalidAccountStatusError < FintocError; end + class InvalidAccountBalanceError < FintocError; end + class InvalidInstitutionIdError < FintocError; end + class CurrencyMismatchError < FintocError; end + class InvalidCommentSizeError < FintocError; end + class InvalidReferenceIdSizeError < FintocError; end class MissingParameterError < FintocError; end + class InvalidPositiveIntegerError < FintocError; end class EmptyStringError < FintocError; end - class UnrecognizedRequestError < FintocError; end + class InvalidStringSizeError < FintocError; end + class InvalidHashError < FintocError; end + class InvalidBooleanError < FintocError; end + class InvalidArrayError < FintocError; end + class InvalidIntegerError < FintocError; end + class InvalidJsonError < FintocError; end + class InvalidParamsError < FintocError; end + class MissingCursorError < FintocError; end + class InvalidEnumError < FintocError; end + class InvalidStringError < FintocError; end + class InvalidUsernameError < FintocError; end + class InvalidLinkTokenError < FintocError; end class InvalidDateError < FintocError; end - class InvalidCredentialsError < FintocError; end - class LockedCredentialsError < FintocError; end + class InvalidHolderIdError < FintocError; end + class InvalidCardNumberError < FintocError; end + class InvalidProductError < FintocError; end + class InvalidWebhookSubscriptionError < FintocError; end + class InvalidIssueTypeError < FintocError; end + class InvalidRefreshTypeError < FintocError; end + class InvalidBusinessProfileTaxIdError < FintocError; end + class InvalidSessionHolderIdError < FintocError; end + class InvalidPaymentRecipientAccountError < FintocError; end + class InvalidPayoutRecipientAccountError < FintocError; end + class InvalidWidgetTokenError < FintocError; end + class InvalidPaymentReferenceNumberError < FintocError; end + class InvalidOnDemandLinkError < FintocError; end + class InvalidHolderTypeError < FintocError; end + class InvalidVoucherDownloadError < FintocError; end + class InvalidModeError < FintocError; end + class InvalidRsaKeyError < FintocError; end + class ExpectedPublicRsaKeyError < FintocError; end + class InvalidCidrBlockError < FintocError; end + class InvalidExpiresAtError < FintocError; end + class InvalidInstallmentsCurrencyError < FintocError; end + class InvalidClabeError < FintocError; end + class MismatchTransferAccountCurrencyError < FintocError; end + + # 401 Unauthorized Errors + class AuthenticationError < FintocError; end class InvalidApiKeyError < FintocError; end + class ExpiredApiKeyError < FintocError; end + class InvalidApiKeyModeError < FintocError; end + class ExpiredExchangeTokenError < FintocError; end + class InvalidExchangeTokenError < FintocError; end + class MissingActiveJwsPublicKeyError < FintocError; end + class InvalidJwsSignatureAlgorithmError < FintocError; end + class InvalidJwsSignatureHeaderError < FintocError; end + class InvalidJwsSignatureNonceError < FintocError; end + class InvalidJwsSignatureTimestampError < FintocError; end + class InvalidJwsSignatureTimestampFormatError < FintocError; end + class InvalidJwsSignatureTimestampValueError < FintocError; end + class MissingJwsSignatureHeaderError < FintocError; end + class JwsNonceAlreadyUsedError < FintocError; end + class InvalidJwsTsError < FintocError; end + + # 402 Payment Required Errors + class PaymentRequiredError < FintocError; end + + # 403 Forbidden Errors + class InvalidAccountError < FintocError; end + class InvalidRecipientAccountError < FintocError; end + class AccountNotActiveError < FintocError; end + class EntityNotOperationalError < FintocError; end + class ForbiddenEntityError < FintocError; end + class ForbiddenAccountError < FintocError; end + class ForbiddenAccountNumberError < FintocError; end + class ForbiddenAccountVerificationError < FintocError; end + class InvalidApiVersionError < FintocError; end + class ProductAccessRequiredError < FintocError; end + class ForbiddenRequestError < FintocError; end + class MissingAllowedCidrBlocksError < FintocError; end + class AllowedCidrBlocksDoesNotContainIpError < FintocError; end + class RecipientBlockedAccountError < FintocError; end + + # 404 Not Found Errors + class MissingResourceError < FintocError; end + class InvalidUrlError < FintocError; end + class OrganizationWithoutEntitiesError < FintocError; end + + # 405 Method Not Allowed Errors + class OperationNotAllowedError < FintocError; end + + # 406 Not Acceptable Errors + class InstitutionCredentialsInvalidError < FintocError; end + class LockedCredentialsError < FintocError; end class UnavailableInstitutionError < FintocError; end + + # 409 Conflict Errors + class InsufficientBalanceError < FintocError; end + class InvalidDuplicatedTransferError < FintocError; end + class InvalidTransferStatusError < FintocError; end + class InvalidTransferDirectionError < FintocError; end + class AccountNumberLimitReachedError < FintocError; end + class AccountCannotBeBlockedError < FintocError; end + + # 422 Unprocessable Entity Errors + class InvalidOtpCodeError < FintocError; end + class OtpNotFoundError < FintocError; end + class OtpBlockedError < FintocError; end + class OtpVerificationFailedError < FintocError; end + class OtpAlreadyExistsError < FintocError; end + class SubscriptionInProgressError < FintocError; end + class OnDemandPolicyRequiredError < FintocError; end + class OnDemandRefreshUnavailableError < FintocError; end + class NotSupportedCountryError < FintocError; end + class NotSupportedCurrencyError < FintocError; end + class NotSupportedModeError < FintocError; end + class NotSupportedProductError < FintocError; end + class RefreshIntentInProgressError < FintocError; end + class RejectedRefreshIntentError < FintocError; end + class SenderBlockedAccountError < FintocError; end + + # 429 Too Many Requests Errors + class RateLimitExceededError < FintocError; end + + # 500 Internal Server Errors class InternalServerError < FintocError; end + class UnrecognizedRequestError < FintocError; end + class CoreResponseError < FintocError; end + + # Webhook Errors + class WebhookSignatureError < FintocError; end + + # Legacy Errors (keeping existing ones for backward compatibility and just in case) + class LinkError < FintocError; end + class InstitutionError < FintocError; end + class ApiError < FintocError; end + class InvalidCredentialsError < FintocError; end end end diff --git a/lib/fintoc/jws.rb b/lib/fintoc/jws.rb new file mode 100644 index 0000000..9110ccd --- /dev/null +++ b/lib/fintoc/jws.rb @@ -0,0 +1,83 @@ +require 'openssl' +require 'json' +require 'base64' +require 'securerandom' + +module Fintoc + class JWS + def initialize(private_key) + unless private_key.is_a?(OpenSSL::PKey::RSA) + raise ArgumentError, 'private_key must be an OpenSSL::PKey::RSA instance' + end + + @private_key = private_key + end + + def generate_signature(raw_body) + body_string = raw_body.is_a?(Hash) ? raw_body.to_json : raw_body.to_s + + headers = { + alg: 'RS256', + nonce: SecureRandom.hex(16), + ts: Time.now.to_i, + crit: %w[ts nonce] + } + + protected_base64 = base64url_encode(headers.to_json) + payload_base64 = base64url_encode(body_string) + signing_input = "#{protected_base64}.#{payload_base64}" + + signature = @private_key.sign(OpenSSL::Digest.new('SHA256'), signing_input) + signature_base64 = base64url_encode(signature) + + "#{protected_base64}.#{signature_base64}" + end + + def parse_signature(signature) + protected_b64, signature_b64 = signature.split('.') + + { + protected_headers: decode_protected_headers(protected_b64), + signature_bytes: decode_signature(signature_b64), + protected_b64: protected_b64, + signature_b64: signature_b64 + } + end + + def verify_signature(signature, payload) + parsed = parse_signature(signature) + + # Reconstruct the signing input + payload_json = payload.is_a?(Hash) ? payload.to_json : payload.to_s + payload_b64 = base64url_encode(payload_json) + signing_input = "#{parsed[:protected_b64]}.#{payload_b64}" + + # Verify with public key + public_key = @private_key.public_key + public_key.verify(OpenSSL::Digest.new('SHA256'), parsed[:signature_bytes], signing_input) + end + + private + + def decode_protected_headers(protected_b64) + padded = add_padding(protected_b64) + + protected_json = Base64.urlsafe_decode64(padded) + JSON.parse(protected_json, symbolize_names: true) + end + + def decode_signature(signature_b64) + padded = add_padding(signature_b64) + + Base64.urlsafe_decode64(padded) + end + + def add_padding(b64) + (b64.length % 4).zero? ? b64 : (b64 + ('=' * (4 - (b64.length % 4)))) + end + + def base64url_encode(data) + Base64.urlsafe_encode64(data).tr('=', '') + end + end +end diff --git a/lib/fintoc/resources/account.rb b/lib/fintoc/resources/account.rb deleted file mode 100644 index db7b5de..0000000 --- a/lib/fintoc/resources/account.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'tabulate' -require 'fintoc/utils' -require 'fintoc/resources/movement' -require 'fintoc/resources/balance' - -module Fintoc - class Account - include Utils - - attr_reader :id, :name, :holder_name, :currency, :type, :refreshed_at, - :official_name, :number, :holder_id, :balance, :movements - - def initialize( - id:, - name:, - official_name:, - number:, - holder_id:, - holder_name:, - type:, - currency:, - refreshed_at: nil, - balance: nil, - movements: nil, - client: nil, - ** - ) - @id = id - @name = name - @official_name = official_name - @number = number - @holder_id = holder_id - @holder_name = holder_name - @type = type - @currency = currency - @refreshed_at = DateTime.iso8601(refreshed_at) if refreshed_at - @balance = Fintoc::Balance.new(**balance) - @movements = movements || [] - @client = client - end - - def update_balance - @balance = Fintoc::Balance.new(**get_account[:balance]) - end - - def get_movements(**params) - _get_movements(**params).lazy.map { |movement| Fintoc::Movement.new(**movement) } - end - - def update_movements(**params) - @movements += get_movements(**params).to_a - @movements = @movements.uniq.sort_by(&:post_date) - end - - def show_movements(rows = 5) - puts("This account has #{Utils.pluralize(@movements.size, 'movement')}.") - - return unless @movements.any? - - movements = - @movements - .to_a - .slice(0, rows) - .map.with_index do |mov, index| - [index + 1, mov.amount, mov.currency, mov.description, mov.locale_date] - end - headers = ['#', 'Amount', 'Currency', 'Description', 'Date'] - puts - puts tabulate(headers, movements, indent: 4, style: 'fancy') - end - - def to_s - "πŸ’° #{@holder_name}’s #{@name} #{@balance}" - end - - private - - def get_account - @client.get.call("accounts/#{@id}") - end - - def _get_movements(**params) - first = @client.get.call("accounts/#{@id}/movements", **params) - return first if params.empty? - - first + Utils.flatten(@client.fetch_next) - end - end -end diff --git a/lib/fintoc/resources/balance.rb b/lib/fintoc/resources/balance.rb deleted file mode 100644 index 33f49e9..0000000 --- a/lib/fintoc/resources/balance.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Fintoc - class Balance - attr_reader :available, :current, :limit - - def initialize(available:, current:, limit:) - @available = available - @current = current - @limit = limit - end - - def id - object_id - end - - def to_s - "#{@available} (#{@current})" - end - - def inspect - "" - end - end -end diff --git a/lib/fintoc/resources/institution.rb b/lib/fintoc/resources/institution.rb deleted file mode 100644 index dd4bb01..0000000 --- a/lib/fintoc/resources/institution.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Fintoc - class Institution - attr_reader :id, :name, :country - - def initialize(id:, name:, country:, **) - @id = id - @name = name - @country = country - end - - def to_s - "🏦 #{@name}" - end - - def inspect - "" - end - end -end diff --git a/lib/fintoc/resources/link.rb b/lib/fintoc/resources/link.rb deleted file mode 100644 index a507487..0000000 --- a/lib/fintoc/resources/link.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'date' -require 'tabulate' -require 'fintoc/utils' -require 'fintoc/resources/account' -require 'fintoc/resources/institution' - -module Fintoc - class Link - attr_reader :id, :username, :holder_type, :institution, :created_at, :mode, - :accounts, :link_token - - include Utils - - def initialize( - id:, - username:, - holder_type:, - institution:, - created_at:, - mode:, - accounts: nil, - link_token: nil, - client: nil, - ** - ) - @id = id - @username = username - @holder_type = holder_type - @institution = Fintoc::Institution.new(**institution) - @created_at = Date.iso8601(created_at) - @mode = mode - @accounts = if accounts.nil? - [] - else - accounts.map { |data| Fintoc::Account.new(**data, client: client) } - end - @token = link_token - @client = client - end - - def find_all(**kwargs) - raise 'You must provide *exactly one* account field.' if kwargs.size != 1 - - field, value = kwargs.to_a.first - @accounts.select do |account| - account.send(field.to_sym) == value - end - end - - def find(**) - results = find_all(**) - results.any? ? results.first : nil - end - - def show_accounts(rows = 5) - puts "This links has #{Utils.pluralize(@accounts.size, 'account')}" - return unless @accounts.any? - - accounts = @accounts.to_a.slice(0, rows) - .map.with_index do |acc, index| - [index + 1, acc.name, acc.holder_name, acc.currency] - end - headers = ['#', 'Name', 'Holder', 'Currency'] - puts - puts tabulate(headers, accounts, indent: 4, style: 'fancy') - end - - def update_accounts - @accounts.each do |account| - account.update_balance - account.update_movements - end - end - - def delete - @client.delete_link(@id) - end - - def to_s - "<#{@username}@#{@institution.name}> πŸ”— " - end - end -end diff --git a/lib/fintoc/resources/movement.rb b/lib/fintoc/resources/movement.rb deleted file mode 100644 index 02ee00e..0000000 --- a/lib/fintoc/resources/movement.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'date' -require 'fintoc/resources/transfer_account' - -module Fintoc - class Movement - attr_reader :id, :amount, :currency, :description, :reference_id, - :post_date, :transaction_date, :type, :recipient_account, - :sender_account, :account, :comment - - def initialize( - id:, - amount:, - currency:, - description:, - post_date:, - transaction_date:, - type:, - reference_id:, - recipient_account:, - sender_account:, - comment:, - ** - ) - @id = id - @amount = amount - @currency = currency - @description = description - @post_date = DateTime.iso8601(post_date) - @transaction_date = DateTime.iso8601(transaction_date) if transaction_date - @type = type - @reference_id = reference_id - @recipient_account = Fintoc::TransferAccount.new(**recipient_account) if recipient_account - @sender_account = Fintoc::TransferAccount.new(**sender_account) if sender_account - @comment = comment - end - - def ==(other) - @id = other.id - end - - alias eql? == - - def hash - @id.hash - end - - def locale_date - @post_date.strftime('%x') - end - - def to_s - "#{@amount} (#{@description} @ #{locale_date})" - end - end -end diff --git a/lib/fintoc/resources/transfer_account.rb b/lib/fintoc/resources/transfer_account.rb deleted file mode 100644 index 804daa8..0000000 --- a/lib/fintoc/resources/transfer_account.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'fintoc/resources/institution' - -module Fintoc - class TransferAccount - attr_reader :holder_id, :holder_name, :number, :institution - - def initialize(holder_id:, holder_name:, number:, institution:, **) - @holder_id = holder_id - @holder_name = holder_name - @number = number - @institution = institution && Fintoc::Institution.new(**institution) - end - - def id - object_id - end - - def to_s - @holder_id.to_s - end - end -end diff --git a/lib/fintoc/v1/client/client.rb b/lib/fintoc/v1/client/client.rb new file mode 100644 index 0000000..b9c8c37 --- /dev/null +++ b/lib/fintoc/v1/client/client.rb @@ -0,0 +1,12 @@ +require 'fintoc/base_client' +require 'fintoc/v1/managers/links_manager' + +module Fintoc + module V1 + class Client < BaseClient + def links + @links ||= Managers::LinksManager.new(self) + end + end + end +end diff --git a/lib/fintoc/v1/managers/links_manager.rb b/lib/fintoc/v1/managers/links_manager.rb new file mode 100644 index 0000000..e7c53d3 --- /dev/null +++ b/lib/fintoc/v1/managers/links_manager.rb @@ -0,0 +1,46 @@ +require 'fintoc/v1/resources/link' + +module Fintoc + module V1 + module Managers + class LinksManager + def initialize(client) + @client = client + end + + def get(link_token) + data = { **_get_link(link_token), link_token: link_token } + build_link(data) + end + + def list + _get_links.map { |data| build_link(data) } + end + + def delete(link_id) + _delete_link(link_id) + end + + private + + def _get_link(link_token) + @client.get(version: :v1).call("links/#{link_token}") + end + + def _get_links + @client.get(version: :v1).call('links') + end + + def _delete_link(link_id) + @client.delete(version: :v1).call("links/#{link_id}") + end + + def build_link(data) + param = Utils.pick(data, 'link_token') + @client.default_params.update(param) + Fintoc::V1::Link.new(**data, client: @client) + end + end + end + end +end diff --git a/lib/fintoc/v1/resources/account.rb b/lib/fintoc/v1/resources/account.rb new file mode 100644 index 0000000..ec9ddb1 --- /dev/null +++ b/lib/fintoc/v1/resources/account.rb @@ -0,0 +1,95 @@ +require 'tabulate' +require 'fintoc/utils' +require 'fintoc/v1/resources/movement' +require 'fintoc/v1/resources/balance' + +module Fintoc + module V1 + class Account + include Utils + + attr_reader :id, :name, :holder_name, :currency, :type, :refreshed_at, + :official_name, :number, :holder_id, :balance, :movements + + HEADERS = ['#', 'Amount', 'Currency', 'Description', 'Date'].freeze + + def initialize( + id:, + name:, + official_name:, + number:, + holder_id:, + holder_name:, + type:, + currency:, + refreshed_at: nil, + balance: nil, + movements: nil, + client: nil, + ** + ) + @id = id + @name = name + @official_name = official_name + @number = number + @holder_id = holder_id + @holder_name = holder_name + @type = type + @currency = currency + @refreshed_at = DateTime.iso8601(refreshed_at) if refreshed_at + @balance = Fintoc::V1::Balance.new(**balance) + @movements = movements || [] + @client = client + end + + def update_balance + @balance = Fintoc::V1::Balance.new(**get_account[:balance]) + end + + def get_movements(**params) + _get_movements(**params).lazy.map do + |movement| Fintoc::V1::Movement.new(**movement, client: @client) + end + end + + def update_movements(**params) + @movements += get_movements(**params).to_a + @movements = @movements.uniq.sort_by(&:post_date) + end + + def show_movements(rows = 5) + puts("This account has #{Utils.pluralize(@movements.size, 'movement')}.") + + return unless @movements.any? + + movements = + @movements + .to_a + .slice(0, rows) + .map.with_index do |mov, index| + [index + 1, mov.amount, mov.currency, mov.description, mov.locale_date] + end + + puts + puts tabulate(HEADERS, movements, indent: 4, style: 'fancy') + end + + def to_s + "πŸ’° #{@holder_name}’s #{@name} #{@balance}" + end + + private + + def get_account + @client.get(version: :v1).call("accounts/#{@id}") + end + + def _get_movements(**params) + first = @client.get(version: :v1).call("accounts/#{@id}/movements", **params) + return first if params.empty? + + first + Utils.flatten(@client.fetch_next) + end + end + end +end diff --git a/lib/fintoc/v1/resources/balance.rb b/lib/fintoc/v1/resources/balance.rb new file mode 100644 index 0000000..4811a77 --- /dev/null +++ b/lib/fintoc/v1/resources/balance.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Fintoc + module V1 + class Balance + attr_reader :available, :current, :limit + + def initialize(available:, current:, limit:) + @available = available + @current = current + @limit = limit + end + + def id + object_id + end + + def to_s + "#{@available} (#{@current})" + end + + def inspect + "" + end + end + end +end diff --git a/lib/fintoc/v1/resources/institution.rb b/lib/fintoc/v1/resources/institution.rb new file mode 100644 index 0000000..b4b6737 --- /dev/null +++ b/lib/fintoc/v1/resources/institution.rb @@ -0,0 +1,21 @@ +module Fintoc + module V1 + class Institution + attr_reader :id, :name, :country + + def initialize(id:, name:, country:, **) + @id = id + @name = name + @country = country + end + + def to_s + "🏦 #{@name}" + end + + def inspect + "" + end + end + end +end diff --git a/lib/fintoc/v1/resources/link.rb b/lib/fintoc/v1/resources/link.rb new file mode 100644 index 0000000..78f9966 --- /dev/null +++ b/lib/fintoc/v1/resources/link.rb @@ -0,0 +1,85 @@ +require 'date' +require 'tabulate' +require 'fintoc/utils' +require 'fintoc/v1/resources/account' +require 'fintoc/v1/resources/institution' + +module Fintoc + module V1 + class Link + attr_reader :id, :username, :holder_type, :institution, :created_at, :mode, + :accounts, :link_token + + include Utils + + def initialize( + id:, + username:, + holder_type:, + institution:, + created_at:, + mode:, + accounts: nil, + link_token: nil, + client: nil, + ** + ) + @id = id + @username = username + @holder_type = holder_type + @institution = Fintoc::V1::Institution.new(**institution) + @created_at = Date.iso8601(created_at) + @mode = mode + @accounts = if accounts.nil? + [] + else + accounts.map { |data| Fintoc::V1::Account.new(**data, client:) } + end + @token = link_token + @client = client + end + + def find_all(**kwargs) + raise 'You must provide *exactly one* account field.' if kwargs.size != 1 + + field, value = kwargs.to_a.first + @accounts.select do |account| + account.send(field.to_sym) == value + end + end + + def find(**) + results = find_all(**) + results.any? ? results.first : nil + end + + def show_accounts(rows = 5) + puts "This links has #{Utils.pluralize(@accounts.size, 'account')}" + return unless @accounts.any? + + accounts = @accounts.to_a.slice(0, rows) + .map.with_index do |acc, index| + [index + 1, acc.name, acc.holder_name, acc.currency] + end + headers = ['#', 'Name', 'Holder', 'Currency'] + puts + puts tabulate(headers, accounts, indent: 4, style: 'fancy') + end + + def update_accounts + @accounts.each do |account| + account.update_balance + account.update_movements + end + end + + def delete + @client.links.delete(@id) + end + + def to_s + "<#{@username}@#{@institution.name}> πŸ”— " + end + end + end +end diff --git a/lib/fintoc/v1/resources/movement.rb b/lib/fintoc/v1/resources/movement.rb new file mode 100644 index 0000000..f752858 --- /dev/null +++ b/lib/fintoc/v1/resources/movement.rb @@ -0,0 +1,62 @@ +require 'date' +require 'fintoc/v1/resources/transfer_account' + +module Fintoc + module V1 + class Movement + attr_reader :id, :amount, :currency, :description, :reference_id, + :post_date, :transaction_date, :type, :recipient_account, + :sender_account, :account, :comment + + def initialize( + id:, + amount:, + currency:, + description:, + post_date:, + transaction_date:, + type:, + reference_id:, + recipient_account:, + sender_account:, + comment:, + client: nil, + ** + ) + @id = id + @amount = amount + @currency = currency + @description = description + @post_date = DateTime.iso8601(post_date) + @transaction_date = DateTime.iso8601(transaction_date) if transaction_date + @type = type + @reference_id = reference_id + @recipient_account = + if recipient_account + Fintoc::V1::TransferAccount.new(**recipient_account) + end + @sender_account = Fintoc::V1::TransferAccount.new(**sender_account) if sender_account + @comment = comment + @client = client + end + + def ==(other) + @id == other.id + end + + alias eql? == + + def hash + @id.hash + end + + def locale_date + @post_date.strftime('%x') + end + + def to_s + "#{@amount} (#{@description} @ #{locale_date})" + end + end + end +end diff --git a/lib/fintoc/v1/resources/transfer_account.rb b/lib/fintoc/v1/resources/transfer_account.rb new file mode 100644 index 0000000..ff11d69 --- /dev/null +++ b/lib/fintoc/v1/resources/transfer_account.rb @@ -0,0 +1,24 @@ +require 'fintoc/v1/resources/institution' + +module Fintoc + module V1 + class TransferAccount + attr_reader :holder_id, :holder_name, :number, :institution + + def initialize(holder_id:, holder_name:, number:, institution:, **) + @holder_id = holder_id + @holder_name = holder_name + @number = number + @institution = institution && Fintoc::V1::Institution.new(**institution) + end + + def id + object_id + end + + def to_s + @holder_id.to_s + end + end + end +end diff --git a/lib/fintoc/v2/client/client.rb b/lib/fintoc/v2/client/client.rb new file mode 100644 index 0000000..7e17cb8 --- /dev/null +++ b/lib/fintoc/v2/client/client.rb @@ -0,0 +1,37 @@ +require 'fintoc/base_client' +require 'fintoc/v2/managers/entities_manager' +require 'fintoc/v2/managers/accounts_manager' +require 'fintoc/v2/managers/account_numbers_manager' +require 'fintoc/v2/managers/transfers_manager' +require 'fintoc/v2/managers/simulate_manager' +require 'fintoc/v2/managers/account_verifications_manager' + +module Fintoc + module V2 + class Client < BaseClient + def entities + @entities ||= Managers::EntitiesManager.new(self) + end + + def accounts + @accounts ||= Managers::AccountsManager.new(self) + end + + def account_numbers + @account_numbers ||= Managers::AccountNumbersManager.new(self) + end + + def transfers + @transfers ||= Managers::TransfersManager.new(self) + end + + def simulate + @simulate ||= Managers::SimulateManager.new(self) + end + + def account_verifications + @account_verifications ||= Managers::AccountVerificationsManager.new(self) + end + end + end +end diff --git a/lib/fintoc/v2/managers/account_numbers_manager.rb b/lib/fintoc/v2/managers/account_numbers_manager.rb new file mode 100644 index 0000000..58ec4e7 --- /dev/null +++ b/lib/fintoc/v2/managers/account_numbers_manager.rb @@ -0,0 +1,59 @@ +require 'fintoc/v2/resources/account_number' + +module Fintoc + module V2 + module Managers + class AccountNumbersManager + def initialize(client) + @client = client + end + + def create(account_id:, description: nil, metadata: nil, **params) + data = _create_account_number(account_id:, description:, metadata:, **params) + build_account_number(data) + end + + def get(account_number_id) + data = _get_account_number(account_number_id) + build_account_number(data) + end + + def list(**params) + _list_account_numbers(**params).map { |data| build_account_number(data) } + end + + def update(account_number_id, **params) + data = _update_account_number(account_number_id, **params) + build_account_number(data) + end + + private + + def _create_account_number(account_id:, description: nil, metadata: nil, **params) + request_params = { account_id: } + request_params[:description] = description if description + request_params[:metadata] = metadata if metadata + request_params.merge!(params) + + @client.post(version: :v2).call('account_numbers', **request_params) + end + + def _get_account_number(account_number_id) + @client.get(version: :v2).call("account_numbers/#{account_number_id}") + end + + def _list_account_numbers(**params) + @client.get(version: :v2).call('account_numbers', **params) + end + + def _update_account_number(account_number_id, **params) + @client.patch(version: :v2).call("account_numbers/#{account_number_id}", **params) + end + + def build_account_number(data) + Fintoc::V2::AccountNumber.new(**data, client: @client) + end + end + end + end +end diff --git a/lib/fintoc/v2/managers/account_verifications_manager.rb b/lib/fintoc/v2/managers/account_verifications_manager.rb new file mode 100644 index 0000000..ffd8e15 --- /dev/null +++ b/lib/fintoc/v2/managers/account_verifications_manager.rb @@ -0,0 +1,45 @@ +require 'fintoc/v2/resources/account_verification' + +module Fintoc + module V2 + module Managers + class AccountVerificationsManager + def initialize(client) + @client = client + end + + def create(account_number:) + data = _create_account_verification(account_number:) + build_account_verification(data) + end + + def get(account_verification_id) + data = _get_account_verification(account_verification_id) + build_account_verification(data) + end + + def list(**params) + _list_account_verifications(**params).map { |data| build_account_verification(data) } + end + + private + + def _create_account_verification(account_number:) + @client.post(version: :v2, use_jws: true).call('account_verifications', account_number:) + end + + def _get_account_verification(account_verification_id) + @client.get(version: :v2).call("account_verifications/#{account_verification_id}") + end + + def _list_account_verifications(**params) + @client.get(version: :v2).call('account_verifications', **params) + end + + def build_account_verification(data) + Fintoc::V2::AccountVerification.new(**data, client: @client) + end + end + end + end +end diff --git a/lib/fintoc/v2/managers/accounts_manager.rb b/lib/fintoc/v2/managers/accounts_manager.rb new file mode 100644 index 0000000..6761f79 --- /dev/null +++ b/lib/fintoc/v2/managers/accounts_manager.rb @@ -0,0 +1,54 @@ +require 'fintoc/v2/resources/account' + +module Fintoc + module V2 + module Managers + class AccountsManager + def initialize(client) + @client = client + end + + def create(entity_id:, description:, **params) + data = _create_account(entity_id:, description:, **params) + build_account(data) + end + + def get(account_id) + data = _get_account(account_id) + build_account(data) + end + + def list(**params) + _list_accounts(**params).map { |data| build_account(data) } + end + + def update(account_id, **params) + data = _update_account(account_id, **params) + build_account(data) + end + + private + + def _create_account(entity_id:, description:, **params) + @client.post(version: :v2).call('accounts', entity_id:, description:, **params) + end + + def _get_account(account_id) + @client.get(version: :v2).call("accounts/#{account_id}") + end + + def _list_accounts(**params) + @client.get(version: :v2).call('accounts', **params) + end + + def _update_account(account_id, **params) + @client.patch(version: :v2).call("accounts/#{account_id}", **params) + end + + def build_account(data) + Fintoc::V2::Account.new(**data, client: @client) + end + end + end + end +end diff --git a/lib/fintoc/v2/managers/entities_manager.rb b/lib/fintoc/v2/managers/entities_manager.rb new file mode 100644 index 0000000..4cd0071 --- /dev/null +++ b/lib/fintoc/v2/managers/entities_manager.rb @@ -0,0 +1,36 @@ +require 'fintoc/v2/resources/entity' + +module Fintoc + module V2 + module Managers + class EntitiesManager + def initialize(client) + @client = client + end + + def get(entity_id) + data = _get_entity(entity_id) + build_entity(data) + end + + def list(**params) + _list_entities(**params).map { |data| build_entity(data) } + end + + private + + def _get_entity(entity_id) + @client.get(version: :v2).call("entities/#{entity_id}") + end + + def _list_entities(**params) + @client.get(version: :v2).call('entities', **params) + end + + def build_entity(data) + Fintoc::V2::Entity.new(**data, client: @client) + end + end + end + end +end diff --git a/lib/fintoc/v2/managers/simulate_manager.rb b/lib/fintoc/v2/managers/simulate_manager.rb new file mode 100644 index 0000000..b9bbd03 --- /dev/null +++ b/lib/fintoc/v2/managers/simulate_manager.rb @@ -0,0 +1,30 @@ +require 'fintoc/v2/resources/transfer' + +module Fintoc + module V2 + module Managers + class SimulateManager + def initialize(client) + @client = client + end + + def receive_transfer(account_number_id:, amount:, currency:) + data = _simulate_receive_transfer(account_number_id:, amount:, currency:) + build_transfer(data) + end + + private + + def _simulate_receive_transfer(account_number_id:, amount:, currency:) + @client + .post(version: :v2) + .call('simulate/receive_transfer', account_number_id:, amount:, currency:) + end + + def build_transfer(data) + Fintoc::V2::Transfer.new(**data, client: @client) + end + end + end + end +end diff --git a/lib/fintoc/v2/managers/transfers_manager.rb b/lib/fintoc/v2/managers/transfers_manager.rb new file mode 100644 index 0000000..adb5a5c --- /dev/null +++ b/lib/fintoc/v2/managers/transfers_manager.rb @@ -0,0 +1,56 @@ +require 'fintoc/v2/resources/transfer' + +module Fintoc + module V2 + module Managers + class TransfersManager + def initialize(client) + @client = client + end + + def create(amount:, currency:, account_id:, counterparty:, **params) + data = _create_transfer(amount:, currency:, account_id:, counterparty:, **params) + build_transfer(data) + end + + def get(transfer_id) + data = _get_transfer(transfer_id) + build_transfer(data) + end + + def list(**params) + _list_transfers(**params).map { |data| build_transfer(data) } + end + + def return(transfer_id) + data = _return_transfer(transfer_id) + build_transfer(data) + end + + private + + def _create_transfer(amount:, currency:, account_id:, counterparty:, **params) + @client + .post(version: :v2, use_jws: true) + .call('transfers', amount:, currency:, account_id:, counterparty:, **params) + end + + def _get_transfer(transfer_id) + @client.get(version: :v2).call("transfers/#{transfer_id}") + end + + def _list_transfers(**params) + @client.get(version: :v2).call('transfers', **params) + end + + def _return_transfer(transfer_id) + @client.post(version: :v2, use_jws: true).call('transfers/return', transfer_id:) + end + + def build_transfer(data) + Fintoc::V2::Transfer.new(**data, client: @client) + end + end + end + end +end diff --git a/lib/fintoc/v2/resources/account.rb b/lib/fintoc/v2/resources/account.rb new file mode 100644 index 0000000..9601a71 --- /dev/null +++ b/lib/fintoc/v2/resources/account.rb @@ -0,0 +1,105 @@ +require 'money' + +module Fintoc + module V2 + class Account + attr_reader :id, :object, :mode, :description, :available_balance, :currency, + :is_root, :root_account_number_id, :root_account_number, :status, :entity + + def initialize( + id:, + object:, + mode:, + description:, + available_balance:, + currency:, + is_root:, + root_account_number_id:, + root_account_number:, + status:, + entity:, + client: nil, + ** + ) + @id = id + @object = object + @mode = mode + @description = description + @available_balance = available_balance + @currency = currency + @is_root = is_root + @root_account_number_id = root_account_number_id + @root_account_number = root_account_number + @status = status + @entity = entity + @client = client + end + + def to_s + "πŸ’° #{@description} (#{@id}) - #{Money.from_cents(@available_balance, @currency).format}" + end + + def refresh + fresh_account = @client.accounts.get(@id) + refresh_from_account(fresh_account) + end + + def update(description: nil) + params = {} + params[:description] = description if description + + updated_account = @client.accounts.update(@id, **params) + refresh_from_account(updated_account) + end + + def active? + @status == 'active' + end + + def blocked? + @status == 'blocked' + end + + def closed? + @status == 'closed' + end + + def test_mode? + @mode == 'test' + end + + def simulate_receive_transfer(amount:) + unless test_mode? + raise Fintoc::Errors::InvalidRequestError, 'Simulation is only available in test mode' + end + + @client.simulate.receive_transfer( + account_number_id: @root_account_number_id, + amount:, + currency: @currency + ) + end + + private + + def refresh_from_account(account) + unless account.id == @id + raise ArgumentError, 'Account must be the same instance' + end + + @object = account.object + @mode = account.mode + @description = account.description + @available_balance = account.available_balance + @currency = account.currency + @is_root = account.is_root + @root_account_number_id = account.root_account_number_id + @root_account_number = account.root_account_number + @status = account.status + @entity = account.entity + + self + end + end + end +end diff --git a/lib/fintoc/v2/resources/account_number.rb b/lib/fintoc/v2/resources/account_number.rb new file mode 100644 index 0000000..a0e4df1 --- /dev/null +++ b/lib/fintoc/v2/resources/account_number.rb @@ -0,0 +1,105 @@ +module Fintoc + module V2 + class AccountNumber + attr_reader :id, :object, :description, :number, :created_at, :updated_at, + :mode, :status, :is_root, :account_id, :metadata + + def initialize( + id:, + object:, + description:, + number:, + created_at:, + updated_at:, + mode:, + status:, + is_root:, + account_id:, + metadata:, + client: nil, + ** + ) + @id = id + @object = object + @description = description + @number = number + @created_at = created_at + @updated_at = updated_at + @mode = mode + @status = status + @is_root = is_root + @account_id = account_id + @metadata = metadata + @client = client + end + + def to_s + "πŸ”’ #{@number} (#{@id}) - #{@description}" + end + + def refresh + fresh_account_number = @client.account_numbers.get(@id) + refresh_from_account_number(fresh_account_number) + end + + def update(description: nil, status: nil, metadata: nil) + params = {} + params[:description] = description if description + params[:status] = status if status + params[:metadata] = metadata if metadata + + updated_account_number = @client.account_numbers.update(@id, **params) + refresh_from_account_number(updated_account_number) + end + + def enabled? + @status == 'enabled' + end + + def disabled? + @status == 'disabled' + end + + def root? + @is_root + end + + def test_mode? + @mode == 'test' + end + + def simulate_receive_transfer(amount:, currency: 'MXN') + unless test_mode? + raise Fintoc::Errors::InvalidRequestError, 'Simulation is only available in test mode' + end + + @client.simulate.receive_transfer( + account_number_id: @id, + amount:, + currency: + ) + end + + private + + def refresh_from_account_number(account_number) + unless account_number.id == @id + raise ArgumentError, 'AccountNumber must be the same instance' + end + + @object = account_number.object + @description = account_number.description + @number = account_number.number + @created_at = account_number.created_at + @updated_at = account_number.updated_at + @mode = account_number.mode + @status = account_number.status + @is_root = account_number.is_root + @account_id = account_number.account_id + @metadata = account_number.metadata + + self + end + end + end +end diff --git a/lib/fintoc/v2/resources/account_verification.rb b/lib/fintoc/v2/resources/account_verification.rb new file mode 100644 index 0000000..f326809 --- /dev/null +++ b/lib/fintoc/v2/resources/account_verification.rb @@ -0,0 +1,73 @@ +module Fintoc + module V2 + class AccountVerification + attr_reader :id, :object, :status, :reason, :transfer_id, :counterparty, :mode, :receipt_url, + :transaction_date + + def initialize( + id:, + object:, + status:, + reason:, + transfer_id:, + counterparty:, + mode:, + receipt_url:, + transaction_date:, + client: nil, + ** + ) + @id = id + @object = object + @status = status + @reason = reason + @transfer_id = transfer_id + @counterparty = counterparty + @mode = mode + @receipt_url = receipt_url + @transaction_date = transaction_date + @client = client + end + + def to_s + "πŸ” Account Verification (#{@id}) - #{@status}" + end + + def refresh + fresh_verification = @client.account_verifications.get(@id) + refresh_from_verification(fresh_verification) + end + + def pending? + @status == 'pending' + end + + def succeeded? + @status == 'succeeded' + end + + def failed? + @status == 'failed' + end + + private + + def refresh_from_verification(verification) + unless verification.id == @id + raise ArgumentError, 'Account verification must be the same instance' + end + + @object = verification.object + @status = verification.status + @reason = verification.reason + @transfer_id = verification.transfer_id + @counterparty = verification.counterparty + @mode = verification.mode + @receipt_url = verification.receipt_url + @transaction_date = verification.transaction_date + + self + end + end + end +end diff --git a/lib/fintoc/v2/resources/entity.rb b/lib/fintoc/v2/resources/entity.rb new file mode 100644 index 0000000..29cc562 --- /dev/null +++ b/lib/fintoc/v2/resources/entity.rb @@ -0,0 +1,51 @@ +module Fintoc + module V2 + class Entity + attr_reader :object, :mode, :id, :holder_name, :holder_id, :is_root + + def initialize( + object:, + mode:, + id:, + holder_name:, + holder_id:, + is_root:, + client: nil, + ** + ) + @object = object + @mode = mode + @id = id + @holder_name = holder_name + @holder_id = holder_id + @is_root = is_root + @client = client + end + + def to_s + "🏒 #{@holder_name} (#{@id})" + end + + def refresh + fresh_entity = @client.entities.get(@id) + refresh_from_entity(fresh_entity) + end + + private + + def refresh_from_entity(entity) + unless entity.id == @id + raise ArgumentError, 'Entity must be the same instance' + end + + @object = entity.object + @mode = entity.mode + @holder_name = entity.holder_name + @holder_id = entity.holder_id + @is_root = entity.is_root + + self + end + end + end +end diff --git a/lib/fintoc/v2/resources/transfer.rb b/lib/fintoc/v2/resources/transfer.rb new file mode 100644 index 0000000..0d78514 --- /dev/null +++ b/lib/fintoc/v2/resources/transfer.rb @@ -0,0 +1,131 @@ +require 'money' + +module Fintoc + module V2 + class Transfer + attr_reader :id, :object, :amount, :currency, :direction, :status, :mode, + :post_date, :transaction_date, :comment, :reference_id, :receipt_url, + :tracking_key, :return_reason, :counterparty, :account_number, + :metadata, :created_at + + def initialize( # rubocop:disable Metrics/MethodLength + id:, + object:, + amount:, + currency:, + status:, + mode:, + counterparty:, + direction: nil, + post_date: nil, + transaction_date: nil, + comment: nil, + reference_id: nil, + receipt_url: nil, + tracking_key: nil, + return_reason: nil, + account_number: nil, + metadata: {}, + created_at: nil, + client: nil, + ** + ) + @id = id + @object = object + @amount = amount + @currency = currency + @direction = direction + @status = status + @mode = mode + @post_date = post_date + @transaction_date = transaction_date + @comment = comment + @reference_id = reference_id + @receipt_url = receipt_url + @tracking_key = tracking_key + @return_reason = return_reason + @counterparty = counterparty + @account_number = account_number + @metadata = metadata || {} + @created_at = created_at + @client = client + end + + def to_s + amount_str = Money.from_cents(@amount, @currency).format + direction_icon = inbound? ? '⬇️' : '⬆️' + "#{direction_icon} #{amount_str} (#{@id}) - #{@status}" + end + + def refresh + fresh_transfer = @client.transfers.get(@id) + refresh_from_transfer(fresh_transfer) + end + + def return_transfer + returned_transfer = @client.transfers.return(@id) + refresh_from_transfer(returned_transfer) + end + + def pending? + @status == 'pending' + end + + def succeeded? + @status == 'succeeded' + end + + def failed? + @status == 'failed' + end + + def returned? + @status == 'returned' + end + + def return_pending? + @status == 'return_pending' + end + + def rejected? + @status == 'rejected' + end + + def inbound? + @direction == 'inbound' + end + + def outbound? + @direction == 'outbound' + end + + private + + def refresh_from_transfer(transfer) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + unless transfer.id == @id + raise ArgumentError, 'Transfer must be the same instance' + end + + @object = transfer.object + @amount = transfer.amount + @currency = transfer.currency + @direction = transfer.direction + @status = transfer.status + @mode = transfer.mode + @post_date = transfer.post_date + @transaction_date = transfer.transaction_date + @comment = transfer.comment + @reference_id = transfer.reference_id + @receipt_url = transfer.receipt_url + @tracking_key = transfer.tracking_key + @return_reason = transfer.return_reason + @counterparty = transfer.counterparty + @account_number = transfer.account_number + @metadata = transfer.metadata + @created_at = transfer.created_at + + self + end + end + end +end diff --git a/lib/fintoc/version.rb b/lib/fintoc/version.rb index 160de32..36e3c44 100644 --- a/lib/fintoc/version.rb +++ b/lib/fintoc/version.rb @@ -1,3 +1,3 @@ module Fintoc - VERSION = '0.2.0' + VERSION = '1.0.0' end diff --git a/lib/fintoc/webhook_signature.rb b/lib/fintoc/webhook_signature.rb new file mode 100644 index 0000000..f0bcd49 --- /dev/null +++ b/lib/fintoc/webhook_signature.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'openssl' +require 'time' +require 'fintoc/errors' + +module Fintoc + class WebhookSignature + EXPECTED_SCHEME = 'v1' + DEFAULT_TOLERANCE = 300 # 5 minutes + + class << self + def verify_header(payload, header, secret, tolerance = DEFAULT_TOLERANCE) # rubocop:disable Naming/PredicateMethod + timestamp, signatures = parse_header(header) + + verify_timestamp(timestamp, tolerance) if tolerance + + expected_signature = compute_signature(payload, timestamp, secret) + signature = signatures[EXPECTED_SCHEME] + + if signature.nil? || signature.empty? # rubocop:disable Rails/Blank + raise Fintoc::Errors::WebhookSignatureError.new("No #{EXPECTED_SCHEME} signature found") + end + + unless same_signatures?(signature, expected_signature) + raise Fintoc::Errors::WebhookSignatureError.new('Signature mismatch') + end + + true + end + + def compute_signature(payload, timestamp, secret) + signed_payload = "#{timestamp}.#{payload}" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, signed_payload) + end + + private + + def parse_header(header) + elements = header.split(',').map(&:strip) + pairs = elements.map { |element| element.split('=', 2).map(&:strip) } + pairs = pairs.to_h + + if pairs['t'].nil? || pairs['t'].empty? # rubocop:disable Rails/Blank + raise Fintoc::Errors::WebhookSignatureError.new('Missing timestamp in header') + end + + timestamp = pairs['t'].to_i + signatures = pairs.except('t') + + [timestamp, signatures] + rescue StandardError => e + raise Fintoc::Errors::WebhookSignatureError.new( + 'Unable to extract timestamp and signatures from header' + ), cause: e + end + + def verify_timestamp(timestamp, tolerance) + now = Time.now.to_i + + if timestamp < (now - tolerance) + raise Fintoc::Errors::WebhookSignatureError.new( + "Timestamp outside the tolerance zone (#{timestamp})" + ) + end + end + + def same_signatures?(signature, expected_signature) + OpenSSL.secure_compare(expected_signature, signature) + end + end + end +end diff --git a/lib/tasks/simplecov_config.rb b/lib/tasks/simplecov_config.rb new file mode 100644 index 0000000..0773caf --- /dev/null +++ b/lib/tasks/simplecov_config.rb @@ -0,0 +1,19 @@ +if ENV['COVERAGE'] == 'true' + require 'simplecov' + require 'simplecov_text_formatter' + require 'simplecov_linter_formatter' + + SimpleCov.start do + formatter SimpleCov::Formatter::MultiFormatter.new([SimpleCov::Formatter::HTMLFormatter]) + + add_filter '/vendor/' + add_filter '/lib/fintoc.rb' + add_filter '/lib/fintoc/version.rb' + add_filter '/lib/tasks/simplecov_config.rb' + + track_files 'lib/**/*.rb' + + minimum_coverage 100 + minimum_coverage_by_file 100 + end +end diff --git a/spec/fintoc_spec.rb b/spec/fintoc_spec.rb deleted file mode 100644 index 85e57ad..0000000 --- a/spec/fintoc_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -RSpec.describe Fintoc do - it 'has a version number' do - expect(Fintoc::VERSION).not_to be_nil - end - - # it "does something useful" do - # expect(false).to eq(true) - # end -end diff --git a/spec/lib/fintoc/base_client_spec.rb b/spec/lib/fintoc/base_client_spec.rb new file mode 100644 index 0000000..7735057 --- /dev/null +++ b/spec/lib/fintoc/base_client_spec.rb @@ -0,0 +1,123 @@ +require 'fintoc/base_client' + +RSpec.describe Fintoc::BaseClient do + let(:api_key) { 'sk_test_SeCreT-aPi_KeY' } + let(:client) { described_class.new(api_key) } + + describe '#initialize' do + it 'creates an instance with api_key' do + expect(client).to be_an_instance_of(described_class) + expect(client.instance_variable_get(:@api_key)).to eq(api_key) + end + + context 'with jws_private_key' do + let(:jws_private_key) { OpenSSL::PKey::RSA.new(2048) } + let(:client_with_jws) { described_class.new(api_key, jws_private_key: jws_private_key) } + + it 'creates an instance with jws support' do + expect(client_with_jws.instance_variable_get(:@jws)).not_to be_nil + end + end + end + + describe '#get' do + it 'returns a proc for GET requests' do + get_request = client.get(version: :v1) + expect(get_request).to be_a(Proc) + end + end + + describe '#delete' do + it 'returns a proc for DELETE requests' do + delete_request = client.delete(version: :v1) + expect(delete_request).to be_a(Proc) + end + end + + describe '#post' do + it 'returns a proc for POST requests' do + post_request = client.post(version: :v1) + expect(post_request).to be_a(Proc) + end + end + + describe '#patch' do + it 'returns a proc for PATCH requests' do + patch_request = client.patch(version: :v1) + expect(patch_request).to be_a(Proc) + end + end + + describe '#request' do + let(:mock_response) { instance_double(HTTP::Response) } + let(:mock_status) { instance_double(HTTP::Response::Status) } + let(:mock_headers) { instance_double(HTTP::Headers) } + + context 'when HTTP request is successful' do + let(:success_response_body) { { data: 'test_data' }.to_json } + + before do + allow(mock_response).to receive_messages( + body: success_response_body, + status: mock_status, + headers: mock_headers + ) + allow(mock_status).to receive_messages( + client_error?: false, + server_error?: false + ) + allow(mock_headers).to receive(:get).with('link').and_return(nil) + allow(client).to receive(:make_request).and_return(mock_response) + end + + it 'returns parsed JSON content' do + request_proc = client.request('get') + result = request_proc.call('/test/resource') + + expect(result).to eq({ data: 'test_data' }) + end + end + + context 'when HTTP request returns an error' do + let(:error_response_body) do + { + error: { + code: 'authentication_error', + message: 'Invalid API key', + doc_url: 'https://docs.fintoc.com/errors' + } + }.to_json + end + + before do + allow(mock_response).to receive_messages( + body: error_response_body, + status: mock_status, + headers: mock_headers + ) + allow(mock_status).to receive_messages( + client_error?: true, + server_error?: false + ) + allow(mock_headers).to receive(:get).with('link').and_return(nil) + allow(client).to receive(:make_request).and_return(mock_response) + end + + it 'raises custom error from response' do + request_proc = client.request('get') + expect { request_proc.call('/test/resource') } + .to raise_error(Fintoc::Errors::AuthenticationError, /Invalid API key/) + end + end + end + + describe '#to_s' do + it 'returns masked API key representation' do + result = client.to_s + expect(result).to include('sk_test_') + expect(result).to include('****') + expect(result).not_to include(api_key) + expect(result).not_to include('SeCreT-aPi_KeY') + end + end +end diff --git a/spec/lib/fintoc/client_spec.rb b/spec/lib/fintoc/client_spec.rb index bba2a11..6111f2e 100644 --- a/spec/lib/fintoc/client_spec.rb +++ b/spec/lib/fintoc/client_spec.rb @@ -1,48 +1,71 @@ require 'fintoc/client' -require 'fintoc/resources/link' -require 'fintoc/resources/account' -require 'fintoc/resources/movement' +require 'fintoc/v1/resources/link' +require 'fintoc/v1/resources/account' +require 'fintoc/v1/resources/movement' RSpec.describe Fintoc::Client do let(:api_key) { 'sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx' } - let(:link_token) { '6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W' } - let(:client) { described_class.new(api_key) } + let(:jws_private_key) { OpenSSL::PKey::RSA.new(2048) } + let(:client) { described_class.new(api_key, jws_private_key: jws_private_key) } describe '.new' do it 'create an instance Client' do expect(client).to be_an_instance_of(described_class) end - end - describe '#get_link' do - it 'get the link from a given link token', :vcr do - link = client.get_link(link_token) - expect(link).to be_an_instance_of(Fintoc::Link) + it 'creates movements and transfers clients' do + expect(client).to respond_to(:v1) + expect(client.v1).to be_an_instance_of(Fintoc::V1::Client) + expect(client).to respond_to(:v2) + expect(client.v2).to be_an_instance_of(Fintoc::V2::Client) end end - describe '#get_links' do - it 'get all the links from a given link token', :vcr do - links = client.get_links - expect(links).to all(be_a(Fintoc::Link)) + describe '#to_s' do + it 'returns the client as a string' do + expect(client.to_s).to match(/Fintoc::Client\(v1: .*, v2: .*\)/) end end - describe '#get_account' do - it 'get a linked account', :vcr do - link = client.get_link(link_token) - account = link.find(type: 'checking_account') - returned_account = client.get_account(link_token, account.id) - expect(returned_account).to be_an_instance_of(Fintoc::Account) + describe 'client separation' do + it 'allows direct access to movements client' do + expect(client.v1.links) + .to respond_to(:get) + .and respond_to(:list) + .and respond_to(:delete) + end + + it 'allows direct access to transfers client' do + expect(client.v2.entities) + .to respond_to(:get) + .and respond_to(:list) + end + + it 'maintains backward compatibility through delegation' do + expect(client) + .to respond_to(:get_link) + .and respond_to(:get_links) + .and respond_to(:delete_link) + .and respond_to(:get_account) end end - describe '#get_accounts', :vcr do - it 'prints accounts to console' do - link = client.get_link(link_token) - expect do - link.show_accounts - end.to output(start_with('This links has 1 account')).to_stdout + describe 'delegation to movements client' do + let(:link) { instance_double(Fintoc::V1::Link) } + let(:account) { instance_double(Fintoc::V1::Account) } + + before do + allow(client.v1.links).to receive(:get).with('token').and_return(link) + allow(client.v1.links).to receive(:list).and_return([link]) + allow(client.v1.links).to receive(:delete).with('link_id').and_return(true) + allow(link).to receive(:find).with(id: 'account_id').and_return(account) + end + + it 'delegates movements methods to movements client' do + expect(client.get_link('token')).to eq(link) + expect(client.get_links).to eq([link]) + expect(client.delete_link('link_id')).to be(true) + expect(client.get_account('token', 'account_id')).to eq(account) end end end diff --git a/spec/lib/fintoc/errors_spec.rb b/spec/lib/fintoc/errors_spec.rb index 547e528..2c6c005 100644 --- a/spec/lib/fintoc/errors_spec.rb +++ b/spec/lib/fintoc/errors_spec.rb @@ -17,6 +17,6 @@ it 'raises a Invalid Request Error with default url doc' do expect { raise Fintoc::Errors::InvalidRequestError.new(error[:message]) } .to(raise_error(an_instance_of(Fintoc::Errors::InvalidRequestError)) - .with_message(%r{https://fintoc.com/docs})) + .with_message(%r{https://docs.fintoc.com/reference/errors})) end end diff --git a/spec/lib/fintoc/fintoc_spec.rb b/spec/lib/fintoc/fintoc_spec.rb new file mode 100644 index 0000000..7e36d86 --- /dev/null +++ b/spec/lib/fintoc/fintoc_spec.rb @@ -0,0 +1,5 @@ +RSpec.describe Fintoc, :module do + it 'is a valid version number' do + expect(Fintoc::VERSION).to match(/\A\d+\.\d+\.\d+\z/) + end +end diff --git a/spec/lib/fintoc/jws_spec.rb b/spec/lib/fintoc/jws_spec.rb new file mode 100644 index 0000000..bcebfec --- /dev/null +++ b/spec/lib/fintoc/jws_spec.rb @@ -0,0 +1,120 @@ +require 'fintoc/jws' +require 'openssl' +require 'json' +require 'base64' + +RSpec.describe Fintoc::JWS do + let(:private_key) do + OpenSSL::PKey::RSA.new(2048) + end + + let(:jws) { described_class.new(private_key) } + + describe '#initialize' do + it 'accepts an OpenSSL::PKey::RSA object' do + expect { described_class.new(private_key) }.not_to raise_error + end + + it 'raises an error for invalid input' do + expect { described_class.new('invalid_string') } + .to raise_error(ArgumentError, 'private_key must be an OpenSSL::PKey::RSA instance') + end + + it 'raises an error for numeric input' do + expect { described_class.new(123) } + .to raise_error(ArgumentError, 'private_key must be an OpenSSL::PKey::RSA instance') + end + end + + describe '#generate_signature' do + let(:payload) { { amount: 1000, currency: 'MXN' } } + + it 'generates a valid JWS signature' do + signature = jws.generate_signature(payload) + + expect(signature).to be_a(String) + expect(signature.split('.').length).to eq(2) + end + + it 'includes required headers' do + signature = jws.generate_signature(payload) + parsed = jws.parse_signature(signature) + + expect(parsed[:protected_headers]).to include( + alg: 'RS256', + nonce: be_a(String), + ts: be_a(Integer), + crit: %w[ts nonce] + ) + end + + it 'accepts string payloads' do + string_payload = '{"amount":1000,"currency":"MXN"}' + signature = jws.generate_signature(string_payload) + + expect(signature).to be_a(String) + expect(signature.split('.').length).to eq(2) + end + + it 'generates different signatures for the same payload' do + signature1 = jws.generate_signature(payload) + signature2 = jws.generate_signature(payload) + + expect(signature1).not_to eq(signature2) + end + + it 'generates verifiable signatures' do + signature = jws.generate_signature(payload) + + expect(jws.verify_signature(signature, payload)).to be true + end + end + + describe '#parse_signature' do + let(:payload) { { amount: 1000, currency: 'MXN' } } + let(:signature) { jws.generate_signature(payload) } + + it 'parses signature components correctly' do + parsed = jws.parse_signature(signature) + + expect(parsed).to include( + protected_headers: be_a(Hash), + signature_bytes: be_a(String), + protected_b64: be_a(String), + signature_b64: be_a(String) + ) + end + + it 'includes correct header values' do + parsed = jws.parse_signature(signature) + + expect(parsed[:protected_headers]).to include( + alg: 'RS256', + crit: %w[ts nonce] + ) + end + end + + describe '#verify_signature' do + let(:payload) { { amount: 1000, currency: 'MXN' } } + + it 'verifies valid signatures' do + signature = jws.generate_signature(payload) + expect(jws.verify_signature(signature, payload)).to be true + end + + it 'rejects signatures with wrong payload' do + signature = jws.generate_signature(payload) + wrong_payload = { amount: 2000, currency: 'CLP' } + + expect(jws.verify_signature(signature, wrong_payload)).to be false + end + + it 'works with string payloads' do + string_payload = '{"amount":1000,"currency":"MXN"}' + signature = jws.generate_signature(string_payload) + + expect(jws.verify_signature(signature, string_payload)).to be true + end + end +end diff --git a/spec/lib/fintoc/resources/fintoc/account_spec.rb b/spec/lib/fintoc/resources/fintoc/account_spec.rb deleted file mode 100644 index afb528b..0000000 --- a/spec/lib/fintoc/resources/fintoc/account_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'fintoc/resources/account' - -RSpec.describe Fintoc::Account do - let(:api_key) { 'sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx' } - let(:client) { Fintoc::Client.new(api_key) } - - let(:data) do - { - id: 'Z6AwnGn4idL7DPj4', - name: 'Cuenta Corriente', - official_name: 'Cuenta Corriente Moneda Local', - number: '9530516286', - holder_id: '134910798', - holder_name: 'Jon Snow', - type: 'checking_account', - currency: 'CLP', - refreshed_at: nil, - balance: { - available: 7_010_510, - current: 7_010_510, - limit: 7_510_510 - }, - client: client - } - end - - let(:link_token) { '6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W' } - let(:link) { client.get_link(link_token) } - let(:account) { described_class.new(**data) } - let(:linked_account) { link.find(type: 'checking_account') } - - describe '#new' do - it 'create an instance of Account' do - expect(account).to be_an_instance_of(described_class) - end - end - - describe '#to_s' do - it "print the account's holder_name, name, and balance when to_s is called" do - expect(account.to_s) - .to eq( - "πŸ’° #{data[:holder_name]}’s #{data[:name]} #{data[:balance][:available]} " \ - "(#{data[:balance][:current]})" - ) - end - end - - describe '#get_movements' do - it "get the last 30 account's movements", :vcr do - movements = linked_account.get_movements - expect(movements.size).to be <= 30 - expect(movements).to all(be_a(Fintoc::Movement)) - end - end - - describe '#movement with since argument' do - it "get account's movements with arguments", :vcr do - movements = linked_account.get_movements(since: '2020-08-15') - linked_account.show_movements - expect(movements).to all(be_a(Fintoc::Movement)) - end - end - - describe '#update_balance' do - it "update account's movements", :vcr do - movements = linked_account.update_movements - expect(movements).to all(be_a(Fintoc::Movement)) - end - end -end diff --git a/spec/lib/fintoc/resources/fintoc/balance_spec.rb b/spec/lib/fintoc/resources/fintoc/balance_spec.rb deleted file mode 100644 index 56a136a..0000000 --- a/spec/lib/fintoc/resources/fintoc/balance_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'fintoc/resources/balance' - -RSpec.describe Fintoc::Balance do - let(:data) { { available: 1000, current: 500, limit: 10 } } - let(:balance) { described_class.new(**data) } - - it 'create an instance of Balance' do - expect(balance).to be_an_instance_of(described_class) - end - - it 'returns their object_id when id_ getter is called' do - expect(balance.id).to eq(balance.object_id) - end -end diff --git a/spec/lib/fintoc/resources/fintoc/institution_spec.rb b/spec/lib/fintoc/resources/fintoc/institution_spec.rb deleted file mode 100644 index e9b1a16..0000000 --- a/spec/lib/fintoc/resources/fintoc/institution_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'fintoc/resources/institution' - -RSpec.describe Fintoc::Institution do - let(:data) do - { id: 'cl_banco_de_chile', name: 'Banco de Chile', country: 'cl' } - end - - let(:institution) { described_class.new(**data) } - - it 'create an instance of Institution' do - expect(institution).to be_an_instance_of(described_class) - end - - it "print the institution's name when to_s is called" do - expect(institution.to_s).to eq("🏦 #{data[:name]}") - end -end diff --git a/spec/lib/fintoc/resources/fintoc/link_spec.rb b/spec/lib/fintoc/resources/fintoc/link_spec.rb deleted file mode 100644 index cd2c678..0000000 --- a/spec/lib/fintoc/resources/fintoc/link_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'fintoc/resources/link' - -RSpec.describe Fintoc::Link do - let(:data) do - { - id: 'nMNejK7BT8oGbvO4', - username: '183917137', - link_token: 'nMNejK7BT8oGbvO4_token_GLtktZX5SKphRtJFe_yJTDWT', - holder_type: 'individual', - created_at: '2020-04-22T21:10:19.254Z', - institution: { - country: 'cl', - id: 'cl_banco_de_chile', - name: 'Banco de Chile' - }, - mode: 'test', - accounts: [ - { - id: 'Z6AwnGn4idL7DPj4', - name: 'Cuenta Corriente', - official_name: 'Cuenta Corriente Moneda Local', - number: '9530516286', - holder_id: '134910798', - holder_name: 'Jon Snow', - type: 'checking_account', - currency: 'CLP', - refreshed_at: nil, - balance: { - available: 7_010_510, - current: 7_010_510, - limit: 7_510_510 - } - }, - { - id: 'BO381oEATXonG6bj', - name: 'LΓ­nea de CrΓ©dito', - official_name: 'Linea De Credito Personas', - number: '19534121467', - holder_id: '134910798', - holder_name: 'Jon Snow', - type: 'line_of_credit', - currency: 'CLP', - refreshed_at: nil, - balance: { - available: 500_000, - current: 500_000, - limit: 500_000 - } - } - ] - } - end - let(:link) { described_class.new(**data) } - - it 'create an instance of Link' do - expect(link).to be_an_instance_of(described_class) - end - - describe '#find' do - it 'returns and valid checking account if the arg is type: "checking_account"' do - checking_account = link.find(type: 'checking_account') - data_acc = data[:accounts][0] - expect(checking_account).to be_an_instance_of(Fintoc::Account) - expect(checking_account.to_s) - .to( - eq( - "πŸ’° #{data_acc[:holder_name]}’s #{data_acc[:name]} #{data_acc[:balance][:available]} " \ - "(#{data_acc[:balance][:current]})" - ) - ) - end - end -end diff --git a/spec/lib/fintoc/resources/fintoc/transfer_account_spec.rb b/spec/lib/fintoc/resources/fintoc/transfer_account_spec.rb deleted file mode 100644 index efe0bec..0000000 --- a/spec/lib/fintoc/resources/fintoc/transfer_account_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'fintoc/resources/transfer_account' - -RSpec.describe Fintoc::TransferAccount do - let(:data) do - { - holder_id: '771806538', - holder_name: 'Comercial y ProducciΓ³n SpA', - number: 1_530_108_000, - institution: { id: 'cl_banco_de_chile', name: 'Banco de Chile', country: 'cl' } - } - end - let(:transfer) { described_class.new(**data) } - - it 'create an instance of TransferAccount' do - expect(transfer).to be_an_instance_of(described_class) - end -end diff --git a/spec/lib/fintoc/v1/account_spec.rb b/spec/lib/fintoc/v1/account_spec.rb new file mode 100644 index 0000000..b9eb14c --- /dev/null +++ b/spec/lib/fintoc/v1/account_spec.rb @@ -0,0 +1,115 @@ +require 'fintoc/v1/resources/account' + +RSpec.describe Fintoc::V1::Account do + let(:api_key) { 'sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx' } + let(:client) { Fintoc::V1::Client.new(api_key) } + + let(:data) do + { + id: 'Z6AwnGn4idL7DPj4', + name: 'Cuenta Corriente', + official_name: 'Cuenta Corriente Moneda Local', + number: '9530516286', + holder_id: '134910798', + holder_name: 'Jon Snow', + type: 'checking_account', + currency: 'CLP', + refreshed_at: nil, + balance: { + available: 7_010_510, + current: 7_010_510, + limit: 7_510_510 + }, + client: client + } + end + + let(:link_token) { '6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W' } + let(:link) { client.links.get(link_token) } + let(:account) { described_class.new(**data) } + let(:linked_account) { link.find(type: 'checking_account') } + + describe '#new' do + it 'create an instance of Account' do + expect(account).to be_an_instance_of(described_class) + end + end + + describe '#to_s' do + it "print the account's holder_name, name, and balance when to_s is called" do + expect(account.to_s) + .to eq( + "πŸ’° #{data[:holder_name]}’s #{data[:name]} #{data[:balance][:available]} " \ + "(#{data[:balance][:current]})" + ) + end + end + + describe '#get_movements' do + it "get the last 30 account's movements", :vcr do + movements = linked_account.get_movements + expect(movements.size).to be <= 30 + expect(movements).to all(be_a(Fintoc::V1::Movement)) + end + end + + describe '#movement with since argument' do + it "get account's movements with arguments", :vcr do + movements = linked_account.get_movements(since: '2020-08-15') + linked_account.show_movements + expect(movements).to all(be_a(Fintoc::V1::Movement)) + end + end + + describe '#show_movements' do + context 'when account has movements' do + let(:movement) do + Fintoc::V1::Movement.new( + id: '1', + amount: 1000, + currency: 'CLP', + description: 'Test movement', + post_date: '2023-01-01T10:00:00Z', + transaction_date: '2023-01-01T10:00:00Z', + type: 'normal_movement', + reference_id: 'ref123', + recipient_account: nil, + sender_account: nil, + comment: nil + ) + end + + let(:account) { described_class.new(**data, movements: [movement]) } + + it 'displays movements information with non-empty movements array' do + expect { account.show_movements }.to output(/This account has 1 movement/).to_stdout + end + end + + context 'when account has no movements' do + let(:account) { described_class.new(**data, movements: []) } + + it 'displays zero movements message' do + expect { account.show_movements }.to output(/This account has 0 movements/).to_stdout + end + end + end + + describe '#update_balance' do + let(:client_for_update_balance) { Fintoc::V1::Client.new(api_key) } + + it "updates the account's balance", :vcr do + initial_balance = account.balance + account.update_balance + expect(account.balance).to be_a(Fintoc::V1::Balance) + expect(account.balance).not_to equal(initial_balance) + end + end + + describe '#update_movements' do + it "update account's movements", :vcr do + movements = linked_account.update_movements + expect(movements).to all(be_a(Fintoc::V1::Movement)) + end + end +end diff --git a/spec/lib/fintoc/v1/balance_spec.rb b/spec/lib/fintoc/v1/balance_spec.rb new file mode 100644 index 0000000..069fecd --- /dev/null +++ b/spec/lib/fintoc/v1/balance_spec.rb @@ -0,0 +1,29 @@ +require 'fintoc/v1/resources/balance' + +RSpec.describe Fintoc::V1::Balance do + let(:data) { { available: 1000, current: 500, limit: 10 } } + let(:balance) { described_class.new(**data) } + + describe '#new' do + it 'create an instance of Balance' do + expect(balance).to be_an_instance_of(described_class) + end + + it 'returns their object_id when id_ getter is called' do + expect(balance.id).to eq(balance.object_id) + end + end + + describe '#to_s' do + it 'returns the balance as a string' do + expect(balance.to_s).to eq('1000 (500)') + end + end + + describe '#inspect' do + it 'returns the balance as a string' do + expect(balance.inspect) + .to eq("") + end + end +end diff --git a/spec/lib/fintoc/v1/client_spec.rb b/spec/lib/fintoc/v1/client_spec.rb new file mode 100644 index 0000000..7754cae --- /dev/null +++ b/spec/lib/fintoc/v1/client_spec.rb @@ -0,0 +1,29 @@ +require 'fintoc/v1/client/client' + +RSpec.describe Fintoc::V1::Client do + let(:api_key) { 'sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx' } + let(:client) { described_class.new(api_key) } + + describe '.new' do + it 'creates an instance of Clients::MovementsClient' do + expect(client).to be_an_instance_of(described_class) + end + end + + describe '#to_s' do + it 'returns a formatted string representation' do + expect(client.to_s) + .to include('Fintoc::V1::Client') + .and include('πŸ”‘=') + end + end + + it 'responds to movements-specific methods' do + expect(client.links) + .to respond_to(:get) + .and respond_to(:list) + .and respond_to(:delete) + end + + it_behaves_like 'a client with links manager' +end diff --git a/spec/lib/fintoc/v1/institution_spec.rb b/spec/lib/fintoc/v1/institution_spec.rb new file mode 100644 index 0000000..d2be9d5 --- /dev/null +++ b/spec/lib/fintoc/v1/institution_spec.rb @@ -0,0 +1,27 @@ +require 'fintoc/v1/resources/institution' + +RSpec.describe Fintoc::V1::Institution do + let(:data) do + { id: 'cl_banco_de_chile', name: 'Banco de Chile', country: 'cl' } + end + + let(:institution) { described_class.new(**data) } + + describe '#new' do + it 'create an instance of Institution' do + expect(institution).to be_an_instance_of(described_class) + end + end + + describe '#to_s' do + it "print the institution's name when to_s is called" do + expect(institution.to_s).to eq("🏦 #{data[:name]}") + end + end + + describe '#inspect' do + it 'returns the institution as a string' do + expect(institution.inspect).to eq("") + end + end +end diff --git a/spec/lib/fintoc/v1/link_spec.rb b/spec/lib/fintoc/v1/link_spec.rb new file mode 100644 index 0000000..d5e2461 --- /dev/null +++ b/spec/lib/fintoc/v1/link_spec.rb @@ -0,0 +1,166 @@ +require 'fintoc/v1/resources/link' + +RSpec.describe Fintoc::V1::Link do + let(:data) do + { + id: 'nMNejK7BT8oGbvO4', + username: '183917137', + link_token: 'nMNejK7BT8oGbvO4_token_GLtktZX5SKphRtJFe_yJTDWT', + holder_type: 'individual', + created_at: '2020-04-22T21:10:19.254Z', + institution: { + country: 'cl', + id: 'cl_banco_de_chile', + name: 'Banco de Chile' + }, + mode: 'test', + accounts: [ + { + id: 'Z6AwnGn4idL7DPj4', + name: 'Cuenta Corriente', + official_name: 'Cuenta Corriente Moneda Local', + number: '9530516286', + holder_id: '134910798', + holder_name: 'Jon Snow', + type: 'checking_account', + currency: 'CLP', + refreshed_at: nil, + balance: { + available: 7_010_510, + current: 7_010_510, + limit: 7_510_510 + } + }, + { + id: 'BO381oEATXonG6bj', + name: 'LΓ­nea de CrΓ©dito', + official_name: 'Linea De Credito Personas', + number: '19534121467', + holder_id: '134910798', + holder_name: 'Jon Snow', + type: 'line_of_credit', + currency: 'CLP', + refreshed_at: nil, + balance: { + available: 500_000, + current: 500_000, + limit: 500_000 + } + } + ] + } + end + let(:link) { described_class.new(**data, client:) } + let(:client) { Fintoc::V1::Client.new(api_key) } + let(:api_key) { 'sk_test_SeCrEt_aPi_KeY' } + + describe '#new' do + it 'create an instance of Link' do + expect(link).to be_an_instance_of(described_class) + end + end + + describe '#to_s' do + it 'returns the link as a string' do + expect(link.to_s).to eq("<#{data[:username]}@#{data[:institution][:name]}> πŸ”— ") + end + end + + describe '#find' do + it 'returns and valid checking account if the arg is type: "checking_account"' do + checking_account = link.find(type: 'checking_account') + data_acc = data[:accounts][0] + expect(checking_account).to be_an_instance_of(Fintoc::V1::Account) + expect(checking_account.to_s) + .to( + eq( + "πŸ’° #{data_acc[:holder_name]}’s #{data_acc[:name]} #{data_acc[:balance][:available]} " \ + "(#{data_acc[:balance][:current]})" + ) + ) + end + end + + describe '#show_accounts' do + context 'when link has accounts' do + it 'displays accounts information' do + expect { link.show_accounts }.to output(/This links has 2 accounts/).to_stdout + end + end + + context 'when link has no accounts' do + it 'displays zero accounts message' do + empty_link = described_class.new(**data, accounts: []) + expect { empty_link.show_accounts }.to output(/This links has 0 accounts/).to_stdout + end + end + end + + describe '#update_accounts' do + let(:api_key) { 'sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx' } + let(:client) { Fintoc::V1::Client.new(api_key) } + let(:link_token) { '6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W' } + let(:linked_link) { client.links.get(link_token) } + let(:account_original_balance) do + { + available: 7_010_510, + current: 7_010_510, + limit: 7_510_510 + } + end + let(:account_data_before_update) do + { + id: 'Z6AwnGn4idL7DPj4', + name: 'Cuenta Corriente', + official_name: 'Cuenta Corriente Moneda Local', + number: '9530516286', + holder_id: '134910798', + holder_name: 'Jon Snow', + type: 'checking_account', + currency: 'CLP', + refreshed_at: nil, + balance: account_original_balance + } + end + + let(:data) do + { + id: 'nMNejK7BT8oGbvO4', + username: '183917137', + link_token: 'nMNejK7BT8oGbvO4_token_GLtktZX5SKphRtJFe_yJTDWT', + holder_type: 'individual', + created_at: '2020-04-22T21:10:19.254Z', + institution: { + country: 'cl', + id: 'cl_banco_de_chile', + name: 'Banco de Chile' + }, + mode: 'test', + accounts: [account_data_before_update] + } + end + + it 'updates balance and movements for all accounts', :vcr do + expect(link.accounts[0].balance.available).to eq(account_original_balance[:available]) + link.update_accounts + expect(link.accounts[0].balance).to be_a(Fintoc::V1::Balance) + expect(link.accounts[0].balance.available).not_to eq(account_original_balance[:available]) + end + end + + describe '#delete' do + let(:delete_proc) { instance_double(Proc) } + + before do + allow(client).to receive(:delete).with(version: :v1).and_return(delete_proc) + allow(delete_proc) + .to receive(:call) + .with("links/#{link.id}") + .and_return(true) + end + + it 'deletes the link successfully' do + expect(link.delete).to be true + end + end +end diff --git a/spec/lib/fintoc/v1/managers/links_manager_spec.rb b/spec/lib/fintoc/v1/managers/links_manager_spec.rb new file mode 100644 index 0000000..725c637 --- /dev/null +++ b/spec/lib/fintoc/v1/managers/links_manager_spec.rb @@ -0,0 +1,77 @@ +require 'fintoc/v1/managers/links_manager' + +RSpec.describe Fintoc::V1::Managers::LinksManager do + let(:api_key) { 'sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx' } + let(:client) { Fintoc::V1::Client.new(api_key) } + let(:get_proc) { instance_double(Proc) } + let(:post_proc) { instance_double(Proc) } + let(:delete_proc) { instance_double(Proc) } + let(:manager) { described_class.new(client) } + let(:link_id) { 'link_123' } + let(:link_token) { '6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W' } + let(:first_link_data) do + { + id: link_id, + object: 'link', + link_token: link_token + } + end + let(:second_link_data) do + { + id: 'link_456', + object: 'link', + link_token: link_token + } + end + + before do + allow(client).to receive(:get).with(version: :v1).and_return(get_proc) + allow(client).to receive(:post).with(version: :v1).and_return(post_proc) + allow(client).to receive(:delete).with(version: :v1).and_return(delete_proc) + + allow(get_proc) + .to receive(:call) + .with("links/#{link_token}") + .and_return(first_link_data) + allow(get_proc) + .to receive(:call) + .with('links') + .and_return([first_link_data, second_link_data]) + allow(post_proc) + .to receive(:call) + .with('links', link_token:) + .and_return(first_link_data) + allow(delete_proc) + .to receive(:call) + .with("links/#{link_id}") + .and_return(true) + + allow(Fintoc::V1::Link).to receive(:new) + end + + describe '#links' do + describe '#get' do + it 'calls build_link with the response' do + manager.get(link_token) + expect(Fintoc::V1::Link) + .to have_received(:new).with(**first_link_data, client:) + end + end + + describe '#list' do + it 'calls build_link with the response' do + manager.list + expect(Fintoc::V1::Link) + .to have_received(:new).with(**first_link_data, client:) + expect(Fintoc::V1::Link) + .to have_received(:new).with(**second_link_data, client:) + end + end + + describe '#delete' do + it 'calls build_link with the response' do + expect(manager.delete(link_id)).to be true + end + end + end +end diff --git a/spec/lib/fintoc/resources/fintoc/movement_spec.rb b/spec/lib/fintoc/v1/movement_spec.rb similarity index 93% rename from spec/lib/fintoc/resources/fintoc/movement_spec.rb rename to spec/lib/fintoc/v1/movement_spec.rb index f2e1245..1339227 100644 --- a/spec/lib/fintoc/resources/fintoc/movement_spec.rb +++ b/spec/lib/fintoc/v1/movement_spec.rb @@ -1,6 +1,6 @@ -require 'fintoc/resources/movement' +require 'fintoc/v1/resources/movement' -RSpec.describe Fintoc::Movement do +RSpec.describe Fintoc::V1::Movement do let(:data) do { id: 'BO381oEATXonG6bj', @@ -89,6 +89,12 @@ end end + describe '#to_s' do + it 'returns the movement as a string' do + expect(movement.to_s).to eq('59400 (Traspaso de:Fintoc SpA @ 04/17/20)') + end + end + it 'return uniq movements using the hash method implemented in Movement Class' do expect(dup_movements.uniq.length).to eq(1) end diff --git a/spec/lib/fintoc/v1/transfer_account_spec.rb b/spec/lib/fintoc/v1/transfer_account_spec.rb new file mode 100644 index 0000000..86951da --- /dev/null +++ b/spec/lib/fintoc/v1/transfer_account_spec.rb @@ -0,0 +1,31 @@ +require 'fintoc/v1/resources/transfer_account' + +RSpec.describe Fintoc::V1::TransferAccount do + let(:data) do + { + holder_id: '771806538', + holder_name: 'Comercial y ProducciΓ³n SpA', + number: 1_530_108_000, + institution: { id: 'cl_banco_de_chile', name: 'Banco de Chile', country: 'cl' } + } + end + let(:transfer) { described_class.new(**data) } + + describe '#new' do + it 'create an instance of TransferAccount' do + expect(transfer).to be_an_instance_of(described_class) + end + end + + describe '#id' do + it 'returns the transfer account as a string' do + expect(transfer.id).to eq(transfer.object_id) + end + end + + describe '#to_s' do + it 'returns the transfer account as a string' do + expect(transfer.to_s).to eq(data[:holder_id].to_s) + end + end +end diff --git a/spec/lib/fintoc/v2/account_number_spec.rb b/spec/lib/fintoc/v2/account_number_spec.rb new file mode 100644 index 0000000..4446f77 --- /dev/null +++ b/spec/lib/fintoc/v2/account_number_spec.rb @@ -0,0 +1,218 @@ +require 'fintoc/v2/resources/account_number' + +RSpec.describe Fintoc::V2::AccountNumber do + subject(:account_number) { described_class.new(**data) } + + let(:api_key) { 'sk_test_SeCreT-aPi_KeY' } + let(:client) { Fintoc::V2::Client.new(api_key) } + + let(:data) do + { + id: 'acno_Kasf91034gj1AD', + object: 'account_number', + description: 'My payins', + number: '738969123456789120', + created_at: '2024-03-01T20:09:42.949787176Z', + updated_at: '2024-03-01T20:09:42.949787176Z', + mode: 'test', + status: 'enabled', + is_root: false, + account_id: 'acc_Lq7dP901xZgA2B', + metadata: { + order_id: '12343212' + }, + client: client + } + end + + describe '#initialize' do + it 'assigns all attributes correctly' do # rubocop:disable RSpec/ExampleLength + expect(account_number).to have_attributes( + id: 'acno_Kasf91034gj1AD', + object: 'account_number', + description: 'My payins', + number: '738969123456789120', + created_at: '2024-03-01T20:09:42.949787176Z', + updated_at: '2024-03-01T20:09:42.949787176Z', + mode: 'test', + status: 'enabled', + is_root: false, + account_id: 'acc_Lq7dP901xZgA2B', + metadata: { order_id: '12343212' } + ) + end + end + + describe '#to_s' do + it 'returns a string representation' do + expected = 'πŸ”’ 738969123456789120 (acno_Kasf91034gj1AD) - My payins' + expect(account_number.to_s).to eq(expected) + end + end + + describe '#enabled?' do + context 'when status is enabled' do + it 'returns true' do + expect(account_number.enabled?).to be true + end + end + + context 'when status is not enabled' do + let(:data) do + super().merge(status: 'disabled') + end + + it 'returns false' do + expect(account_number.enabled?).to be false + end + end + end + + describe '#disabled?' do + context 'when status is disabled' do + let(:data) do + super().merge(status: 'disabled') + end + + it 'returns true' do + expect(account_number.disabled?).to be true + end + end + + context 'when status is not disabled' do + it 'returns false' do + expect(account_number.disabled?).to be false + end + end + end + + describe '#root?' do + context 'when is_root is true' do + let(:data) do + super().merge(is_root: true) + end + + it 'returns true' do + expect(account_number.root?).to be true + end + end + + context 'when is_root is false' do + it 'returns false' do + expect(account_number.root?).to be false + end + end + end + + describe '#refresh' do + let(:refreshed_data) do + data.merge(description: 'Updated description') + end + let(:refreshed_account_number) { described_class.new(**refreshed_data) } + + before do + allow(client.account_numbers) + .to receive(:get) + .with('acno_Kasf91034gj1AD') + .and_return(refreshed_account_number) + end + + it 'refreshes the account number with the latest data' do + account_number.refresh + expect(account_number.description).to eq('Updated description') + end + + it 'calls get_account_number with the correct id' do + account_number.refresh + expect(client.account_numbers).to have_received(:get).with('acno_Kasf91034gj1AD') + end + + it 'raises an error if the account number ID does not match' do + wrong_account_number = described_class.new(**data, id: 'wrong_id') + + allow(client.account_numbers) + .to receive(:get) + .with('acno_Kasf91034gj1AD') + .and_return(wrong_account_number) + + expect { account_number.refresh } + .to raise_error(ArgumentError, 'AccountNumber must be the same instance') + end + end + + describe '#update' do + let(:new_metadata) { { user_id: '54321' } } + let(:new_description) { 'New description' } + let(:new_status) { 'disabled' } + let(:updated_data) do + data.merge(description: new_description, status: new_status, metadata: new_metadata) + end + let(:updated_account_number) { described_class.new(**updated_data) } + + before do + allow(client.account_numbers).to receive(:update).and_return(updated_account_number) + end + + it 'updates all provided parameters' do + account_number + .update(description: new_description, status: new_status, metadata: new_metadata) + + expect(account_number.description).to eq(new_description) + expect(account_number.status).to eq(new_status) + expect(account_number.metadata).to eq(new_metadata) + end + + it 'calls update_account_number with all parameters' do + account_number + .update(description: new_description, status: new_status, metadata: new_metadata) + + expect(client.account_numbers).to have_received(:update).with( + account_number.id, + description: new_description, + status: new_status, + metadata: new_metadata + ) + end + end + + describe '#test_mode?' do + it 'returns true when mode is test' do + expect(account_number.test_mode?).to be true + end + + it 'returns false when mode is not test' do + live_account_number = described_class.new(**data, mode: 'live') + expect(live_account_number.test_mode?).to be false + end + end + + describe '#simulate_receive_transfer' do + let(:expected_transfer) { instance_double(Fintoc::V2::Transfer) } + + context 'when in test mode' do + before do + allow(client.simulate) + .to receive(:receive_transfer) + .with(account_number_id: account_number.id, amount: 10000, currency: 'MXN') + .and_return(expected_transfer) + end + + it 'simulates receiving a transfer using account number id' do + result = account_number.simulate_receive_transfer(amount: 10000) + expect(result).to eq(expected_transfer) + end + end + + context 'when not in test mode' do + let(:live_account_number) { described_class.new(**data, mode: 'live', client: client) } + + it 'raises an error' do + expect { live_account_number.simulate_receive_transfer(amount: 10000) } + .to raise_error( + Fintoc::Errors::InvalidRequestError, + /Simulation is only available in test mode/ + ) + end + end + end +end diff --git a/spec/lib/fintoc/v2/account_spec.rb b/spec/lib/fintoc/v2/account_spec.rb new file mode 100644 index 0000000..956af9c --- /dev/null +++ b/spec/lib/fintoc/v2/account_spec.rb @@ -0,0 +1,213 @@ +require 'fintoc/v2/resources/account' + +RSpec.describe Fintoc::V2::Account do + let(:api_key) { 'sk_test_SeCreT-aPi_KeY' } + let(:client) { Fintoc::V2::Client.new(api_key) } + + let(:entity_data) do + { + id: 'ent_12345', + holder_name: 'ACME Inc.', + holder_id: 'ND' + } + end + + let(:data) do + { + object: 'account', + mode: 'test', + id: 'acc_123', + description: 'My root account', + available_balance: 23459183, + currency: 'MXN', + is_root: true, + root_account_number_id: 'acno_Kasf91034gj1AD', + root_account_number: '738969123456789120', + status: 'active', + entity: entity_data, + client: client + } + end + + let(:account) { described_class.new(**data) } + + describe '#new' do + it 'creates an instance of Account' do + expect(account).to be_an_instance_of(described_class) + end + + it 'sets all attributes correctly' do # rubocop:disable RSpec/ExampleLength + expect(account).to have_attributes( + object: 'account', + mode: 'test', + id: 'acc_123', + description: 'My root account', + available_balance: 23459183, + currency: 'MXN', + is_root: true, + root_account_number_id: 'acno_Kasf91034gj1AD', + root_account_number: '738969123456789120', + status: 'active', + entity: entity_data + ) + end + end + + describe '#to_s' do + context 'with MXN currency' do + it 'returns a formatted string representation with MXN currency' do + expect(account.to_s).to eq('πŸ’° My root account (acc_123) - $234,591.83') + end + end + + context 'with CLP currency' do + let(:clp_data) { data.merge(currency: 'CLP', available_balance: 1000000) } + let(:clp_account) { described_class.new(**clp_data) } + + it 'returns a formatted string representation with CLP currency' do + expect(Money.from_cents(1000000, 'CLP').currency.thousands_separator).to eq('.') + expect(clp_account.to_s).to eq('πŸ’° My root account (acc_123) - $1.000.000') + end + end + end + + describe 'status methods' do + describe '#active?' do + it 'returns true when status is active' do + expect(account.active?).to be true + end + + it 'returns false when status is not active' do + blocked_account = described_class.new(**data, status: 'blocked') + expect(blocked_account.active?).to be false + end + end + + describe '#blocked?' do + it 'returns false when status is active' do + expect(account.blocked?).to be false + end + + it 'returns true when status is blocked' do + blocked_account = described_class.new(**data, status: 'blocked') + expect(blocked_account.blocked?).to be true + end + end + + describe '#closed?' do + it 'returns false when status is active' do + expect(account.closed?).to be false + end + + it 'returns true when status is closed' do + closed_account = described_class.new(**data, status: 'closed') + expect(closed_account.closed?).to be true + end + end + end + + describe '#refresh' do + let(:updated_data) { data.merge(description: 'Updated account description') } + + let(:updated_account) { described_class.new(**updated_data, client: client) } + + before do + allow(client.accounts).to receive(:get).with('acc_123').and_return(updated_account) + end + + it 'refreshes the account with updated data from the API' do + expect(account.description).to eq('My root account') + + account.refresh + + expect(client.accounts).to have_received(:get).with('acc_123') + + expect(account.description).to eq('Updated account description') + end + + it 'raises an error if the account ID does not match' do + wrong_account = described_class.new(**data, id: 'wrong_id') + + allow(client.accounts).to receive(:get).with('acc_123').and_return(wrong_account) + + expect { account.refresh }.to raise_error(ArgumentError, 'Account must be the same instance') + end + end + + describe '#update' do + let(:updated_data) { data.merge(description: 'New account description') } + + let(:updated_account) { described_class.new(**updated_data, client: client) } + + before do + allow(client.accounts).to receive(:update) do |_id, params| + updated_data_for_call = { **data, **params } + described_class.new(**updated_data_for_call, client: client) + end + end + + it 'updates the account description and refreshes the instance' do + expect(account.description).to eq('My root account') + + account.update(description: 'New account description') + + expect(client.accounts) + .to have_received(:update) + .with('acc_123', description: 'New account description') + + expect(account.description).to eq('New account description') + end + + it 'only sends provided parameters' do + account.update(description: 'Test description') + expect(client.accounts) + .to have_received(:update) + .with('acc_123', description: 'Test description') + end + end + + describe '#test_mode?' do + it 'returns true when mode is test' do + expect(account.test_mode?).to be true + end + + it 'returns false when mode is not test' do + live_account = described_class.new(**data, mode: 'live') + expect(live_account.test_mode?).to be false + end + end + + describe '#simulate_receive_transfer' do + let(:expected_transfer) { instance_double(Fintoc::V2::Transfer) } + + context 'when in test mode' do + before do + allow(client.simulate) + .to receive(:receive_transfer) + .with( + account_number_id: account.root_account_number_id, + amount: 10000, + currency: account.currency + ) + .and_return(expected_transfer) + end + + it 'simulates receiving a transfer using account currency' do + result = account.simulate_receive_transfer(amount: 10000) + expect(result).to eq(expected_transfer) + end + end + + context 'when not in test mode' do + let(:live_account) { described_class.new(**data, mode: 'live', client: client) } + + it 'raises an error' do + expect { live_account.simulate_receive_transfer(amount: 10000) } + .to raise_error( + Fintoc::Errors::InvalidRequestError, + /Simulation is only available in test mode/ + ) + end + end + end +end diff --git a/spec/lib/fintoc/v2/account_verification_spec.rb b/spec/lib/fintoc/v2/account_verification_spec.rb new file mode 100644 index 0000000..50bf6e6 --- /dev/null +++ b/spec/lib/fintoc/v2/account_verification_spec.rb @@ -0,0 +1,152 @@ +require 'fintoc/v2/resources/account_verification' + +RSpec.describe Fintoc::V2::AccountVerification do + let(:api_key) { 'sk_test_SeCreT-aPi_KeY' } + let(:client) { Fintoc::V2::Client.new(api_key) } + + let(:counterparty_data) do + { + account_number: '123456789123455789', + holder_id: 'FLA1234567890', + holder_name: 'Carmen Marcela', + account_type: 'clabe', + institution: { + id: 'mx_banco_bbva', + name: 'BBVA Mexico', + country: 'mx' + } + } + end + + let(:data) do + { + object: 'account_verification', + id: 'accv_fdme30s11j5k7l1mekq4', + status: 'succeeded', + reason: nil, + transfer_id: 'tr_fdskjldasjkl', + counterparty: counterparty_data, + mode: 'test', + receipt_url: 'https://www.banxico.org.mx/cep/', + transaction_date: '2020-04-17T00:00:00.000Z', + client: client + } + end + + let(:account_verification) { described_class.new(**data) } + + describe '#new' do + it 'creates an instance of AccountVerification' do + expect(account_verification).to be_an_instance_of(described_class) + end + + it 'sets all attributes correctly' do # rubocop:disable RSpec/ExampleLength + expect(account_verification).to have_attributes( + object: 'account_verification', + id: 'accv_fdme30s11j5k7l1mekq4', + status: 'succeeded', + reason: nil, + transfer_id: 'tr_fdskjldasjkl', + counterparty: counterparty_data, + mode: 'test', + receipt_url: 'https://www.banxico.org.mx/cep/', + transaction_date: '2020-04-17T00:00:00.000Z' + ) + end + end + + describe '#to_s' do + it 'returns a string representation' do + expect(account_verification.to_s) + .to eq('πŸ” Account Verification (accv_fdme30s11j5k7l1mekq4) - succeeded') + end + end + + describe 'status methods' do + context 'when status is pending' do + let(:pending_verification) { described_class.new(**data, status: 'pending') } + + it '#pending? returns true' do + expect(pending_verification.pending?).to be true + end + + it '#succeeded? returns false' do + expect(pending_verification.succeeded?).to be false + end + + it '#failed? returns false' do + expect(pending_verification.failed?).to be false + end + end + + context 'when status is succeeded' do + it '#pending? returns false' do + expect(account_verification.pending?).to be false + end + + it '#succeeded? returns true' do + expect(account_verification.succeeded?).to be true + end + + it '#failed? returns false' do + expect(account_verification.failed?).to be false + end + end + + context 'when status is failed' do + let(:failed_verification) { described_class.new(**data, status: 'failed') } + + it '#pending? returns false' do + expect(failed_verification.pending?).to be false + end + + it '#succeeded? returns false' do + expect(failed_verification.succeeded?).to be false + end + + it '#failed? returns true' do + expect(failed_verification.failed?).to be true + end + end + end + + describe '#refresh' do + let(:fresh_data) do + data.merge( + status: 'failed', + reason: 'insufficient_funds' + ) + end + + let(:fresh_verification) { described_class.new(**fresh_data) } + + before do + allow(client.account_verifications) + .to receive(:get) + .with('accv_fdme30s11j5k7l1mekq4') + .and_return(fresh_verification) + end + + it 'refreshes the account verification data' do + result = account_verification.refresh + + expect(result).to eq(account_verification) + expect(account_verification).to have_attributes( + status: 'failed', + reason: 'insufficient_funds' + ) + end + + it 'raises an error if the verification ID does not match' do + wrong_verification = described_class.new(**fresh_data, id: 'wrong_id') + + allow(client.account_verifications) + .to receive(:get) + .with('accv_fdme30s11j5k7l1mekq4') + .and_return(wrong_verification) + + expect { account_verification.refresh } + .to raise_error(ArgumentError, 'Account verification must be the same instance') + end + end +end diff --git a/spec/lib/fintoc/v2/client_spec.rb b/spec/lib/fintoc/v2/client_spec.rb new file mode 100644 index 0000000..7c53eb1 --- /dev/null +++ b/spec/lib/fintoc/v2/client_spec.rb @@ -0,0 +1,33 @@ +require 'fintoc/v2/client/client' + +RSpec.describe Fintoc::V2::Client do + let(:api_key) { 'sk_test_SeCreT-aPi_KeY' } + let(:jws_private_key) { nil } + let(:client) { described_class.new(api_key, jws_private_key: jws_private_key) } + + describe '.new' do + it 'creates an instance of TransfersClient' do + expect(client).to be_an_instance_of(described_class) + end + end + + describe '#to_s' do + it 'returns a formatted string representation' do + expect(client.to_s) + .to include('Fintoc::V2::Client') + .and include('πŸ”‘=') + end + end + + it_behaves_like 'a client with entities manager' + + it_behaves_like 'a client with accounts manager' + + it_behaves_like 'a client with account numbers manager' + + it_behaves_like 'a client with transfers manager' + + it_behaves_like 'a client with simulate manager' + + it_behaves_like 'a client with account verifications manager' +end diff --git a/spec/lib/fintoc/v2/entity_spec.rb b/spec/lib/fintoc/v2/entity_spec.rb new file mode 100644 index 0000000..067d3de --- /dev/null +++ b/spec/lib/fintoc/v2/entity_spec.rb @@ -0,0 +1,80 @@ +require 'fintoc/v2/resources/entity' + +RSpec.describe Fintoc::V2::Entity do + let(:api_key) { 'sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx' } + let(:client) { Fintoc::V2::Client.new(api_key) } + + let(:data) do + { + object: 'entity', + mode: 'test', + id: 'ent_12345', + holder_name: 'Test Company LLC', + holder_id: '12345678-9', + is_root: true, + client: client + } + end + + let(:entity) { described_class.new(**data) } + + describe '#new' do + it 'creates an instance of Entity' do + expect(entity).to be_an_instance_of(described_class) + end + + it 'sets all attributes correctly' do + expect(entity).to have_attributes( + object: 'entity', + mode: 'test', + id: 'ent_12345', + holder_name: 'Test Company LLC', + holder_id: '12345678-9', + is_root: true + ) + end + end + + describe '#to_s' do + it 'returns a formatted string representation' do + expect(entity.to_s).to eq('🏒 Test Company LLC (ent_12345)') + end + end + + describe '#refresh' do + let(:updated_data) do + { + **data, + holder_name: 'Updated Company LLC' + } + end + + let(:updated_entity) { described_class.new(**updated_data, client: client) } + + before do + allow(client.entities).to receive(:get).with('ent_12345').and_return(updated_entity) + end + + it 'refreshes the entity with updated data from the API' do + expect(entity).to have_attributes( + holder_name: 'Test Company LLC' + ) + + entity.refresh + + expect(client.entities).to have_received(:get).with('ent_12345') + + expect(entity).to have_attributes( + holder_name: 'Updated Company LLC' + ) + end + + it 'raises an error if the entity ID does not match' do + wrong_entity = described_class.new(**data, id: 'wrong_id') + + allow(client.entities).to receive(:get).with('ent_12345').and_return(wrong_entity) + + expect { entity.refresh }.to raise_error(ArgumentError, 'Entity must be the same instance') + end + end +end diff --git a/spec/lib/fintoc/v2/managers/account_numbers_manager_spec.rb b/spec/lib/fintoc/v2/managers/account_numbers_manager_spec.rb new file mode 100644 index 0000000..9a83494 --- /dev/null +++ b/spec/lib/fintoc/v2/managers/account_numbers_manager_spec.rb @@ -0,0 +1,103 @@ +require 'fintoc/v2/managers/account_numbers_manager' + +RSpec.describe Fintoc::V2::Managers::AccountNumbersManager do + let(:client) { instance_double(Fintoc::BaseClient) } + let(:get_proc) { instance_double(Proc) } + let(:post_proc) { instance_double(Proc) } + let(:patch_proc) { instance_double(Proc) } + let(:manager) { described_class.new(client) } + let(:account_number_id) { 'acno_123' } + let(:account_id) { 'acc_123' } + let(:first_account_number_data) do + { + id: account_number_id, + object: 'account_number', + description: 'My account number', + number: '1234567890', + created_at: '2021-01-01', + updated_at: '2021-01-01', + mode: 'test', + status: 'enabled', + is_root: false, + account_id: account_id, + metadata: {} + } + end + let(:second_account_number_data) do + { + id: 'acno_456', + object: 'account_number', + description: 'My second account number', + number: '0987654321', + created_at: '2021-01-02', + updated_at: '2021-01-02', + mode: 'test', + status: 'enabled', + is_root: false, + account_id: account_id, + metadata: {} + } + end + let(:updated_account_number_data) do + first_account_number_data.merge(description: 'Updated description') + end + + before do + allow(client).to receive(:get).with(version: :v2).and_return(get_proc) + allow(client).to receive(:post).with(version: :v2).and_return(post_proc) + allow(client).to receive(:patch).with(version: :v2).and_return(patch_proc) + + allow(get_proc) + .to receive(:call) + .with("account_numbers/#{account_number_id}") + .and_return(first_account_number_data) + allow(get_proc) + .to receive(:call) + .with('account_numbers') + .and_return([first_account_number_data, second_account_number_data]) + allow(patch_proc) + .to receive(:call) + .with("account_numbers/#{account_number_id}", description: 'Updated description') + .and_return(updated_account_number_data) + allow(post_proc) + .to receive(:call) + .with('account_numbers', account_id:, description: 'My account number', metadata: {}) + .and_return(first_account_number_data) + + allow(Fintoc::V2::AccountNumber).to receive(:new) + end + + describe '#create' do + it 'calls build_account_number with the response' do + manager.create(account_id: 'acc_123', description: 'My account number', metadata: {}) + expect(Fintoc::V2::AccountNumber) + .to have_received(:new).with(**first_account_number_data, client:) + end + end + + describe '#get' do + it 'calls build_account_number with the response' do + manager.get('acno_123') + expect(Fintoc::V2::AccountNumber) + .to have_received(:new).with(**first_account_number_data, client:) + end + end + + describe '#list' do + it 'calls build_account_number for each response' do + manager.list + expect(Fintoc::V2::AccountNumber) + .to have_received(:new).with(**first_account_number_data, client:) + expect(Fintoc::V2::AccountNumber) + .to have_received(:new).with(**second_account_number_data, client:) + end + end + + describe '#update' do + it 'calls build_account_number with the response' do + manager.update('acno_123', description: 'Updated description') + expect(Fintoc::V2::AccountNumber) + .to have_received(:new).with(**updated_account_number_data, client:) + end + end +end diff --git a/spec/lib/fintoc/v2/managers/account_verifications_manager_spec.rb b/spec/lib/fintoc/v2/managers/account_verifications_manager_spec.rb new file mode 100644 index 0000000..5c524f4 --- /dev/null +++ b/spec/lib/fintoc/v2/managers/account_verifications_manager_spec.rb @@ -0,0 +1,72 @@ +require 'fintoc/v2/managers/account_verifications_manager' + +RSpec.describe Fintoc::V2::Managers::AccountVerificationsManager do + let(:client) { instance_double(Fintoc::BaseClient) } + let(:get_proc) { instance_double(Proc) } + let(:post_proc) { instance_double(Proc) } + let(:manager) { described_class.new(client) } + let(:account_verification_id) { 'accv_123' } + let(:account_number) { '735969000000203226' } + let(:first_account_verification_data) do + { + id: account_verification_id, + object: 'account_verification', + account_number: account_number, + status: 'pending' + } + end + let(:second_account_verification_data) do + { + id: 'accv_456', + object: 'account_verification', + account_number: account_number, + status: 'pending' + } + end + + before do + allow(client).to receive(:get).with(version: :v2).and_return(get_proc) + allow(client).to receive(:post).with(version: :v2, use_jws: true).and_return(post_proc) + + allow(get_proc) + .to receive(:call) + .with("account_verifications/#{account_verification_id}") + .and_return(first_account_verification_data) + allow(get_proc) + .to receive(:call) + .with('account_verifications') + .and_return([first_account_verification_data, second_account_verification_data]) + allow(post_proc) + .to receive(:call) + .with('account_verifications', account_number:) + .and_return(first_account_verification_data) + + allow(Fintoc::V2::AccountVerification).to receive(:new) + end + + describe '#create' do + it 'calls build_account_verification with the response' do + manager.create(account_number: '735969000000203226') + expect(Fintoc::V2::AccountVerification) + .to have_received(:new).with(**first_account_verification_data, client:) + end + end + + describe '#get' do + it 'calls build_account_verification with the response' do + manager.get('accv_123') + expect(Fintoc::V2::AccountVerification) + .to have_received(:new).with(**first_account_verification_data, client:) + end + end + + describe '#list' do + it 'calls build_account_verification for each response item' do + manager.list + expect(Fintoc::V2::AccountVerification) + .to have_received(:new).with(**first_account_verification_data, client:) + expect(Fintoc::V2::AccountVerification) + .to have_received(:new).with(**second_account_verification_data, client:) + end + end +end diff --git a/spec/lib/fintoc/v2/managers/accounts_manager_spec.rb b/spec/lib/fintoc/v2/managers/accounts_manager_spec.rb new file mode 100644 index 0000000..a13e1df --- /dev/null +++ b/spec/lib/fintoc/v2/managers/accounts_manager_spec.rb @@ -0,0 +1,111 @@ +require 'fintoc/v2/managers/accounts_manager' + +RSpec.describe Fintoc::V2::Managers::AccountsManager do + let(:client) { instance_double(Fintoc::BaseClient) } + let(:get_proc) { instance_double(Proc) } + let(:post_proc) { instance_double(Proc) } + let(:patch_proc) { instance_double(Proc) } + let(:manager) { described_class.new(client) } + let(:account_id) { 'acc_123' } + let(:entity_id) { 'ent_123' } + let(:first_account_data) do + { + id: account_id, + name: 'My account', + official_name: 'My account', + number: '1234567890', + holder_id: '1234567890', + holder_name: 'My account', + type: 'checking', + currency: 'MXN', + refreshed_at: '2021-01-01', + balance: { + available: 100000, + current: 100000, + limit: 0 + }, + movements: [] + } + end + let(:second_account_data) do + { + id: 'acc_456', + name: 'My second account', + official_name: 'My second account', + number: '0987654321', + holder_id: '1234567890', + holder_name: 'My account', + type: 'checking', + currency: 'MXN', + refreshed_at: '2021-01-01', + balance: { + available: 100000, + current: 100000, + limit: 0 + }, + movements: [] + } + end + let(:updated_account_data) do + first_account_data.merge(name: 'Updated name') + end + + before do + allow(client).to receive(:get).with(version: :v2).and_return(get_proc) + allow(client).to receive(:post).with(version: :v2).and_return(post_proc) + allow(client).to receive(:patch).with(version: :v2).and_return(patch_proc) + + allow(get_proc) + .to receive(:call) + .with("accounts/#{account_id}") + .and_return(first_account_data) + allow(get_proc) + .to receive(:call) + .with('accounts') + .and_return([first_account_data, second_account_data]) + allow(patch_proc) + .to receive(:call) + .with("accounts/#{account_id}", name: 'Updated name') + .and_return(updated_account_data) + allow(post_proc) + .to receive(:call) + .with('accounts', entity_id:, description: 'My account') + .and_return(first_account_data) + + allow(Fintoc::V2::Account).to receive(:new) + end + + describe '#create' do + it 'calls build_account with the response' do + manager.create(entity_id:, description: 'My account') + expect(Fintoc::V2::Account) + .to have_received(:new).with(**first_account_data, client:) + end + end + + describe '#get' do + it 'calls build_account with the response' do + manager.get(account_id) + expect(Fintoc::V2::Account) + .to have_received(:new).with(**first_account_data, client:) + end + end + + describe '#list' do + it 'calls build_account for each response' do + manager.list + expect(Fintoc::V2::Account) + .to have_received(:new).with(**first_account_data, client:) + expect(Fintoc::V2::Account) + .to have_received(:new).with(**second_account_data, client:) + end + end + + describe '#update' do + it 'calls build_account with the response' do + manager.update(account_id, name: 'Updated name') + expect(Fintoc::V2::Account) + .to have_received(:new).with(**updated_account_data, client:) + end + end +end diff --git a/spec/lib/fintoc/v2/managers/entities_manager_spec.rb b/spec/lib/fintoc/v2/managers/entities_manager_spec.rb new file mode 100644 index 0000000..8a27b47 --- /dev/null +++ b/spec/lib/fintoc/v2/managers/entities_manager_spec.rb @@ -0,0 +1,68 @@ +require 'fintoc/v2/managers/entities_manager' + +RSpec.describe Fintoc::V2::Managers::EntitiesManager do + let(:client) { instance_double(Fintoc::BaseClient) } + let(:get_proc) { instance_double(Proc) } + + let(:manager) { described_class.new(client) } + let(:entity_id) { 'ent_31t0VhhrAXASFQTVYfCfIBnljbT' } + + let(:first_entity_data) do + { + object: 'entity', + mode: 'live', + id: entity_id, + holder_name: 'Fintoc', + holder_id: '12345678-9', + is_root: true + } + end + + let(:second_entity_data) do + { + object: 'entity', + mode: 'live', + id: 'ent_1234567890', + holder_name: 'Fintoc', + holder_id: '12345678-9', + is_root: false + } + end + + let(:entities_data) { [first_entity_data, second_entity_data] } + + before do + allow(client).to receive(:get).with(version: :v2).and_return(get_proc) + + allow(get_proc).to receive(:call).with("entities/#{entity_id}").and_return(first_entity_data) + allow(get_proc).to receive(:call).with('entities').and_return(entities_data) + + allow(Fintoc::V2::Entity).to receive(:new) + end + + describe '#get' do + it 'fetches and builds an entity' do + manager.get(entity_id) + + expect(Fintoc::V2::Entity).to have_received(:new).with(**first_entity_data, client:) + end + end + + describe '#list' do + it 'fetches and builds a list of entities' do + manager.list + + expect(Fintoc::V2::Entity).to have_received(:new).with(**first_entity_data, client:) + expect(Fintoc::V2::Entity).to have_received(:new).with(**second_entity_data, client:) + end + + it 'passes parameters to the API call' do + params = { page: 2, per_page: 50 } + allow(get_proc).to receive(:call).with('entities', **params).and_return(entities_data) + + manager.list(**params) + + expect(get_proc).to have_received(:call).with('entities', **params) + end + end +end diff --git a/spec/lib/fintoc/v2/managers/simulate_manager_spec.rb b/spec/lib/fintoc/v2/managers/simulate_manager_spec.rb new file mode 100644 index 0000000..7e289d7 --- /dev/null +++ b/spec/lib/fintoc/v2/managers/simulate_manager_spec.rb @@ -0,0 +1,39 @@ +require 'fintoc/v2/managers/simulate_manager' + +RSpec.describe Fintoc::V2::Managers::SimulateManager do + let(:client) { instance_double(Fintoc::BaseClient) } + let(:post_proc) { instance_double(Proc) } + let(:manager) { described_class.new(client) } + let(:account_number_id) { 'acno_123' } + let(:amount) { 10000 } + let(:currency) { 'MXN' } + let(:transfer_data) do + { + id: 'trf_123', + object: 'transfer', + amount: 10000, + currency: 'MXN', + account_id: 'acc_123', + reference_id: '123456' + } + end + + before do + allow(client).to receive(:post).with(version: :v2).and_return(post_proc) + + allow(post_proc) + .to receive(:call) + .with('simulate/receive_transfer', account_number_id:, amount:, currency:) + .and_return(transfer_data) + + allow(Fintoc::V2::Transfer).to receive(:new) + end + + describe '#receive_transfer' do + it 'calls build_transfer with the response' do + manager.receive_transfer(account_number_id:, amount:, currency:) + + expect(Fintoc::V2::Transfer).to have_received(:new).with(**transfer_data, client:) + end + end +end diff --git a/spec/lib/fintoc/v2/managers/transfers_manager_spec.rb b/spec/lib/fintoc/v2/managers/transfers_manager_spec.rb new file mode 100644 index 0000000..ef1ebdf --- /dev/null +++ b/spec/lib/fintoc/v2/managers/transfers_manager_spec.rb @@ -0,0 +1,107 @@ +require 'fintoc/v2/managers/account_verifications_manager' + +RSpec.describe Fintoc::V2::Managers::TransfersManager do + let(:client) { instance_double(Fintoc::BaseClient) } + let(:get_proc) { instance_double(Proc) } + let(:post_proc) { instance_double(Proc) } + let(:manager) { described_class.new(client) } + let(:counterparty) do + { + holder_id: 'LFHU290523OG0', + holder_name: 'Jon Snow', + account_number: '735969000000203297', + account_type: 'clabe', + institution_id: '40012' + } + end + let(:transfer_id) { 'trf_123' } + let(:first_transfer_data) do + { + id: transfer_id, + object: 'transfer', + amount: 10000, + currency: 'MXN', + account_id: 'acc_123', + counterparty:, + reference_id: '123456' + } + end + let(:second_transfer_data) do + { + id: 'trf_456', + object: 'transfer', + amount: 50000, + currency: 'MXN', + account_id: 'acc_456', + counterparty:, + reference_id: '123457' + } + end + let(:returned_transfer_data) do + first_transfer_data.merge(status: 'returned') + end + + before do + allow(client).to receive(:get).with(version: :v2).and_return(get_proc) + allow(client).to receive(:post).with(version: :v2, use_jws: true).and_return(post_proc) + + allow(get_proc) + .to receive(:call) + .with("transfers/#{transfer_id}") + .and_return(first_transfer_data) + allow(get_proc) + .to receive(:call) + .with('transfers') + .and_return([first_transfer_data, second_transfer_data]) + allow(post_proc) + .to receive(:call) + .with( + 'transfers', + amount: 10000, + currency: 'MXN', + account_id: 'acc_123', + counterparty: + ) + .and_return(first_transfer_data) + allow(post_proc) + .to receive(:call) + .with('transfers/return', transfer_id:) + .and_return(returned_transfer_data) + + allow(Fintoc::V2::Transfer).to receive(:new) + end + + describe '#create' do + it 'calls build_transfer with the response' do + manager.create(amount: 10000, currency: 'MXN', account_id: 'acc_123', counterparty:) + expect(Fintoc::V2::Transfer) + .to have_received(:new).with(**first_transfer_data, client:) + end + end + + describe '#get' do + it 'calls build_transfer with the response' do + manager.get('trf_123') + expect(Fintoc::V2::Transfer) + .to have_received(:new).with(**first_transfer_data, client:) + end + end + + describe '#list' do + it 'calls build_transfer for each response item' do + manager.list + expect(Fintoc::V2::Transfer) + .to have_received(:new).with(**first_transfer_data, client:) + expect(Fintoc::V2::Transfer) + .to have_received(:new).with(**second_transfer_data, client:) + end + end + + describe '#return' do + it 'calls build_transfer with the response' do + manager.return('trf_123') + expect(Fintoc::V2::Transfer) + .to have_received(:new).with(**returned_transfer_data, client:) + end + end +end diff --git a/spec/lib/fintoc/v2/transfer_spec.rb b/spec/lib/fintoc/v2/transfer_spec.rb new file mode 100644 index 0000000..a3ca8e2 --- /dev/null +++ b/spec/lib/fintoc/v2/transfer_spec.rb @@ -0,0 +1,261 @@ +require 'fintoc/v2/resources/transfer' + +RSpec.describe Fintoc::V2::Transfer do + let(:api_key) { 'sk_test_SeCreT-aPi_KeY' } + let(:client) { Fintoc::V2::Client.new(api_key) } + + let(:counterparty_data) do + { + holder_id: 'LFHU290523OG0', + holder_name: 'Jon Snow', + account_number: '735969000000203297', + account_type: 'clabe', + institution: { + id: '40012', + name: 'BBVA MEXICO', + country: 'mx' + } + } + end + + let(:account_number_data) do + { + id: 'acno_326dzRGqxLee3j9TkaBBBMfs2i0', + object: 'account_number', + account_id: 'acc_31yYL7h9LVPg121AgFtCyJPDsgM', + description: 'Mis payins', + number: '735969000000203365', + created_at: '2024-03-01T20:09:42.949787176Z', + mode: 'test', + metadata: { + id_cliente: '12343212' + } + } + end + + let(:data) do + { + id: 'tr_329NGN1M4If6VvcMRALv4gjAQJx', + object: 'transfer', + amount: 59013, + currency: 'MXN', + direction: 'outbound', + status: 'pending', + mode: 'test', + post_date: nil, + transaction_date: nil, + comment: 'Pago de credito 10451', + reference_id: '150195', + receipt_url: nil, + tracking_key: nil, + return_reason: nil, + counterparty: counterparty_data, + account_number: account_number_data, + metadata: {}, + created_at: '2020-04-17T00:00:00.000Z', + client: client + } + end + + let(:transfer) { described_class.new(**data) } + + describe '#new' do + it 'creates an instance of Transfer' do + expect(transfer).to be_an_instance_of(described_class) + end + + it 'sets all attributes correctly' do # rubocop:disable RSpec/ExampleLength + expect(transfer).to have_attributes( + id: 'tr_329NGN1M4If6VvcMRALv4gjAQJx', + object: 'transfer', + amount: 59013, + currency: 'MXN', + direction: 'outbound', + status: 'pending', + mode: 'test', + post_date: nil, + transaction_date: nil, + comment: 'Pago de credito 10451', + reference_id: '150195', + receipt_url: nil, + tracking_key: nil, + return_reason: nil, + counterparty: counterparty_data, + account_number: account_number_data, + metadata: {}, + created_at: '2020-04-17T00:00:00.000Z' + ) + end + end + + describe '#to_s' do + it 'returns a string representation' do + expect(transfer.to_s) + .to include('⬆️') + .and include('tr_329NGN1M4If6VvcMRALv4gjAQJx') + .and include('pending') + end + + context 'when transfer is inbound' do + before { data[:direction] = 'inbound' } + + it 'uses the inbound arrow' do + expect(transfer.to_s).to include('⬇️') + end + end + end + + describe 'status predicates' do + it 'responds to status predicate methods' do + expect(transfer) + .to respond_to(:pending?) + .and respond_to(:succeeded?) + .and respond_to(:failed?) + .and respond_to(:returned?) + .and respond_to(:return_pending?) + .and respond_to(:rejected?) + end + + it 'returns correct status for pending transfer' do # rubocop:disable RSpec/MultipleExpectations + expect(transfer).to be_pending + expect(transfer).not_to be_succeeded + expect(transfer).not_to be_failed + expect(transfer).not_to be_returned + expect(transfer).not_to be_return_pending + expect(transfer).not_to be_rejected + end + + context 'when status is succeeded' do + before { data[:status] = 'succeeded' } + + it 'returns correct status' do # rubocop:disable RSpec/MultipleExpectations + expect(transfer).not_to be_pending + expect(transfer).to be_succeeded + expect(transfer).not_to be_failed + expect(transfer).not_to be_returned + expect(transfer).not_to be_return_pending + expect(transfer).not_to be_rejected + end + end + + context 'when status is failed' do + before { data[:status] = 'failed' } + + it 'returns correct status' do # rubocop:disable RSpec/MultipleExpectations + expect(transfer).not_to be_pending + expect(transfer).not_to be_succeeded + expect(transfer).to be_failed + expect(transfer).not_to be_returned + expect(transfer).not_to be_return_pending + expect(transfer).not_to be_rejected + end + end + + context 'when status is returned' do + before { data[:status] = 'returned' } + + it 'returns correct status' do # rubocop:disable RSpec/MultipleExpectations + expect(transfer).not_to be_pending + expect(transfer).not_to be_succeeded + expect(transfer).not_to be_failed + expect(transfer).to be_returned + expect(transfer).not_to be_return_pending + expect(transfer).not_to be_rejected + end + end + + context 'when status is return_pending' do + before { data[:status] = 'return_pending' } + + it 'returns correct status' do # rubocop:disable RSpec/MultipleExpectations + expect(transfer).not_to be_pending + expect(transfer).not_to be_succeeded + expect(transfer).not_to be_failed + expect(transfer).not_to be_returned + expect(transfer).to be_return_pending + expect(transfer).not_to be_rejected + end + end + + context 'when status is rejected' do + before { data[:status] = 'rejected' } + + it 'returns correct status' do # rubocop:disable RSpec/MultipleExpectations + expect(transfer).not_to be_pending + expect(transfer).not_to be_succeeded + expect(transfer).not_to be_failed + expect(transfer).not_to be_returned + expect(transfer).not_to be_return_pending + expect(transfer).to be_rejected + end + end + end + + describe 'direction predicates' do + it 'responds to direction predicate methods' do + expect(transfer) + .to respond_to(:inbound?) + .and respond_to(:outbound?) + end + + it 'returns correct direction for outbound transfer' do + expect(transfer).to be_outbound + expect(transfer).not_to be_inbound + end + + context 'when direction is inbound' do + before { data[:direction] = 'inbound' } + + it 'returns correct direction' do + expect(transfer).to be_inbound + expect(transfer).not_to be_outbound + end + end + end + + describe '#refresh' do + let(:refreshed_data) { data.merge(status: 'succeeded') } + let(:refreshed_transfer) { described_class.new(**refreshed_data) } + + before do + allow(client.transfers).to receive(:get).with(data[:id]).and_return(refreshed_transfer) + end + + it 'refreshes the transfer data' do + expect { transfer.refresh }.to change { transfer.status }.from('pending').to('succeeded') + end + + it 'returns self' do + expect(transfer.refresh).to eq(transfer) + end + + it 'raises an error if the transfer ID does not match' do + wrong_transfer = described_class.new(**data, id: 'wrong_id') + + allow(client.transfers) + .to receive(:get).with('tr_329NGN1M4If6VvcMRALv4gjAQJx').and_return(wrong_transfer) + + expect { transfer.refresh } + .to raise_error(ArgumentError, 'Transfer must be the same instance') + end + end + + describe '#return_transfer' do + let(:returned_data) { data.merge(status: 'return_pending') } + let(:returned_transfer) { described_class.new(**returned_data) } + + before do + allow(client.transfers).to receive(:return).with(data[:id]).and_return(returned_transfer) + end + + it 'returns the transfer and updates status' do + expect { transfer.return_transfer } + .to change { transfer.status }.from('pending').to('return_pending') + end + + it 'returns self' do + expect(transfer.return_transfer).to eq(transfer) + expect(transfer.status).to eq('return_pending') + end + end +end diff --git a/spec/lib/fintoc/webhook_signature_spec.rb b/spec/lib/fintoc/webhook_signature_spec.rb new file mode 100644 index 0000000..7d2cb22 --- /dev/null +++ b/spec/lib/fintoc/webhook_signature_spec.rb @@ -0,0 +1,132 @@ +require 'fintoc/webhook_signature' + +RSpec.describe Fintoc::WebhookSignature do + let(:secret) { 'test_secret_key' } + let(:payload) { '{"test": "payload"}' } + let(:frozen_time) { Time.parse('2025-09-05 10:10:10 UTC') } + let(:timestamp) { frozen_time.to_i } + let(:signature) { OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, "#{timestamp}.#{payload}") } + let(:valid_header) { "t=#{timestamp},v1=#{signature}" } + + before do + allow(Time).to receive_messages(current: frozen_time, now: frozen_time) + end + + describe '.verify_header' do + context 'when signature is valid' do + it 'returns true' do + expect(described_class.verify_header(payload, valid_header, secret)).to be true + end + + it 'returns true with custom tolerance' do + expect(described_class.verify_header(payload, valid_header, secret, 600)).to be true + end + + it 'returns true when tolerance is nil (no timestamp verification)' do + old_timestamp = timestamp - 1000 + old_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, + "#{old_timestamp}.#{payload}") + old_header = "t=#{old_timestamp},v1=#{old_signature}" + + expect(described_class.verify_header(payload, old_header, secret, nil)).to be true + end + end + + context 'when header does not have a timestamp' do + let(:invalid_header) { "v1=#{signature}" } + + it 'raises WebhookSignatureError' do + expect do + described_class.verify_header(payload, invalid_header, secret) + end.to raise_error( + Fintoc::Errors::WebhookSignatureError, + "\nUnable to extract timestamp and signatures from header\n " \ + 'Please check the docs at: https://docs.fintoc.com/reference/errors' + ) + end + end + + context 'when timestamp is too old' do + let(:old_timestamp) { timestamp - 400 } + let(:old_signature) do + OpenSSL::HMAC.hexdigest( + OpenSSL::Digest.new('sha256'), + secret, + "#{old_timestamp}.#{payload}" + ) + end + let(:old_header) { "t=#{old_timestamp},v1=#{old_signature}" } + + it 'raises WebhookSignatureError with default tolerance' do + expect do + described_class.verify_header(payload, old_header, secret) + end.to raise_error( + Fintoc::Errors::WebhookSignatureError, + "\nTimestamp outside the tolerance zone (#{old_timestamp})\n " \ + 'Please check the docs at: https://docs.fintoc.com/reference/errors' + ) + end + + it 'raises WebhookSignatureError with custom tolerance' do + expect do + described_class.verify_header(payload, old_header, secret, 100) + end.to raise_error( + Fintoc::Errors::WebhookSignatureError, + "\nTimestamp outside the tolerance zone (#{old_timestamp})\n " \ + 'Please check the docs at: https://docs.fintoc.com/reference/errors' + ) + end + end + + context 'when header does not contain signature scheme' do + let(:header_without_scheme) { "t=#{timestamp}" } + + it 'raises WebhookSignatureError' do + expect do + described_class.verify_header(payload, header_without_scheme, secret) + end.to raise_error( + Fintoc::Errors::WebhookSignatureError, + "\nNo v1 signature found\n " \ + 'Please check the docs at: https://docs.fintoc.com/reference/errors' + ) + end + end + + context 'when signature and expected signature do not match' do + let(:wrong_signature) { 'wrong_signature_value' } + let(:invalid_header) { "t=#{timestamp},v1=#{wrong_signature}" } + + it 'raises WebhookSignatureError' do + expect do + described_class.verify_header(payload, invalid_header, secret) + end.to raise_error( + Fintoc::Errors::WebhookSignatureError, + "\nSignature mismatch\n " \ + 'Please check the docs at: https://docs.fintoc.com/reference/errors' + ) + end + end + + context 'with different signature schemes' do + it 'ignores non-v1 signatures and uses v1' do + header_with_multiple = "t=#{timestamp},v0=wrong_signature,v1=#{signature},v2=another_wrong" + + expect(described_class.verify_header(payload, header_with_multiple, secret)).to be true + end + end + + context 'with malformed headers' do + it 'raises WebhookSignatureError' do + header_empty_timestamp = "t=,v1=#{signature}" + + expect do + described_class.verify_header(payload, header_empty_timestamp, secret) + end.to raise_error( + Fintoc::Errors::WebhookSignatureError, + "\nUnable to extract timestamp and signatures from header\n " \ + 'Please check the docs at: https://docs.fintoc.com/reference/errors' + ) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index de398e8..1ff67c5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,13 @@ require 'bundler/setup' +require 'tasks/simplecov_config' require 'fintoc' require 'vcr' +# Load shared examples +Dir[File.join(File.dirname(__FILE__), 'support', 'shared_examples', '**', '*.rb')].each do |f| + require f +end + VCR.configure do |c| vcr_mode = /rec/i.match?(ENV.fetch('VCR_MODE', nil)) ? :all : :once diff --git a/spec/support/shared_examples/clients/account_numbers_client_examples.rb b/spec/support/shared_examples/clients/account_numbers_client_examples.rb new file mode 100644 index 0000000..8a96a9e --- /dev/null +++ b/spec/support/shared_examples/clients/account_numbers_client_examples.rb @@ -0,0 +1,71 @@ +RSpec.shared_examples 'a client with account numbers manager' do + let(:account_number_id) { 'acno_326dzRGqxLee3j9TkaBBBMfs2i0' } + let(:account_id) { 'acc_31yYL7h9LVPg121AgFtCyJPDsgM' } + + it 'responds to account number-specific methods' do + expect(client).to respond_to(:account_numbers) + expect(client.account_numbers).to be_a(Fintoc::V2::Managers::AccountNumbersManager) + expect(client.account_numbers) + .to respond_to(:create) + .and respond_to(:get) + .and respond_to(:list) + .and respond_to(:update) + end + + describe '#account_numbers' do + describe '#create' do + it 'returns an AccountNumber instance', :vcr do + account_number = client.account_numbers.create( + account_id:, description: 'Test account number', metadata: { test_id: '12345' } + ) + + expect(account_number) + .to be_an_instance_of(Fintoc::V2::AccountNumber) + .and have_attributes( + account_id:, + description: 'Test account number', + object: 'account_number' + ) + end + end + + describe '#get' do + it 'returns an AccountNumber instance', :vcr do + account_number = client.account_numbers.get(account_number_id) + + expect(account_number) + .to be_an_instance_of(Fintoc::V2::AccountNumber) + .and have_attributes( + id: account_number_id, + object: 'account_number' + ) + end + end + + describe '#list' do + it 'returns an array of AccountNumber instances', :vcr do + account_numbers = client.account_numbers.list + + expect(account_numbers).to all(be_a(Fintoc::V2::AccountNumber)) + expect(account_numbers.size).to be >= 1 + expect(account_numbers.first.id).to eq(account_number_id) + end + end + + describe '#update' do + it 'returns an updated AccountNumber instance', :vcr do + updated_description = 'Updated account number description' + account_number = client.account_numbers.update( + account_number_id, description: updated_description + ) + + expect(account_number) + .to be_an_instance_of(Fintoc::V2::AccountNumber) + .and have_attributes( + id: account_number_id, + description: updated_description + ) + end + end + end +end diff --git a/spec/support/shared_examples/clients/account_verifications_client_examples.rb b/spec/support/shared_examples/clients/account_verifications_client_examples.rb new file mode 100644 index 0000000..9dd7082 --- /dev/null +++ b/spec/support/shared_examples/clients/account_verifications_client_examples.rb @@ -0,0 +1,93 @@ +require 'openssl' + +RSpec.shared_examples 'a client with account verifications manager' do + let(:account_verification_id) { 'accv_32F2NLQOOwbeOvfuw8Y1zZCfGdw' } + let(:account_number) { '735969000000203226' } + let(:jws_private_key) do + key_string = "-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDNLwwQr/uFToDH +8x1GHlW5gRsngNp8J+sg8vZc5jX+dISZh42CNM2eMSyPOMLSZL08xIA9ISoxjCJb +rpJ7MY9OKOZdrFheclz8e3z/hVcXpmgT0JARYIyKUb2sgoh1JsH3aSQILTc4KuDM +dm0+WIWl9tOqKXm23j9RcYL+WOUCcLYNj3xga39CnXdqT3dy2fMIOJ+vZxfSxPhG +EBTyV6v9jrMbRukhxllTqDc64WkVdt0MOvzFzcSNkGcvHRdrK1w+x5IhfsYGtv+9 +mz+fvmI88qGsb0x8peGVDgWfQjykxrB/8umpQKANn9bqjyay+ogRRwv05dmaB/gm +IycvGXE7AgMBAAECggEAI4gTKcyf3MzkZjvGhP75z175KdUZgMiU4ibQ3POMxBy/ +XaroqXSlatCPK9ojerWxQ5Wvs2ZL3TqsNH49pZHGhD127x/KSci6K4ri8YjQtSq+ ++Tdzy16R194h337XTJpCmqqdb8EMv/BE74NOla5UrpHYw63dAvrnsh3bFlqkhdBZ +E5OBfdLyxGy5FYdewV803a8XGfnDfT7RrsdWhPib8E3i+wix+/dv10AX/+Y6VPpG +2EPXRV63UtmO2EVXyIGT5kSAnzZBJPIB3EYTlm1A86PxQGVD4X8LAUXIj6VRVC8h +B1KXb5YZ9W1vYmKyWUZPyMQHMpUTNGEuU/EtN0aOCQKBgQD+zMd1+3BhoSwBusXb +EK2SBJwn9TfqdUghsFHNz0xjvpAFKpO55qA7XyilZwZeJsOzPQZ33ERCRk18crCd +Q6oWI15xKjPl+Dfxf4UYjokx/iQCCHu8lJ6TXcEwXniIs6CVsUq9QV+s6JBlb3C4 +qD/wwp7VrmbcMLfIUs3nb3tqHQKBgQDOJnGylmqC/l4BCZj9BhLiVg7nioY24lG1 +9DY0/nnnbuMtDQ+8VUKtt93Or8giejOVqj3BZ8/TflkwCQAt9cIvFG50aTVZBo2E +4uPJLGSBLrQqpuUNPR2O239o4RqGgDICkh9TRH9D9GsekLRCgefLRubUyuZZ4pI9 +j1ty2kMpNwKBgGtYJmgEKBJZbkrEPvrNifJMUuVan9X81wiqWaxVOx+CdvZWO6pE +CRk6O8uDHeGofyYR/ZmdiHxLVfWp89ItYYi2GeGfIAIwkpEBYjc4RYB0SwM4Q7js +++mlw+/2vN0KoAqwiIY29nHIAJ1bV6fT6iwqMfRf5yG4vJR+nhR0mQ/ZAoGAfgLJ +5RxEpyXNWF0Bg0i/KlLocWgfelUFFW/d4q7a3TjO7K7bO4fyZjXKA5k3gLup5IZX +kW1fgCvvYIlf7rgWpqiai9XzoiN7RgtaqZHVLZHa12eFA36kHrrVOsq+aBDcgO3I +8CEimetBv0E8rpqxkXQZjWEpRTBVrAOBJsd73ikCgYAwf4fnpTcEa4g6ejmbjkXw +yacTlKFuxyLZHZ5R7D0+Fj19gwm9EzvrRIEcX84ebiJ8P1bL3kycQmLV19zE5B3M +pcsQmZ28/Oa3xCPiy8CDyDuiDbbNfnR1Ot3IbgfFL7xPYySljJbMyl7vhKJIacWs +draAAQ5iJEb5BR8AmL6tAQ== +-----END PRIVATE KEY-----" + OpenSSL::PKey::RSA.new(key_string) + end + + it 'responds to account verification-specific methods' do + expect(client).to respond_to(:account_verifications) + expect(client.account_verifications) + .to be_a(Fintoc::V2::Managers::AccountVerificationsManager) + expect(client.account_verifications) + .to respond_to(:create) + .and respond_to(:get) + .and respond_to(:list) + end + + describe '#account_verifications' do + describe '#create' do + it 'returns an AccountVerification instance', :vcr do + account_verification = client.account_verifications.create(account_number:) + + expect(account_verification) + .to be_an_instance_of(Fintoc::V2::AccountVerification) + .and have_attributes( + object: 'account_verification', + status: 'pending' + ) + end + end + + describe '#get' do + it 'returns an AccountVerification instance', :vcr do + account_verification = client.account_verifications.get(account_verification_id) + + expect(account_verification) + .to be_an_instance_of(Fintoc::V2::AccountVerification) + .and have_attributes( + id: account_verification_id, + object: 'account_verification' + ) + end + end + + describe '#list' do + it 'returns an array of AccountVerification instances', :vcr do + account_verifications = client.account_verifications.list + + expect(account_verifications).to all(be_a(Fintoc::V2::AccountVerification)) + expect(account_verifications.size).to be >= 1 + end + + it 'accepts filtering parameters', :vcr do + account_verifications = client.account_verifications.list( + since: '2020-01-01T00:00:00.000Z', + limit: 10 + ) + + expect(account_verifications).to all(be_a(Fintoc::V2::AccountVerification)) + end + end + end +end diff --git a/spec/support/shared_examples/clients/accounts_client_examples.rb b/spec/support/shared_examples/clients/accounts_client_examples.rb new file mode 100644 index 0000000..d8d5943 --- /dev/null +++ b/spec/support/shared_examples/clients/accounts_client_examples.rb @@ -0,0 +1,67 @@ +RSpec.shared_examples 'a client with accounts manager' do + let(:account_id) { 'acc_31yYL7h9LVPg121AgFtCyJPDsgM' } + let(:entity_id) { 'ent_31t0VhhrAXASFQTVYfCfIBnljbT' } + + it 'responds to account-specific methods' do + expect(client).to respond_to(:accounts) + expect(client.accounts).to be_a(Fintoc::V2::Managers::AccountsManager) + expect(client.accounts) + .to respond_to(:create) + .and respond_to(:get) + .and respond_to(:list) + .and respond_to(:update) + end + + describe '#accounts' do + describe '#create' do + it 'returns an Account instance', :vcr do + account = client.accounts.create(entity_id:, description: 'Test account') + + expect(account) + .to be_an_instance_of(Fintoc::V2::Account) + .and have_attributes( + description: 'Test account', + currency: 'MXN', + status: 'active' + ) + end + end + + describe '#get' do + it 'returns an Account instance', :vcr do + account = client.accounts.get(account_id) + + expect(account) + .to be_an_instance_of(Fintoc::V2::Account) + .and have_attributes( + id: account_id, + description: 'Test account' + ) + end + end + + describe '#list' do + it 'returns an array of Account instances', :vcr do + accounts = client.accounts.list + + expect(accounts).to all(be_a(Fintoc::V2::Account)) + expect(accounts.size).to be >= 1 + expect(accounts.first.id).to eq(account_id) + end + end + + describe '#update' do + it 'returns an updated Account instance', :vcr do + updated_description = 'Updated account description' + account = client.accounts.update(account_id, description: updated_description) + + expect(account) + .to be_an_instance_of(Fintoc::V2::Account) + .and have_attributes( + id: account_id, + description: updated_description + ) + end + end + end +end diff --git a/spec/support/shared_examples/clients/entities_client_examples.rb b/spec/support/shared_examples/clients/entities_client_examples.rb new file mode 100644 index 0000000..d237fb3 --- /dev/null +++ b/spec/support/shared_examples/clients/entities_client_examples.rb @@ -0,0 +1,36 @@ +RSpec.shared_examples 'a client with entities manager' do + let(:entity_id) { 'ent_31t0VhhrAXASFQTVYfCfIBnljbT' } + + it 'provides an entities manager' do + expect(client).to respond_to(:entities) + expect(client.entities).to be_a(Fintoc::V2::Managers::EntitiesManager) + expect(client.entities) + .to respond_to(:get) + .and respond_to(:list) + end + + describe '#entities' do + describe '#get' do + it 'returns an Entity instance', :vcr do + entity = client.entities.get(entity_id) + + expect(entity) + .to be_an_instance_of(Fintoc::V2::Entity) + .and have_attributes( + id: entity_id, + holder_name: 'Fintoc' + ) + end + end + + describe '#list' do + it 'returns an array of Entity instances', :vcr do + entities = client.entities.list + + expect(entities).to all(be_a(Fintoc::V2::Entity)) + expect(entities.size).to eq(1) + expect(entities.first.id).to eq('ent_31t0VhhrAXASFQTVYfCfIBnljbT') + end + end + end +end diff --git a/spec/support/shared_examples/clients/links_client_examples.rb b/spec/support/shared_examples/clients/links_client_examples.rb new file mode 100644 index 0000000..318350e --- /dev/null +++ b/spec/support/shared_examples/clients/links_client_examples.rb @@ -0,0 +1,28 @@ +RSpec.shared_examples 'a client with links manager' do + let(:link_token) { '6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W' } + + it 'responds to link-specific methods' do + expect(client).to respond_to(:links) + expect(client.links).to be_a(Fintoc::V1::Managers::LinksManager) + expect(client.links) + .to respond_to(:get) + .and respond_to(:list) + .and respond_to(:delete) + end + + describe '#links' do + describe '#get' do + it 'get the link from a given link token', :vcr do + link = client.links.get(link_token) + expect(link).to be_an_instance_of(Fintoc::V1::Link) + end + end + + describe '#list' do + it 'get all the links from a given link token', :vcr do + links = client.links.list + expect(links).to all(be_a(Fintoc::V1::Link)) + end + end + end +end diff --git a/spec/support/shared_examples/clients/simulate_client_examples.rb b/spec/support/shared_examples/clients/simulate_client_examples.rb new file mode 100644 index 0000000..ff731be --- /dev/null +++ b/spec/support/shared_examples/clients/simulate_client_examples.rb @@ -0,0 +1,32 @@ +RSpec.shared_examples 'a client with simulate manager' do + it 'responds to simulate-specific methods' do + expect(client).to respond_to(:simulate) + expect(client.simulate).to be_a(Fintoc::V2::Managers::SimulateManager) + expect(client.simulate) + .to respond_to(:receive_transfer) + end + + describe '#simulate' do + describe '#receive_transfer' do + let(:simulate_transfer_data) do + { + account_number_id: 'acno_326dzRGqxLee3j9TkaBBBMfs2i0', + amount: 10000, + currency: 'MXN' + } + end + + it 'simulates receiving a transfer and returns Transfer object', :vcr do + transfer = client.simulate.receive_transfer(**simulate_transfer_data) + + expect(transfer) + .to be_an_instance_of(Fintoc::V2::Transfer) + .and have_attributes( + amount: simulate_transfer_data[:amount], + currency: simulate_transfer_data[:currency], + account_number: include(id: simulate_transfer_data[:account_number_id]) + ) + end + end + end +end diff --git a/spec/support/shared_examples/clients/transfers_client_examples.rb b/spec/support/shared_examples/clients/transfers_client_examples.rb new file mode 100644 index 0000000..03f5fff --- /dev/null +++ b/spec/support/shared_examples/clients/transfers_client_examples.rb @@ -0,0 +1,130 @@ +require 'openssl' + +RSpec.shared_examples 'a client with transfers manager' do + let(:jws_private_key) do + key_string = "-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDNLwwQr/uFToDH +8x1GHlW5gRsngNp8J+sg8vZc5jX+dISZh42CNM2eMSyPOMLSZL08xIA9ISoxjCJb +rpJ7MY9OKOZdrFheclz8e3z/hVcXpmgT0JARYIyKUb2sgoh1JsH3aSQILTc4KuDM +dm0+WIWl9tOqKXm23j9RcYL+WOUCcLYNj3xga39CnXdqT3dy2fMIOJ+vZxfSxPhG +EBTyV6v9jrMbRukhxllTqDc64WkVdt0MOvzFzcSNkGcvHRdrK1w+x5IhfsYGtv+9 +mz+fvmI88qGsb0x8peGVDgWfQjykxrB/8umpQKANn9bqjyay+ogRRwv05dmaB/gm +IycvGXE7AgMBAAECggEAI4gTKcyf3MzkZjvGhP75z175KdUZgMiU4ibQ3POMxBy/ +XaroqXSlatCPK9ojerWxQ5Wvs2ZL3TqsNH49pZHGhD127x/KSci6K4ri8YjQtSq+ ++Tdzy16R194h337XTJpCmqqdb8EMv/BE74NOla5UrpHYw63dAvrnsh3bFlqkhdBZ +E5OBfdLyxGy5FYdewV803a8XGfnDfT7RrsdWhPib8E3i+wix+/dv10AX/+Y6VPpG +2EPXRV63UtmO2EVXyIGT5kSAnzZBJPIB3EYTlm1A86PxQGVD4X8LAUXIj6VRVC8h +B1KXb5YZ9W1vYmKyWUZPyMQHMpUTNGEuU/EtN0aOCQKBgQD+zMd1+3BhoSwBusXb +EK2SBJwn9TfqdUghsFHNz0xjvpAFKpO55qA7XyilZwZeJsOzPQZ33ERCRk18crCd +Q6oWI15xKjPl+Dfxf4UYjokx/iQCCHu8lJ6TXcEwXniIs6CVsUq9QV+s6JBlb3C4 +qD/wwp7VrmbcMLfIUs3nb3tqHQKBgQDOJnGylmqC/l4BCZj9BhLiVg7nioY24lG1 +9DY0/nnnbuMtDQ+8VUKtt93Or8giejOVqj3BZ8/TflkwCQAt9cIvFG50aTVZBo2E +4uPJLGSBLrQqpuUNPR2O239o4RqGgDICkh9TRH9D9GsekLRCgefLRubUyuZZ4pI9 +j1ty2kMpNwKBgGtYJmgEKBJZbkrEPvrNifJMUuVan9X81wiqWaxVOx+CdvZWO6pE +CRk6O8uDHeGofyYR/ZmdiHxLVfWp89ItYYi2GeGfIAIwkpEBYjc4RYB0SwM4Q7js +++mlw+/2vN0KoAqwiIY29nHIAJ1bV6fT6iwqMfRf5yG4vJR+nhR0mQ/ZAoGAfgLJ +5RxEpyXNWF0Bg0i/KlLocWgfelUFFW/d4q7a3TjO7K7bO4fyZjXKA5k3gLup5IZX +kW1fgCvvYIlf7rgWpqiai9XzoiN7RgtaqZHVLZHa12eFA36kHrrVOsq+aBDcgO3I +8CEimetBv0E8rpqxkXQZjWEpRTBVrAOBJsd73ikCgYAwf4fnpTcEa4g6ejmbjkXw +yacTlKFuxyLZHZ5R7D0+Fj19gwm9EzvrRIEcX84ebiJ8P1bL3kycQmLV19zE5B3M +pcsQmZ28/Oa3xCPiy8CDyDuiDbbNfnR1Ot3IbgfFL7xPYySljJbMyl7vhKJIacWs +draAAQ5iJEb5BR8AmL6tAQ== +-----END PRIVATE KEY-----" + OpenSSL::PKey::RSA.new(key_string) + end + let(:transfer_id) { 'tr_329NGN1M4If6VvcMRALv4gjAQJx' } + let(:account_id) { 'acc_31yYL7h9LVPg121AgFtCyJPDsgM' } + + let(:counterparty) do + { + holder_id: 'LFHU290523OG0', + holder_name: 'Jon Snow', + account_number: '735969000000203297', + account_type: 'clabe', + institution_id: '40012' + } + end + + let(:transfer_data) do + { + amount: 50000, + currency: 'MXN', + account_id:, + counterparty:, + comment: 'Test payment', + reference_id: '123456' + } + end + + it 'responds to transfer-specific methods' do + expect(client).to respond_to(:transfers) + expect(client.transfers).to be_a(Fintoc::V2::Managers::TransfersManager) + expect(client.transfers) + .to respond_to(:create) + .and respond_to(:get) + .and respond_to(:list) + .and respond_to(:return) + end + + describe '#transfers' do + describe '#create' do + it 'returns a Transfer instance', :vcr do + transfer = client.transfers.create(**transfer_data) + + expect(transfer) + .to be_an_instance_of(Fintoc::V2::Transfer) + .and have_attributes( + amount: 50000, + currency: 'MXN', + comment: 'Test payment', + reference_id: '123456', + status: 'pending' + ) + end + end + + describe '#get' do + it 'returns a Transfer instance', :vcr do + transfer = client.transfers.get(transfer_id) + + expect(transfer) + .to be_an_instance_of(Fintoc::V2::Transfer) + .and have_attributes( + id: transfer_id, + object: 'transfer' + ) + end + end + + describe '#list' do + it 'returns an array of Transfer instances', :vcr do + transfers = client.transfers.list + + expect(transfers).to all(be_a(Fintoc::V2::Transfer)) + expect(transfers.size).to be >= 1 + end + + it 'accepts filtering parameters', :vcr do + transfers = client.transfers.list(status: 'succeeded', direction: 'outbound') + + expect(transfers).to all(be_a(Fintoc::V2::Transfer)) + expect(transfers).to all(have_attributes(status: 'succeeded', direction: 'outbound')) + end + end + + describe '#return' do + let(:transfer_id) { 'tr_329R3l5JksDkoevCGTOBsugCsnb' } + + it 'returns a Transfer instance with return_pending status', :vcr do + transfer = client.transfers.return(transfer_id) + + expect(transfer) + .to be_an_instance_of(Fintoc::V2::Transfer) + .and have_attributes( + id: transfer_id, + status: 'return_pending' + ) + end + end + end +end diff --git a/spec/vcr/Fintoc_Client/_account/get_a_link_account.yml b/spec/vcr/Fintoc_Client/_account/get_a_link_account.yml deleted file mode 100644 index c64ccff..0000000 --- a/spec/vcr/Fintoc_Client/_account/get_a_link_account.yml +++ /dev/null @@ -1,151 +0,0 @@ ---- -http_interactions: -- request: - method: get - uri: https://api.fintoc.com/v1/links/6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W - body: - encoding: UTF-8 - string: '' - headers: - Authorization: - - sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx - User-Agent: - - fintoc-ruby/0.1.0 - Connection: - - close - Host: - - api.fintoc.com - response: - status: - code: 200 - message: OK - headers: - Date: - - Fri, 11 Sep 2020 18:21:20 GMT - Content-Type: - - application/json; charset=utf-8 - Transfer-Encoding: - - chunked - Connection: - - close - Set-Cookie: - - __cfduid=d2558e1ce5725a4144ffa971d816a8f7a1599848479; expires=Sun, 11-Oct-20 - 18:21:19 GMT; path=/; domain=.fintoc.com; HttpOnly; SameSite=Lax; Secure - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - 1; mode=block - X-Content-Type-Options: - - nosniff - X-Download-Options: - - noopen - X-Permitted-Cross-Domain-Policies: - - none - Referrer-Policy: - - strict-origin-when-cross-origin - Etag: - - W/"94045ec584e62887fbeed1515a5e8029" - Cache-Control: - - max-age=0, private, must-revalidate - X-Request-Id: - - d24b3efd-7ac9-4152-9d3c-a96148e4fdf7 - X-Runtime: - - '0.418800' - Strict-Transport-Security: - - max-age=31536000; includeSubDomains - Vary: - - Origin - Via: - - 1.1 vegur - Cf-Cache-Status: - - DYNAMIC - Cf-Request-Id: - - 051fff93580000f7c2ef9a5200000001 - Expect-Ct: - - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" - Server: - - cloudflare - Cf-Ray: - - 5d1368655d26f7c2-EZE - body: - encoding: UTF-8 - string: '{"id":"6n12zLmai3lLE9Dq","holder_id":"782592211","username":"416148503","holder_type":"business","created_at":"2020-08-18T17:37:24.550Z","institution":{"id":"cl_banco_de_chile","name":"Banco - de Chile","country":"cl"},"link_token":null,"mode":"test","accounts":[{"id":"JjEQx2rPTGGKbrP5","type":"checking_account","number":"813990168","name":"Cuenta - Corriente","official_name":"Cuenta Corriente","balance":{"available":62255521,"current":62255521,"limit":62255521},"holder_id":"416148503","holder_name":"MarΓ­a - del Carmen GirΓ³n Raya","currency":"CLP"}]}' - recorded_at: Fri, 11 Sep 2020 18:21:20 GMT -- request: - method: get - uri: https://api.fintoc.com/v1/links/6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W?link_token=6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W - body: - encoding: UTF-8 - string: '' - headers: - Authorization: - - sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx - User-Agent: - - fintoc-ruby/0.1.0 - Connection: - - close - Host: - - api.fintoc.com - response: - status: - code: 200 - message: OK - headers: - Date: - - Fri, 11 Sep 2020 18:21:21 GMT - Content-Type: - - application/json; charset=utf-8 - Transfer-Encoding: - - chunked - Connection: - - close - Set-Cookie: - - __cfduid=d3f0cd4044924512fd5bddb926ab1fc651599848480; expires=Sun, 11-Oct-20 - 18:21:20 GMT; path=/; domain=.fintoc.com; HttpOnly; SameSite=Lax; Secure - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - 1; mode=block - X-Content-Type-Options: - - nosniff - X-Download-Options: - - noopen - X-Permitted-Cross-Domain-Policies: - - none - Referrer-Policy: - - strict-origin-when-cross-origin - Etag: - - W/"94045ec584e62887fbeed1515a5e8029" - Cache-Control: - - max-age=0, private, must-revalidate - X-Request-Id: - - c1a35086-bef1-491b-abda-285d2caa1821 - X-Runtime: - - '0.397008' - Strict-Transport-Security: - - max-age=31536000; includeSubDomains - Vary: - - Origin - Via: - - 1.1 vegur - Cf-Cache-Status: - - DYNAMIC - Cf-Request-Id: - - 051fff986a0000e522273b9200000001 - Expect-Ct: - - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" - Server: - - cloudflare - Cf-Ray: - - 5d13686d7c4be522-ARI - body: - encoding: UTF-8 - string: '{"id":"6n12zLmai3lLE9Dq","holder_id":"782592211","username":"416148503","holder_type":"business","created_at":"2020-08-18T17:37:24.550Z","institution":{"id":"cl_banco_de_chile","name":"Banco - de Chile","country":"cl"},"link_token":null,"mode":"test","accounts":[{"id":"JjEQx2rPTGGKbrP5","type":"checking_account","number":"813990168","name":"Cuenta - Corriente", "refreshed_at": null, "official_name":"Cuenta Corriente","balance":{"available":62255521,"current":62255521,"limit":62255521},"holder_id":"416148503","holder_name":"MarΓ­a - del Carmen GirΓ³n Raya","currency":"CLP"}]}' - recorded_at: Fri, 11 Sep 2020 18:21:22 GMT -recorded_with: VCR 6.0.0 diff --git a/spec/vcr/Fintoc_Client/_account/get_a_linked_account.yml b/spec/vcr/Fintoc_Client/_account/get_a_linked_account.yml deleted file mode 100644 index b8658b5..0000000 --- a/spec/vcr/Fintoc_Client/_account/get_a_linked_account.yml +++ /dev/null @@ -1,151 +0,0 @@ ---- -http_interactions: -- request: - method: get - uri: https://api.fintoc.com/v1/links/6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W - body: - encoding: UTF-8 - string: '' - headers: - Authorization: - - sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx - User-Agent: - - fintoc-ruby/0.1.0 - Connection: - - close - Host: - - api.fintoc.com - response: - status: - code: 200 - message: OK - headers: - Date: - - Fri, 18 Sep 2020 19:30:22 GMT - Content-Type: - - application/json; charset=utf-8 - Transfer-Encoding: - - chunked - Connection: - - close - Set-Cookie: - - __cfduid=d422e7ea5f64ed2f17d7849b72bc08f861600457421; expires=Sun, 18-Oct-20 - 19:30:21 GMT; path=/; domain=.fintoc.com; HttpOnly; SameSite=Lax; Secure - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - 1; mode=block - X-Content-Type-Options: - - nosniff - X-Download-Options: - - noopen - X-Permitted-Cross-Domain-Policies: - - none - Referrer-Policy: - - strict-origin-when-cross-origin - Etag: - - W/"897ae694e5399ed740681eeb2602478d" - Cache-Control: - - max-age=0, private, must-revalidate - X-Request-Id: - - bbfc257f-e666-4de8-8943-13b5311319e2 - X-Runtime: - - '0.382821' - Strict-Transport-Security: - - max-age=31536000; includeSubDomains - Vary: - - Origin - Via: - - 1.1 vegur - Cf-Cache-Status: - - DYNAMIC - Cf-Request-Id: - - 05444b49530000e52eca0a5200000001 - Expect-Ct: - - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" - Server: - - cloudflare - Cf-Ray: - - 5d4d7b221a91e52e-ARI - body: - encoding: UTF-8 - string: '{"id":"6n12zLmai3lLE9Dq","holder_id":"782592211","username":"416148503","holder_type":"business","created_at":"2020-08-18T17:37:24.550Z","institution":{"id":"cl_banco_de_chile","name":"Banco - de Chile","country":"cl"},"link_token":null,"mode":"test","accounts":[{"id":"JjEQx2rPTGGKbrP5","type":"checking_account","number":"813990168","name":"Cuenta - Corriente","official_name":"Cuenta Corriente","balance":{"available":23457460,"current":23457460,"limit":23457460},"holder_id":"404276727","holder_name":"Sta. - Francisco OrdΓ³Γ±ez Esquivel","currency":"CLP"}]}' - recorded_at: Fri, 18 Sep 2020 19:30:22 GMT -- request: - method: get - uri: https://api.fintoc.com/v1/links/6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W?link_token=6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W - body: - encoding: UTF-8 - string: '' - headers: - Authorization: - - sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx - User-Agent: - - fintoc-ruby/0.1.0 - Connection: - - close - Host: - - api.fintoc.com - response: - status: - code: 200 - message: OK - headers: - Date: - - Fri, 18 Sep 2020 19:30:23 GMT - Content-Type: - - application/json; charset=utf-8 - Transfer-Encoding: - - chunked - Connection: - - close - Set-Cookie: - - __cfduid=dcbe25b41d5cf53120aeb80d08e32f03e1600457422; expires=Sun, 18-Oct-20 - 19:30:22 GMT; path=/; domain=.fintoc.com; HttpOnly; SameSite=Lax; Secure - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - 1; mode=block - X-Content-Type-Options: - - nosniff - X-Download-Options: - - noopen - X-Permitted-Cross-Domain-Policies: - - none - Referrer-Policy: - - strict-origin-when-cross-origin - Etag: - - W/"897ae694e5399ed740681eeb2602478d" - Cache-Control: - - max-age=0, private, must-revalidate - X-Request-Id: - - 72ddb4c5-36b3-4dfd-b85d-33fd23e8661c - X-Runtime: - - '0.330236' - Strict-Transport-Security: - - max-age=31536000; includeSubDomains - Vary: - - Origin - Via: - - 1.1 vegur - Cf-Cache-Status: - - DYNAMIC - Cf-Request-Id: - - 05444b4ee00000d7d13612e200000001 - Expect-Ct: - - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" - Server: - - cloudflare - Cf-Ray: - - 5d4d7b2b0a7cd7d1-EZE - body: - encoding: UTF-8 - string: '{"id":"6n12zLmai3lLE9Dq","holder_id":"782592211","username":"416148503","holder_type":"business","created_at":"2020-08-18T17:37:24.550Z","institution":{"id":"cl_banco_de_chile","name":"Banco - de Chile","country":"cl"},"link_token":null,"mode":"test","accounts":[{"id":"JjEQx2rPTGGKbrP5","type":"checking_account","number":"813990168","name":"Cuenta - Corriente","refreshed_at":null,"official_name":"Cuenta Corriente","balance":{"available":23457460,"current":23457460,"limit":23457460},"holder_id":"404276727","holder_name":"Sta. - Francisco OrdΓ³Γ±ez Esquivel","currency":"CLP"}]}' - recorded_at: Fri, 18 Sep 2020 19:30:23 GMT -recorded_with: VCR 6.0.0 diff --git a/spec/vcr/Fintoc_Client/_get_accounts/prints_accounts_to_console.yml b/spec/vcr/Fintoc_Client/_get_accounts/prints_accounts_to_console.yml deleted file mode 100644 index 33c5caf..0000000 --- a/spec/vcr/Fintoc_Client/_get_accounts/prints_accounts_to_console.yml +++ /dev/null @@ -1,78 +0,0 @@ ---- -http_interactions: -- request: - method: get - uri: https://api.fintoc.com/v1/links/6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W - body: - encoding: UTF-8 - string: '' - headers: - Authorization: - - sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx - User-Agent: - - fintoc-ruby/0.1.0 - Connection: - - close - Host: - - api.fintoc.com - response: - status: - code: 200 - message: OK - headers: - Date: - - Tue, 08 Jun 2021 15:18:13 GMT - Content-Type: - - application/json; charset=utf-8 - Transfer-Encoding: - - chunked - Connection: - - close - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - 1; mode=block - X-Content-Type-Options: - - nosniff - X-Download-Options: - - noopen - X-Permitted-Cross-Domain-Policies: - - none - Referrer-Policy: - - strict-origin-when-cross-origin - Etag: - - W/"3c5aa7315fb87065354f5169836f2c13" - Cache-Control: - - max-age=0, private, must-revalidate - X-Request-Id: - - '06048509-ff44-4c97-bea5-a0fd0c7cdc63' - X-Runtime: - - '0.296151' - Strict-Transport-Security: - - max-age=31536000; includeSubDomains - Vary: - - Origin - Via: - - 1.1 vegur - Cf-Cache-Status: - - DYNAMIC - Cf-Request-Id: - - 0a8dccf4390000cfbfa3336000000001 - Expect-Ct: - - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" - Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v2?s=Blsb52h5pNeOJDXBJHlt%2Bx3thBGcMHFbdTc3ivN%2FaVZt1fGU9KzIoFH5kwA6zGkcnt2bto%2BwIyUioFDYbOGEa7lO%2B1Gltk8gQI%2BsatfY8X9z1rLlka8ArFY2UQ%3D%3D"}],"group":"cf-nel","max_age":604800}' - Nel: - - '{"report_to":"cf-nel","max_age":604800}' - Server: - - cloudflare - Cf-Ray: - - 65c31766c84fcfbf-SCL - body: - encoding: UTF-8 - string: '{"id":"6n12zLmai3lLE9Dq","holder_id":"782592211","username":"416148503","holder_type":"business","created_at":"2020-08-18T17:37:24.550Z","institution":{"id":"cl_banco_de_chile","name":"Banco - de Chile","country":"cl"},"link_token":null,"mode":"test","active":true,"status":"active","object":"link","accounts":[{"id":"JjEQx2rPTGGKbrP5","type":"checking_account","number":"813990168","name":"Cuenta - Corriente","official_name":"Cuenta Corriente","balance":{"available":78785969,"current":78785969,"limit":78785969},"holder_id":"416148503","holder_name":"Teodoro - Carbajal Hidalgo","currency":"CLP","refreshed_at":null,"object":"account"}]}' - recorded_at: Tue, 08 Jun 2021 15:18:13 GMT -recorded_with: VCR 6.0.0 diff --git a/spec/vcr/Fintoc_Client/_link/get_the_link_from_a_given_link_token.yml b/spec/vcr/Fintoc_Client/_link/get_the_link_from_a_given_link_token.yml deleted file mode 100644 index 9f1af0d..0000000 --- a/spec/vcr/Fintoc_Client/_link/get_the_link_from_a_given_link_token.yml +++ /dev/null @@ -1,77 +0,0 @@ ---- -http_interactions: -- request: - method: get - uri: https://api.fintoc.com/v1/links/6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W - body: - encoding: UTF-8 - string: '' - headers: - Authorization: - - sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx - User-Agent: - - fintoc-ruby/0.1.0 - Connection: - - close - Host: - - api.fintoc.com - response: - status: - code: 200 - message: OK - headers: - Date: - - Fri, 11 Sep 2020 18:21:18 GMT - Content-Type: - - application/json; charset=utf-8 - Transfer-Encoding: - - chunked - Connection: - - close - Set-Cookie: - - __cfduid=deae8e9e8f76a047dcd4868adfe72bf081599848477; expires=Sun, 11-Oct-20 - 18:21:17 GMT; path=/; domain=.fintoc.com; HttpOnly; SameSite=Lax; Secure - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - 1; mode=block - X-Content-Type-Options: - - nosniff - X-Download-Options: - - noopen - X-Permitted-Cross-Domain-Policies: - - none - Referrer-Policy: - - strict-origin-when-cross-origin - Etag: - - W/"94045ec584e62887fbeed1515a5e8029" - Cache-Control: - - max-age=0, private, must-revalidate - X-Request-Id: - - 26836f44-6c7c-4d02-b795-83f1a4bee81a - X-Runtime: - - '0.535821' - Strict-Transport-Security: - - max-age=31536000; includeSubDomains - Vary: - - Origin - Via: - - 1.1 vegur - Cf-Cache-Status: - - DYNAMIC - Cf-Request-Id: - - 051fff89490000e52e201a9200000001 - Expect-Ct: - - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" - Server: - - cloudflare - Cf-Ray: - - 5d1368554ccde52e-ARI - body: - encoding: UTF-8 - string: '{"id":"6n12zLmai3lLE9Dq","holder_id":"782592211","username":"416148503","holder_type":"business","created_at":"2020-08-18T17:37:24.550Z","institution":{"id":"cl_banco_de_chile","name":"Banco - de Chile","country":"cl"},"link_token":null,"mode":"test","accounts":[{"id":"JjEQx2rPTGGKbrP5","type":"checking_account","number":"813990168","name":"Cuenta - Corriente","official_name":"Cuenta Corriente","balance":{"available":62255521,"current":62255521,"limit":62255521},"holder_id":"416148503","holder_name":"MarΓ­a - del Carmen GirΓ³n Raya","currency":"CLP"}]}' - recorded_at: Fri, 11 Sep 2020 18:21:18 GMT -recorded_with: VCR 6.0.0 diff --git a/spec/vcr/Fintoc_Account/_get_movements/get_the_last_30_account_s_movements.yml b/spec/vcr/Fintoc_V1_Account/_get_movements/get_the_last_30_account_s_movements.yml similarity index 100% rename from spec/vcr/Fintoc_Account/_get_movements/get_the_last_30_account_s_movements.yml rename to spec/vcr/Fintoc_V1_Account/_get_movements/get_the_last_30_account_s_movements.yml diff --git a/spec/vcr/Fintoc_Account/_movement_with_since_argument/get_account_s_movements_with_arguments.yml b/spec/vcr/Fintoc_V1_Account/_movement_with_since_argument/get_account_s_movements_with_arguments.yml similarity index 100% rename from spec/vcr/Fintoc_Account/_movement_with_since_argument/get_account_s_movements_with_arguments.yml rename to spec/vcr/Fintoc_V1_Account/_movement_with_since_argument/get_account_s_movements_with_arguments.yml diff --git a/spec/vcr/Fintoc_Account/_movements/get_the_last_30_account_s_movements.yml b/spec/vcr/Fintoc_V1_Account/_movements/get_the_last_30_account_s_movements.yml similarity index 100% rename from spec/vcr/Fintoc_Account/_movements/get_the_last_30_account_s_movements.yml rename to spec/vcr/Fintoc_V1_Account/_movements/get_the_last_30_account_s_movements.yml diff --git a/spec/vcr/Fintoc_Client/_links/get_all_the_links_from_a_given_link_token.yml b/spec/vcr/Fintoc_V1_Account/_update_balance/updates_the_account_s_balance.yml similarity index 59% rename from spec/vcr/Fintoc_Client/_links/get_all_the_links_from_a_given_link_token.yml rename to spec/vcr/Fintoc_V1_Account/_update_balance/updates_the_account_s_balance.yml index 3ff8947..da40abd 100644 --- a/spec/vcr/Fintoc_Client/_links/get_all_the_links_from_a_given_link_token.yml +++ b/spec/vcr/Fintoc_V1_Account/_update_balance/updates_the_account_s_balance.yml @@ -2,7 +2,7 @@ http_interactions: - request: method: get - uri: https://api.fintoc.com/v1/links + uri: https://api.fintoc.com/v1/accounts/Z6AwnGn4idL7DPj4 body: encoding: UTF-8 string: '' @@ -21,7 +21,7 @@ http_interactions: message: OK headers: Date: - - Fri, 11 Sep 2020 18:21:19 GMT + - Fri, 18 Sep 2020 19:32:33 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -29,8 +29,8 @@ http_interactions: Connection: - close Set-Cookie: - - __cfduid=d067424ee2e91e31de078cbd3f1cc5c011599848478; expires=Sun, 11-Oct-20 - 18:21:18 GMT; path=/; domain=.fintoc.com; HttpOnly; SameSite=Lax; Secure + - __cfduid=dbfae5f92e0ae1b5fe843ee4eba3eb1ce1600457552; expires=Sun, 18-Oct-20 + 19:32:32 GMT; path=/; domain=.fintoc.com; HttpOnly; SameSite=Lax; Secure X-Frame-Options: - SAMEORIGIN X-Xss-Protection: @@ -43,19 +43,14 @@ http_interactions: - none Referrer-Policy: - strict-origin-when-cross-origin - Link: - - ; rel="first", ; - rel="last" - X-Total-Count: - - '1' Etag: - - W/"80e799153079ddb19ef402cbfea3c804" + - W/"897ae694e5399ed740681eeb2602478d" Cache-Control: - max-age=0, private, must-revalidate X-Request-Id: - - 1a1a5c10-d715-4ad8-bb64-76496333feb6 + - 5155665b-825d-4a62-84a4-910f4750e187 X-Runtime: - - '0.058623' + - '0.336331' Strict-Transport-Security: - max-age=31536000; includeSubDomains Vary: @@ -65,16 +60,18 @@ http_interactions: Cf-Cache-Status: - DYNAMIC Cf-Request-Id: - - 051fff8f49000074dfb32ae200000001 + - 05444d4bea0000e51adc97e200000001 Expect-Ct: - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" Server: - cloudflare Cf-Ray: - - 5d13685eda8c74df-EZE + - 5d4d7e597cb8e51a-ARI body: encoding: UTF-8 - string: '[{"id":"6n12zLmai3lLE9Dq","holder_id":"782592211","username":"416148503","holder_type":"business","created_at":"2020-08-18T17:37:24.550Z","institution":{"id":"cl_banco_de_chile","name":"Banco - de Chile","country":"cl"},"link_token":null,"mode":"test","accounts":null}]' - recorded_at: Fri, 11 Sep 2020 18:21:19 GMT + string: '{"id":"JjEQx2rPTGGKbrP5","type":"checking_account","number":"813990168","name":"Cuenta + Corriente","official_name":"Cuenta Corriente","balance":{"available":23457460,"current":23457460,"limit":23457460},"holder_id":"404276727","holder_name":"Sta. + Francisco OrdΓ³Γ±ez Esquivel","currency":"CLP"}' + recorded_at: Fri, 18 Sep 2020 19:32:33 GMT + recorded_with: VCR 6.0.0 diff --git a/spec/vcr/Fintoc_Account/_update_balance/update_account_s_movements.yml b/spec/vcr/Fintoc_V1_Account/_update_movements/update_account_s_movements.yml similarity index 100% rename from spec/vcr/Fintoc_Account/_update_balance/update_account_s_movements.yml rename to spec/vcr/Fintoc_V1_Account/_update_movements/update_account_s_movements.yml diff --git a/spec/vcr/Fintoc_Client/_get_link/get_the_link_from_a_given_link_token.yml b/spec/vcr/Fintoc_V1_Client/behaves_like_a_client_with_links_manager/_links/_get/get_the_link_from_a_given_link_token.yml similarity index 100% rename from spec/vcr/Fintoc_Client/_get_link/get_the_link_from_a_given_link_token.yml rename to spec/vcr/Fintoc_V1_Client/behaves_like_a_client_with_links_manager/_links/_get/get_the_link_from_a_given_link_token.yml diff --git a/spec/vcr/Fintoc_Client/_get_links/get_all_the_links_from_a_given_link_token.yml b/spec/vcr/Fintoc_V1_Client/behaves_like_a_client_with_links_manager/_links/_list/get_all_the_links_from_a_given_link_token.yml similarity index 100% rename from spec/vcr/Fintoc_Client/_get_links/get_all_the_links_from_a_given_link_token.yml rename to spec/vcr/Fintoc_V1_Client/behaves_like_a_client_with_links_manager/_links/_list/get_all_the_links_from_a_given_link_token.yml diff --git a/spec/vcr/Fintoc_Client/_get_account/get_a_linked_account.yml b/spec/vcr/Fintoc_V1_Link/_update_accounts/updates_balance_and_movements_for_all_accounts.yml similarity index 67% rename from spec/vcr/Fintoc_Client/_get_account/get_a_linked_account.yml rename to spec/vcr/Fintoc_V1_Link/_update_accounts/updates_balance_and_movements_for_all_accounts.yml index d3b9db9..f05e3e0 100644 --- a/spec/vcr/Fintoc_Client/_get_account/get_a_linked_account.yml +++ b/spec/vcr/Fintoc_V1_Link/_update_accounts/updates_balance_and_movements_for_all_accounts.yml @@ -2,7 +2,7 @@ http_interactions: - request: method: get - uri: https://api.fintoc.com/v1/links/6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W + uri: https://api.fintoc.com/v1/accounts/Z6AwnGn4idL7DPj4 body: encoding: UTF-8 string: '' @@ -21,7 +21,7 @@ http_interactions: message: OK headers: Date: - - Fri, 18 Sep 2020 19:32:32 GMT + - Fri, 18 Sep 2020 19:32:33 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -29,8 +29,8 @@ http_interactions: Connection: - close Set-Cookie: - - __cfduid=def4c4e84aea25dab831e626b094e47e11600457551; expires=Sun, 18-Oct-20 - 19:32:31 GMT; path=/; domain=.fintoc.com; HttpOnly; SameSite=Lax; Secure + - __cfduid=dbfae5f92e0ae1b5fe843ee4eba3eb1ce1600457552; expires=Sun, 18-Oct-20 + 19:32:32 GMT; path=/; domain=.fintoc.com; HttpOnly; SameSite=Lax; Secure X-Frame-Options: - SAMEORIGIN X-Xss-Protection: @@ -48,9 +48,9 @@ http_interactions: Cache-Control: - max-age=0, private, must-revalidate X-Request-Id: - - 87980ff7-a12c-4524-89e2-c66062907680 + - 5155665b-825d-4a62-84a4-910f4750e187 X-Runtime: - - '0.379295' + - '0.336331' Strict-Transport-Security: - max-age=31536000; includeSubDomains Vary: @@ -60,23 +60,22 @@ http_interactions: Cf-Cache-Status: - DYNAMIC Cf-Request-Id: - - 05444d46ad0000681fd9284200000001 + - 05444d4bea0000e51adc97e200000001 Expect-Ct: - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" Server: - cloudflare Cf-Ray: - - 5d4d7e511dc0681f-EZE + - 5d4d7e597cb8e51a-ARI body: encoding: UTF-8 - string: '{"id":"6n12zLmai3lLE9Dq","holder_id":"782592211","username":"416148503","holder_type":"business","created_at":"2020-08-18T17:37:24.550Z","institution":{"id":"cl_banco_de_chile","name":"Banco - de Chile","country":"cl"},"link_token":null,"mode":"test","accounts":[{"id":"JjEQx2rPTGGKbrP5","type":"checking_account","number":"813990168","name":"Cuenta + string: '{"id":"JjEQx2rPTGGKbrP5","type":"checking_account","number":"813990168","name":"Cuenta Corriente","official_name":"Cuenta Corriente","balance":{"available":23457460,"current":23457460,"limit":23457460},"holder_id":"404276727","holder_name":"Sta. - Francisco OrdΓ³Γ±ez Esquivel","currency":"CLP"}]}' - recorded_at: Fri, 18 Sep 2020 19:32:32 GMT + Francisco OrdΓ³Γ±ez Esquivel","currency":"CLP"}' + recorded_at: Fri, 18 Sep 2020 19:32:33 GMT - request: method: get - uri: https://api.fintoc.com/v1/links/6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W?link_token=6n12zLmai3lLE9Dq_token_gvEJi8FrBge4fb3cz7Wp856W + uri: https://api.fintoc.com/v1/accounts/Z6AwnGn4idL7DPj4/movements body: encoding: UTF-8 string: '' @@ -143,9 +142,7 @@ http_interactions: - 5d4d7e597cb8e51a-ARI body: encoding: UTF-8 - string: '{"id":"6n12zLmai3lLE9Dq","holder_id":"782592211","username":"416148503","holder_type":"business","created_at":"2020-08-18T17:37:24.550Z","institution":{"id":"cl_banco_de_chile","name":"Banco - de Chile","country":"cl"},"link_token":null,"mode":"test","accounts":[{"id":"JjEQx2rPTGGKbrP5","type":"checking_account","number":"813990168","name":"Cuenta - Corriente","official_name":"Cuenta Corriente","balance":{"available":23457460,"current":23457460,"limit":23457460},"holder_id":"404276727","holder_name":"Sta. - Francisco OrdΓ³Γ±ez Esquivel","currency":"CLP"}]}' + string: '[]' recorded_at: Fri, 18 Sep 2020 19:32:33 GMT + recorded_with: VCR 6.0.0 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_numbers_manager/_account_numbers/_create/returns_an_AccountNumber_instance.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_numbers_manager/_account_numbers/_create/returns_an_AccountNumber_instance.yml new file mode 100644 index 0000000..9a08e04 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_numbers_manager/_account_numbers/_create/returns_an_AccountNumber_instance.yml @@ -0,0 +1,77 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.fintoc.com/v2/account_numbers + body: + encoding: UTF-8 + string: '{"account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","description":"Test + account number","metadata":{"test_id":"12345"}}' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Mon, 01 Sep 2025 16:46:57 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '332' + Connection: + - close + Cf-Ray: + - 97863f28a88118e5-MIA + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"442b2cd87d7e1a87203a6faed3009bdd" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 74814dce-220d-4d89-ab7f-9790440bd84a + X-Runtime: + - '0.133817' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=ffhqW6LhQ%2Fypc8%2Bz6iw5sbozBYa8WCjEdl8rvTmLhum%2B%2FT2N5fcCmK2OiVQKMoGm4RGFN2s0fZcjr40UsCGu9OdrW8fA6p2h67rrSqYOlJtnSbzXwxTYUS2TqD%2Fn%2F%2F%2B8"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=130539&min_rtt=130055&rtt_var=49117&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=1982&delivery_rate=32755&cwnd=59&unsent_bytes=0&cid=db9dd6d09b32fd20&ts=475&x=0" + body: + encoding: UTF-8 + string: '{"id":"acno_326dzRGqxLee3j9TkaBBBMfs2i0","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","number":"735969000000203365","created_at":"2025-09-01T16:46:57Z","updated_at":"2025-09-01T16:46:57Z","mode":"test","description":"Test + account number","metadata":{"test_id":"12345"},"status":"enabled","is_root":false,"object":"account_number"}' + recorded_at: Mon, 01 Sep 2025 16:46:57 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_numbers_manager/_account_numbers/_get/returns_an_AccountNumber_instance.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_numbers_manager/_account_numbers/_get/returns_an_AccountNumber_instance.yml new file mode 100644 index 0000000..51b183e --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_numbers_manager/_account_numbers/_get/returns_an_AccountNumber_instance.yml @@ -0,0 +1,74 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.fintoc.com/v2/account_numbers/acno_326dzRGqxLee3j9TkaBBBMfs2i0 + body: + encoding: ASCII-8BIT + string: '' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Mon, 01 Sep 2025 16:52:36 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '332' + Connection: + - close + Cf-Ray: + - 9786476f1fd41e7b-EZE + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"442b2cd87d7e1a87203a6faed3009bdd" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 53940869-7188-40bd-8aa9-8453edf6c082 + X-Runtime: + - '0.062726' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=79CIOfUYM4KWBPevnJKUJNnuZFH%2BbqmQO8Xf5LUGlE%2FXFbngDgRNaifrW1ZB3uPTEOv90vGlEbJJ7x6PkDv4mau8JGOBI9kBWUxb1c2uHCyWcHrnrxv6xTJ0buEW68l4"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=31870&min_rtt=29868&rtt_var=12630&sent=6&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=1831&delivery_rate=142627&cwnd=244&unsent_bytes=0&cid=714e21d2176eeb80&ts=329&x=0" + body: + encoding: UTF-8 + string: '{"id":"acno_326dzRGqxLee3j9TkaBBBMfs2i0","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","number":"735969000000203365","created_at":"2025-09-01T16:46:57Z","updated_at":"2025-09-01T16:46:57Z","mode":"test","description":"Test + account number","metadata":{"test_id":"12345"},"status":"enabled","is_root":false,"object":"account_number"}' + recorded_at: Mon, 01 Sep 2025 16:52:36 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_numbers_manager/_account_numbers/_list/returns_an_array_of_AccountNumber_instances.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_numbers_manager/_account_numbers/_list/returns_an_array_of_AccountNumber_instances.yml new file mode 100644 index 0000000..844ebe4 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_numbers_manager/_account_numbers/_list/returns_an_array_of_AccountNumber_instances.yml @@ -0,0 +1,76 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.fintoc.com/v2/account_numbers + body: + encoding: ASCII-8BIT + string: '' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Mon, 01 Sep 2025 16:46:58 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '930' + Connection: + - close + Cf-Ray: + - 97863f2f0c391ac8-GRU + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Link: + - '' + Etag: + - W/"58e4fdf440240df4932ac91479501c77" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - a83b322e-ac48-4797-a170-ff6bfd49f9e1 + X-Runtime: + - '0.051392' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=dRTTdxBVoYQuL895%2FcWmvxxIdm4KCPnpFHLGMJaF0KvPcssrPKaWnOAH97Odm4SZt0gGKrBmolwmLFck4dHoBTYXFdsVTTiCDECq8cVg%2BGYBpKTRUbzwCxXhrZl7wCMz"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=60788&min_rtt=60655&rtt_var=22841&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=1798&delivery_rate=70233&cwnd=33&unsent_bytes=0&cid=1248c3ff5fad2964&ts=383&x=0" + body: + encoding: UTF-8 + string: '[{"id":"acno_326dzRGqxLee3j9TkaBBBMfs2i0","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","number":"735969000000203365","created_at":"2025-09-01T16:46:57Z","updated_at":"2025-09-01T16:46:57Z","mode":"test","description":"Test + account number","metadata":{"test_id":"12345"},"status":"enabled","is_root":false,"object":"account_number"},{"id":"acno_31yYL5uOb39vzvRIszPhoXYMjt4","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","number":"735969000000203297","created_at":"2025-08-29T20:02:00Z","updated_at":"2025-08-29T20:02:00Z","mode":"test","description":null,"metadata":{},"status":"enabled","is_root":true,"object":"account_number"},{"id":"acno_31t0VhIYoVNy4mYjyo8cWZTYcK9","account_id":"acc_31t0VhaF6uFoj0O9oreXw3jDtzL","number":"735969000000203226","created_at":"2025-08-27T20:54:46Z","updated_at":"2025-08-27T20:54:46Z","mode":"test","description":null,"metadata":{},"status":"enabled","is_root":true,"object":"account_number"}]' + recorded_at: Mon, 01 Sep 2025 16:46:58 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_numbers_manager/_account_numbers/_update/returns_an_updated_AccountNumber_instance.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_numbers_manager/_account_numbers/_update/returns_an_updated_AccountNumber_instance.yml new file mode 100644 index 0000000..f72b7d7 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_numbers_manager/_account_numbers/_update/returns_an_updated_AccountNumber_instance.yml @@ -0,0 +1,76 @@ +--- +http_interactions: +- request: + method: patch + uri: https://api.fintoc.com/v2/account_numbers/acno_326dzRGqxLee3j9TkaBBBMfs2i0 + body: + encoding: UTF-8 + string: '{"description":"Updated account number description"}' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Mon, 01 Sep 2025 17:04:19 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '347' + Connection: + - close + Cf-Ray: + - 9786589b39b2634f-EZE + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"82628675d4c8cf1521db24ac25986c9e" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - aa8a0a15-5f5f-4505-b232-869f9b0fd8ca + X-Runtime: + - '0.059235' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=zmI4LMK8CCyLk0eNFxF9GFHTqxyri6qzOSTiJ4tg9Xqhz21k9LEjK1ZAfAU4NausD9Kv2SnsSkIlvRmX4V10SnA%2Bi9R0rRdMtCWtvuk1oVIqqKRie9%2Bnbr7YbGemxjiD"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=51825&min_rtt=30574&rtt_var=26644&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3880&recv_bytes=1952&delivery_rate=139334&cwnd=252&unsent_bytes=0&cid=1dbd524bd165a7ea&ts=323&x=0" + body: + encoding: UTF-8 + string: '{"id":"acno_326dzRGqxLee3j9TkaBBBMfs2i0","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","number":"735969000000203365","created_at":"2025-09-01T16:46:57Z","updated_at":"2025-09-01T16:54:40Z","mode":"test","description":"Updated + account number description","metadata":{"test_id":"12345"},"status":"enabled","is_root":false,"object":"account_number"}' + recorded_at: Mon, 01 Sep 2025 17:04:19 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_verifications_manager/_account_verifications/_create/returns_an_AccountVerification_instance.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_verifications_manager/_account_verifications/_create/returns_an_AccountVerification_instance.yml new file mode 100644 index 0000000..22e08c8 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_verifications_manager/_account_verifications/_create/returns_an_AccountVerification_instance.yml @@ -0,0 +1,77 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.fintoc.com/v2/account_verifications + body: + encoding: UTF-8 + string: '{"account_number":"735969000000203226"}' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Fintoc-Jws-Signature: + - eyJhbGciOiJSUzI1NiIsIm5vbmNlIjoiNDk3OTQ0MWIxNzg0Mzg4OTlkYzA5MzNhYTcxZjVmZmUiLCJ0cyI6MTc1NzAwMTk1NywiY3JpdCI6WyJ0cyIsIm5vbmNlIl19.VqYcVdHBcQiYaXZFtNKqS5giFTQDW6syn1VHj64ZzEY59Qk4JCT2G8ne0KwqVtNZv6FtUbeCzzfuucR9pGlaVM-iYJiBhy-HOjI9VUDUA_GYvWbs3M_kqowBr3hi5yVWV1Xb6FlFZs_zoIdKWPGYXNPqHEHXT7Qv8LFBZNYgc5r9jjzQy38gNVktmcKFcXTcVRoNzrzRe5VjmPFzLNJ0B8_j_kyvJJdCckQ79UIYKjA9Ocmu6nN1jmiG4SdZkG69cAouXDwZmGSnrQtsWSkQOnU4Mke_m4buVzOGD2zrzmCzu4rY9LmkwbgOxJKO825GadgZWXq4LJPINXr-Eo7Wzw + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Host: + - api.fintoc.com + response: + status: + code: 201 + message: Created + headers: + Date: + - Thu, 04 Sep 2025 16:05:59 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '402' + Connection: + - close + Cf-Ray: + - 979ebb409a8ea0c8-CPH + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"1658fee2632f17fae8461056906cb5b5" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 524eefcc-c549-49f1-a580-7c40ede80cc7 + X-Runtime: + - '0.437973' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=qfNKs9IPvaEYbQp%2FW973gSQIfZ4FWbQCND8e7nVZDSqeVVzFVlZ8xu2xcS3ClzIZ63J%2FsqM6okeJCgeI6TWwF33xZrrh6cX98RnV5UsKIWcevebo%2BbYvGvvRmzwiTBpQ"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=337737&min_rtt=298128&rtt_var=140090&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=2406&delivery_rate=14289&cwnd=33&unsent_bytes=0&cid=59bf549ecb9beb68&ts=1299&x=0" + body: + encoding: UTF-8 + string: '{"object":"account_verification","id":"accv_32F2NLQOOwbeOvfuw8Y1zZCfGdw","status":"pending","reason":null,"transfer_id":"tr_32F2NNCg5s9JQy5AX5A6toWX9rd","counterparty":{"holder_id":null,"holder_name":null,"account_number":"735969000000203226","account_type":"clabe","institution":{"id":"90735","name":"FINTOC","country":"mx"}},"mode":"test","receipt_url":null,"transaction_date":"2025-09-04T16:05:58Z"}' + recorded_at: Thu, 04 Sep 2025 16:05:59 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_verifications_manager/_account_verifications/_get/returns_an_AccountVerification_instance.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_verifications_manager/_account_verifications/_get/returns_an_AccountVerification_instance.yml new file mode 100644 index 0000000..4d90e82 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_verifications_manager/_account_verifications/_get/returns_an_AccountVerification_instance.yml @@ -0,0 +1,74 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.fintoc.com/v2/account_verifications/accv_32F2NLQOOwbeOvfuw8Y1zZCfGdw + body: + encoding: ASCII-8BIT + string: '' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Thu, 04 Sep 2025 16:06:50 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '603' + Connection: + - close + Cf-Ray: + - 979ebc832f69c9be-CPH + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"912e11cf6908023f112c73707dbb82cb" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 3f90fdd2-c468-4a93-bb74-763aee816c05 + X-Runtime: + - '0.216110' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=zqdiTM9nU8Qd6m59OzNrTLbjhQMmXzGe0Way81DobYWZZwTSUrKSBDOHAfPmrZx%2BDi4c%2F1jW9bEgvG26J2bxCDro%2FszTccBpILKFysVXlDV2BU1PPe9I8POu2YsanUm4"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=262243&min_rtt=249577&rtt_var=118924&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3880&recv_bytes=1837&delivery_rate=12139&cwnd=33&unsent_bytes=0&cid=28465d7eaadf9d83&ts=1103&x=0" + body: + encoding: UTF-8 + string: '{"object":"account_verification","id":"accv_32F2NLQOOwbeOvfuw8Y1zZCfGdw","status":"succeeded","reason":null,"transfer_id":"tr_32F2NNCg5s9JQy5AX5A6toWX9rd","counterparty":{"holder_id":"PPPR510220DB1","holder_name":"Adrianne + Murray","account_number":"735969000000203226","account_type":"clabe","institution":{"id":"90735","name":"FINTOC","country":"mx"}},"mode":"test","receipt_url":"https://www.banxico.org.mx/cep-beta/go?i=90735\u0026s=dummy\u0026d=ce2zbPihXLg1MobUPCRrTrw9bB97KEYDGHg6rSGK7PaklMO9uV2ancSh1z2p8bIQCcwNCndMOBQx6ISkSOLTcLQ44r6s3qO7aVSNcWSiOp4%3D","transaction_date":"2025-09-04T16:05:58Z"}' + recorded_at: Thu, 04 Sep 2025 16:06:50 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_verifications_manager/_account_verifications/_list/accepts_filtering_parameters.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_verifications_manager/_account_verifications/_list/accepts_filtering_parameters.yml new file mode 100644 index 0000000..33286f8 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_verifications_manager/_account_verifications/_list/accepts_filtering_parameters.yml @@ -0,0 +1,76 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.fintoc.com/v2/account_verifications?limit=10&since=2020-01-01T00:00:00.000Z + body: + encoding: ASCII-8BIT + string: '' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Thu, 04 Sep 2025 16:06:54 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '605' + Connection: + - close + Cf-Ray: + - 979ebc9b5dbfa94d-SYD + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Link: + - '' + Etag: + - W/"9b52bfc2b4c200446ac3f7bc672c6bd4" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 3753ef34-9c63-40f2-afab-150600aa2505 + X-Runtime: + - '0.072507' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=FlX9RxXLN8cEbpqb%2FmxNG5yyxKiTowsxAbk61hhP64pnuDSagg8wwbL1xKXM7fhFdizhe7P8sVMPek7gvwzxyq5tzGj5rLEMTh2IvMbtXRluT9RhbZ5wbcvsuE%2Bc8CCb"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=359899&min_rtt=359292&rtt_var=135168&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3880&recv_bytes=1848&delivery_rate=11856&cwnd=33&unsent_bytes=0&cid=99ecee391b9b8fc8&ts=1091&x=0" + body: + encoding: UTF-8 + string: '[{"object":"account_verification","id":"accv_32F2NLQOOwbeOvfuw8Y1zZCfGdw","status":"succeeded","reason":null,"transfer_id":"tr_32F2NNCg5s9JQy5AX5A6toWX9rd","counterparty":{"holder_id":"PPPR510220DB1","holder_name":"Adrianne + Murray","account_number":"735969000000203226","account_type":"clabe","institution":{"id":"90735","name":"FINTOC","country":"mx"}},"mode":"test","receipt_url":"https://www.banxico.org.mx/cep-beta/go?i=90735\u0026s=dummy\u0026d=ce2zbPihXLg1MobUPCRrTrw9bB97KEYDGHg6rSGK7PaklMO9uV2ancSh1z2p8bIQCcwNCndMOBQx6ISkSOLTcLQ44r6s3qO7aVSNcWSiOp4%3D","transaction_date":"2025-09-04T16:05:58Z"}]' + recorded_at: Thu, 04 Sep 2025 16:06:54 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_verifications_manager/_account_verifications/_list/returns_an_array_of_AccountVerification_instances.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_verifications_manager/_account_verifications/_list/returns_an_array_of_AccountVerification_instances.yml new file mode 100644 index 0000000..f90c530 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_account_verifications_manager/_account_verifications/_list/returns_an_array_of_AccountVerification_instances.yml @@ -0,0 +1,76 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.fintoc.com/v2/account_verifications + body: + encoding: ASCII-8BIT + string: '' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Thu, 04 Sep 2025 16:06:52 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '605' + Connection: + - close + Cf-Ray: + - 979ebc8caefbebc6-CPH + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Link: + - '' + Etag: + - W/"9b52bfc2b4c200446ac3f7bc672c6bd4" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - f9aaa487-0ccb-4318-b3dc-73c094c8d744 + X-Runtime: + - '0.746597' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=fFpcOJGutfgafODqY5V0pWn72LC8syGZaQSv4HIBjaVHNSFo2SDTLflPrYtNj3%2FKAySKD0yoe5%2F7i50KRkLwZtY%2FPAOLGZEdMp%2Bq2VB9QaNE9xsf7H1vPHGdjYz5RmfQ"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=260621&min_rtt=247216&rtt_var=119517&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3878&recv_bytes=1804&delivery_rate=12018&cwnd=33&unsent_bytes=0&cid=d4549baa8c4f945c&ts=1552&x=0" + body: + encoding: UTF-8 + string: '[{"object":"account_verification","id":"accv_32F2NLQOOwbeOvfuw8Y1zZCfGdw","status":"succeeded","reason":null,"transfer_id":"tr_32F2NNCg5s9JQy5AX5A6toWX9rd","counterparty":{"holder_id":"PPPR510220DB1","holder_name":"Adrianne + Murray","account_number":"735969000000203226","account_type":"clabe","institution":{"id":"90735","name":"FINTOC","country":"mx"}},"mode":"test","receipt_url":"https://www.banxico.org.mx/cep-beta/go?i=90735\u0026s=dummy\u0026d=ce2zbPihXLg1MobUPCRrTrw9bB97KEYDGHg6rSGK7PaklMO9uV2ancSh1z2p8bIQCcwNCndMOBQx6ISkSOLTcLQ44r6s3qO7aVSNcWSiOp4%3D","transaction_date":"2025-09-04T16:05:58Z"}]' + recorded_at: Thu, 04 Sep 2025 16:06:52 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_accounts_manager/_accounts/_create/returns_an_Account_instance.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_accounts_manager/_accounts/_create/returns_an_Account_instance.yml new file mode 100644 index 0000000..534fec1 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_accounts_manager/_accounts/_create/returns_an_Account_instance.yml @@ -0,0 +1,77 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.fintoc.com/v2/accounts + body: + encoding: UTF-8 + string: '{"entity_id":"ent_31t0VhhrAXASFQTVYfCfIBnljbT","description":"Test + account"}' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Host: + - api.fintoc.com + response: + status: + code: 201 + message: Created + headers: + Date: + - Fri, 29 Aug 2025 20:02:00 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '383' + Connection: + - close + Cf-Ray: + - 976ea4bf29a642f0-MIA + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"dfa167aa8ce1e0f1d5764f021583ccfb" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 1fed91d6-8877-4d68-884c-54de16443d43 + X-Runtime: + - '0.282044' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=m1B2GZTfOo6Y24n8mMkDyjW%2Fpf5dVb1QH8vBVV0%2BIS9i7qAS%2FHrpWNcjpWKMX91r%2FsV67xtSI1gc0UWqgoawtWxhHZ%2FVgSP8V6331v6OATD3KkLJIA3gW0WKoxl%2B5B%2Fb"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=132154&min_rtt=131328&rtt_var=49838&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=1935&delivery_rate=32437&cwnd=38&unsent_bytes=0&cid=b5f864790f975efa&ts=696&x=0" + body: + encoding: UTF-8 + string: '{"id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","object":"account","mode":"test","root_account_number":"735969000000203297","is_root":false,"root_account_number_id":"acno_31yYL5uOb39vzvRIszPhoXYMjt4","available_balance":0,"currency":"MXN","entity":{"id":"ent_31t0VhhrAXASFQTVYfCfIBnljbT","holder_name":"Fintoc","holder_id":"ND","is_root":true},"description":"Test + account","status":"active"}' + recorded_at: Fri, 29 Aug 2025 20:02:00 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_accounts_manager/_accounts/_get/returns_an_Account_instance.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_accounts_manager/_accounts/_get/returns_an_Account_instance.yml new file mode 100644 index 0000000..1b8d889 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_accounts_manager/_accounts/_get/returns_an_Account_instance.yml @@ -0,0 +1,74 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.fintoc.com/v2/accounts/acc_31yYL7h9LVPg121AgFtCyJPDsgM + body: + encoding: ASCII-8BIT + string: '' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 29 Aug 2025 20:02:42 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '383' + Connection: + - close + Cf-Ray: + - 976ea5c5ee95f1f9-GRU + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"dfa167aa8ce1e0f1d5764f021583ccfb" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - f5c4a054-f4ce-4a20-bd01-897f574b75d5 + X-Runtime: + - '0.167283' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=vMY0I4XXrgNfMRSDKhJGiurTSJqsamrvy7mb%2BX25LYFjI%2BbmHc9IzI2BWc%2F5C97bQlCgfeoVaCWac75V6Dcmjf94Uc%2BZPwaLhsgwiVFH7XA8VGWcH4i7Fe%2BLu39x3JWb"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=70398&min_rtt=59455&rtt_var=44182&sent=7&recv=8&lost=0&retrans=1&sent_bytes=4920&recv_bytes=1823&delivery_rate=29058&cwnd=163&unsent_bytes=0&cid=a1434ae19ea63fff&ts=512&x=0" + body: + encoding: UTF-8 + string: '{"id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","object":"account","mode":"test","root_account_number":"735969000000203297","is_root":false,"root_account_number_id":"acno_31yYL5uOb39vzvRIszPhoXYMjt4","available_balance":0,"currency":"MXN","entity":{"id":"ent_31t0VhhrAXASFQTVYfCfIBnljbT","holder_name":"Fintoc","holder_id":"ND","is_root":true},"description":"Test + account","status":"active"}' + recorded_at: Fri, 29 Aug 2025 20:02:42 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_accounts_manager/_accounts/_list/returns_an_array_of_Account_instances.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_accounts_manager/_accounts/_list/returns_an_array_of_Account_instances.yml new file mode 100644 index 0000000..ba1f10e --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_accounts_manager/_accounts/_list/returns_an_array_of_Account_instances.yml @@ -0,0 +1,76 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.fintoc.com/v2/accounts + body: + encoding: ASCII-8BIT + string: '' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 29 Aug 2025 20:02:43 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '758' + Connection: + - close + Cf-Ray: + - 976ea5c9caab8bc1-EZE + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Link: + - '' + Etag: + - W/"db90e857bbfff3b588f63b53be770a09" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - f2247a46-e47d-4bd6-9839-73ae79c603a4 + X-Runtime: + - '0.122842' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=SAJpKW0SU6ZiCsnlxevZ64wjOntOithUhDYqR7JYNjIh4SXSRYAJvrno1yfY%2BId%2FpCsXD%2BTevxrf%2BmcnvuThHBw9YraScB0Nps0l29xG7E4R1tOtgR8%2BL01iCH%2F8D3jM"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=31428&min_rtt=30960&rtt_var=11944&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3880&recv_bytes=1791&delivery_rate=137596&cwnd=236&unsent_bytes=0&cid=5023caf54b582d86&ts=379&x=0" + body: + encoding: UTF-8 + string: '[{"id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","object":"account","mode":"test","root_account_number":"735969000000203297","is_root":false,"root_account_number_id":"acno_31yYL5uOb39vzvRIszPhoXYMjt4","available_balance":0,"currency":"MXN","entity":{"id":"ent_31t0VhhrAXASFQTVYfCfIBnljbT","holder_name":"Fintoc","holder_id":"ND","is_root":true},"description":"Test + account","status":"active"},{"id":"acc_31t0VhaF6uFoj0O9oreXw3jDtzL","object":"account","mode":"test","root_account_number":"735969000000203226","is_root":true,"root_account_number_id":"acno_31t0VhIYoVNy4mYjyo8cWZTYcK9","available_balance":0,"currency":"MXN","entity":{"id":"ent_31t0VhhrAXASFQTVYfCfIBnljbT","holder_name":"Fintoc","holder_id":"ND","is_root":true},"description":null,"status":"active"}]' + recorded_at: Fri, 29 Aug 2025 20:02:43 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_accounts_manager/_accounts/_update/returns_an_updated_Account_instance.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_accounts_manager/_accounts/_update/returns_an_updated_Account_instance.yml new file mode 100644 index 0000000..4286b05 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_accounts_manager/_accounts/_update/returns_an_updated_Account_instance.yml @@ -0,0 +1,76 @@ +--- +http_interactions: +- request: + method: patch + uri: https://api.fintoc.com/v2/accounts/acc_31yYL7h9LVPg121AgFtCyJPDsgM + body: + encoding: UTF-8 + string: '{"description":"Updated account description"}' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 29 Aug 2025 20:02:43 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '398' + Connection: + - close + Cf-Ray: + - 976ea5cd28dda58f-EZE + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"2021b3a9eceabbed2b5e2539dae3ca57" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 12f3b99f-2368-46fb-a72f-67e20705813c + X-Runtime: + - '0.150449' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=ZIwfCgCpGUQdsU6gArPxnP5wee6vB8eJiHk61oVLdCzUTbbZkF59fnPvcuQ4U%2FM1SvgZyVkDGxGviYwCERBP8Fr2h%2BDm%2BJMQIbsogweD%2FYBDrteSOe1etqMFiblLGAOv"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=32296&min_rtt=31883&rtt_var=12251&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=1937&delivery_rate=133613&cwnd=253&unsent_bytes=0&cid=ba5dd9e9de4421c8&ts=417&x=0" + body: + encoding: UTF-8 + string: '{"id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","object":"account","mode":"test","root_account_number":"735969000000203297","is_root":false,"root_account_number_id":"acno_31yYL5uOb39vzvRIszPhoXYMjt4","available_balance":0,"currency":"MXN","entity":{"id":"ent_31t0VhhrAXASFQTVYfCfIBnljbT","holder_name":"Fintoc","holder_id":"ND","is_root":true},"description":"Updated + account description","status":"active"}' + recorded_at: Fri, 29 Aug 2025 20:02:43 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_entities_manager/_entities/_get/returns_an_Entity_instance.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_entities_manager/_entities/_get/returns_an_Entity_instance.yml new file mode 100644 index 0000000..3c38327 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_entities_manager/_entities/_get/returns_an_Entity_instance.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.fintoc.com/v2/entities/ent_31t0VhhrAXASFQTVYfCfIBnljbT + body: + encoding: ASCII-8BIT + string: '' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 29 Aug 2025 15:21:40 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '127' + Connection: + - close + Cf-Ray: + - 976d0a19fe74d94e-GRU + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"f896e8d1f096825161c240052d9acb4a" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - e74046b0-bcae-4651-a845-9705b2375a43 + X-Runtime: + - '0.242517' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=mfXjDHsKGkEx3BFFZkGTiu9SFzAphuic90v7abBlNrgOnkfgHoMyJKqhxMUMqeVCjTjhpRNn7TwFk7nr1f%2FUvxz1No26uGqS1uyfIimV4ockxJRU%2Bu0xDuLHBKd40ctF"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=58995&min_rtt=58862&rtt_var=22169&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=1823&delivery_rate=72372&cwnd=107&unsent_bytes=0&cid=2cf15dc447adbd92&ts=558&x=0" + body: + encoding: UTF-8 + string: '{"id":"ent_31t0VhhrAXASFQTVYfCfIBnljbT","mode":"test","holder_name":"Fintoc","holder_id":"ND","is_root":true,"object":"entity"}' + recorded_at: Fri, 29 Aug 2025 15:21:40 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_entities_manager/_entities/_list/returns_an_array_of_Entity_instances.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_entities_manager/_entities/_list/returns_an_array_of_Entity_instances.yml new file mode 100644 index 0000000..1c10930 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_entities_manager/_entities/_list/returns_an_array_of_Entity_instances.yml @@ -0,0 +1,75 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.fintoc.com/v2/entities + body: + encoding: ASCII-8BIT + string: '' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 29 Aug 2025 15:21:01 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '129' + Connection: + - close + Cf-Ray: + - 976d09250985a4ff-GRU + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Link: + - '' + Etag: + - W/"4e0fddc47966ed6497da73e42163ad1e" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 72b515f0-cf44-4ff5-b97c-8998ed4bdf6d + X-Runtime: + - '0.099485' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=tktryiSMTGe%2BnX0QteWiKwGyNjTIAgr7K7akMekvkj4%2F%2BYuO7g5Xc54CyOTI7x7UAapA2mVexTcYGY%2FMso36qPIpNNavkBb5eKurglaGkMY2Kp3e8qm5ozscyU97y8nZ"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=60112&min_rtt=59235&rtt_var=22840&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=1791&delivery_rate=71916&cwnd=253&unsent_bytes=0&cid=b15990fef133e00b&ts=411&x=0" + body: + encoding: UTF-8 + string: '[{"id":"ent_31t0VhhrAXASFQTVYfCfIBnljbT","mode":"test","holder_name":"Fintoc","holder_id":"ND","is_root":true,"object":"entity"}]' + recorded_at: Fri, 29 Aug 2025 15:21:01 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_simulate_manager/_simulate/_receive_transfer/simulates_receiving_a_transfer_and_returns_Transfer_object.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_simulate_manager/_simulate/_receive_transfer/simulates_receiving_a_transfer_and_returns_Transfer_object.yml new file mode 100644 index 0000000..2610ccc --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_simulate_manager/_simulate/_receive_transfer/simulates_receiving_a_transfer_and_returns_Transfer_object.yml @@ -0,0 +1,78 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.fintoc.com/v2/simulate/receive_transfer + body: + encoding: UTF-8 + string: '{"account_number_id":"acno_326dzRGqxLee3j9TkaBBBMfs2i0","amount":10000,"currency":"MXN"}' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Host: + - api.fintoc.com + response: + status: + code: 201 + message: Created + headers: + Date: + - Wed, 03 Sep 2025 19:42:17 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '924' + Connection: + - close + Cf-Ray: + - 9797baba0e1cf1dd-GRU + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"9b8507bbddf62fbbc39dc0f60d4b934b" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 86ee4f18-ab70-4f72-8c7e-13fbfdb54c82 + X-Runtime: + - '0.805720' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=zweUJRHwMtzbEa6DKV8VCWV%2F%2FBNzngx9kLr%2FuhozNtY28G8rqA3vSZ8sgAoLJHSfU1mWsNinNDoRu%2Fe7eLWa3n2Ayzg%2FfZRqtlyNiah3gRfe8jXhcn6PXoevXNtc%2F0p5"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=59037&min_rtt=58831&rtt_var=22475&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=1964&delivery_rate=70429&cwnd=253&unsent_bytes=0&cid=a8dd37502f9ccbd5&ts=1128&x=0" + body: + encoding: UTF-8 + string: '{"object":"transfer","id":"tr_32CdYk4p58orHAqG8E3f5yrEjeo","amount":10000,"currency":"MXN","direction":"inbound","status":"pending","transaction_date":"2025-09-03T19:42:17Z","post_date":"2025-09-03T00:00:00Z","comment":null,"reference_id":"3","tracking_key":"202509039073500000000000000012","receipt_url":null,"mode":"test","counterparty":{"holder_id":"PPPR510220DB1","holder_name":"Adrianne + Murray","account_number":"631969456789123456","account_type":"clabe","institution":{"id":"90631","name":"CI + BOLSA","country":"mx"}},"account_number":{"id":"acno_326dzRGqxLee3j9TkaBBBMfs2i0","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","number":"735969000000203365","created_at":"2025-09-01T16:46:57Z","updated_at":"2025-09-01T16:54:40Z","mode":"test","description":"Updated + account number description","metadata":{"test_id":"12345"},"status":"enabled","is_root":false,"object":"account_number"},"metadata":{},"return_reason":null}' + recorded_at: Wed, 03 Sep 2025 19:42:17 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_create/returns_a_Transfer_instance.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_create/returns_a_Transfer_instance.yml new file mode 100644 index 0000000..0701bbd --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_create/returns_a_Transfer_instance.yml @@ -0,0 +1,81 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.fintoc.com/v2/transfers + body: + encoding: UTF-8 + string: '{"amount":50000,"currency":"MXN","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","counterparty":{"holder_id":"LFHU290523OG0","holder_name":"Jon + Snow","account_number":"735969000000203297","account_type":"clabe","institution_id":"40012"},"comment":"Test + payment","reference_id":"123456"}' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Fintoc-Jws-Signature: + - eyJhbGciOiJSUzI1NiIsIm5vbmNlIjoiMWU0NzU1ZGNlYzFhZjc2NmNhNjQ2ZjI4NzJlOTc1MmYiLCJ0cyI6MTc1NjgyODczMSwiY3JpdCI6WyJ0cyIsIm5vbmNlIl19.jXtZ3_PClHDiL_wMDGXQqtcUu3Oomn-jRACxvmNmJkCK_ETBHNRA1TMW4SFQqSuEW12XX1R1gA2UlOoKDJOLIZHRX6d4muvbcR32gio9mGEDst9e1cHZkUG0QT7eF_Cb4mseAvjeCTbWvIV3f6-FQ9zF0biffejPVsZvg3KArgbyTt3O1_sst1bng4zuTyVaSB4XEHK8gy-HNyJOHKX9GXGGsE7Mma--LeOneDJQCcoLe3-oXmBtkHwLEFRsgNK6jF8-jYJkDPKR5oFA6EuT-Yr1oXiAhsPr5FDZy3H6i99cz14dPk0iUN-UdozW8jj7wLUtWSYLBKrWoPkm5iXn_w + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Host: + - api.fintoc.com + response: + status: + code: 201 + message: Created + headers: + Date: + - Tue, 02 Sep 2025 15:58:51 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '863' + Connection: + - close + Cf-Ray: + - 978e36138f38b861-EZE + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"3bcd22908f94497674e96ead614e8a01" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 96e11256-7b3e-42e6-8514-882728242e85 + X-Runtime: + - '0.293159' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=Iv7AS7YO%2FSvC6uTUZgyWC2%2BaUfgdXl7xakqe1BSg2PGNCHtalgXnlviJpWj1wQJK3dsFWG9U6L0AuPx2E0tMdXKTtuVH8gXuCvBOmscmnic9ljwLzFlpxqN0l1IZeAL4"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=30125&min_rtt=30083&rtt_var=11366&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=2641&delivery_rate=140016&cwnd=252&unsent_bytes=0&cid=b848458890c32a83&ts=558&x=0" + body: + encoding: UTF-8 + string: '{"object":"transfer","id":"tr_329NGN1M4If6VvcMRALv4gjAQJx","amount":50000,"currency":"MXN","direction":"outbound","status":"pending","transaction_date":"2025-09-02T15:58:51Z","post_date":null,"comment":"Test + payment","reference_id":"123456","tracking_key":"202509029073500000000000000006","receipt_url":null,"mode":"test","counterparty":{"holder_id":"LFHU290523OG0","holder_name":"Jon + Snow","account_number":"735969000000203297","account_type":"clabe","institution":{"id":"90735","name":"FINTOC","country":"mx"}},"account_number":{"id":"acno_31yYL5uOb39vzvRIszPhoXYMjt4","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","number":"735969000000203297","created_at":"2025-08-29T20:02:00Z","updated_at":"2025-08-29T20:02:00Z","mode":"test","description":null,"metadata":{},"status":"enabled","is_root":true,"object":"account_number"},"metadata":{},"return_reason":null}' + recorded_at: Tue, 02 Sep 2025 15:58:51 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_get/returns_a_Transfer_instance.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_get/returns_a_Transfer_instance.yml new file mode 100644 index 0000000..e5409f2 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_get/returns_a_Transfer_instance.yml @@ -0,0 +1,75 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.fintoc.com/v2/transfers/tr_329NGN1M4If6VvcMRALv4gjAQJx + body: + encoding: ASCII-8BIT + string: '' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 02 Sep 2025 15:59:54 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '1070' + Connection: + - close + Cf-Ray: + - 978e379f2c927842-EZE + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"37bbc649f214cfce2f8d9aba6caecf21" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 726056fc-bf78-483a-9983-f2453fba746b + X-Runtime: + - '0.059927' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=FAx0QL7Eef4ZkT1QR%2BkB0cgqRpZSc8mZ1kyh6SsnZcqbg6fJZ8bi7euv7xDBrfgrqPI1HfMqlYq9EgszMsvIeR37MiKYP%2FXsLOzy6%2FyMrq3a9QkVFFsPv2s7H5extDUw"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=32899&min_rtt=32105&rtt_var=12607&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=1823&delivery_rate=132689&cwnd=253&unsent_bytes=0&cid=a29d81c8d5b56467&ts=316&x=0" + body: + encoding: UTF-8 + string: '{"object":"transfer","id":"tr_329NGN1M4If6VvcMRALv4gjAQJx","amount":50000,"currency":"MXN","direction":"outbound","status":"succeeded","transaction_date":"2025-09-02T15:58:51Z","post_date":"2025-09-02T00:00:00Z","comment":"Test + payment","reference_id":"123456","tracking_key":"202509029073500000000000000006","receipt_url":"https://www.banxico.org.mx/cep-beta/go?i=90735\u0026s=dummy\u0026d=qOK7ARfoEwlVpkLWUmFvihhtr5%2B4Gf1LA%2FnWvzdRnC8vLzAGeXauPtgz1bSaTV%2BmUP9oThn6%2B%2Bujt23nuyI3u8OR00eq5WezdAT1eG%2BhBvo%3D","mode":"test","counterparty":{"holder_id":"LFHU290523OG0","holder_name":"Jon + Snow","account_number":"735969000000203297","account_type":"clabe","institution":{"id":"90735","name":"FINTOC","country":"mx"}},"account_number":{"id":"acno_31yYL5uOb39vzvRIszPhoXYMjt4","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","number":"735969000000203297","created_at":"2025-08-29T20:02:00Z","updated_at":"2025-08-29T20:02:00Z","mode":"test","description":null,"metadata":{},"status":"enabled","is_root":true,"object":"account_number"},"metadata":{},"return_reason":null}' + recorded_at: Tue, 02 Sep 2025 15:59:54 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_list/accepts_filtering_parameters.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_list/accepts_filtering_parameters.yml new file mode 100644 index 0000000..c6ec808 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_list/accepts_filtering_parameters.yml @@ -0,0 +1,77 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.fintoc.com/v2/transfers?direction=outbound&status=succeeded + body: + encoding: ASCII-8BIT + string: '' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 02 Sep 2025 16:01:43 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '1072' + Connection: + - close + Cf-Ray: + - 978e3a451c00a4fc-GRU + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Link: + - '' + Etag: + - W/"8cd7042fdca7259ed4ef7a4518bdf6ee" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - d53e9642-da84-46ef-875c-6144fd090fd0 + X-Runtime: + - '0.064205' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=%2B1u%2FnTIHzyJohdVDT2cSiuJNrBQxE3acdGpl7gOBsai8IUWqJ%2B2ZgXyEbbEy37QiN9OMvKCM6%2BkDEF7sdBZNcaWXlGzUcgefErVT0PqggHoMOb7D1jlrzGh5iYP5wl3F"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=59799&min_rtt=59513&rtt_var=22521&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=1828&delivery_rate=71580&cwnd=45&unsent_bytes=0&cid=e3345cf16d5e8803&ts=392&x=0" + body: + encoding: UTF-8 + string: '[{"object":"transfer","id":"tr_329NGN1M4If6VvcMRALv4gjAQJx","amount":50000,"currency":"MXN","direction":"outbound","status":"succeeded","transaction_date":"2025-09-02T15:58:51Z","post_date":"2025-09-02T00:00:00Z","comment":"Test + payment","reference_id":"123456","tracking_key":"202509029073500000000000000006","receipt_url":"https://www.banxico.org.mx/cep-beta/go?i=90735\u0026s=dummy\u0026d=qOK7ARfoEwlVpkLWUmFvihhtr5%2B4Gf1LA%2FnWvzdRnC8vLzAGeXauPtgz1bSaTV%2BmUP9oThn6%2B%2Bujt23nuyI3u8OR00eq5WezdAT1eG%2BhBvo%3D","mode":"test","counterparty":{"holder_id":"LFHU290523OG0","holder_name":"Jon + Snow","account_number":"735969000000203297","account_type":"clabe","institution":{"id":"90735","name":"FINTOC","country":"mx"}},"account_number":{"id":"acno_31yYL5uOb39vzvRIszPhoXYMjt4","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","number":"735969000000203297","created_at":"2025-08-29T20:02:00Z","updated_at":"2025-08-29T20:02:00Z","mode":"test","description":null,"metadata":{},"status":"enabled","is_root":true,"object":"account_number"},"metadata":{},"return_reason":null}]' + recorded_at: Tue, 02 Sep 2025 16:01:43 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_list/returns_an_array_of_Transfer_instances.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_list/returns_an_array_of_Transfer_instances.yml new file mode 100644 index 0000000..99718b6 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_list/returns_an_array_of_Transfer_instances.yml @@ -0,0 +1,80 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.fintoc.com/v2/transfers + body: + encoding: ASCII-8BIT + string: '' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Connection: + - close + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 02 Sep 2025 15:59:14 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '2218' + Connection: + - close + Cf-Ray: + - 978e36a3cd4e0ba0-EZE + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Link: + - '' + Etag: + - W/"c44d2930120083fb1fcfe202e1d2c7c5" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - 9f2d1b9e-ab81-412c-b2c8-46a21d943e44 + X-Runtime: + - '0.123093' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=dGWez9dCHhHPY3gIOowmzhSbR5PjvTHLFLD2kGv8rpM2Aopvl00MqFrSvjwLfv0b8SN7sBFSRqHfTLsqRSAr0RyMm2v3L1WRrkg3IJHXnMcfpFPH9msZJiFM8Ch1a3pC"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=31982&min_rtt=31972&rtt_var=12010&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=1792&delivery_rate=132896&cwnd=253&unsent_bytes=0&cid=cc7a1dbdbfbf7d50&ts=384&x=0" + body: + encoding: UTF-8 + string: '[{"object":"transfer","id":"tr_329NGN1M4If6VvcMRALv4gjAQJx","amount":50000,"currency":"MXN","direction":"outbound","status":"succeeded","transaction_date":"2025-09-02T15:58:51Z","post_date":"2025-09-02T00:00:00Z","comment":"Test + payment","reference_id":"123456","tracking_key":"202509029073500000000000000006","receipt_url":"https://www.banxico.org.mx/cep-beta/go?i=90735\u0026s=dummy\u0026d=qOK7ARfoEwlVpkLWUmFvihhtr5%2B4Gf1LA%2FnWvzdRnC8vLzAGeXauPtgz1bSaTV%2BmUP9oThn6%2B%2Bujt23nuyI3u8OR00eq5WezdAT1eG%2BhBvo%3D","mode":"test","counterparty":{"holder_id":"LFHU290523OG0","holder_name":"Jon + Snow","account_number":"735969000000203297","account_type":"clabe","institution":{"id":"90735","name":"FINTOC","country":"mx"}},"account_number":{"id":"acno_31yYL5uOb39vzvRIszPhoXYMjt4","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","number":"735969000000203297","created_at":"2025-08-29T20:02:00Z","updated_at":"2025-08-29T20:02:00Z","mode":"test","description":null,"metadata":{},"status":"enabled","is_root":true,"object":"account_number"},"metadata":{},"return_reason":null},{"object":"transfer","id":"tr_329N35lH813ZhFPBvHV8mXiKKMn","amount":10000000,"currency":"MXN","direction":"inbound","status":"succeeded","transaction_date":"2025-09-02T15:57:04Z","post_date":"2025-09-02T00:00:00Z","comment":null,"reference_id":"3","tracking_key":"202509029073500000000000000005","receipt_url":"https://www.banxico.org.mx/cep-beta/go?i=90735\u0026s=dummy\u0026d=qOK7ARfoEwlVpkLWUmFvihhtr5%2B4Gf1LA%2FnWvzdRnC%2FtIlWZFTyXKH99y1SPS%2FioW%2FpFGGl1VboyxCzRY946tah7TfWJHw%2FM75NVz2XXmBZLqjqrQknbHmKnMMubiNsD","mode":"test","counterparty":{"holder_id":"VFHD640116IY8","holder_name":"Nakesha + Breitenberg","account_number":"680969192837645191","account_type":"clabe","institution":{"id":"90680","name":"CRISTOBAL + COLON","country":"mx"}},"account_number":{"id":"acno_326dzRGqxLee3j9TkaBBBMfs2i0","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","number":"735969000000203365","created_at":"2025-09-01T16:46:57Z","updated_at":"2025-09-01T16:54:40Z","mode":"test","description":"Updated + account number description","metadata":{"test_id":"12345"},"status":"enabled","is_root":false,"object":"account_number"},"metadata":{},"return_reason":null}]' + recorded_at: Tue, 02 Sep 2025 15:59:14 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_return/returns_a_Transfer_instance_with_return_pending_status.yml b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_return/returns_a_Transfer_instance_with_return_pending_status.yml new file mode 100644 index 0000000..cd92f83 --- /dev/null +++ b/spec/vcr/Fintoc_V2_Client/behaves_like_a_client_with_transfers_manager/_transfers/_return/returns_a_Transfer_instance_with_return_pending_status.yml @@ -0,0 +1,80 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.fintoc.com/v2/transfers/return + body: + encoding: UTF-8 + string: '{"transfer_id":"tr_329R3l5JksDkoevCGTOBsugCsnb"}' + headers: + Authorization: + - Bearer sk_test_SeCreT-aPi_KeY + User-Agent: + - fintoc-ruby/0.2.0 + Fintoc-Jws-Signature: + - eyJhbGciOiJSUzI1NiIsIm5vbmNlIjoiMGQ3MGY1MzFkNjk0MmM4MjQzMDdiZjZlZTIzZDMxNjYiLCJ0cyI6MTc1Njg1MTYyMywiY3JpdCI6WyJ0cyIsIm5vbmNlIl19.knGOuq89bDkhnw5JwAZ57rI6Sm405T3aXA8KazSTl_DsavvEeN9MY34athHc_JZt3rsf6daOR0ZK-RnaAwdtGP33XzxGqW8iDYaUo7UysiVN0gIJUl_cJazJWxG1joG8t58RWMxyfiE-VQHYER9c_XWB-t0E17ou8WmCMUgWndlGLQ7vgr03mjWXdDT5AYoZ1jXrXqTsgzy7cUAaA0j84H2aH9KFDnKZ17bKdg4NDnSJ-lalzicQeNgjzVzA0BZ-h5t07stsJiWU_EJwJDiqHOaUSSRiqDr-H2cQpppUJMQyGwBCkbtWR0G8PG39f7e97_yFmOcHkeUCHz8nlthdKg + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Host: + - api.fintoc.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 02 Sep 2025 22:20:24 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '1128' + Connection: + - close + Cf-Ray: + - 979064f7bbd7f21f-GRU + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Etag: + - W/"3954a8d24b91a22476a95d941ad9e2fa" + Cache-Control: + - max-age=0, private, must-revalidate + X-Request-Id: + - c7cb38f4-7136-4d76-8837-0aa2b5aa0381 + X-Runtime: + - '0.363494' + Vary: + - Origin + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=tVgzLymGEFhRDHaueKi3ZCGKvNSYWPxiAgv6Nbcgp0c152jGVD9oaeNNIyp575JdfHU1llo0RZ8eCm853iwMTMe9jb%2FHC3BGP73GbngYphHVz0KonnREFqeqyXX0qmJ8"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Server: + - cloudflare + Server-Timing: + - cfL4;desc="?proto=TCP&rtt=59502&min_rtt=58986&rtt_var=22488&sent=7&recv=8&lost=0&retrans=0&sent_bytes=3879&recv_bytes=2410&delivery_rate=72220&cwnd=178&unsent_bytes=0&cid=5523d13563682b99&ts=692&x=0" + body: + encoding: UTF-8 + string: '{"object":"transfer","id":"tr_329R3l5JksDkoevCGTOBsugCsnb","amount":1000000,"currency":"MXN","direction":"inbound","status":"return_pending","transaction_date":"2025-09-02T16:30:04Z","post_date":"2025-09-02T00:00:00Z","comment":null,"reference_id":"6","tracking_key":"202509029073500000000000000009","receipt_url":"https://www.banxico.org.mx/cep-beta/go?i=90735\u0026s=dummy\u0026d=qOK7ARfoEwlVpkLWUmFvihhtr5%2B4Gf1LA%2FnWvzdRnC8iI16fI6nvQ9dAhBV8NYBa7pR6J8gKo6rGoff3qWJ2xOPIDPJsG2njYPkZrzpG1waTgRRYwe2jS7Y4eikheYeK","mode":"test","counterparty":{"holder_id":"RBZZ190718TEA","holder_name":"Dede + Kuhlman","account_number":"110969987654321988","account_type":"clabe","institution":{"id":"40110","name":"JP + MORGAN","country":"mx"}},"account_number":{"id":"acno_326dzRGqxLee3j9TkaBBBMfs2i0","account_id":"acc_31yYL7h9LVPg121AgFtCyJPDsgM","number":"735969000000203365","created_at":"2025-09-01T16:46:57Z","updated_at":"2025-09-01T16:54:40Z","mode":"test","description":"Updated + account number description","metadata":{"test_id":"12345"},"status":"enabled","is_root":false,"object":"account_number"},"metadata":{},"return_reason":null}' + recorded_at: Tue, 02 Sep 2025 22:20:24 GMT +recorded_with: VCR 6.3.1