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..c2dccb7 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 +other_account = Unit::DepositAccount.create( + deposit_product: 'checking', + customer: customer +) + +book_payment = Unit::BookPayment.create( + amount: 10_000, # in cents + description: 'Transfer between accounts', + idempotency_key: "book-payment-for-#{my_user_id}", + account: deposit_account, + counterparty_account: other_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..e646dd7 --- /dev/null +++ b/lib/unit-ruby/book_payment.rb @@ -0,0 +1,25 @@ +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::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..f27f220 --- /dev/null +++ b/spec/features/book_payment_spec.rb @@ -0,0 +1,52 @@ +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 + Factory.fund_deposit_account(deposit_account, 10_000) + expect(Unit::DepositAccount.find(deposit_account.id).available).to be >= 5_000 + + payment = Unit::BookPayment.create( + amount: 5_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.status).to eq('Sent'), "Book payment was #{payment.status}: #{payment.reason}" + 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 + expect(payment.counterparty_account.id).to eq counterparty_deposit_account.id + end +end 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') 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