diff --git a/README.md b/README.md index 700aabc..86114d0 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 ``` @@ -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. diff --git a/lib/fintoc/base_client.rb b/lib/fintoc/base_client.rb index 3dc64ed..a011a1a 100644 --- a/lib/fintoc/base_client.rb +++ b/lib/fintoc/base_client.rb @@ -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) @@ -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 diff --git a/lib/fintoc/v2/managers/entities_manager.rb b/lib/fintoc/v2/managers/entities_manager.rb index 4cd0071..ebea19c 100644 --- a/lib/fintoc/v2/managers/entities_manager.rb +++ b/lib/fintoc/v2/managers/entities_manager.rb @@ -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) @@ -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 diff --git a/lib/fintoc/v2/managers/onboardings_manager.rb b/lib/fintoc/v2/managers/onboardings_manager.rb new file mode 100644 index 0000000..d93f1d0 --- /dev/null +++ b/lib/fintoc/v2/managers/onboardings_manager.rb @@ -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 diff --git a/lib/fintoc/v2/resources/entity.rb b/lib/fintoc/v2/resources/entity.rb index 29cc562..045776d 100644 --- a/lib/fintoc/v2/resources/entity.rb +++ b/lib/fintoc/v2/resources/entity.rb @@ -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:, @@ -10,6 +12,8 @@ def initialize( holder_name:, holder_id:, is_root:, + status: nil, + country_code: nil, client: nil, ** ) @@ -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 @@ -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) @@ -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 diff --git a/lib/fintoc/v2/resources/onboarding.rb b/lib/fintoc/v2/resources/onboarding.rb new file mode 100644 index 0000000..40cd483 --- /dev/null +++ b/lib/fintoc/v2/resources/onboarding.rb @@ -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 diff --git a/spec/lib/fintoc/base_client_spec.rb b/spec/lib/fintoc/base_client_spec.rb index 90901a9..7a3a28a 100644 --- a/spec/lib/fintoc/base_client_spec.rb +++ b/spec/lib/fintoc/base_client_spec.rb @@ -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) @@ -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 { @@ -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 diff --git a/spec/lib/fintoc/v2/entity_spec.rb b/spec/lib/fintoc/v2/entity_spec.rb index 067d3de..44a60e7 100644 --- a/spec/lib/fintoc/v2/entity_spec.rb +++ b/spec/lib/fintoc/v2/entity_spec.rb @@ -12,6 +12,8 @@ holder_name: 'Test Company LLC', holder_id: '12345678-9', is_root: true, + status: 'active', + country_code: 'CL', client: client } end @@ -25,6 +27,19 @@ 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, + status: 'active', + country_code: 'CL' + ) + end + + it 'defaults status and country_code to nil when not provided' do + minimal = described_class.new( object: 'entity', mode: 'test', id: 'ent_12345', @@ -32,6 +47,21 @@ holder_id: '12345678-9', is_root: true ) + + expect(minimal).to have_attributes(status: nil, country_code: nil) + end + end + + describe '#onboardings' do + it 'returns an OnboardingsManager instance' do + expect(entity.onboardings) + .to be_an_instance_of(Fintoc::V2::Managers::OnboardingsManager) + end + + it 'memoizes the manager instance' do + first_call = entity.onboardings + second_call = entity.onboardings + expect(first_call).to be(second_call) end end diff --git a/spec/lib/fintoc/v2/managers/entities_manager_spec.rb b/spec/lib/fintoc/v2/managers/entities_manager_spec.rb index 8a27b47..3583b0e 100644 --- a/spec/lib/fintoc/v2/managers/entities_manager_spec.rb +++ b/spec/lib/fintoc/v2/managers/entities_manager_spec.rb @@ -3,6 +3,7 @@ RSpec.describe Fintoc::V2::Managers::EntitiesManager 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(:entity_id) { 'ent_31t0VhhrAXASFQTVYfCfIBnljbT' } @@ -31,15 +32,43 @@ let(:entities_data) { [first_entity_data, second_entity_data] } + let(:create_params) { { holder_name: 'Fintoc', holder_id: '12345678-9', country_code: 'cl' } } + before do allow(client).to receive(:get).with(version: :v2).and_return(get_proc) + allow(client).to receive(:post).with(version: :v2, idempotency_key: nil).and_return(post_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(post_proc).to receive(:call).with('entities', + **create_params).and_return(first_entity_data) allow(Fintoc::V2::Entity).to receive(:new) end + describe '#create' do + it 'posts the entity payload and builds the entity' do + manager.create(**create_params) + + expect(post_proc).to have_received(:call).with('entities', **create_params) + expect(Fintoc::V2::Entity).to have_received(:new).with(**first_entity_data, client:) + end + + context 'when idempotency_key is provided' do + let(:idempotency_key) { '123e4567-e89b-12d3-a456-426614174000' } + + before do + allow(client).to receive(:post).with(version: :v2, idempotency_key:).and_return(post_proc) + end + + it 'passes idempotency_key to the POST method' do + manager.create(idempotency_key:, **create_params) + + expect(client).to have_received(:post).with(version: :v2, idempotency_key:) + end + end + end + describe '#get' do it 'fetches and builds an entity' do manager.get(entity_id) diff --git a/spec/lib/fintoc/v2/managers/onboardings_manager_spec.rb b/spec/lib/fintoc/v2/managers/onboardings_manager_spec.rb new file mode 100644 index 0000000..3b7439f --- /dev/null +++ b/spec/lib/fintoc/v2/managers/onboardings_manager_spec.rb @@ -0,0 +1,220 @@ +require 'fintoc/v2/managers/onboardings_manager' + +RSpec.describe Fintoc::V2::Managers::OnboardingsManager do + let(:client) { instance_double(Fintoc::BaseClient) } + let(:get_proc) { instance_double(Proc) } + let(:post_proc) { instance_double(Proc) } + let(:put_proc) { instance_double(Proc) } + let(:entity_id) { 'ent_12345' } + let(:onboarding_id) { 'onbprc_0ujs' } + let(:shareholder_id) { 'onbsh_123' } + let(:base_path) { "entities/#{entity_id}/onboardings" } + let(:manager) { described_class.new(client, entity_id) } + + let(:first_onboarding_data) do + { + id: onboarding_id, + object: 'onboarding', + entity_id: entity_id, + status: 'in_progress', + source: 'api', + submitted_at: nil, + reviewed_at: nil + } + end + + let(:second_onboarding_data) do + { + id: 'onbprc_other', + object: 'onboarding', + entity_id: entity_id, + status: 'submitted', + source: 'dashboard', + submitted_at: '2026-06-22T00:00:00Z', + reviewed_at: nil + } + end + + let(:full_onboarding_data) do + first_onboarding_data.merge( + submittable: true, + data: {}, + shareholders: [], + documents: [] + ) + end + + let(:create_params) do + { + company_information: { legal_name: 'ACME Inc.' }, + legal_representative: { first_name: 'Jane', last_name: 'Doe' }, + transactional_profile: { monthly_amount_range: '0-1000' }, + shareholders: [{ type: 'natural_person', name: 'Jane', percentage: 100 }] + } + end + + before do + allow(client).to receive(:get).with(version: :v2).and_return(get_proc) + allow(client).to receive(:post).with(version: :v2, idempotency_key: nil).and_return(post_proc) + allow(client).to receive(:put).with(version: :v2, idempotency_key: nil).and_return(put_proc) + + allow(get_proc) + .to receive(:call) + .with(base_path) + .and_return([first_onboarding_data, second_onboarding_data]) + allow(get_proc) + .to receive(:call) + .with("#{base_path}/#{onboarding_id}") + .and_return(full_onboarding_data) + allow(post_proc) + .to receive(:call) + .with(base_path, **create_params) + .and_return(full_onboarding_data) + allow(post_proc) + .to receive(:call) + .with("#{base_path}/#{onboarding_id}/submit") + .and_return(full_onboarding_data) + allow(put_proc).to receive(:call).and_return(full_onboarding_data) + + allow(Fintoc::V2::Onboarding).to receive(:new) + end + + describe '#list' do + it 'fetches and builds onboardings (light shape)' do + manager.list + + expect(get_proc).to have_received(:call).with(base_path) + expect(Fintoc::V2::Onboarding).to have_received(:new).with(**first_onboarding_data, client:) + expect(Fintoc::V2::Onboarding).to have_received(:new).with(**second_onboarding_data, client:) + end + + it 'passes parameters to the API call' do + params = { per_page: 50 } + allow(get_proc).to receive(:call).with(base_path, **params).and_return([]) + + manager.list(**params) + + expect(get_proc).to have_received(:call).with(base_path, **params) + end + end + + describe '#get' do + it 'fetches and builds a full onboarding' do + manager.get(onboarding_id) + + expect(get_proc).to have_received(:call).with("#{base_path}/#{onboarding_id}") + expect(Fintoc::V2::Onboarding).to have_received(:new).with(**full_onboarding_data, client:) + end + end + + describe '#create' do + it 'forwards the nested structures as JSON and builds the onboarding' do + manager.create(**create_params) + + expect(post_proc).to have_received(:call).with(base_path, **create_params) + expect(Fintoc::V2::Onboarding).to have_received(:new).with(**full_onboarding_data, client:) + end + + context 'when idempotency_key is provided' do + let(:idempotency_key) { '123e4567-e89b-12d3-a456-426614174000' } + + before do + allow(client).to receive(:post).with(version: :v2, idempotency_key:).and_return(post_proc) + allow(post_proc).to receive(:call).with(base_path, **create_params) + .and_return(full_onboarding_data) + end + + it 'passes idempotency_key to the POST method' do + manager.create(idempotency_key:, **create_params) + + expect(client).to have_received(:post).with(version: :v2, idempotency_key:) + end + end + end + + describe '#submit' do + it 'posts to the submit path without a body and builds the onboarding' do + manager.submit(onboarding_id) + + expect(post_proc).to have_received(:call).with("#{base_path}/#{onboarding_id}/submit") + expect(Fintoc::V2::Onboarding).to have_received(:new).with(**full_onboarding_data, client:) + end + + context 'when idempotency_key is provided' do + let(:idempotency_key) { '123e4567-e89b-12d3-a456-426614174000' } + + before do + allow(client).to receive(:post).with(version: :v2, idempotency_key:).and_return(post_proc) + allow(post_proc).to receive(:call).with("#{base_path}/#{onboarding_id}/submit") + .and_return(full_onboarding_data) + end + + it 'passes idempotency_key to the POST method' do + manager.submit(onboarding_id, idempotency_key:) + + expect(client).to have_received(:post).with(version: :v2, idempotency_key:) + end + end + end + + describe '#upload_document' do + let(:slot_key) { 'incorporation_certificate' } + let(:file_path) { 'spec/support/fixtures/sample_document.pdf' } + + it 'puts a multipart file to the document slot path' do + manager.upload_document(onboarding_id, slot_key, file: file_path) + + expect(client).to have_received(:put).with(version: :v2, idempotency_key: nil) + expect(put_proc).to have_received(:call).with( + "#{base_path}/#{onboarding_id}/documents/#{slot_key}", + form: { file: file_path } + ) + expect(Fintoc::V2::Onboarding).to have_received(:new).with(**full_onboarding_data, client:) + end + + context 'when idempotency_key is provided' do + let(:idempotency_key) { '123e4567-e89b-12d3-a456-426614174000' } + + before do + allow(client).to receive(:put).with(version: :v2, idempotency_key:).and_return(put_proc) + end + + it 'passes idempotency_key to the PUT method' do + manager.upload_document(onboarding_id, slot_key, file: file_path, idempotency_key:) + + expect(client).to have_received(:put).with(version: :v2, idempotency_key:) + end + end + end + + describe '#upload_shareholder_document' do + let(:file_path) { 'spec/support/fixtures/sample_document.pdf' } + + it 'puts a multipart file to the shareholder document path' do + manager.upload_shareholder_document(onboarding_id, shareholder_id, file: file_path) + + expect(client).to have_received(:put).with(version: :v2, idempotency_key: nil) + expect(put_proc).to have_received(:call).with( + "#{base_path}/#{onboarding_id}/shareholders/#{shareholder_id}/document", + form: { file: file_path } + ) + expect(Fintoc::V2::Onboarding).to have_received(:new).with(**full_onboarding_data, client:) + end + + context 'when idempotency_key is provided' do + let(:idempotency_key) { '123e4567-e89b-12d3-a456-426614174000' } + + before do + allow(client).to receive(:put).with(version: :v2, idempotency_key:).and_return(put_proc) + end + + it 'passes idempotency_key to the PUT method' do + manager.upload_shareholder_document( + onboarding_id, shareholder_id, file: file_path, idempotency_key: + ) + + expect(client).to have_received(:put).with(version: :v2, idempotency_key:) + end + end + end +end diff --git a/spec/lib/fintoc/v2/onboarding_spec.rb b/spec/lib/fintoc/v2/onboarding_spec.rb new file mode 100644 index 0000000..0899018 --- /dev/null +++ b/spec/lib/fintoc/v2/onboarding_spec.rb @@ -0,0 +1,112 @@ +require 'fintoc/v2/resources/onboarding' + +RSpec.describe Fintoc::V2::Onboarding do + let(:api_key) { 'sk_test_SeCreT-aPi_KeY' } + let(:client) { Fintoc::V2::Client.new(api_key) } + + let(:shareholder_data) do + { + id: 'onbsh_123', + object: 'onboarding_shareholder', + type: 'natural_person', + name: 'Jane', + last_name: 'Doe', + percentage: 60, + holder_id: '12345678-9', + parent_id: nil, + document: { slot_key: 'shareholder_id', status: 'missing' } + } + end + + let(:document_data) do + { + slot_key: 'incorporation_certificate', + status: 'uploaded', + filename: 'cert.pdf', + uploaded_at: '2026-06-22T00:00:00Z' + } + end + + let(:data) do + { + id: 'onbprc_0ujs', + object: 'onboarding', + entity_id: 'ent_12345', + status: 'in_progress', + source: 'api', + submitted_at: nil, + reviewed_at: nil, + submittable: false, + data: { company_information: { legal_name: 'ACME Inc.' } }, + shareholders: [shareholder_data], + documents: [document_data], + client: client + } + end + + let(:onboarding) { described_class.new(**data) } + + describe '#new' do + it 'creates an instance of Onboarding' do + expect(onboarding).to be_an_instance_of(described_class) + end + + it 'sets all attributes correctly' do # rubocop:disable RSpec/ExampleLength + expect(onboarding).to have_attributes( + id: 'onbprc_0ujs', + object: 'onboarding', + entity_id: 'ent_12345', + status: 'in_progress', + source: 'api', + submitted_at: nil, + reviewed_at: nil, + submittable: false, + data: { company_information: { legal_name: 'ACME Inc.' } }, + shareholders: [shareholder_data], + documents: [document_data] + ) + end + + it 'keeps shareholders as raw hashes' do + expect(onboarding.shareholders.first).to eq(shareholder_data) + end + + it 'keeps documents as raw hashes' do + expect(onboarding.documents.first).to eq(document_data) + end + end + + describe 'light shape' do + let(:light_data) do + { + id: 'onbprc_0ujs', + object: 'onboarding', + entity_id: 'ent_12345', + status: 'pending', + source: 'dashboard', + submitted_at: nil, + reviewed_at: nil + } + end + + let(:light_onboarding) { described_class.new(**light_data) } + + it 'builds without full attributes' do + expect(light_onboarding).to have_attributes( + id: 'onbprc_0ujs', + status: 'pending', + source: 'dashboard', + submittable: nil, + data: nil, + shareholders: nil, + documents: nil + ) + end + end + + describe '#to_s' do + it 'returns a formatted string representation' do + expect(onboarding.to_s).to eq('📋 Onboarding onbprc_0ujs (in_progress)') + end + end +end diff --git a/spec/support/fixtures/sample_document.pdf b/spec/support/fixtures/sample_document.pdf new file mode 100644 index 0000000..8b93536 Binary files /dev/null and b/spec/support/fixtures/sample_document.pdf differ