Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,3 @@ Ej.:

¿Es seguro hacer rollback?
¿Cuáles son los pasos para hacer rollback?
****
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 1.1.0 - 2025-09-15

### 🚀 New Features

- **Idempotency Key Support**: Added comprehensive idempotency key support across all V2 API operations to help prevent duplicate operations during network issues or retries
- **Accounts**: `create` and `update` methods now accept the `idempotency_key` parameter
- **Account Numbers**: `create` and `update` methods support idempotency keys
- **Transfers**: `create` and `return` operations support idempotency keys
- **Account Verifications**: `create` method supports idempotency keys
- **Simulation**: `receive_transfer` method supports idempotency keys
- **Resource-level methods**: Instance methods like `account.update`, `transfer.return_transfer`, and `account.simulate_receive_transfer` all support idempotency keys

## 1.0.0 - 2025-09-05

### 🚀 New Features
Expand All @@ -12,7 +24,7 @@

- **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
- **Accounts**: Create, read, update, and list accounts
- **Account Numbers**: Manage account numbers/CLABEs
- **Transfers**: Create, retrieve, list, and return transfers
- **Simulation**: Simulate receiving transfers for testing
Expand Down Expand Up @@ -42,4 +54,4 @@

Initial version

* Up to date with the [2020-11-17](https://docs.fintoc.com/docs/api-changelog#2020-11-17) API version
- Up to date with the [2020-11-17](https://docs.fintoc.com/docs/api-changelog#2020-11-17) API version
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
fintoc (1.0.0)
fintoc (1.1.0)
http
money-rails
tabulate
Expand Down
126 changes: 122 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,19 @@ Do yourself a favor: go grab some ice cubes by installing this refreshing librar
- [Get movements](#get-movements)
- [Transfers API Examples](#transfers-api-examples)
- [Entities](#entities)
- [Transfer Accounts](#transfer-accounts)
- [Accounts](#accounts)
- [Account Numbers](#account-numbers)
- [Transfers](#transfers)
- [Simulate](#simulate)
- [Account Verifications](#account-verifications)
- [Idempotency Keys](#idempotency-keys)
- [Idempotency Examples](#idempotency-examples)
- [Account Methods with Idempotency Key](#account-methods-with-idempotency-key)
- [Account Number Methods with Idempotency Key](#account-number-methods-with-idempotency-key)
- [Transfer Methods with Idempotency Key](#transfer-methods-with-idempotency-key)
- [Simulation with Idempotency Key](#simulation-with-idempotency-key)
- [Account Verification with Idempotency Key](#account-verification-with-idempotency-key)
- [About idempotency keys](#about-idempotency-keys)
- [Development](#development)
- [Dependencies](#dependencies)
- [Setup](#setup)
Expand Down Expand Up @@ -107,7 +115,7 @@ client = Fintoc::Client.new('api_key', jws_private_key: 'jws_private_key')
entities = client.v2.entities.list
entity = client.v2.entities.get('entity_id')

# Transfer Accounts
# 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')
Expand Down Expand Up @@ -265,14 +273,14 @@ You can also list entities with pagination:
entities = client.v2.entities.list(limit: 10, starting_after: 'entity_id')
```

#### Transfer Accounts
#### Accounts

```ruby
require 'fintoc'

client = Fintoc::Client.new('api_key', jws_private_key: 'jws_private_key')

# Create a transfer account
# Create an account
account = client.v2.accounts.create(
entity_id: 'entity_id',
description: 'My Business Account'
Expand Down Expand Up @@ -378,6 +386,116 @@ account_verification = client.v2.account_verifications.get('account_verification
account_verifications = client.v2.account_verifications.list
```

## Idempotency Keys

The Fintoc API supports [idempotency](https://docs.fintoc.com/reference/idempotent-requests) for safely retrying requests without accidentally performing the same operation twice. This is particularly useful when creating transfers, account numbers, accounts, or other resources where you want to avoid duplicates due to network issues.

To use idempotency keys, provide an `idempotency_key` parameter when making POST/PATCH requests:

### Idempotency Examples

#### Account Methods with Idempotency Key

Create and update methods support the use of idempotency keys to prevent duplication:

```ruby
require 'fintoc'
require 'securerandom'

client = Fintoc::Client.new('api_key', jws_private_key: 'jws_private_key')

idempotency_key = SecureRandom.uuid
account = client.v2.accounts.create(
entity_id: 'entity_id', description: 'My Business Account', idempotency_key:
)

idempotency_key = SecureRandom.uuid
updated_account = client.v2.accounts.update(
'account_id', description: 'Updated Description', idempotency_key:
)
```

Simulation of transfers can also be done with idempotency key:

```ruby
idempotency_key = SecureRandom.uuid
account.simulate_receive_transfer(amount: 1000, idempotency_key:)
```

#### Account Number Methods with Idempotency Key

Create and update methods support the use of idempotency keys as well:

```ruby
idempotency_key = SecureRandom.uuid
account_number = client.v2.account_numbers.create(
account_id: 'account_id', description: 'Main account number', idempotency_key:
)

idempotency_key = SecureRandom.uuid
updated_account_number = client.v2.account_numbers.update(
'account_number_id', description: 'Updated description', idempotency_key:
)
```

Simulation of transfers can also be done with idempotency key:

```ruby
account_number.simulate_receive_transfer(amount: 1000, currency: 'MXN', idempotency_key:)
```

#### Transfer Methods with Idempotency Key

Creating and returning transfers support the use of idempotency keys:

```ruby
idempotency_key = SecureRandom.uuid
transfer = client.v2.transfers.create(
amount: 10000, currency: 'CLP', account_id: 'account_id', counterparty: { ... }, idempotency_key:
)

idempotency_key = SecureRandom.uuid
returned_transfer = client.v2.transfers.return('transfer_id', idempotency_key:)
```

Returning a transfer as an instance method also supports the use of idempotency key:

```ruby
idempotency_key = SecureRandom.uuid
transfer.return_transfer(idempotency_key:)
```

#### Simulation with Idempotency Key

For simulating transfers, the use of idempotency keys is also supported:

```ruby
idempotency_key = SecureRandom.uuid
simulated_transfer = client.v2.simulate.receive_transfer(
account_number_id: 'account_number_id', amount: 5000, currency: 'CLP', idempotency_key:
)
```

#### Account Verification with Idempotency Key

```ruby
idempotency_key = SecureRandom.uuid
account_verification = client.v2.account_verifications.create(
account_number: 'account_number', idempotency_key:
)
```

### About idempotency keys

- Idempotency keys can be up to 255 characters long
- Use consistent unique identifiers for the same logical operation (e.g. order IDs, transaction references). If you set them randomly, we suggest using V4 UUIDs, or another random string with enough entropy to avoid collisions.
- The same idempotency key will return the same result, including errors
- Keys are automatically removed after 24 hours
- Only POST and PATCH requests currently support idempotency keys
- If parameters differ with the same key, an error will be raised

For more information, see the [Fintoc API documentation on idempotent requests](https://docs.fintoc.com/reference/idempotent-requests).

## Development

### Dependencies
Expand Down
33 changes: 22 additions & 11 deletions lib/fintoc/base_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,25 @@ def initialize(api_key, jws_private_key: nil)
end

def get(version: :v1)
request('get', version: version)
request('get', version:)
end

def delete(version: :v1)
request('delete', version: version)
request('delete', version:)
end

def post(version: :v1, use_jws: false)
request('post', version: version, use_jws: use_jws)
def post(version: :v1, use_jws: false, idempotency_key: nil)
request('post', version:, use_jws:, idempotency_key:)
end

def patch(version: :v1, use_jws: false)
request('patch', version: version, use_jws: use_jws)
def patch(version: :v1, use_jws: false, idempotency_key: nil)
request('patch', version:, use_jws:, idempotency_key:)
end

def request(method, version: :v1, use_jws: false)
def request(method, version: :v1, use_jws: false, idempotency_key: nil)
proc do |resource, **kwargs|
parameters = params(method, **kwargs)
response = make_request(method, resource, parameters, version: version, use_jws: use_jws)
response = make_request(method, resource, parameters, version:, use_jws:, idempotency_key:)
content = JSON.parse(response.body, symbolize_names: true)

if response.status.client_error? || response.status.server_error?
Expand Down Expand Up @@ -91,23 +91,34 @@ 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)
def should_use_idempotency_key?(method, idempotency_key)
idempotency_key && %w[post patch put].include?(method.downcase)
end

def make_request(
method, resource, parameters, version: :v1, use_jws: false, idempotency_key: nil
)
# 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:)
request_client = client

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)
request_client = request_client.headers('Fintoc-JWS-Signature' => jws_signature)
end

if should_use_idempotency_key?(method, idempotency_key)
request_client = request_client.headers('Idempotency-Key' => idempotency_key)
end

client.send(method, url, parameters)
request_client.send(method, url, parameters)
end

def params(method, **kwargs)
Expand Down
21 changes: 13 additions & 8 deletions lib/fintoc/v2/managers/account_numbers_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ def initialize(client)
@client = client
end

def create(account_id:, description: nil, metadata: nil, **params)
data = _create_account_number(account_id:, description:, metadata:, **params)
def create(account_id:, description: nil, metadata: nil, idempotency_key: nil, **params)
data = _create_account_number(
account_id:, description:, metadata:, idempotency_key:, **params
)
build_account_number(data)
end

Expand All @@ -22,20 +24,22 @@ 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)
def update(account_number_id, idempotency_key: nil, **params)
data = _update_account_number(account_number_id, idempotency_key:, **params)
build_account_number(data)
end

private

def _create_account_number(account_id:, description: nil, metadata: nil, **params)
def _create_account_number(
account_id:, description: nil, metadata: nil, idempotency_key: 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)
@client.post(version: :v2, idempotency_key:).call('account_numbers', **request_params)
end

def _get_account_number(account_number_id)
Expand All @@ -46,8 +50,9 @@ 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)
def _update_account_number(account_number_id, idempotency_key: nil, **params)
@client.patch(version: :v2, idempotency_key:)
.call("account_numbers/#{account_number_id}", **params)
end

def build_account_number(data)
Expand Down
9 changes: 5 additions & 4 deletions lib/fintoc/v2/managers/account_verifications_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ def initialize(client)
@client = client
end

def create(account_number:)
data = _create_account_verification(account_number:)
def create(account_number:, idempotency_key: nil)
data = _create_account_verification(account_number:, idempotency_key:)
build_account_verification(data)
end

Expand All @@ -24,8 +24,9 @@ def list(**params)

private

def _create_account_verification(account_number:)
@client.post(version: :v2, use_jws: true).call('account_verifications', account_number:)
def _create_account_verification(account_number:, idempotency_key: nil)
@client.post(version: :v2, use_jws: true, idempotency_key:)
.call('account_verifications', account_number:)
end

def _get_account_verification(account_verification_id)
Expand Down
Loading