From f06707d4a6a0df325e4afb0ff3b38002004e8e52 Mon Sep 17 00:00:00 2001 From: Pedro Bahamondes Date: Wed, 3 Sep 2025 15:53:06 -0400 Subject: [PATCH 1/5] feature(simulate): Implement simulate methods --- lib/fintoc/transfers/client/client.rb | 2 + .../transfers/client/simulation_methods.rb | 23 ++++++ spec/lib/fintoc/transfers/client_spec.rb | 2 + .../transfers/simulation_methods_spec.rb | 37 +++++++++ .../clients/simulation_client_examples.rb | 23 ++++++ ...a_transfer_and_returns_Transfer_object.yml | 78 +++++++++++++++++++ 6 files changed, 165 insertions(+) create mode 100644 lib/fintoc/transfers/client/simulation_methods.rb create mode 100644 spec/lib/fintoc/transfers/simulation_methods_spec.rb create mode 100644 spec/support/shared_examples/clients/simulation_client_examples.rb create mode 100644 spec/vcr/Fintoc_Transfers_Client/behaves_like_a_client_with_simulation_methods/_simulate_receive_transfer/simulates_receiving_a_transfer_and_returns_Transfer_object.yml diff --git a/lib/fintoc/transfers/client/client.rb b/lib/fintoc/transfers/client/client.rb index 6c945ba..03ac06c 100644 --- a/lib/fintoc/transfers/client/client.rb +++ b/lib/fintoc/transfers/client/client.rb @@ -3,6 +3,7 @@ require 'fintoc/transfers/client/accounts_methods' require 'fintoc/transfers/client/account_numbers_methods' require 'fintoc/transfers/client/transfers_methods' +require 'fintoc/transfers/client/simulation_methods' module Fintoc module Transfers @@ -11,6 +12,7 @@ class Client < BaseClient include AccountsMethods include AccountNumbersMethods include TransfersMethods + include SimulationMethods end end end diff --git a/lib/fintoc/transfers/client/simulation_methods.rb b/lib/fintoc/transfers/client/simulation_methods.rb new file mode 100644 index 0000000..58d7b90 --- /dev/null +++ b/lib/fintoc/transfers/client/simulation_methods.rb @@ -0,0 +1,23 @@ +require 'fintoc/transfers/resources/transfer' + +module Fintoc + module Transfers + module SimulationMethods + def simulate_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:) + post(version: :v2) + .call('simulate/receive_transfer', account_number_id:, amount:, currency:) + end + + def build_transfer(data) + Fintoc::Transfers::Transfer.new(**data, client: self) + end + end + end +end diff --git a/spec/lib/fintoc/transfers/client_spec.rb b/spec/lib/fintoc/transfers/client_spec.rb index cc6c026..b0bbac4 100644 --- a/spec/lib/fintoc/transfers/client_spec.rb +++ b/spec/lib/fintoc/transfers/client_spec.rb @@ -26,4 +26,6 @@ it_behaves_like 'a client with account numbers methods' it_behaves_like 'a client with transfers methods' + + it_behaves_like 'a client with simulation methods' end diff --git a/spec/lib/fintoc/transfers/simulation_methods_spec.rb b/spec/lib/fintoc/transfers/simulation_methods_spec.rb new file mode 100644 index 0000000..91094d6 --- /dev/null +++ b/spec/lib/fintoc/transfers/simulation_methods_spec.rb @@ -0,0 +1,37 @@ +require 'fintoc/transfers/client/simulation_methods' + +RSpec.describe Fintoc::Transfers::SimulationMethods do + let(:client) { test_class.new(api_key) } + + let(:test_class) do + Class.new do + include Fintoc::Transfers::SimulationMethods + + def initialize(api_key) + @api_key = api_key + end + + # Mock the base client methods needed for testing + def post(*, **) + proc { |_resource, **_kwargs| { mock: 'response' } } + end + end + end + + let(:api_key) { 'sk_test_9c8d8CeyBTx1VcJzuDgpm4H-bywJCeSx' } + + describe '#simulate_receive_transfer' do + before do + allow(client).to receive(:build_transfer) + end + + it 'calls build_transfer with the response' do + client.simulate_receive_transfer( + account_number_id: 'acno_123', + amount: 10000, + currency: 'MXN' + ) + expect(client).to have_received(:build_transfer).with({ mock: 'response' }) + end + end +end diff --git a/spec/support/shared_examples/clients/simulation_client_examples.rb b/spec/support/shared_examples/clients/simulation_client_examples.rb new file mode 100644 index 0000000..29aacad --- /dev/null +++ b/spec/support/shared_examples/clients/simulation_client_examples.rb @@ -0,0 +1,23 @@ +RSpec.shared_examples 'a client with simulation methods' do + describe '#simulate_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::Transfers::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 diff --git a/spec/vcr/Fintoc_Transfers_Client/behaves_like_a_client_with_simulation_methods/_simulate_receive_transfer/simulates_receiving_a_transfer_and_returns_Transfer_object.yml b/spec/vcr/Fintoc_Transfers_Client/behaves_like_a_client_with_simulation_methods/_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_Transfers_Client/behaves_like_a_client_with_simulation_methods/_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 From fa1ec07b491a0939bbf9ab5d4273d759eff0a275 Mon Sep 17 00:00:00 2001 From: Pedro Bahamondes Date: Wed, 3 Sep 2025 15:56:49 -0400 Subject: [PATCH 2/5] feature(account/simulate): Use simulate_receive_transfer as isntance method for Account --- lib/fintoc/transfers/resources/account.rb | 8 ++++++++ spec/lib/fintoc/transfers/account_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/fintoc/transfers/resources/account.rb b/lib/fintoc/transfers/resources/account.rb index b32d652..6946098 100644 --- a/lib/fintoc/transfers/resources/account.rb +++ b/lib/fintoc/transfers/resources/account.rb @@ -67,6 +67,14 @@ def closed? @status == 'closed' end + def simulate_receive_transfer(amount:) + @client.simulate_receive_transfer( + account_number_id: @root_account_number_id, + amount:, + currency: @currency + ) + end + private def refresh_from_account(account) diff --git a/spec/lib/fintoc/transfers/account_spec.rb b/spec/lib/fintoc/transfers/account_spec.rb index 9e134cc..d0424ab 100644 --- a/spec/lib/fintoc/transfers/account_spec.rb +++ b/spec/lib/fintoc/transfers/account_spec.rb @@ -157,4 +157,24 @@ .with('acc_123', description: 'Test description') end end + + describe '#simulate_receive_transfer' do + let(:expected_transfer) { instance_double(Fintoc::Transfers::Transfer) } + + before do + allow(client) + .to receive(:simulate_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 defaults' do + result = account.simulate_receive_transfer(amount: 10000) + expect(result).to eq(expected_transfer) + end + end end From 5449324696923d09d239e22862ae90c82237d7fc Mon Sep 17 00:00:00 2001 From: Pedro Bahamondes Date: Wed, 3 Sep 2025 15:58:07 -0400 Subject: [PATCH 3/5] feature(account-number/simulate): Use simulate_receive_transfer as isntance method for AccountNumber --- lib/fintoc/transfers/resources/account_number.rb | 8 ++++++++ spec/lib/fintoc/transfers/account_number_spec.rb | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/fintoc/transfers/resources/account_number.rb b/lib/fintoc/transfers/resources/account_number.rb index 4eb9d37..0e11d26 100644 --- a/lib/fintoc/transfers/resources/account_number.rb +++ b/lib/fintoc/transfers/resources/account_number.rb @@ -68,6 +68,14 @@ def root? @is_root end + def simulate_receive_transfer(amount:, currency: 'MXN') + @client.simulate_receive_transfer( + account_number_id: @id, + amount:, + currency: + ) + end + private def refresh_from_account_number(account_number) diff --git a/spec/lib/fintoc/transfers/account_number_spec.rb b/spec/lib/fintoc/transfers/account_number_spec.rb index 7da62fb..ffd6db8 100644 --- a/spec/lib/fintoc/transfers/account_number_spec.rb +++ b/spec/lib/fintoc/transfers/account_number_spec.rb @@ -162,4 +162,20 @@ ) end end + + describe '#simulate_receive_transfer' do + let(:expected_transfer) { instance_double(Fintoc::Transfers::Transfer) } + + before do + allow(client) + .to receive(:simulate_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 end From 23b6b154d5a4895fc9710308c88a7073125a4181 Mon Sep 17 00:00:00 2001 From: Pedro Bahamondes Date: Wed, 3 Sep 2025 16:07:38 -0400 Subject: [PATCH 4/5] feature(simulate): Only allow simulating inbound transfers on test mode --- lib/fintoc/transfers/resources/account.rb | 8 +++ .../transfers/resources/account_number.rb | 8 +++ .../fintoc/transfers/account_number_spec.rb | 41 +++++++++++--- spec/lib/fintoc/transfers/account_spec.rb | 53 ++++++++++++++----- 4 files changed, 88 insertions(+), 22 deletions(-) diff --git a/lib/fintoc/transfers/resources/account.rb b/lib/fintoc/transfers/resources/account.rb index 6946098..135beb8 100644 --- a/lib/fintoc/transfers/resources/account.rb +++ b/lib/fintoc/transfers/resources/account.rb @@ -67,7 +67,15 @@ 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:, diff --git a/lib/fintoc/transfers/resources/account_number.rb b/lib/fintoc/transfers/resources/account_number.rb index 0e11d26..4663b57 100644 --- a/lib/fintoc/transfers/resources/account_number.rb +++ b/lib/fintoc/transfers/resources/account_number.rb @@ -68,7 +68,15 @@ 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:, diff --git a/spec/lib/fintoc/transfers/account_number_spec.rb b/spec/lib/fintoc/transfers/account_number_spec.rb index ffd6db8..cb8b960 100644 --- a/spec/lib/fintoc/transfers/account_number_spec.rb +++ b/spec/lib/fintoc/transfers/account_number_spec.rb @@ -163,19 +163,44 @@ 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::Transfers::Transfer) } - before do - allow(client) - .to receive(:simulate_receive_transfer) - .with(account_number_id: account_number.id, amount: 10000, currency: 'MXN') - .and_return(expected_transfer) + context 'when in test mode' do + before do + allow(client) + .to receive(:simulate_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 - it 'simulates receiving a transfer using account number id' do - result = account_number.simulate_receive_transfer(amount: 10000) - expect(result).to eq(expected_transfer) + 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/transfers/account_spec.rb b/spec/lib/fintoc/transfers/account_spec.rb index d0424ab..d72346f 100644 --- a/spec/lib/fintoc/transfers/account_spec.rb +++ b/spec/lib/fintoc/transfers/account_spec.rb @@ -158,23 +158,48 @@ 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::Transfers::Transfer) } - before do - allow(client) - .to receive(:simulate_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 defaults' do - result = account.simulate_receive_transfer(amount: 10000) - expect(result).to eq(expected_transfer) + context 'when in test mode' do + before do + allow(client) + .to receive(:simulate_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 From e2a421eb1ecfa7dbc5c32cd2b20d2a777cceaa29 Mon Sep 17 00:00:00 2001 From: Pedro Bahamondes Date: Wed, 3 Sep 2025 18:39:54 -0400 Subject: [PATCH 5/5] refactor(transfers-methods/spec): Better mocks for unit tests of methods --- .../transfers/account_numbers_methods_spec.rb | 23 +++++++++++-------- .../transfers/simulation_methods_spec.rb | 5 ++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/spec/lib/fintoc/transfers/account_numbers_methods_spec.rb b/spec/lib/fintoc/transfers/account_numbers_methods_spec.rb index c64fead..a297712 100644 --- a/spec/lib/fintoc/transfers/account_numbers_methods_spec.rb +++ b/spec/lib/fintoc/transfers/account_numbers_methods_spec.rb @@ -30,23 +30,25 @@ def patch(*, **) describe '#create_account_number' do before do - allow(client).to receive(:build_account_number) + allow(Fintoc::Transfers::AccountNumber).to receive(:new) end it 'calls build_account_number with the response' do client.create_account_number(account_id: 'acc_123') - expect(client).to have_received(:build_account_number).with({ mock: 'response' }) + expect(Fintoc::Transfers::AccountNumber) + .to have_received(:new).with(mock: 'response', client:) end end describe '#get_account_number' do before do - allow(client).to receive(:build_account_number) + allow(Fintoc::Transfers::AccountNumber).to receive(:new) end it 'calls build_account_number with the response' do client.get_account_number('acno_123') - expect(client).to have_received(:build_account_number).with({ mock: 'response' }) + expect(Fintoc::Transfers::AccountNumber) + .to have_received(:new).with(mock: 'response', client:) end end @@ -55,24 +57,27 @@ def patch(*, **) allow(client) .to receive(:_list_account_numbers) .and_return([{ mock: 'response1' }, { mock: 'response2' }]) - allow(client).to receive(:build_account_number) + allow(Fintoc::Transfers::AccountNumber).to receive(:new) end it 'calls build_account_number for each response' do client.list_account_numbers - expect(client).to have_received(:build_account_number).with({ mock: 'response1' }) - expect(client).to have_received(:build_account_number).with({ mock: 'response2' }) + expect(Fintoc::Transfers::AccountNumber) + .to have_received(:new).with(mock: 'response1', client:) + expect(Fintoc::Transfers::AccountNumber) + .to have_received(:new).with(mock: 'response2', client:) end end describe '#update_account_number' do before do - allow(client).to receive(:build_account_number) + allow(Fintoc::Transfers::AccountNumber).to receive(:new) end it 'calls build_account_number with the response' do client.update_account_number('acno_123', description: 'Updated') - expect(client).to have_received(:build_account_number).with({ mock: 'response' }) + expect(Fintoc::Transfers::AccountNumber) + .to have_received(:new).with(mock: 'response', client:) end end end diff --git a/spec/lib/fintoc/transfers/simulation_methods_spec.rb b/spec/lib/fintoc/transfers/simulation_methods_spec.rb index 91094d6..5aa4068 100644 --- a/spec/lib/fintoc/transfers/simulation_methods_spec.rb +++ b/spec/lib/fintoc/transfers/simulation_methods_spec.rb @@ -22,7 +22,7 @@ def post(*, **) describe '#simulate_receive_transfer' do before do - allow(client).to receive(:build_transfer) + allow(Fintoc::Transfers::Transfer).to receive(:new) end it 'calls build_transfer with the response' do @@ -31,7 +31,8 @@ def post(*, **) amount: 10000, currency: 'MXN' ) - expect(client).to have_received(:build_transfer).with({ mock: 'response' }) + expect(Fintoc::Transfers::Transfer) + .to have_received(:new).with(mock: 'response', client:) end end end