diff --git a/lib/blnk/resourceable.rb b/lib/blnk/resourceable.rb index 853726f..5964635 100644 --- a/lib/blnk/resourceable.rb +++ b/lib/blnk/resourceable.rb @@ -4,6 +4,9 @@ module Blnk # Resoureable module that bring some tweaks for basic REST api integration class Resourceable < OpenStruct extend Client + extend Forwardable + + def_delegators :'self.class', :put_request, :post_request, :get_request, :with_handler class SearchResult < OpenStruct; end @@ -18,35 +21,30 @@ class << self attr_accessor :resource_name, :id_field, :create_contract, :search_contract - def find(id) = with_req resp: get_request(path: "/#{resource_name}/#{id}") + def resources_path = "/#{resource_name}" + def resource_path(id) = "/#{resource_name}/#{id}" + def search_path = "/search/#{resource_name}" + + def find(id) = with_handler resp: find_request(id) + def find_request(id) = get_request(path: resource_path(id)) def all check_vars - res = get_request(path: "/#{resource_name}") - return Failure(res.parse&.transform_keys(&:to_sym)) unless res.status.success? - - Success(res.parse.map { |r| new(r) }) + resp = get_request(path: resources_path) + with_handler(resp:, block: method(:all_handler)) end def create(**args) - contract = wrap_call(create_contract_new, args) - return contract if contract.failure? - - res = post_request(path: "/#{resource_name}", body: contract.to_h) - return Failure(res.parse&.transform_keys(&:to_sym)) unless res.status.success? - - Success(new(res.parse)) + wrap_call(create_contract_new, args) do |contract| + with_handler(resp: post_request(path: resources_path, body: contract.to_h)) + end end def search(**args) - contract = wrap_call(search_contract_new, args) - return contract if contract.failure? - - res = post_request(path: "/search/#{resource_name}", body: contract.to_h) - return Failure(res.parse&.transform_keys(&:to_sym)) unless res.status.success? - - result = SearchResult.new(res.parse.merge(resource_name:)) - Success(result) + wrap_call(search_contract_new, args) do |contract| + with_handler(resp: post_request(path: search_path, body: contract.to_h), + block: method(:search_handler)) + end end def check_vars @@ -60,6 +58,8 @@ def wrap_call(contract, args) ccall = contract.call(args) return Failure(ccall.errors.to_h) if ccall.failure? + return yield ccall if block_given? + ccall end @@ -71,27 +71,55 @@ def create_contract_new def search_contract_new = (search_contract || DefaultSearchContract).new - def handler(parsed, status) = (status.success? ? Success(new(parsed)) : Failure(parsed)) - def with_req(resp:, self_caller: nil) = using_resp(resp:, self_caller:, &method(:handler)) + def search_handler(parsed, status) + success = status.success? + result = SearchResult.new(parsed) if success - def using_resp(resp:, self_caller: nil, &block) + inj_handler(result:, success:, error: parsed) + end + + def all_handler(parsed, status) + success = status.success? + result = parsed.map { |r| new(r) } if success + inj_handler(success:, result:, error: parsed) + end + + def handler(parsed, status) + inj_handler( + result: new(parsed), + error: parsed, + success: status.success? + ) + end + + def inj_handler(result:, error:, success:) + (success ? Success(result) : Failure(error)) + end + + def with_handler(resp:, kself: nil, block: method(:handler)) + using_resp(resp:, kself:, &block) + end + + def using_resp(resp:, kself: nil, &block) check_vars - parsed = resp.parse.transform_keys(&:to_sym) - self_caller&.reload + parsed = resp.parse + parsed = parsed.transform_keys(&:to_sym) unless parsed.is_a?(Array) + + kself&.reload block.call(parsed, resp.status) end end def reload - self.class.find(_id) do |res| + self.class.find_request(_id).tap do |res| next unless res.status.success? res.parse.each_pair do |k, v| self[k] = v end - self end + self end # table[self.class.id_field] diff --git a/lib/blnk/transaction.rb b/lib/blnk/transaction.rb index 334c9ad..d8d3991 100644 --- a/lib/blnk/transaction.rb +++ b/lib/blnk/transaction.rb @@ -23,15 +23,16 @@ class CreateContract < Dry::Validation::Contract self.id_field = :transaction_id self.create_contract = CreateContract - def refund = self.class.with_req(req: request_refund, self_caller: self) - def void = raise NotImplementedError - def commit = raise NotImplementedError + def refund = short_hander(resp: req_refund) + def void = short_hander(resp: req_inflight(body: { status: 'void' })) + def commit = short_hander(resp: req_inflight(body: { status: 'commit' })) private + def short_hander(resp:) = with_handler(resp:, kself: self) def inflight_path = "/transactions/inflight/#{_id}" - def refund_path = "/refund-transactions/#{_id}" - def request_update_inflight(body:, path: inflight_path) = self.class.put_request(path:, body:) - def request_refund(path: refund_path) = self.class.post_request(path:, body: nil) + def refund_path = "/refund-transaction/#{_id}" + def req_inflight(body:) = put_request(path: inflight_path, body:) + def req_refund = post_request(path: refund_path, body: nil) end end diff --git a/test/blnk/test_balance.rb b/test/blnk/test_balance.rb index fe2ae3a..e50f1ee 100644 --- a/test/blnk/test_balance.rb +++ b/test/blnk/test_balance.rb @@ -7,7 +7,7 @@ def stub_find_balance_request_with_error .to_return_json(body: { error: 'balance with ID \'BALANCE_ID\' not found' }, status: 400) end -def balance_response_body # rubocop:disable Metrics/MethodLength +def balance_response_body(opts: {}) # rubocop:disable Metrics/MethodLength { balance: 0, version: 1, inflight_balance: 0, @@ -23,12 +23,12 @@ def balance_response_body # rubocop:disable Metrics/MethodLength currency: 'USD', created_at: '2024-06-26T01:19:35.122774Z', inflight_expires_at: '0001-01-01T00:00:00Z', - meta_data: nil } + meta_data: nil }.merge(opts) end -def stub_find_balance_request_with_success +def stub_find_balance_request_with_success(opts: {}) stub_request(:get, %r{/balances/(.*)}) - .to_return_json(body: balance_response_body, status: 200) + .to_return_json(body: balance_response_body(opts:), status: 200) end def stub_create_balance_request_with_success @@ -60,6 +60,22 @@ def test_that_balance_not_found assert find.failure? end + def test_that_balance_reload # rubocop:disable Metrics/AbcSize + stub_find_balance_request_with_success(opts: { credit_balance: 20 }) + find = Blnk::Balance.find 'BALANCE_ID' + + assert find.success? + assert find.value!.is_a?(Blnk::Balance) + assert find.value!.credit_balance.eql?(20) + assert find.value!.balance_id.eql?(balance_response_body[:balance_id]) + + stub_find_balance_request_with_success(opts: { credit_balance: 100 }) + + find.value!.reload + + assert find.value!.credit_balance.eql?(100) + end + def test_that_balance_find_success stub_find_balance_request_with_success find = Blnk::Balance.find 'BALANCE_ID' diff --git a/test/blnk/test_transaction.rb b/test/blnk/test_transaction.rb index 061d5fd..4741ccf 100644 --- a/test/blnk/test_transaction.rb +++ b/test/blnk/test_transaction.rb @@ -7,7 +7,37 @@ def stub_find_transaction_request_with_error .to_return_json(body: { error: 'balance with ID \'transaction_id\' not found' }, status: 400) end -def transaction_response_body # rubocop:disable Metrics/MethodLength +def refunded_txn_body # rubocop:disable Metrics/MethodLength + { + transaction_id: 'txn_043d4ac3-4caf-4149-9c0d-927ae637d83f', + tag: 'Refund', + reference: 'ref_70ffd90f-7bcf-43b8-90ba-70505c113b52', + amount: 300, + currency: 'NGN', + payment_method: '', + description: '', + drcr: 'Debit', + status: 'APPLIED', + ledger_id: 'ldg_073f7ffe-9dfd-42ce-aa50-d1dca1788adc', + balance_id: 'bln_0be360ca-86fe-457d-be43-daa3f966d8f0', + credit_balance_before: 600, + debit_balance_before: 0, + credit_balance_after: 600, + debit_balance_after: 300, + balance_before: 600, + balance_after: 300, + created_at: '2024-02-20T05:40:52.630481718Z', + scheduled_for: '0001-01-01T00:00:00Z', + risk_tolerance_threshold: 0, + risk_score: 0.03108, + meta_data: { + refunded_transaction_id: 'txn_5bbbe4d3-2d82-4da1-8191-aaa491d025de' + }, + group_ids: nil + } +end + +def transaction_response_body(opts: {}) # rubocop:disable Metrics/MethodLength { precise_amount: 7500, amount: 75, @@ -27,7 +57,17 @@ def transaction_response_body # rubocop:disable Metrics/MethodLength created_at: '2024-06-27T20:23:17.737289826Z', scheduled_for: '0001-01-01T00:00:00Z', inflight_expiry_date: '0001-01-01T00:00:00Z' - } + }.merge(opts) +end + +def stub_refund_transaction_request_with_success + stub_request(:post, %r{/refund-transaction/(.*)}) + .to_return_json(body: refunded_txn_body, status: 200) +end + +def stub_refund_transaction_request_with_error + stub_request(:post, %r{/refund-transaction/(.*)}) + .to_return_json(body: { error: 'failed_refund_transaction' }, status: 400) end def stub_find_transaction_request_with_success @@ -119,4 +159,47 @@ def test_that_transaction_create_success # rubocop:disable Metric/AbcSize, Metri assert create.value!.transaction_id.eql?(transaction_response_body[:transaction_id]) assert create.value!.name.eql?(transaction_response_body[:name]) end + + def test_that_transaction_refund_error # rubocop:disable Metric/Metrics/MethodLength + stub_find_transaction_request_with_success + stub_create_transaction_request_with_success + stub_refund_transaction_request_with_error + + txn = Blnk::Transaction.create( + amount: 75, + reference: 'ref_005', + currency: 'BRLX', + precision: 100, + source: '@world', + destination: 'bln_469f93bc-40e9-4e0e-b6ab-d11c3638c15d', + description: 'For fees', + allow_overdraft: true + ) + + refund_txn = txn.value!.refund + + assert refund_txn.failure? + end + + def test_that_transaction_refund_success # rubocop:disable Metric/Metrics/MethodLength + stub_find_transaction_request_with_success + stub_create_transaction_request_with_success + stub_refund_transaction_request_with_success + + txn = Blnk::Transaction.create( + amount: 75, + reference: 'ref_005', + currency: 'BRLX', + precision: 100, + source: '@world', + destination: 'bln_469f93bc-40e9-4e0e-b6ab-d11c3638c15d', + description: 'For fees', + allow_overdraft: true + ) + + refund_txn = txn.value!.refund + + assert refund_txn.success? + assert txn.value!.transaction_id != refund_txn.value!.transaction_id + end end