From 433a9bb8f0d884586256354145bb39fa4c141a3e Mon Sep 17 00:00:00 2001 From: Trevor Nelson Date: Wed, 7 Oct 2026 09:59:15 -0400 Subject: [PATCH 1/5] Add BookPayment resource for booking transfers between accounts Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 5 +++ README.md | 14 ++++++++ lib/unit-ruby.rb | 1 + lib/unit-ruby/book_payment.rb | 27 ++++++++++++++++ lib/unit-ruby/util/api_resource.rb | 9 ++++-- lib/unit-ruby/version.rb | 2 +- spec/features/book_payment_spec.rb | 51 ++++++++++++++++++++++++++++++ spec/version_spec.rb | 2 +- 8 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 lib/unit-ruby/book_payment.rb create mode 100644 spec/features/book_payment_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c185e0..1625fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 1.2.0 + +- Add `Unit::BookPayment` for booking transfers between Unit accounts. +- Allow `belongs_to` relationships to specify the JSON:API `type` sent to Unit. + ## 1.1.1 - Publish releases automatically to RubyGems.org via OIDC trusted publishing when `version.rb` changes on `main`. diff --git a/README.md b/README.md index 98bb7a2..6ebe8ae 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,20 @@ debit_card = Unit::IndividualDebitCard.create( shipping_address: customer.address, tags: { externalUserId: my_user_id } ) + +# Book a transfer between two Unit accounts +savings_account = Unit::DepositAccount.create( + deposit_product: 'savings', + customer: customer +) + +book_payment = Unit::BookPayment.create( + amount: 10_000, # in cents + description: 'Transfer to savings', + idempotency_key: "book-payment-for-#{my_user_id}", + account: deposit_account, + counterparty_account: savings_account +) ``` > [!TIP] diff --git a/lib/unit-ruby.rb b/lib/unit-ruby.rb index 5353713..8ff1798 100644 --- a/lib/unit-ruby.rb +++ b/lib/unit-ruby.rb @@ -29,6 +29,7 @@ require 'unit-ruby/application_form' require 'unit-ruby/atm_location' require 'unit-ruby/authorization' +require 'unit-ruby/book_payment' require 'unit-ruby/ach_counterparty' require 'unit-ruby/customer_token_verification' require 'unit-ruby/customer_token' diff --git a/lib/unit-ruby/book_payment.rb b/lib/unit-ruby/book_payment.rb new file mode 100644 index 0000000..db330fc --- /dev/null +++ b/lib/unit-ruby/book_payment.rb @@ -0,0 +1,27 @@ +module Unit + # A transfer of funds between two Unit accounts + class BookPayment < APIResource + path '/payments' + + attribute :amount, Types::Integer # Amount to transfer in cents. + attribute :description, Types::String # Description of the payment. Max of 50 characters. + attribute :transaction_summary_override, Types::String # Optional. Overrides the transaction summary. Max of 100 characters. + attribute :idempotency_key, Types::String # Optional + attribute :tags, Types::Hash # Optional + + attribute :direction, Types::String, readonly: true + attribute :summary, Types::String, readonly: true + attribute :status, Types::String, readonly: true + attribute :reason, Types::String, readonly: true + attribute :created_at, Types::DateTime, readonly: true + + belongs_to :account, class_name: 'Unit::DepositAccount' + belongs_to :counterparty_account, class_name: 'Unit::DepositAccount', type: :depositAccount + belongs_to :customer, class_name: 'Unit::IndividualCustomer' + belongs_to :counterparty_customer, class_name: 'Unit::IndividualCustomer' + + include ResourceOperations::Find + include ResourceOperations::List + include ResourceOperations::Create + end +end diff --git a/lib/unit-ruby/util/api_resource.rb b/lib/unit-ruby/util/api_resource.rb index 16a27c8..3a12876 100644 --- a/lib/unit-ruby/util/api_resource.rb +++ b/lib/unit-ruby/util/api_resource.rb @@ -98,8 +98,13 @@ def resource_type # Creates an association to a related resource # This will create a helper method to traverse into a resource's related resource(s) - def self.belongs_to(resource_name, class_name: nil) + # + # @param resource_name [Symbol] the name of the relationship + # @param class_name [String] the class of the related resource + # @param type [Symbol] the JSON:API type sent for the relationship, when it differs from the relationship name + def self.belongs_to(resource_name, class_name: nil, type: nil) class_name ||= resource_name.to_s.camelize + type ||= resource_name.to_s.camelize(:lower).to_sym define_method(resource_name) do relationship_id = relationships.dig(resource_name, :data, :id) @@ -111,7 +116,7 @@ def self.belongs_to(resource_name, class_name: nil) define_method("#{resource_name}=") do |resource| relationships[resource_name] = { - data: { type: resource_name.to_s.camelize(:lower).to_sym, id: resource.id } + data: { type: type, id: resource.id } } end end diff --git a/lib/unit-ruby/version.rb b/lib/unit-ruby/version.rb index cd69ddc..82394a9 100644 --- a/lib/unit-ruby/version.rb +++ b/lib/unit-ruby/version.rb @@ -1,3 +1,3 @@ module Unit - VERSION = '1.1.1' + VERSION = '1.2.0' end diff --git a/spec/features/book_payment_spec.rb b/spec/features/book_payment_spec.rb new file mode 100644 index 0000000..a7cb6e6 --- /dev/null +++ b/spec/features/book_payment_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' +require 'securerandom' + +RSpec.describe Unit::BookPayment do + before do + establish_connection_to_api! + end + + let(:new_individual_customer) do + Factory.create_individual_customer + end + + let(:deposit_account) do + Factory.create_deposit_account(new_individual_customer) + end + + let(:counterparty_deposit_account) do + Factory.create_deposit_account(new_individual_customer) + end + + it 'serializes the counterparty account relationship as a deposit account' do + payment = Unit::BookPayment.new + payment.counterparty_account = Unit::DepositAccount.new.tap { |account| account.id = '10000' } + + expect(payment.relationships[:counterparty_account]).to eq( + data: { type: :depositAccount, id: '10000' } + ) + end + + it 'creates a book payment between two accounts' do + payment = Unit::BookPayment.create( + amount: 10_000, + description: 'Funding', + transaction_summary_override: 'Transfer to savings', + idempotency_key: SecureRandom.uuid, + account: deposit_account, + counterparty_account: counterparty_deposit_account, + tags: { tag1: 'value1' } + ) + + expect(payment.type).to eq 'bookPayment' + expect(payment.amount).to eq 10_000 + expect(payment.description).to eq 'Funding' + expect(payment.tags[:tag1]).to eq 'value1' + expect(payment.account.id).to eq deposit_account.id + expect(payment.counterparty_account.id).to eq counterparty_deposit_account.id + + expect(Unit::BookPayment.find(payment.id).id).to eq payment.id + expect(Unit::BookPayment.list(where: { type: ['BookPayment'] })).not_to be_empty + end +end diff --git a/spec/version_spec.rb b/spec/version_spec.rb index 9e12a83..979f52a 100644 --- a/spec/version_spec.rb +++ b/spec/version_spec.rb @@ -2,7 +2,7 @@ RSpec.describe Unit do it 'returns the correct version' do - expect(Unit::VERSION).to eq '1.1.1' + expect(Unit::VERSION).to eq '1.2.0' end end From 81449b7f2923ab16e8c1ab30830784e22e5b990a Mon Sep 17 00:00:00 2001 From: Trevor Nelson Date: Wed, 7 Oct 2026 11:54:14 -0400 Subject: [PATCH 2/5] Limit BookPayment to create Co-Authored-By: Claude Opus 5.5 (1M context) --- lib/unit-ruby/book_payment.rb | 2 -- spec/features/book_payment_spec.rb | 3 --- 2 files changed, 5 deletions(-) diff --git a/lib/unit-ruby/book_payment.rb b/lib/unit-ruby/book_payment.rb index db330fc..e646dd7 100644 --- a/lib/unit-ruby/book_payment.rb +++ b/lib/unit-ruby/book_payment.rb @@ -20,8 +20,6 @@ class BookPayment < APIResource belongs_to :customer, class_name: 'Unit::IndividualCustomer' belongs_to :counterparty_customer, class_name: 'Unit::IndividualCustomer' - include ResourceOperations::Find - include ResourceOperations::List include ResourceOperations::Create end end diff --git a/spec/features/book_payment_spec.rb b/spec/features/book_payment_spec.rb index a7cb6e6..dc2f184 100644 --- a/spec/features/book_payment_spec.rb +++ b/spec/features/book_payment_spec.rb @@ -44,8 +44,5 @@ expect(payment.tags[:tag1]).to eq 'value1' expect(payment.account.id).to eq deposit_account.id expect(payment.counterparty_account.id).to eq counterparty_deposit_account.id - - expect(Unit::BookPayment.find(payment.id).id).to eq payment.id - expect(Unit::BookPayment.list(where: { type: ['BookPayment'] })).not_to be_empty end end From 6b20b7cbc162e1e3cb28158d6875d0e6bed55cc5 Mon Sep 17 00:00:00 2001 From: Trevor Nelson Date: Wed, 7 Oct 2026 14:45:50 -0400 Subject: [PATCH 3/5] Fund source account in book payment spec and use checking in README Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 8 ++++---- spec/features/book_payment_spec.rb | 3 +++ spec/spec_helper.rb | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6ebe8ae..c2dccb7 100644 --- a/README.md +++ b/README.md @@ -97,17 +97,17 @@ debit_card = Unit::IndividualDebitCard.create( ) # Book a transfer between two Unit accounts -savings_account = Unit::DepositAccount.create( - deposit_product: 'savings', +other_account = Unit::DepositAccount.create( + deposit_product: 'checking', customer: customer ) book_payment = Unit::BookPayment.create( amount: 10_000, # in cents - description: 'Transfer to savings', + description: 'Transfer between accounts', idempotency_key: "book-payment-for-#{my_user_id}", account: deposit_account, - counterparty_account: savings_account + counterparty_account: other_account ) ``` diff --git a/spec/features/book_payment_spec.rb b/spec/features/book_payment_spec.rb index dc2f184..3744388 100644 --- a/spec/features/book_payment_spec.rb +++ b/spec/features/book_payment_spec.rb @@ -28,6 +28,8 @@ end it 'creates a book payment between two accounts' do + Factory.fund_deposit_account(deposit_account, 10_000) + payment = Unit::BookPayment.create( amount: 10_000, description: 'Funding', @@ -39,6 +41,7 @@ ) expect(payment.type).to eq 'bookPayment' + expect(payment.status).to eq 'Sent' expect(payment.amount).to eq 10_000 expect(payment.description).to eq 'Funding' expect(payment.tags[:tag1]).to eq 'value1' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3b3085d..df7ff3f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -48,6 +48,20 @@ def self.create_deposit_account(customer) Unit::DepositAccount.create(deposit_product: 'checking', customer: customer) end + # Simulates an incoming ACH credit to fund an account (sandbox only) + def self.fund_deposit_account(deposit_account, amount) + Unit::APIResource.connection.post( + '/sandbox/payments', + { + data: { + type: 'achPayment', + attributes: { amount: amount, direction: 'Credit', description: 'Sandbox funding' }, + relationships: { account: { data: { type: 'depositAccount', id: deposit_account.id } } } + } + } + ) + end + def self.create_debit_card(customer, deposit_account) random_user_id = rand(10**9).to_s.rjust(9, '0') From 0bf3e0e0dbdf1e142a91eb790e5f7b89686be36a Mon Sep 17 00:00:00 2001 From: Trevor Nelson Date: Wed, 7 Oct 2026 14:48:28 -0400 Subject: [PATCH 4/5] Surface book payment rejection reason and verify funding in spec Co-Authored-By: Claude Opus 5.5 (1M context) --- spec/features/book_payment_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/features/book_payment_spec.rb b/spec/features/book_payment_spec.rb index 3744388..4c3f74f 100644 --- a/spec/features/book_payment_spec.rb +++ b/spec/features/book_payment_spec.rb @@ -29,6 +29,7 @@ it 'creates a book payment between two accounts' do Factory.fund_deposit_account(deposit_account, 10_000) + expect(Unit::DepositAccount.find(deposit_account.id).available).to be >= 10_000 payment = Unit::BookPayment.create( amount: 10_000, @@ -41,7 +42,7 @@ ) expect(payment.type).to eq 'bookPayment' - expect(payment.status).to eq 'Sent' + expect(payment.status).to eq('Sent'), "Book payment was #{payment.status}: #{payment.reason}" expect(payment.amount).to eq 10_000 expect(payment.description).to eq 'Funding' expect(payment.tags[:tag1]).to eq 'value1' From 1be32e9345c2fb83ca6a2199f85ce2d2432b1144 Mon Sep 17 00:00:00 2001 From: Trevor Nelson Date: Wed, 7 Oct 2026 14:50:50 -0400 Subject: [PATCH 5/5] Book less than the funded amount in book payment spec Co-Authored-By: Claude Opus 5.5 (1M context) --- spec/features/book_payment_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/book_payment_spec.rb b/spec/features/book_payment_spec.rb index 4c3f74f..f27f220 100644 --- a/spec/features/book_payment_spec.rb +++ b/spec/features/book_payment_spec.rb @@ -29,10 +29,10 @@ it 'creates a book payment between two accounts' do Factory.fund_deposit_account(deposit_account, 10_000) - expect(Unit::DepositAccount.find(deposit_account.id).available).to be >= 10_000 + expect(Unit::DepositAccount.find(deposit_account.id).available).to be >= 5_000 payment = Unit::BookPayment.create( - amount: 10_000, + amount: 5_000, description: 'Funding', transaction_summary_override: 'Transfer to savings', idempotency_key: SecureRandom.uuid, @@ -43,7 +43,7 @@ expect(payment.type).to eq 'bookPayment' expect(payment.status).to eq('Sent'), "Book payment was #{payment.status}: #{payment.reason}" - expect(payment.amount).to eq 10_000 + expect(payment.amount).to eq 5_000 expect(payment.description).to eq 'Funding' expect(payment.tags[:tag1]).to eq 'value1' expect(payment.account.id).to eq deposit_account.id