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
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Do yourself a favor: go grab some ice cubes by installing this refreshing librar
- [Transfers](#transfers)
- [Simulate](#simulate)
- [Account Verifications](#account-verifications)
- [Entity onboardings API Examples](#entity-onboardings-api-examples)
- [Idempotency Keys](#idempotency-keys)
- [Idempotency Examples](#idempotency-examples)
- [Account Methods with Idempotency Key](#account-methods-with-idempotency-key)
Expand Down Expand Up @@ -150,6 +151,11 @@ 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')

# Entity onboardings
entity = client.v2.entities.get('entity_id')
onboardings = entity.onboardings.list
onboarding = entity.onboardings.get('onboarding_id')

# TODO: Movements
```

Expand Down Expand Up @@ -386,6 +392,76 @@ account_verification = client.v2.account_verifications.get('account_verification
account_verifications = client.v2.account_verifications.list
```

### Entity onboardings API Examples

An entity onboarding represents the onboarding process for an entity. You access it from
an `Entity` instance (`entity.onboardings`); the full lifecycle is available: list,
retrieve, create, submit, and upload documents to a slot.

```ruby
require 'fintoc'

client = Fintoc::Client.new('api_key', jws_private_key: 'jws_private_key')
entity = client.v2.entities.get('entity_id')

# List the onboardings of an entity (cursor-paginated, light shape)
onboardings = entity.onboardings.list

# Retrieve a single onboarding (full shape, includes shareholders and documents)
onboarding = entity.onboardings.get('onboarding_id')
puts onboarding # 📋 Onboarding onbprc_0ujs... (in_progress)
onboarding.shareholders # => array of shareholder hashes
onboarding.documents # => array of document hashes

# Create an onboarding. The nested structures are passed through as-is and
# validated by the API.
onboarding = entity.onboardings.create(
company_information: {
incorporation_date: '2020-01-01',
business_activity: 'Software',
legal_name: 'ACME Inc.',
fiscal_address: 'Av. Siempre Viva 123',
business_address: 'Av. Siempre Viva 123',
settlement_account: '1234567890',
phone: '+56912345678'
},
legal_representative: {
first_name: 'Jane',
last_name: 'Doe',
email: 'jane@acme.com',
nationality: 'CL',
identification_number: '12345678-9',
position: 'CEO'
},
transactional_profile: {
resource_origins: ['sales'],
monthly_amount_range: '0-1000000',
monthly_operations_range: '0-100'
},
shareholders: [
{
type: 'natural_person',
name: 'Jane',
last_name: 'Doe',
holder_id: '12345678-9',
nationality: 'CL',
percentage: 100
}
]
)

# Upload a document for a given slot (multipart). `file:` accepts a path or an IO.
entity.onboardings.upload_document('onboarding_id', 'slot_key', file: 'path/to/file.pdf')

# Upload a document for a specific shareholder (multipart).
entity.onboardings.upload_shareholder_document(
'onboarding_id', 'shareholder_id', file: 'path/to/file.pdf'
)

# Submit the onboarding for review.
onboarding = entity.onboardings.submit('onboarding_id')
```

## 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.
Expand Down
18 changes: 16 additions & 2 deletions lib/fintoc/base_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ def patch(version: :v1, use_jws: false, idempotency_key: nil)
request('patch', version:, use_jws:, idempotency_key:)
end

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

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

Expand Down Expand Up @@ -129,6 +133,16 @@ def params(method, **kwargs)
end
end

def build_form(form)
form.transform_values do |value|
file?(value) ? HTTP::FormData::File.new(value) : value
end
end

def file?(value)
value.respond_to?(:read) || (value.is_a?(String) && File.file?(value))
end

def raise_custom_error(error)
raise error_class(error[:code]).new(error[:message], error[:doc_url])
end
Expand Down
10 changes: 10 additions & 0 deletions lib/fintoc/v2/managers/entities_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ def initialize(client)
@client = client
end

def create(holder_name:, holder_id:, country_code:, idempotency_key: nil, **params)
data = _create_entity(holder_name:, holder_id:, country_code:, idempotency_key:, **params)
build_entity(data)
end

def get(entity_id)
data = _get_entity(entity_id)
build_entity(data)
Expand All @@ -19,6 +24,11 @@ def list(**params)

private

def _create_entity(holder_name:, holder_id:, country_code:, idempotency_key: nil, **params)
@client.post(version: :v2, idempotency_key:)
.call('entities', holder_name:, holder_id:, country_code:, **params)
end

def _get_entity(entity_id)
@client.get(version: :v2).call("entities/#{entity_id}")
end
Expand Down
86 changes: 86 additions & 0 deletions lib/fintoc/v2/managers/onboardings_manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require 'fintoc/v2/resources/onboarding'

module Fintoc
module V2
module Managers
class OnboardingsManager
def initialize(client, entity_id)
@client = client
@entity_id = entity_id
end

def list(**params)
_list_onboardings(**params).map { |data| build_onboarding(data) }
end

def get(onboarding_id)
data = _get_onboarding(onboarding_id)
build_onboarding(data)
end

def create(idempotency_key: nil, **params)
data = _create_onboarding(idempotency_key:, **params)
build_onboarding(data)
end

def submit(onboarding_id, idempotency_key: nil)
data = _submit_onboarding(onboarding_id, idempotency_key:)
build_onboarding(data)
end

def upload_document(onboarding_id, slot_key, file:, idempotency_key: nil)
data = _upload_document(onboarding_id, slot_key, file:, idempotency_key:)
build_onboarding(data)
end

def upload_shareholder_document(onboarding_id, shareholder_id, file:, idempotency_key: nil)
data = _upload_shareholder_document(
onboarding_id, shareholder_id, file:, idempotency_key:
)
build_onboarding(data)
end

private

def base_path
"entities/#{@entity_id}/onboardings"
end

def _list_onboardings(**params)
@client.get(version: :v2).call(base_path, **params)
end

def _get_onboarding(onboarding_id)
@client.get(version: :v2).call("#{base_path}/#{onboarding_id}")
end

def _create_onboarding(idempotency_key: nil, **params)
@client.post(version: :v2, idempotency_key:).call(base_path, **params)
end

def _submit_onboarding(onboarding_id, idempotency_key: nil)
@client.post(version: :v2, idempotency_key:)
.call("#{base_path}/#{onboarding_id}/submit")
end

def _upload_document(onboarding_id, slot_key, file:, idempotency_key: nil)
@client.put(version: :v2, idempotency_key:).call(
"#{base_path}/#{onboarding_id}/documents/#{slot_key}",
form: { file: }
)
end

def _upload_shareholder_document(onboarding_id, shareholder_id, file:, idempotency_key: nil)
@client.put(version: :v2, idempotency_key:).call(
"#{base_path}/#{onboarding_id}/shareholders/#{shareholder_id}/document",
form: { file: }
)
end

def build_onboarding(data)
Fintoc::V2::Onboarding.new(**data, client: @client)
end
end
end
end
end
14 changes: 13 additions & 1 deletion lib/fintoc/v2/resources/entity.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
require 'fintoc/v2/managers/onboardings_manager'

module Fintoc
module V2
class Entity
attr_reader :object, :mode, :id, :holder_name, :holder_id, :is_root
attr_reader :object, :mode, :id, :holder_name, :holder_id, :is_root, :status, :country_code

def initialize(
object:,
Expand All @@ -10,6 +12,8 @@ def initialize(
holder_name:,
holder_id:,
is_root:,
status: nil,
country_code: nil,
client: nil,
**
)
Expand All @@ -19,6 +23,8 @@ def initialize(
@holder_name = holder_name
@holder_id = holder_id
@is_root = is_root
@status = status
@country_code = country_code
@client = client
end

Expand All @@ -31,6 +37,10 @@ def refresh
refresh_from_entity(fresh_entity)
end

def onboardings
@onboardings ||= Managers::OnboardingsManager.new(@client, @id)
end

private

def refresh_from_entity(entity)
Expand All @@ -43,6 +53,8 @@ def refresh_from_entity(entity)
@holder_name = entity.holder_name
@holder_id = entity.holder_id
@is_root = entity.is_root
@status = entity.status
@country_code = entity.country_code

self
end
Expand Down
41 changes: 41 additions & 0 deletions lib/fintoc/v2/resources/onboarding.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module Fintoc
module V2
class Onboarding
attr_reader :object, :id, :entity_id, :status, :source, :submitted_at, :reviewed_at,
:submittable, :data, :shareholders, :documents

def initialize(
object:,
id:,
entity_id: nil,
status: nil,
source: nil,
submitted_at: nil,
reviewed_at: nil,
submittable: nil,
data: nil,
shareholders: nil,
documents: nil,
client: nil,
**
)
@object = object
@id = id
@entity_id = entity_id
@status = status
@source = source
@submitted_at = submitted_at
@reviewed_at = reviewed_at
@submittable = submittable
@data = data
@shareholders = shareholders
@documents = documents
@client = client
end

def to_s
"📋 Onboarding #{@id} (#{@status})"
end
end
end
end
46 changes: 46 additions & 0 deletions spec/lib/fintoc/base_client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@
end
end

describe '#put' do
it 'returns a proc for PUT requests' do
put_request = client.put(version: :v1)
expect(put_request).to be_a(Proc)
end
end

describe '#post' do
it 'returns a proc for POST requests' do
post_request = client.post(version: :v1)
Expand Down Expand Up @@ -168,6 +175,25 @@
end
end

context 'when a form is given' do
before do
allow(mock_response).to receive_messages(
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 'builds a form body instead of params' do
pdf = 'spec/support/fixtures/sample_document.pdf'
client.request('put').call('test/resource', form: { file: pdf })

expect(client).to have_received(:make_request)
.with('put', 'test/resource', hash_including(:form), any_args)
end
end

context 'when HTTP request returns an error' do
let(:error_response_body) do
{
Expand Down Expand Up @@ -317,6 +343,26 @@
end
end

describe '#params' do
it 'builds a JSON body for non-GET requests' do
expect(client.send(:params, 'post', amount: 100)).to eq(json: { amount: 100 })
end

it 'builds query params for GET requests' do
expect(client.send(:params, 'get', page: 2)).to eq(params: { page: 2 })
end
end

describe '#build_form' do
it 'wraps file values and leaves other fields untouched' do
result = client.send(:build_form, file: 'spec/support/fixtures/sample_document.pdf',
text: 'x')

expect(result[:file]).to be_a(HTTP::FormData::File)
expect(result[:text]).to eq('x')
end
end

describe '#to_s' do
it 'returns masked API key representation' do
result = client.to_s
Expand Down
Loading
Loading