From 53c5411b66d67ed0fdf601faaf985eb20c4f76dd Mon Sep 17 00:00:00 2001 From: Bolo Michelin <72155+bolom@users.noreply.github.com> Date: Wed, 12 Nov 2025 08:37:28 -0400 Subject: [PATCH 01/11] Refactor util.rb to fix rubocop issues and improve maintainability - Reduced Util module from 113+ lines to under 100 lines to satisfy rubocop Metrics/ModuleLength - Fixed assignment in condition warnings by using proper parentheses - Replaced method-based constants with frozen constants for better performance - Removed redundant methods and consolidated logic for better maintainability - Used Object.const_get() to properly handle resource constant loading - Improved code efficiency with transform_keys and transform_values - Organized bridge_api.rb to properly load service files Co-authored-by: Qwen-Coder --- lib/bridge_api.rb | 7 ++ lib/bridge_api/util.rb | 187 +++++++++++++++-------------------------- 2 files changed, 76 insertions(+), 118 deletions(-) diff --git a/lib/bridge_api.rb b/lib/bridge_api.rb index 118acd3..edefe55 100644 --- a/lib/bridge_api.rb +++ b/lib/bridge_api.rb @@ -73,3 +73,10 @@ def default_base_url require_relative 'bridge_api/resources/total_balance' require_relative 'bridge_api/models/wallets_collection' require_relative 'bridge_api/client' +require_relative 'bridge_api/services/base_service' +require_relative 'bridge_api/services/kyc_link_service' +require_relative 'bridge_api/services/customer_service' +require_relative 'bridge_api/services/wallet_service' +require_relative 'bridge_api/services/external_account_service' +require_relative 'bridge_api/services/virtual_account_service' +require_relative 'bridge_api/services/webhook_service' diff --git a/lib/bridge_api/util.rb b/lib/bridge_api/util.rb index 2040160..489ca12 100644 --- a/lib/bridge_api/util.rb +++ b/lib/bridge_api/util.rb @@ -2,145 +2,96 @@ module BridgeApi module Util - # Map of object names to their corresponding classes for automatic conversion - def self.object_classes - { - 'wallet' => BridgeApi::Resources::Wallet, - 'customer' => BridgeApi::Resources::Customer, - 'transaction_history' => BridgeApi::Resources::TransactionHistory, - 'reward_rate' => BridgeApi::Resources::RewardRate, - 'kyc_link' => BridgeApi::Resources::KycLink, - 'webhook' => BridgeApi::Resources::Webhook, - 'webhook_event' => BridgeApi::Resources::WebhookEvent, - 'webhook_event_delivery_log' => BridgeApi::Resources::WebhookEventDeliveryLog, - 'virtual_account' => BridgeApi::Resources::VirtualAccount, - } - end + OBJECT_CLASS_NAMES = { + 'wallet' => 'BridgeApi::Resources::Wallet', + 'customer' => 'BridgeApi::Resources::Customer', + 'transaction_history' => 'BridgeApi::Resources::TransactionHistory', + 'reward_rate' => 'BridgeApi::Resources::RewardRate', + 'kyc_link' => 'BridgeApi::Resources::KycLink', + 'webhook' => 'BridgeApi::Resources::Webhook', + 'webhook_event' => 'BridgeApi::Resources::WebhookEvent', + 'webhook_event_delivery_log' => 'BridgeApi::Resources::WebhookEventDeliveryLog', + 'virtual_account' => 'BridgeApi::Resources::VirtualAccount', + }.freeze - # Convert API response data to appropriate resource objects - def self.convert_to_bridged_object(data, opts = {}) - resource_hint = opts[:resource_hint] + RESOURCE_PATTERNS = { + 'wallet' => { all: %i[id chain address] }, + 'customer' => { all: %i[id], any: %i[email name customer_type] }, + 'virtual_account' => { all: %i[id account_number] }, + 'transaction_history' => { all: %i[id], any: %i[amount transaction_date] }, + 'kyc_link' => { all: %i[id redirect_url] }, + }.freeze - case data - when Array - data.map { |item| convert_to_bridged_object(item, opts) } - when Hash - # Check if this is a list object (has 'data' key, optional 'count') - if data.key?('data') || data.key?(:data) - # This looks like a list response, convert to ListObject - list_data = symbolize_keys(data) - # Convert the data array items recursively if data exists - if list_data[:data].is_a?(Array) - list_data[:data] = list_data[:data].map do |item| - convert_to_bridged_object(item, opts) - end + class << self + # Convert API response data to appropriate resource objects + def convert_to_bridged_object(data, opts = {}) + case data + when Array + data.map { |item| convert_to_bridged_object(item, opts) } + when Hash + if data.key?('data') || data.key?(:data) + convert_list_object(data, opts) + elsif (object_name = detect_resource_type_from_object_field(data)) + construct_resource(object_name, symbolize_keys(data), opts) + elsif (resource_hint = opts[:resource_hint]) && OBJECT_CLASS_NAMES.key?(resource_hint.to_s) + construct_resource(resource_hint.to_s, symbolize_keys(data), opts) + elsif (detected_type = detect_resource_type(data)) + construct_resource(detected_type, symbolize_keys(data), opts) + else + symbolize_keys(data).transform_values { |value| convert_to_bridged_object(value, opts) } end - BridgeApi::ListObject.new(list_data) - # Check if this is a resource object with an 'object' field - elsif (object_name = data['object'] || data[:object]) && object_classes.key?(object_name.to_s) - construct_resource(object_name.to_s, symbolize_keys(data), opts) - elsif resource_hint && object_classes.key?(resource_hint.to_s) - # Use hint if no object field present - construct_resource(resource_hint.to_s, symbolize_keys(data), opts) - elsif likely_resource_type(data) - # Auto-detect resource type based on common fields - object_name = likely_resource_type(data) - construct_resource(object_name, symbolize_keys(data), opts) else - # For non-resource objects, return as is or convert nested objects - convert_nested_objects(symbolize_keys(data)) + data end - else - data end - end - # Convert hash keys to symbols recursively - def self.symbolize_keys(obj) - case obj - when Hash - obj.each_with_object({}) do |(key, value), new_hash| - new_hash[key.to_sym] = symbolize_keys(value) - end - when Array - obj.map { |item| symbolize_keys(item) } - else - obj - end - end + private - def self.construct_resource(object_name, data, opts) - klass = object_classes[object_name] - if klass.respond_to?(:construct_from) - klass.construct_from(data, opts) - else - klass.new(data) + def detect_resource_type_from_object_field(data) + object_name = data['object'] || data[:object] + object_name && OBJECT_CLASS_NAMES.key?(object_name.to_s) ? object_name.to_s : nil end - end - # Attempt to detect resource type based on common identifying fields - def self.likely_resource_type(data) - return false unless data.is_a?(Hash) - - # Search for the first matching resource type - resource_checks = { - 'wallet' => ->(d) { all_keys?(d, %i[id chain address]) }, - 'customer' => ->(d) { all_keys?(d, %i[id]) && any_key?(d, %i[email name customer_type]) }, - 'virtual_account' => ->(d) { all_keys?(d, %i[id account_number]) }, - 'transaction_history' => ->(d) { all_keys?(d, %i[id]) && any_key?(d, %i[amount transaction_date]) }, - 'kyc_link' => ->(d) { all_keys?(d, %i[id redirect_url]) }, - } - - resource_checks.each do |resource_type, check_proc| - return resource_type if check_proc.call(data) && object_classes.key?(resource_type) + def convert_list_object(data, opts) + list_data = symbolize_keys(data) + if list_data[:data].is_a?(Array) + list_data[:data] = list_data[:data].map do |item| + convert_to_bridged_object(item, opts) + end + end + BridgeApi::ListObject.new(list_data) end - false - end + def detect_resource_type(data) + return nil unless data.is_a?(Hash) - # Helper method to check if data has all the specified keys - def self.all_keys?(data, keys) - keys.all? { |key| data.key?(key) } - end - - # Helper method to check if data has at least one of the specified keys - def self.any_key?(data, keys) - keys.any? { |key| data.key?(key) } - end + RESOURCE_PATTERNS.each do |resource_type, pattern| + all_keys_present = Array(pattern[:all]).all? { |key| data.key?(key) || data.key?(key.to_s) } + any_key_present = !pattern[:any] || Array(pattern[:any]).any? { |key| data.key?(key) || data.key?(key.to_s) } - # Recursively convert nested objects that look like API resources - def self.convert_nested_objects(data) - case data - when Hash - # Check if this is a likely resource object (has id, object type, etc.) - if data[:id] && data[:object] - convert_resource_object(data) - else - # Recursively check nested values - data.transform_values do |value| - convert_nested_objects(value) - end + return resource_type if all_keys_present && any_key_present && OBJECT_CLASS_NAMES.key?(resource_type) end - when Array - data.map { |item| convert_nested_objects(item) } - else - data + + nil end - end - private_class_method def self.convert_resource_object(data) - object_name = data[:object] - if object_classes.key?(object_name.to_s) - klass = object_classes[object_name.to_s] + def construct_resource(object_name, data, opts) + klass = Object.const_get(OBJECT_CLASS_NAMES[object_name]) if klass.respond_to?(:construct_from) - klass.construct_from(data) + klass.construct_from(data, opts) else klass.new(data) end - else - # Recursively check nested values - data.transform_values do |value| - convert_nested_objects(value) + end + + def symbolize_keys(obj) + case obj + when Hash + obj.transform_keys(&:to_sym).transform_values { |value| symbolize_keys(value) } + when Array + obj.map { |item| symbolize_keys(item) } + else + obj end end end From 976070b817dc7dcb3a03be61939589638341983a Mon Sep 17 00:00:00 2001 From: Bolo Michelin <72155+bolom@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:46:16 -0400 Subject: [PATCH 02/11] Modify customer retrieve to return Customer object directly instead of response wrapper - Updated CustomerService#retrieve to return a Customer object directly when successful - Modified example script to handle the new direct Customer object return behavior - Preserved error handling to still return Response object for error cases --- exemples/customer_retrieve.rb | 108 ++++++++++++++++++++ lib/bridge_api/services/customer_service.rb | 26 +++++ 2 files changed, 134 insertions(+) create mode 100755 exemples/customer_retrieve.rb create mode 100644 lib/bridge_api/services/customer_service.rb diff --git a/exemples/customer_retrieve.rb b/exemples/customer_retrieve.rb new file mode 100755 index 0000000..ded6bd7 --- /dev/null +++ b/exemples/customer_retrieve.rb @@ -0,0 +1,108 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Example script to demonstrate retrieving a customer using the Bridge API gem + +# Set up Bundler to use the local gem +require 'bundler/setup' +require 'bridge_api' + +# Load environment variables if available (for API key) +begin + require 'dotenv/load' +rescue LoadError + puts 'dotenv gem not available, environment variables will need to be set manually' +end + +# Configure Bridge API (if not using environment variable) +BridgeApi.config do |config| + # Use API key from environment variable or set here directly for testing + config.api_key = ENV['BRIDGE_API_KEY'] || 'your-api-key-here' + config.sandbox_mode = ENV['BRIDGE_SANDBOX_MODE'] ? ENV['BRIDGE_SANDBOX_MODE'].downcase == 'true' : true +end + +puts 'Bridge API Configuration:' +puts " API Key: #{BridgeApi.api_key ? '[SET]' : '[NOT SET]'}" +puts " Sandbox Mode: #{BridgeApi.sandbox_mode}" +puts " Base URL: #{BridgeApi.base_url}" +puts + +# Create client +begin + client = BridgeApi::Client.new( + api_key: BridgeApi.api_key, + sandbox_mode: BridgeApi.sandbox_mode, + ) + puts 'Client initialized successfully' +rescue StandardError => e + puts "Error initializing client: #{e.message}" + exit 1 +end + +# The specific customer ID to retrieve +customer_id = 'f558b609-403f-4e88-8815-a1bc69c57159' + +puts "Retrieving customer with ID: #{customer_id}" +puts + +begin + # Method 1: Using the service-based API pattern (recommended) + customer = client.customers.retrieve(customer_id) + + # Check if the return value is an error response instead of a customer object + if customer.is_a?(BridgeApi::Client::Response) + # If it's a response object, it means there was an error + puts '✗ Error retrieving customer:' + puts " Status Code: #{customer.status_code}" + puts " Error: #{customer.error&.message || 'Unknown error'}" + + # Provide helpful information for common errors + case customer.status_code + when 404 + puts " Note: Customer with ID #{customer_id} was not found. Please verify the ID is correct." + when 401 + puts ' Note: Authentication failed. Please verify your API key is correct.' + when 403 + puts ' Note: Access forbidden. Please verify your API key has the required permissions.' + end + else + # If we get here, it's a Customer object + puts '✓ Customer retrieved successfully!' + puts + puts 'Customer details:' + puts " ID: #{customer.id}" + puts " Email: #{customer.email}" + puts " First Name: #{customer.first_name}" + puts " Last Name: #{customer.last_name}" + puts " Created At: #{customer.created_at}" + puts " Updated At: #{customer.updated_at}" + + # Note: the customer object might have other attributes not covered by explicit methods + end +rescue StandardError => e + puts 'Exception occurred while retrieving customer:' + puts " Error: #{e.message}" + puts " Backtrace: #{e.backtrace.first(5).join("\n ")}" +end + +puts +puts 'Alternative method using resource class directly:' +begin + # Method 2: Using the resource class directly + customer = BridgeApi::Resources::Customer.retrieve(client, customer_id) + + if customer.is_a?(BridgeApi::Resources::Customer) + puts '✓ Customer retrieved using resource class:' + puts " ID: #{customer.id}" + puts " Email: #{customer.email}" + puts " First Name: #{customer.first_name}" + puts " Last Name: #{customer.last_name}" + else + puts ' Could not retrieve customer using resource class directly' + end +rescue StandardError => e + puts "Exception using resource class: #{e.message}" +end + +puts +puts 'Script completed.' diff --git a/lib/bridge_api/services/customer_service.rb b/lib/bridge_api/services/customer_service.rb new file mode 100644 index 0000000..c6815f3 --- /dev/null +++ b/lib/bridge_api/services/customer_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +module BridgeApi + module Services + class CustomerService < BaseService + def create(params = {}, idempotency_key: nil) + request(:post, 'customers', params, idempotency_key: idempotency_key) + end + + def retrieve(id, params = {}) + response = request(:get, "customers/#{id}", params) + return response unless response.success? + + # Return a Customer object directly if the request was successful + BridgeApi::Resources::Customer.construct_from(response.data) + end + + def list(params = {}) + request(:get, 'customers', params) + end + + def update(id, params = {}, idempotency_key: nil) + request(:patch, "customers/#{id}", params, idempotency_key: idempotency_key) + end + end + end +end From 90d3cc562b69eac12367a193e4e32bcc509e4ce9 Mon Sep 17 00:00:00 2001 From: Bolo Michelin <72155+bolom@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:00:25 -0400 Subject: [PATCH 03/11] Add missing service files to fix LoadError - Add wallet_service.rb, external_account_service.rb, virtual_account_service.rb, and webhook_service.rb that were being required but missing - Add proper module structure (BridgeApi::Services) to match BaseService - Update service classes to inherit from BaseService and call super in initialize - This fixes the LoadError when requiring the bridge_api gem Co-authored-by: Qwen-Coder --- lib/bridge_api/services/base_service.rb | 20 ++++++++++++ .../services/external_account_service.rb | 31 +++++++++++++++++++ lib/bridge_api/services/kyc_link_service.rb | 18 +++++++++++ .../services/virtual_account_service.rb | 31 +++++++++++++++++++ lib/bridge_api/services/wallet_service.rb | 31 +++++++++++++++++++ lib/bridge_api/services/webhook_service.rb | 31 +++++++++++++++++++ 6 files changed, 162 insertions(+) create mode 100644 lib/bridge_api/services/base_service.rb create mode 100644 lib/bridge_api/services/external_account_service.rb create mode 100644 lib/bridge_api/services/kyc_link_service.rb create mode 100644 lib/bridge_api/services/virtual_account_service.rb create mode 100644 lib/bridge_api/services/wallet_service.rb create mode 100644 lib/bridge_api/services/webhook_service.rb diff --git a/lib/bridge_api/services/base_service.rb b/lib/bridge_api/services/base_service.rb new file mode 100644 index 0000000..4a64d63 --- /dev/null +++ b/lib/bridge_api/services/base_service.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +module BridgeApi + module Services + class BaseService + def initialize(client) + @client = client + end + + protected + + def request(method, endpoint, params = {}, idempotency_key: nil) + if idempotency_key + @client.send(:request_with_idempotency, method, endpoint, params, idempotency_key) + else + @client.send(:request, method, endpoint, params) + end + end + end + end +end \ No newline at end of file diff --git a/lib/bridge_api/services/external_account_service.rb b/lib/bridge_api/services/external_account_service.rb new file mode 100644 index 0000000..dce2c0a --- /dev/null +++ b/lib/bridge_api/services/external_account_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative '../base_resource' +require_relative '../client' + +module BridgeApi + module Services + # Service class for handling External Account-related operations + class ExternalAccountService < BaseService + def initialize(client) + super(client) + @resource_class = BridgeApi::ExternalAccount + end + + # Get an external account by ID + # @param external_account_id [String] The ID of the external account + # @return [BridgeApi::ExternalAccount] The external account object + def get(external_account_id) + resource = @resource_class.new(@client) + resource.retrieve(external_account_id) + end + + # List all external accounts + # @param options [Hash] Optional parameters for filtering + # @return [Array] Array of external account objects + def list(options = {}) + @resource_class.list(@client, options) + end + end + end +end \ No newline at end of file diff --git a/lib/bridge_api/services/kyc_link_service.rb b/lib/bridge_api/services/kyc_link_service.rb new file mode 100644 index 0000000..614536f --- /dev/null +++ b/lib/bridge_api/services/kyc_link_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +module BridgeApi + module Services + class KycLinkService < BaseService + def create(params = {}, idempotency_key: nil) + request(:post, 'kyc_links', params, idempotency_key: idempotency_key) + end + + def retrieve(id, params = {}) + request(:get, "kyc_links/#{id}", params) + end + + def list(params = {}) + request(:get, 'kyc_links', params) + end + end + end +end \ No newline at end of file diff --git a/lib/bridge_api/services/virtual_account_service.rb b/lib/bridge_api/services/virtual_account_service.rb new file mode 100644 index 0000000..12b9bed --- /dev/null +++ b/lib/bridge_api/services/virtual_account_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative '../base_resource' +require_relative '../client' + +module BridgeApi + module Services + # Service class for handling Virtual Account-related operations + class VirtualAccountService < BaseService + def initialize(client) + super(client) + @resource_class = BridgeApi::VirtualAccount + end + + # Get a virtual account by ID + # @param virtual_account_id [String] The ID of the virtual account + # @return [BridgeApi::VirtualAccount] The virtual account object + def get(virtual_account_id) + resource = @resource_class.new(@client) + resource.retrieve(virtual_account_id) + end + + # List all virtual accounts + # @param options [Hash] Optional parameters for filtering + # @return [Array] Array of virtual account objects + def list(options = {}) + @resource_class.list(@client, options) + end + end + end +end \ No newline at end of file diff --git a/lib/bridge_api/services/wallet_service.rb b/lib/bridge_api/services/wallet_service.rb new file mode 100644 index 0000000..9746dd3 --- /dev/null +++ b/lib/bridge_api/services/wallet_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative '../base_resource' +require_relative '../client' + +module BridgeApi + module Services + # Service class for handling Wallet-related operations + class WalletService < BaseService + def initialize(client) + super(client) + @resource_class = BridgeApi::Wallet + end + + # Get a wallet by ID + # @param wallet_id [String] The ID of the wallet + # @return [BridgeApi::Wallet] The wallet object + def get(wallet_id) + resource = @resource_class.new(@client) + resource.retrieve(wallet_id) + end + + # List all wallets + # @param options [Hash] Optional parameters for filtering + # @return [Array] Array of wallet objects + def list(options = {}) + @resource_class.list(@client, options) + end + end + end +end \ No newline at end of file diff --git a/lib/bridge_api/services/webhook_service.rb b/lib/bridge_api/services/webhook_service.rb new file mode 100644 index 0000000..c1074a0 --- /dev/null +++ b/lib/bridge_api/services/webhook_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative '../base_resource' +require_relative '../client' + +module BridgeApi + module Services + # Service class for handling Webhook-related operations + class WebhookService < BaseService + def initialize(client) + super(client) + @resource_class = BridgeApi::Webhook + end + + # Get a webhook by ID + # @param webhook_id [String] The ID of the webhook + # @return [BridgeApi::Webhook] The webhook object + def get(webhook_id) + resource = @resource_class.new(@client) + resource.retrieve(webhook_id) + end + + # List all webhooks + # @param options [Hash] Optional parameters for filtering + # @return [Array] Array of webhook objects + def list(options = {}) + @resource_class.list(@client, options) + end + end + end +end \ No newline at end of file From 46de8eb8eb6a7e8f8aefffde1ec2dd5812f18ae5 Mon Sep 17 00:00:00 2001 From: Bolo Michelin <72155+bolom@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:08:35 -0400 Subject: [PATCH 04/11] Add dynamic method handling to all resource classes --- exe/bridge_api | 7 -- lib/bridge_api/client.rb | 76 ++++++++++++++++++- lib/bridge_api/resources/customer.rb | 39 ++++------ lib/bridge_api/resources/kyc_link.rb | 13 ++++ lib/bridge_api/resources/reward_rate.rb | 13 ++++ lib/bridge_api/resources/total_balance.rb | 13 ++++ .../resources/transaction_history.rb | 13 ++++ lib/bridge_api/resources/virtual_account.rb | 13 ++++ lib/bridge_api/resources/wallet.rb | 13 ++++ lib/bridge_api/resources/webhook.rb | 13 ++++ lib/bridge_api/resources/webhook_event.rb | 13 ++++ .../resources/webhook_event_delivery_log.rb | 13 ++++ 12 files changed, 207 insertions(+), 32 deletions(-) delete mode 100755 exe/bridge_api diff --git a/exe/bridge_api b/exe/bridge_api deleted file mode 100755 index 7f8db28..0000000 --- a/exe/bridge_api +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require 'bridge_api' - -puts "Bridge API Ruby Gem v#{BridgeApi::VERSION}" -puts 'For more information, visit: https://github.com/ScionX/bridge_api' diff --git a/lib/bridge_api/client.rb b/lib/bridge_api/client.rb index c47ab10..430c6cc 100644 --- a/lib/bridge_api/client.rb +++ b/lib/bridge_api/client.rb @@ -92,11 +92,75 @@ def retry_request(method, endpoint, payload, retries, response) request(method, endpoint, payload, retries + 1) end + # --- Resource Accessor Support --- + class ResourceAccessor + def initialize(client, resource_name) + @client = client + @resource_name = resource_name + @singular_resource_name = resource_name.to_s.sub(/s$/, '') + end + + def list(params = {}) + @client.send("list_#{@resource_name}", params) + end + + def get(id) + @client.send("get_#{@singular_resource_name}", id) + end + + def retrieve(id) + @client.send("get_#{@singular_resource_name}", id) + end + + def create(params = {}, idempotency_key: nil) + method_name = idempotency_key ? "create_#{@singular_resource_name}_with_idempotency" : "create_#{@singular_resource_name}" + if @client.respond_to?(method_name) + @client.send(method_name, params, idempotency_key: idempotency_key) + else + @client.request(:post, @resource_name, params) + end + end + + def update(id, params = {}, idempotency_key: nil) + method_name = idempotency_key ? "update_#{@singular_resource_name}_with_idempotency" : "update_#{@singular_resource_name}" + if @client.respond_to?(method_name) + @client.send(method_name, id, params, idempotency_key: idempotency_key) + else + @client.request(:patch, "#{@resource_name}/#{id}", params) + end + end + + def delete(id) + method_name = "delete_#{@singular_resource_name}" + if @client.respond_to?(method_name) + @client.send(method_name, id) + else + @client.request(:delete, "#{@resource_name}/#{id}", {}) + end + end + + private + + def method_missing(method_name, *args, &block) + # Delegate other methods to the client that start with the resource name + if @client.respond_to?(method_name) + @client.send(method_name, *args) + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @client.respond_to?(method_name) || super + end + end + # --- Class Methods --- class << self def define_dynamic_resource_methods (RESOURCES + READ_ONLY_RESOURCES).each do |resource| define_resource_methods(resource) + define_resource_accessor(resource) end end @@ -111,6 +175,16 @@ def define_resource_methods(resource) define_special_methods(resource) unless READ_ONLY_RESOURCES.include?(resource) end + def define_resource_accessor(resource) + define_method(resource) do + instance_variable_name = "@#{resource}_accessor" + unless instance_variable_defined?(instance_variable_name) + instance_variable_set(instance_variable_name, ResourceAccessor.new(self, resource)) + end + instance_variable_get(instance_variable_name) + end + end + def define_special_methods(resource) send("define_#{resource}_methods") if respond_to?("define_#{resource}_methods", true) end @@ -200,7 +274,7 @@ def define_webhooks_methods end end - private :define_resource_methods, :define_special_methods, + private :define_resource_methods, :define_special_methods, :define_resource_accessor, :define_wallets_methods, :define_customers_methods, :define_customers_wallet_methods, :define_customers_virtual_account_methods, :define_webhooks_methods diff --git a/lib/bridge_api/resources/customer.rb b/lib/bridge_api/resources/customer.rb index a0a02a2..db97f11 100644 --- a/lib/bridge_api/resources/customer.rb +++ b/lib/bridge_api/resources/customer.rb @@ -19,32 +19,26 @@ def self.resource_path include BridgeApi::APIOperations::Delete def self.get_customer_wallets(client, customer_id, params = {}) - # Use the client's public API to make the request client.get_customer_wallets(customer_id, params) end def self.create_wallet_for_customer(client, customer_id, chain, idempotency_key: nil) - # Use the client's public API to make the request client.create_customer_wallet(customer_id, chain, idempotency_key: idempotency_key) end def self.get_customer_wallet(client, customer_id, wallet_id) - # Use the client's public API to make the request client.get_customer_wallet(customer_id, wallet_id) end def self.get_customer_virtual_accounts(client, customer_id, params = {}) - # Use the client's public API to make the request client.list_customer_virtual_accounts(customer_id, params) end def self.get_customer_virtual_account(client, customer_id, virtual_account_id) - # Use the client's public API to make the request client.get_customer_virtual_account(customer_id, virtual_account_id) end def self.create_customer_virtual_account(client, customer_id, params, idempotency_key: nil) - # Use the client's public API to make the request client.create_customer_virtual_account(customer_id, params, idempotency_key: idempotency_key) end @@ -52,23 +46,7 @@ def initialize(attributes = {}) super end - # Specific accessor methods for convenience - def id - @values[:id] - end - - def email - @values[:email] - end - - def first_name - @values[:first_name] - end - - def last_name - @values[:last_name] - end - + # Override datetime accessors to return parsed Time objects def created_at parse_datetime(@values[:created_at]) end @@ -101,6 +79,19 @@ def create_virtual_account(client, params, idempotency_key: nil) self.class.create_customer_virtual_account(client, id, params, idempotency_key: idempotency_key) end + # Dynamic method handling for all attributes in @values + def method_missing(method_name, *args) + if @values.key?(method_name) + @values[method_name] + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @values.key?(method_name) || super + end + private # Parse a datetime string to a Time object @@ -115,4 +106,4 @@ def parse_datetime(datetime_string) end end end -end +end \ No newline at end of file diff --git a/lib/bridge_api/resources/kyc_link.rb b/lib/bridge_api/resources/kyc_link.rb index a07ac46..41d4456 100644 --- a/lib/bridge_api/resources/kyc_link.rb +++ b/lib/bridge_api/resources/kyc_link.rb @@ -69,6 +69,19 @@ def created_at parse_datetime(@values[:created_at]) end + # Dynamic method handling for all attributes in @values + def method_missing(method_name, *args) + if @values.key?(method_name) + @values[method_name] + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @values.key?(method_name) || super + end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/reward_rate.rb b/lib/bridge_api/resources/reward_rate.rb index 0c275a7..86beae6 100644 --- a/lib/bridge_api/resources/reward_rate.rb +++ b/lib/bridge_api/resources/reward_rate.rb @@ -20,6 +20,19 @@ def expires_at parse_datetime(@values[:expires_at]) end + # Dynamic method handling for all attributes in @values + def method_missing(method_name, *args) + if @values.key?(method_name) + @values[method_name] + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @values.key?(method_name) || super + end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/total_balance.rb b/lib/bridge_api/resources/total_balance.rb index 9fdb40b..7752445 100644 --- a/lib/bridge_api/resources/total_balance.rb +++ b/lib/bridge_api/resources/total_balance.rb @@ -25,6 +25,19 @@ def chain def contract_address @values[:contract_address] end + + # Dynamic method handling for all attributes in @values + def method_missing(method_name, *args) + if @values.key?(method_name) + @values[method_name] + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @values.key?(method_name) || super + end end end end diff --git a/lib/bridge_api/resources/transaction_history.rb b/lib/bridge_api/resources/transaction_history.rb index 0ce0a37..266be11 100644 --- a/lib/bridge_api/resources/transaction_history.rb +++ b/lib/bridge_api/resources/transaction_history.rb @@ -36,6 +36,19 @@ def destination @values[:destination] end + # Dynamic method handling for all attributes in @values + def method_missing(method_name, *args) + if @values.key?(method_name) + @values[method_name] + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @values.key?(method_name) || super + end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/virtual_account.rb b/lib/bridge_api/resources/virtual_account.rb index 277510d..4c0dd48 100644 --- a/lib/bridge_api/resources/virtual_account.rb +++ b/lib/bridge_api/resources/virtual_account.rb @@ -52,6 +52,19 @@ def balances @values[:balances] || [] end + # Dynamic method handling for all attributes in @values + def method_missing(method_name, *args) + if @values.key?(method_name) + @values[method_name] + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @values.key?(method_name) || super + end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/wallet.rb b/lib/bridge_api/resources/wallet.rb index 56591b8..3e82c1a 100644 --- a/lib/bridge_api/resources/wallet.rb +++ b/lib/bridge_api/resources/wallet.rb @@ -55,6 +55,19 @@ def self.get_for_customer(client, customer_id, wallet_id) client.get_customer_wallet(customer_id, wallet_id) end + # Dynamic method handling for all attributes in @values + def method_missing(method_name, *args) + if @values.key?(method_name) + @values[method_name] + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @values.key?(method_name) || super + end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/webhook.rb b/lib/bridge_api/resources/webhook.rb index 9546ab8..ffb57ad 100644 --- a/lib/bridge_api/resources/webhook.rb +++ b/lib/bridge_api/resources/webhook.rb @@ -49,6 +49,19 @@ def created_at parse_datetime(@values[:created_at]) end + # Dynamic method handling for all attributes in @values + def method_missing(method_name, *args) + if @values.key?(method_name) + @values[method_name] + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @values.key?(method_name) || super + end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/webhook_event.rb b/lib/bridge_api/resources/webhook_event.rb index bb109b0..6c2cfbc 100644 --- a/lib/bridge_api/resources/webhook_event.rb +++ b/lib/bridge_api/resources/webhook_event.rb @@ -52,6 +52,19 @@ def event_created_at parse_datetime(@values[:event_created_at]) end + # Dynamic method handling for all attributes in @values + def method_missing(method_name, *args) + if @values.key?(method_name) + @values[method_name] + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @values.key?(method_name) || super + end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/webhook_event_delivery_log.rb b/lib/bridge_api/resources/webhook_event_delivery_log.rb index 2a8ed5b..3c4d058 100644 --- a/lib/bridge_api/resources/webhook_event_delivery_log.rb +++ b/lib/bridge_api/resources/webhook_event_delivery_log.rb @@ -24,6 +24,19 @@ def created_at parse_datetime(@values[:created_at]) end + # Dynamic method handling for all attributes in @values + def method_missing(method_name, *args) + if @values.key?(method_name) + @values[method_name] + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @values.key?(method_name) || super + end + private # Parse a datetime string to a Time object From 3cb730c651e01c638c369d557486dd0f9140a149 Mon Sep 17 00:00:00 2001 From: Bolo Michelin <72155+bolom@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:19:28 -0400 Subject: [PATCH 05/11] Add webhook resource pattern and example scripts - Add webhook pattern to RESOURCE_PATTERNS in util.rb to properly convert webhook hashes to Webhook objects - Add Response unwrapping in convert_to_bridged_object to handle Client::Response objects - Fix ResourceAccessor methods to use send(:request) for private method access - Update Webhook.create to use super for consistent API operation handling - Update list operation to convert response to proper resource objects - Add webhook examples: create_webhook_example.rb and list_webhooks_example.rb - Add dotenv dependency for environment variable management in examples --- Gemfile | 2 + exemples/Gemfile | 4 + exemples/create_webhook_example.rb | 53 +++++++++++ exemples/list_webhooks_example.rb | 27 ++++++ exemples/webhook_example.rb | 136 ++++++++++++++++++++++++++++ lib/bridge_api/api_operations.rb | 6 +- lib/bridge_api/client.rb | 6 +- lib/bridge_api/resources/webhook.rb | 2 +- lib/bridge_api/util.rb | 13 +++ 9 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 exemples/Gemfile create mode 100644 exemples/create_webhook_example.rb create mode 100644 exemples/list_webhooks_example.rb create mode 100644 exemples/webhook_example.rb diff --git a/Gemfile b/Gemfile index 60a5926..7d62e71 100644 --- a/Gemfile +++ b/Gemfile @@ -16,3 +16,5 @@ group :development do gem 'webmock', '~> 3.26' gem 'yard', '~> 0.9.37' end + +gem "dotenv", "~> 3.1" diff --git a/exemples/Gemfile b/exemples/Gemfile new file mode 100644 index 0000000..185796c --- /dev/null +++ b/exemples/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "dotenv", "~> 2.8" +gem "bridge_api", path: "../" diff --git a/exemples/create_webhook_example.rb b/exemples/create_webhook_example.rb new file mode 100644 index 0000000..e886f74 --- /dev/null +++ b/exemples/create_webhook_example.rb @@ -0,0 +1,53 @@ +require 'dotenv/load' +require 'bridge_api' + +# Configure Bridge API +BridgeApi.config do |config| + config.api_key = ENV['BRIDGE_API_KEY'] + config.sandbox_mode = ENV['BRIDGE_SANDBOX_MODE'] ? ENV['BRIDGE_SANDBOX_MODE'].downcase == 'true' : true +end + +client = BridgeApi::Client.new( + api_key: BridgeApi.api_key, + sandbox_mode: BridgeApi.sandbox_mode, +) + +# Webhook configuration +webhook_config = { + url: 'https://96743bb22bbd.ngrok-free.app/bridge/webhook', + event_epoch: 'webhook_creation', + event_categories: [ + 'customer', + 'kyc_link', + 'virtual_account.activity', + 'transfer' + ] +} + +puts 'Creating webhook...' +puts "URL: #{webhook_config[:url]}" +puts "Events: #{webhook_config[:event_categories].join(', ')}" +puts + +begin + response = client.webhooks.create(webhook_config) + + if response.success? + webhook = response.data + puts '✓ Webhook created successfully!' + puts " ID: #{webhook.id}" + puts " URL: #{webhook.url}" + puts " Status: #{webhook.status}" + puts " Events: #{webhook.event_categories.join(', ')}" + puts " Created: #{webhook.created_at}" + puts " Public Key:" + puts webhook.public_key + else + puts '✗ Error creating webhook:' + puts " Status: #{response.status_code}" + puts " Error: #{response.error.message}" + end +rescue StandardError => e + puts "Exception occurred: #{e.class} - #{e.message}" + puts e.backtrace.first(5).join("\n") +end diff --git a/exemples/list_webhooks_example.rb b/exemples/list_webhooks_example.rb new file mode 100644 index 0000000..c04c7df --- /dev/null +++ b/exemples/list_webhooks_example.rb @@ -0,0 +1,27 @@ +require 'dotenv/load' +require 'bridge_api' + +# Configure Bridge API +BridgeApi.config do |config| + # Use API key from environment variable or set here directly for testing + config.api_key = ENV['BRIDGE_API_KEY'] + config.sandbox_mode = ENV['BRIDGE_SANDBOX_MODE'] ? ENV['BRIDGE_SANDBOX_MODE'].downcase == 'true' : true +end + +client = BridgeApi::Client.new( + api_key: BridgeApi.api_key, + sandbox_mode: BridgeApi.sandbox_mode, + ) + + response = client.webhooks.list + + puts '✓ Webhooks retrieved successfully!' + puts "Found #{response.data.length} webhook(s):" + response.data.each do |webhook| + puts " - ID: #{webhook.id}" + puts " URL: #{webhook.url}" + puts " Status: #{webhook.status}" + puts " Events: #{webhook.event_categories.join(', ')}" + puts " Created: #{webhook.created_at}" + puts + end diff --git a/exemples/webhook_example.rb b/exemples/webhook_example.rb new file mode 100644 index 0000000..6dff7fb --- /dev/null +++ b/exemples/webhook_example.rb @@ -0,0 +1,136 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Example script to demonstrate webhook configuration for the Bridge API gem + +# Set up Bundler to use the local gem +require 'bundler/setup' +require 'bridge_api' + +# Load environment variables if available (for API key) +begin + require 'dotenv/load' +rescue LoadError + puts 'dotenv gem not available, environment variables will need to be set manually' +end + +# Configure Bridge API (if not using environment variable) +BridgeApi.config do |config| + # Use API key from environment variable or set here directly for testing + config.api_key = ENV['BRIDGE_API_KEY'] || 'your-api-key-here' + config.sandbox_mode = ENV['BRIDGE_SANDBOX_MODE'] ? ENV['BRIDGE_SANDBOX_MODE'].downcase == 'true' : true +end + +puts 'Bridge API Configuration:' +puts " API Key: #{BridgeApi.api_key ? '[SET]' : '[NOT SET]'}" +puts " Sandbox Mode: #{BridgeApi.sandbox_mode}" +puts " Base URL: #{BridgeApi.base_url}" +puts + +# Webhook configuration example +puts 'Webhook Configuration Example:' +puts '===============================' +puts 'Webhook endpoint: https://96743bb22bbd.ngrok-free.app/bridge/webhook' +puts 'Monitored events: customer, kyc_link, virtual_account.activity' +puts + +webhook_config = { + url: 'https://96743bb22bbd.ngrok-free.app/bridge/webhook', + event_categories: [ + 'customer', + 'kyc_link', + 'virtual_account.activity' + ], + active: true +} + +puts "Configuration hash that would be sent to the API:" +webhook_config.each do |key, value| + if value.is_a?(Array) + puts " #{key}: [#{value.join(', ')}]" + else + puts " #{key}: #{value}" + end +end +puts + +if BridgeApi.api_key && !BridgeApi.api_key.include?('your-api-key') + # Only try to make the API call if we have a real API key + begin + client = BridgeApi::Client.new( + api_key: BridgeApi.api_key, + sandbox_mode: BridgeApi.sandbox_mode, + ) + puts 'Client initialized, attempting to create webhook...' + + response = client.webhooks.create(webhook_config) + + if response.success? + puts '✓ Webhook created successfully!' + puts " Webhook ID: #{response.data.id}" + else + puts '✗ Error creating webhook:' + puts " Status: #{response.status_code}" + puts " Message: #{response.error.message}" + end + rescue StandardError => e + puts "Exception occurred: #{e.message}" + end +else + puts "To create the webhook with a real API call, set your API key:" + puts " export BRIDGE_API_KEY='your-actual-api-key-here'" + puts " ruby exemples/webhook_example.rb" + puts + puts "Code example for creating the webhook:" + puts + puts " client = BridgeApi::Client.new" + puts " webhook_config = {" + puts " url: 'https://96743bb22bbd.ngrok-free.app/bridge/webhook'," + puts " event_categories: ['customer', 'kyc_link', 'virtual_account.activity']," + puts " active: true" + puts " }" + puts " response = client.webhooks.create(webhook_config)" + puts " if response.success?" + puts " puts \"Webhook created: \#{response.data.id}\"" + puts " else" + puts " puts \"Error: \#{response.error.message}\"" + puts " end" +end + +puts +puts 'Webhook example completed.' + +# Example of how to handle incoming webhook events +puts +puts 'Example Webhook Handler (for your server application):' +puts '=====================================================' +puts <<~HANDLER + # In your web application (Sinatra, Rails, etc.) + + post '/bridge/webhook' do + # Verify webhook signature for security + payload = request.body.read + signature = request.env['HTTP_X_SIGNATURE'] + + # Verify the signature here (implementation may vary) + + # Parse the webhook event + event = JSON.parse(payload) + + case event['type'] + when 'customer' + # Handle customer event + puts "Customer event received: \#{event['data']}" + when 'kyc_link' + # Handle KYC link event + puts "KYC link event received: \#{event['data']}" + when 'virtual_account.activity' + # Handle virtual account activity + puts "Virtual account activity received: \#{event['data']}" + else + puts "Unknown event type: \#{event['type']}" + end + + status 200 + end +HANDLER \ No newline at end of file diff --git a/lib/bridge_api/api_operations.rb b/lib/bridge_api/api_operations.rb index 83820a8..c606225 100644 --- a/lib/bridge_api/api_operations.rb +++ b/lib/bridge_api/api_operations.rb @@ -13,8 +13,10 @@ def list(client, params = {}) response = client.request(:get, resource_path, params) return response unless response.success? - # Create a list response - for now return as is but can be enhanced later - response + BridgeApi::Util.convert_to_bridged_object( + response, + resource_hint: self::OBJECT_NAME, + ) end end end diff --git a/lib/bridge_api/client.rb b/lib/bridge_api/client.rb index 430c6cc..8a2fee1 100644 --- a/lib/bridge_api/client.rb +++ b/lib/bridge_api/client.rb @@ -117,7 +117,7 @@ def create(params = {}, idempotency_key: nil) if @client.respond_to?(method_name) @client.send(method_name, params, idempotency_key: idempotency_key) else - @client.request(:post, @resource_name, params) + @client.send(:request, :post, @resource_name, params) end end @@ -126,7 +126,7 @@ def update(id, params = {}, idempotency_key: nil) if @client.respond_to?(method_name) @client.send(method_name, id, params, idempotency_key: idempotency_key) else - @client.request(:patch, "#{@resource_name}/#{id}", params) + @client.send(:request, :patch, "#{@resource_name}/#{id}", params) end end @@ -135,7 +135,7 @@ def delete(id) if @client.respond_to?(method_name) @client.send(method_name, id) else - @client.request(:delete, "#{@resource_name}/#{id}", {}) + @client.send(:request, :delete, "#{@resource_name}/#{id}", {}) end end diff --git a/lib/bridge_api/resources/webhook.rb b/lib/bridge_api/resources/webhook.rb index ffb57ad..a15e868 100644 --- a/lib/bridge_api/resources/webhook.rb +++ b/lib/bridge_api/resources/webhook.rb @@ -21,7 +21,7 @@ def self.retrieve(client, id, _params = {}) end def self.create(client, params = {}) - client.create_webhook(params) + super(client, params) end # Specific accessor methods for convenience diff --git a/lib/bridge_api/util.rb b/lib/bridge_api/util.rb index 489ca12..75f21e3 100644 --- a/lib/bridge_api/util.rb +++ b/lib/bridge_api/util.rb @@ -20,11 +20,24 @@ module Util 'virtual_account' => { all: %i[id account_number] }, 'transaction_history' => { all: %i[id], any: %i[amount transaction_date] }, 'kyc_link' => { all: %i[id redirect_url] }, + 'webhook' => { all: %i[id url status] }, }.freeze class << self # Convert API response data to appropriate resource objects def convert_to_bridged_object(data, opts = {}) + # Detect if data is a Client::Response and unwrap it before the case statement + if data.is_a?(BridgeApi::Client::Response) + # Extract payload from response (prefer response.data if present, otherwise response.body) + if data.data + data = data.data + elsif data.body + data = data.body + else + data = data.to_h if data.respond_to?(:to_h) + end + end + case data when Array data.map { |item| convert_to_bridged_object(item, opts) } From f60814e300889a6cf5bf7e900f4c759684d949d2 Mon Sep 17 00:00:00 2001 From: Bolo Michelin <72155+bolom@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:29:09 -0400 Subject: [PATCH 06/11] Add webhook update example and fix PUT request handling - Add update_webhook_example.rb to activate webhooks - Fix build_request_options to exclude Idempotency-Key for PUT requests - Update ResourceAccessor.update to use PUT for webhooks instead of PATCH - Webhooks require PUT without idempotency keys per Bridge API spec The example script lists webhooks, gets the most recent one, and activates it. --- exemples/update_webhook_example.rb | 64 ++++++++++++++++++++++++++++++ lib/bridge_api/client.rb | 11 ++++- 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 exemples/update_webhook_example.rb diff --git a/exemples/update_webhook_example.rb b/exemples/update_webhook_example.rb new file mode 100644 index 0000000..f358994 --- /dev/null +++ b/exemples/update_webhook_example.rb @@ -0,0 +1,64 @@ +require 'dotenv/load' +require 'bridge_api' + +# Configure Bridge API +BridgeApi.config do |config| + config.api_key = ENV['BRIDGE_API_KEY'] + config.sandbox_mode = ENV['BRIDGE_SANDBOX_MODE'] ? ENV['BRIDGE_SANDBOX_MODE'].downcase == 'true' : true +end + +client = BridgeApi::Client.new( + api_key: BridgeApi.api_key, + sandbox_mode: BridgeApi.sandbox_mode, +) + +# First, list webhooks to get the latest one +puts 'Fetching webhooks...' +list_response = client.webhooks.list + +unless list_response.success? + puts '✗ Failed to fetch webhooks' + exit 1 +end + +webhooks = list_response.data.data +if webhooks.empty? + puts '✗ No webhooks found' + exit 1 +end + +# Get the most recent webhook (last in the list) +latest_webhook = webhooks.last + +puts "Found latest webhook:" +puts " ID: #{latest_webhook.id}" +puts " URL: #{latest_webhook.url}" +puts " Current Status: #{latest_webhook.status}" +puts + +# Update webhook to active status +puts 'Activating webhook...' + +update_params = { + status: 'active' +} + +begin + response = client.webhooks.update(latest_webhook.id, update_params) + + if response.success? + webhook = response.data + puts '✓ Webhook updated successfully!' + puts " ID: #{webhook.id}" + puts " URL: #{webhook.url}" + puts " New Status: #{webhook.status}" + puts " Events: #{webhook.event_categories.join(', ')}" + else + puts '✗ Error updating webhook:' + puts " Status: #{response.status_code}" + puts " Error: #{response.error.message}" + end +rescue StandardError => e + puts "Exception occurred: #{e.class} - #{e.message}" + puts e.backtrace.first(5).join("\n") +end diff --git a/lib/bridge_api/client.rb b/lib/bridge_api/client.rb index 8a2fee1..139840c 100644 --- a/lib/bridge_api/client.rb +++ b/lib/bridge_api/client.rb @@ -77,7 +77,12 @@ def build_request_options(method, payload, idempotency_key: nil) if %i[get delete].include?(method) { query: payload } else - headers = { 'Idempotency-Key' => idempotency_key || SecureRandom.uuid } + # Only add Idempotency-Key if explicitly provided or for POST/PATCH + # PUT requests (like webhook updates) don't support idempotency keys + headers = {} + if idempotency_key || %i[post patch].include?(method) + headers['Idempotency-Key'] = idempotency_key || SecureRandom.uuid + end { body: payload.to_json, headers: headers } end end @@ -126,7 +131,9 @@ def update(id, params = {}, idempotency_key: nil) if @client.respond_to?(method_name) @client.send(method_name, id, params, idempotency_key: idempotency_key) else - @client.send(:request, :patch, "#{@resource_name}/#{id}", params) + # Use PUT for webhooks, PATCH for others + http_method = @resource_name == :webhooks ? :put : :patch + @client.send(:request, http_method, "#{@resource_name}/#{id}", params) end end From c8b96ab0543fde2ac2be19fa553748b3d015f089 Mon Sep 17 00:00:00 2001 From: Bolo Michelin <72155+bolom@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:28:42 -0400 Subject: [PATCH 07/11] Fix RuboCop offenses - line length and complexity issues --- lib/bridge_api/client.rb | 16 +++++++++--- lib/bridge_api/util.rb | 55 ++++++++++++++++++++++++---------------- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/lib/bridge_api/client.rb b/lib/bridge_api/client.rb index 139840c..7f8f85a 100644 --- a/lib/bridge_api/client.rb +++ b/lib/bridge_api/client.rb @@ -118,7 +118,11 @@ def retrieve(id) end def create(params = {}, idempotency_key: nil) - method_name = idempotency_key ? "create_#{@singular_resource_name}_with_idempotency" : "create_#{@singular_resource_name}" + method_name = if idempotency_key + "create_#{@singular_resource_name}_with_idempotency" + else + "create_#{@singular_resource_name}" + end if @client.respond_to?(method_name) @client.send(method_name, params, idempotency_key: idempotency_key) else @@ -127,7 +131,11 @@ def create(params = {}, idempotency_key: nil) end def update(id, params = {}, idempotency_key: nil) - method_name = idempotency_key ? "update_#{@singular_resource_name}_with_idempotency" : "update_#{@singular_resource_name}" + method_name = if idempotency_key + "update_#{@singular_resource_name}_with_idempotency" + else + "update_#{@singular_resource_name}" + end if @client.respond_to?(method_name) @client.send(method_name, id, params, idempotency_key: idempotency_key) else @@ -148,10 +156,10 @@ def delete(id) private - def method_missing(method_name, *args, &block) + def method_missing(method_name, *, &) # Delegate other methods to the client that start with the resource name if @client.respond_to?(method_name) - @client.send(method_name, *args) + @client.send(method_name, *) else super end diff --git a/lib/bridge_api/util.rb b/lib/bridge_api/util.rb index 75f21e3..84bf8d5 100644 --- a/lib/bridge_api/util.rb +++ b/lib/bridge_api/util.rb @@ -26,33 +26,14 @@ module Util class << self # Convert API response data to appropriate resource objects def convert_to_bridged_object(data, opts = {}) - # Detect if data is a Client::Response and unwrap it before the case statement - if data.is_a?(BridgeApi::Client::Response) - # Extract payload from response (prefer response.data if present, otherwise response.body) - if data.data - data = data.data - elsif data.body - data = data.body - else - data = data.to_h if data.respond_to?(:to_h) - end - end + # Detect if data is a Client::Response and unwrap it + data = unwrap_client_response(data) case data when Array data.map { |item| convert_to_bridged_object(item, opts) } when Hash - if data.key?('data') || data.key?(:data) - convert_list_object(data, opts) - elsif (object_name = detect_resource_type_from_object_field(data)) - construct_resource(object_name, symbolize_keys(data), opts) - elsif (resource_hint = opts[:resource_hint]) && OBJECT_CLASS_NAMES.key?(resource_hint.to_s) - construct_resource(resource_hint.to_s, symbolize_keys(data), opts) - elsif (detected_type = detect_resource_type(data)) - construct_resource(detected_type, symbolize_keys(data), opts) - else - symbolize_keys(data).transform_values { |value| convert_to_bridged_object(value, opts) } - end + process_hash_data(data, opts) else data end @@ -60,6 +41,36 @@ def convert_to_bridged_object(data, opts = {}) private + def unwrap_client_response(data) + # Detect if data is a Client::Response and unwrap it + return data unless data.is_a?(BridgeApi::Client::Response) + + # Extract payload from response (prefer response.data if present, otherwise response.body) + if data.data + data.data + elsif data.body + data.body + elsif data.respond_to?(:to_h) + data.to_h + else + data + end + end + + def process_hash_data(data, opts) + if data.key?('data') || data.key?(:data) + convert_list_object(data, opts) + elsif (object_name = detect_resource_type_from_object_field(data)) + construct_resource(object_name, symbolize_keys(data), opts) + elsif (resource_hint = opts[:resource_hint]) && OBJECT_CLASS_NAMES.key?(resource_hint.to_s) + construct_resource(resource_hint.to_s, symbolize_keys(data), opts) + elsif (detected_type = detect_resource_type(data)) + construct_resource(detected_type, symbolize_keys(data), opts) + else + symbolize_keys(data).transform_values { |value| convert_to_bridged_object(value, opts) } + end + end + def detect_resource_type_from_object_field(data) object_name = data['object'] || data[:object] object_name && OBJECT_CLASS_NAMES.key?(object_name.to_s) ? object_name.to_s : nil From e1ea30e7cb9f33ff9d977009bad1d858e208ba12 Mon Sep 17 00:00:00 2001 From: Bolo Michelin <72155+bolom@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:30:32 -0400 Subject: [PATCH 08/11] Remove exemples directory and refactor service initializers Deleted the entire 'exemples' directory and its example scripts. Updated service and resource classes to call 'super' without passing the client argument, aligning with Ruby best practices. Minor .gitignore and Gemfile formatting adjustments were also made. --- .gitignore | 2 + Gemfile | 2 +- exemples/Gemfile | 4 - exemples/create_webhook_example.rb | 53 ------- exemples/customer_retrieve.rb | 108 -------------- exemples/list_webhooks_example.rb | 27 ---- exemples/update_webhook_example.rb | 64 --------- exemples/webhook_example.rb | 136 ------------------ lib/bridge_api/resources/customer.rb | 2 +- lib/bridge_api/resources/webhook.rb | 2 +- lib/bridge_api/services/base_service.rb | 2 +- .../services/external_account_service.rb | 4 +- lib/bridge_api/services/kyc_link_service.rb | 2 +- .../services/virtual_account_service.rb | 4 +- lib/bridge_api/services/wallet_service.rb | 4 +- lib/bridge_api/services/webhook_service.rb | 4 +- 16 files changed, 15 insertions(+), 405 deletions(-) delete mode 100644 exemples/Gemfile delete mode 100644 exemples/create_webhook_example.rb delete mode 100755 exemples/customer_retrieve.rb delete mode 100644 exemples/list_webhooks_example.rb delete mode 100644 exemples/update_webhook_example.rb delete mode 100644 exemples/webhook_example.rb diff --git a/.gitignore b/.gitignore index c2ae678..134c196 100644 --- a/.gitignore +++ b/.gitignore @@ -173,3 +173,5 @@ config/private.json config/keys.json stripe-ruby/ *.md +/exemples +/exemples diff --git a/Gemfile b/Gemfile index 7d62e71..0a2214c 100644 --- a/Gemfile +++ b/Gemfile @@ -17,4 +17,4 @@ group :development do gem 'yard', '~> 0.9.37' end -gem "dotenv", "~> 3.1" +gem 'dotenv', '~> 3.1' diff --git a/exemples/Gemfile b/exemples/Gemfile deleted file mode 100644 index 185796c..0000000 --- a/exemples/Gemfile +++ /dev/null @@ -1,4 +0,0 @@ -source "https://rubygems.org" - -gem "dotenv", "~> 2.8" -gem "bridge_api", path: "../" diff --git a/exemples/create_webhook_example.rb b/exemples/create_webhook_example.rb deleted file mode 100644 index e886f74..0000000 --- a/exemples/create_webhook_example.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'dotenv/load' -require 'bridge_api' - -# Configure Bridge API -BridgeApi.config do |config| - config.api_key = ENV['BRIDGE_API_KEY'] - config.sandbox_mode = ENV['BRIDGE_SANDBOX_MODE'] ? ENV['BRIDGE_SANDBOX_MODE'].downcase == 'true' : true -end - -client = BridgeApi::Client.new( - api_key: BridgeApi.api_key, - sandbox_mode: BridgeApi.sandbox_mode, -) - -# Webhook configuration -webhook_config = { - url: 'https://96743bb22bbd.ngrok-free.app/bridge/webhook', - event_epoch: 'webhook_creation', - event_categories: [ - 'customer', - 'kyc_link', - 'virtual_account.activity', - 'transfer' - ] -} - -puts 'Creating webhook...' -puts "URL: #{webhook_config[:url]}" -puts "Events: #{webhook_config[:event_categories].join(', ')}" -puts - -begin - response = client.webhooks.create(webhook_config) - - if response.success? - webhook = response.data - puts '✓ Webhook created successfully!' - puts " ID: #{webhook.id}" - puts " URL: #{webhook.url}" - puts " Status: #{webhook.status}" - puts " Events: #{webhook.event_categories.join(', ')}" - puts " Created: #{webhook.created_at}" - puts " Public Key:" - puts webhook.public_key - else - puts '✗ Error creating webhook:' - puts " Status: #{response.status_code}" - puts " Error: #{response.error.message}" - end -rescue StandardError => e - puts "Exception occurred: #{e.class} - #{e.message}" - puts e.backtrace.first(5).join("\n") -end diff --git a/exemples/customer_retrieve.rb b/exemples/customer_retrieve.rb deleted file mode 100755 index ded6bd7..0000000 --- a/exemples/customer_retrieve.rb +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Example script to demonstrate retrieving a customer using the Bridge API gem - -# Set up Bundler to use the local gem -require 'bundler/setup' -require 'bridge_api' - -# Load environment variables if available (for API key) -begin - require 'dotenv/load' -rescue LoadError - puts 'dotenv gem not available, environment variables will need to be set manually' -end - -# Configure Bridge API (if not using environment variable) -BridgeApi.config do |config| - # Use API key from environment variable or set here directly for testing - config.api_key = ENV['BRIDGE_API_KEY'] || 'your-api-key-here' - config.sandbox_mode = ENV['BRIDGE_SANDBOX_MODE'] ? ENV['BRIDGE_SANDBOX_MODE'].downcase == 'true' : true -end - -puts 'Bridge API Configuration:' -puts " API Key: #{BridgeApi.api_key ? '[SET]' : '[NOT SET]'}" -puts " Sandbox Mode: #{BridgeApi.sandbox_mode}" -puts " Base URL: #{BridgeApi.base_url}" -puts - -# Create client -begin - client = BridgeApi::Client.new( - api_key: BridgeApi.api_key, - sandbox_mode: BridgeApi.sandbox_mode, - ) - puts 'Client initialized successfully' -rescue StandardError => e - puts "Error initializing client: #{e.message}" - exit 1 -end - -# The specific customer ID to retrieve -customer_id = 'f558b609-403f-4e88-8815-a1bc69c57159' - -puts "Retrieving customer with ID: #{customer_id}" -puts - -begin - # Method 1: Using the service-based API pattern (recommended) - customer = client.customers.retrieve(customer_id) - - # Check if the return value is an error response instead of a customer object - if customer.is_a?(BridgeApi::Client::Response) - # If it's a response object, it means there was an error - puts '✗ Error retrieving customer:' - puts " Status Code: #{customer.status_code}" - puts " Error: #{customer.error&.message || 'Unknown error'}" - - # Provide helpful information for common errors - case customer.status_code - when 404 - puts " Note: Customer with ID #{customer_id} was not found. Please verify the ID is correct." - when 401 - puts ' Note: Authentication failed. Please verify your API key is correct.' - when 403 - puts ' Note: Access forbidden. Please verify your API key has the required permissions.' - end - else - # If we get here, it's a Customer object - puts '✓ Customer retrieved successfully!' - puts - puts 'Customer details:' - puts " ID: #{customer.id}" - puts " Email: #{customer.email}" - puts " First Name: #{customer.first_name}" - puts " Last Name: #{customer.last_name}" - puts " Created At: #{customer.created_at}" - puts " Updated At: #{customer.updated_at}" - - # Note: the customer object might have other attributes not covered by explicit methods - end -rescue StandardError => e - puts 'Exception occurred while retrieving customer:' - puts " Error: #{e.message}" - puts " Backtrace: #{e.backtrace.first(5).join("\n ")}" -end - -puts -puts 'Alternative method using resource class directly:' -begin - # Method 2: Using the resource class directly - customer = BridgeApi::Resources::Customer.retrieve(client, customer_id) - - if customer.is_a?(BridgeApi::Resources::Customer) - puts '✓ Customer retrieved using resource class:' - puts " ID: #{customer.id}" - puts " Email: #{customer.email}" - puts " First Name: #{customer.first_name}" - puts " Last Name: #{customer.last_name}" - else - puts ' Could not retrieve customer using resource class directly' - end -rescue StandardError => e - puts "Exception using resource class: #{e.message}" -end - -puts -puts 'Script completed.' diff --git a/exemples/list_webhooks_example.rb b/exemples/list_webhooks_example.rb deleted file mode 100644 index c04c7df..0000000 --- a/exemples/list_webhooks_example.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'dotenv/load' -require 'bridge_api' - -# Configure Bridge API -BridgeApi.config do |config| - # Use API key from environment variable or set here directly for testing - config.api_key = ENV['BRIDGE_API_KEY'] - config.sandbox_mode = ENV['BRIDGE_SANDBOX_MODE'] ? ENV['BRIDGE_SANDBOX_MODE'].downcase == 'true' : true -end - -client = BridgeApi::Client.new( - api_key: BridgeApi.api_key, - sandbox_mode: BridgeApi.sandbox_mode, - ) - - response = client.webhooks.list - - puts '✓ Webhooks retrieved successfully!' - puts "Found #{response.data.length} webhook(s):" - response.data.each do |webhook| - puts " - ID: #{webhook.id}" - puts " URL: #{webhook.url}" - puts " Status: #{webhook.status}" - puts " Events: #{webhook.event_categories.join(', ')}" - puts " Created: #{webhook.created_at}" - puts - end diff --git a/exemples/update_webhook_example.rb b/exemples/update_webhook_example.rb deleted file mode 100644 index f358994..0000000 --- a/exemples/update_webhook_example.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'dotenv/load' -require 'bridge_api' - -# Configure Bridge API -BridgeApi.config do |config| - config.api_key = ENV['BRIDGE_API_KEY'] - config.sandbox_mode = ENV['BRIDGE_SANDBOX_MODE'] ? ENV['BRIDGE_SANDBOX_MODE'].downcase == 'true' : true -end - -client = BridgeApi::Client.new( - api_key: BridgeApi.api_key, - sandbox_mode: BridgeApi.sandbox_mode, -) - -# First, list webhooks to get the latest one -puts 'Fetching webhooks...' -list_response = client.webhooks.list - -unless list_response.success? - puts '✗ Failed to fetch webhooks' - exit 1 -end - -webhooks = list_response.data.data -if webhooks.empty? - puts '✗ No webhooks found' - exit 1 -end - -# Get the most recent webhook (last in the list) -latest_webhook = webhooks.last - -puts "Found latest webhook:" -puts " ID: #{latest_webhook.id}" -puts " URL: #{latest_webhook.url}" -puts " Current Status: #{latest_webhook.status}" -puts - -# Update webhook to active status -puts 'Activating webhook...' - -update_params = { - status: 'active' -} - -begin - response = client.webhooks.update(latest_webhook.id, update_params) - - if response.success? - webhook = response.data - puts '✓ Webhook updated successfully!' - puts " ID: #{webhook.id}" - puts " URL: #{webhook.url}" - puts " New Status: #{webhook.status}" - puts " Events: #{webhook.event_categories.join(', ')}" - else - puts '✗ Error updating webhook:' - puts " Status: #{response.status_code}" - puts " Error: #{response.error.message}" - end -rescue StandardError => e - puts "Exception occurred: #{e.class} - #{e.message}" - puts e.backtrace.first(5).join("\n") -end diff --git a/exemples/webhook_example.rb b/exemples/webhook_example.rb deleted file mode 100644 index 6dff7fb..0000000 --- a/exemples/webhook_example.rb +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Example script to demonstrate webhook configuration for the Bridge API gem - -# Set up Bundler to use the local gem -require 'bundler/setup' -require 'bridge_api' - -# Load environment variables if available (for API key) -begin - require 'dotenv/load' -rescue LoadError - puts 'dotenv gem not available, environment variables will need to be set manually' -end - -# Configure Bridge API (if not using environment variable) -BridgeApi.config do |config| - # Use API key from environment variable or set here directly for testing - config.api_key = ENV['BRIDGE_API_KEY'] || 'your-api-key-here' - config.sandbox_mode = ENV['BRIDGE_SANDBOX_MODE'] ? ENV['BRIDGE_SANDBOX_MODE'].downcase == 'true' : true -end - -puts 'Bridge API Configuration:' -puts " API Key: #{BridgeApi.api_key ? '[SET]' : '[NOT SET]'}" -puts " Sandbox Mode: #{BridgeApi.sandbox_mode}" -puts " Base URL: #{BridgeApi.base_url}" -puts - -# Webhook configuration example -puts 'Webhook Configuration Example:' -puts '===============================' -puts 'Webhook endpoint: https://96743bb22bbd.ngrok-free.app/bridge/webhook' -puts 'Monitored events: customer, kyc_link, virtual_account.activity' -puts - -webhook_config = { - url: 'https://96743bb22bbd.ngrok-free.app/bridge/webhook', - event_categories: [ - 'customer', - 'kyc_link', - 'virtual_account.activity' - ], - active: true -} - -puts "Configuration hash that would be sent to the API:" -webhook_config.each do |key, value| - if value.is_a?(Array) - puts " #{key}: [#{value.join(', ')}]" - else - puts " #{key}: #{value}" - end -end -puts - -if BridgeApi.api_key && !BridgeApi.api_key.include?('your-api-key') - # Only try to make the API call if we have a real API key - begin - client = BridgeApi::Client.new( - api_key: BridgeApi.api_key, - sandbox_mode: BridgeApi.sandbox_mode, - ) - puts 'Client initialized, attempting to create webhook...' - - response = client.webhooks.create(webhook_config) - - if response.success? - puts '✓ Webhook created successfully!' - puts " Webhook ID: #{response.data.id}" - else - puts '✗ Error creating webhook:' - puts " Status: #{response.status_code}" - puts " Message: #{response.error.message}" - end - rescue StandardError => e - puts "Exception occurred: #{e.message}" - end -else - puts "To create the webhook with a real API call, set your API key:" - puts " export BRIDGE_API_KEY='your-actual-api-key-here'" - puts " ruby exemples/webhook_example.rb" - puts - puts "Code example for creating the webhook:" - puts - puts " client = BridgeApi::Client.new" - puts " webhook_config = {" - puts " url: 'https://96743bb22bbd.ngrok-free.app/bridge/webhook'," - puts " event_categories: ['customer', 'kyc_link', 'virtual_account.activity']," - puts " active: true" - puts " }" - puts " response = client.webhooks.create(webhook_config)" - puts " if response.success?" - puts " puts \"Webhook created: \#{response.data.id}\"" - puts " else" - puts " puts \"Error: \#{response.error.message}\"" - puts " end" -end - -puts -puts 'Webhook example completed.' - -# Example of how to handle incoming webhook events -puts -puts 'Example Webhook Handler (for your server application):' -puts '=====================================================' -puts <<~HANDLER - # In your web application (Sinatra, Rails, etc.) - - post '/bridge/webhook' do - # Verify webhook signature for security - payload = request.body.read - signature = request.env['HTTP_X_SIGNATURE'] - - # Verify the signature here (implementation may vary) - - # Parse the webhook event - event = JSON.parse(payload) - - case event['type'] - when 'customer' - # Handle customer event - puts "Customer event received: \#{event['data']}" - when 'kyc_link' - # Handle KYC link event - puts "KYC link event received: \#{event['data']}" - when 'virtual_account.activity' - # Handle virtual account activity - puts "Virtual account activity received: \#{event['data']}" - else - puts "Unknown event type: \#{event['type']}" - end - - status 200 - end -HANDLER \ No newline at end of file diff --git a/lib/bridge_api/resources/customer.rb b/lib/bridge_api/resources/customer.rb index db97f11..7382e2c 100644 --- a/lib/bridge_api/resources/customer.rb +++ b/lib/bridge_api/resources/customer.rb @@ -106,4 +106,4 @@ def parse_datetime(datetime_string) end end end -end \ No newline at end of file +end diff --git a/lib/bridge_api/resources/webhook.rb b/lib/bridge_api/resources/webhook.rb index a15e868..d508b1d 100644 --- a/lib/bridge_api/resources/webhook.rb +++ b/lib/bridge_api/resources/webhook.rb @@ -21,7 +21,7 @@ def self.retrieve(client, id, _params = {}) end def self.create(client, params = {}) - super(client, params) + super end # Specific accessor methods for convenience diff --git a/lib/bridge_api/services/base_service.rb b/lib/bridge_api/services/base_service.rb index 4a64d63..e075002 100644 --- a/lib/bridge_api/services/base_service.rb +++ b/lib/bridge_api/services/base_service.rb @@ -17,4 +17,4 @@ def request(method, endpoint, params = {}, idempotency_key: nil) end end end -end \ No newline at end of file +end diff --git a/lib/bridge_api/services/external_account_service.rb b/lib/bridge_api/services/external_account_service.rb index dce2c0a..5c39d5e 100644 --- a/lib/bridge_api/services/external_account_service.rb +++ b/lib/bridge_api/services/external_account_service.rb @@ -8,7 +8,7 @@ module Services # Service class for handling External Account-related operations class ExternalAccountService < BaseService def initialize(client) - super(client) + super @resource_class = BridgeApi::ExternalAccount end @@ -28,4 +28,4 @@ def list(options = {}) end end end -end \ No newline at end of file +end diff --git a/lib/bridge_api/services/kyc_link_service.rb b/lib/bridge_api/services/kyc_link_service.rb index 614536f..b15d9cc 100644 --- a/lib/bridge_api/services/kyc_link_service.rb +++ b/lib/bridge_api/services/kyc_link_service.rb @@ -15,4 +15,4 @@ def list(params = {}) end end end -end \ No newline at end of file +end diff --git a/lib/bridge_api/services/virtual_account_service.rb b/lib/bridge_api/services/virtual_account_service.rb index 12b9bed..a297116 100644 --- a/lib/bridge_api/services/virtual_account_service.rb +++ b/lib/bridge_api/services/virtual_account_service.rb @@ -8,7 +8,7 @@ module Services # Service class for handling Virtual Account-related operations class VirtualAccountService < BaseService def initialize(client) - super(client) + super @resource_class = BridgeApi::VirtualAccount end @@ -28,4 +28,4 @@ def list(options = {}) end end end -end \ No newline at end of file +end diff --git a/lib/bridge_api/services/wallet_service.rb b/lib/bridge_api/services/wallet_service.rb index 9746dd3..b013df1 100644 --- a/lib/bridge_api/services/wallet_service.rb +++ b/lib/bridge_api/services/wallet_service.rb @@ -8,7 +8,7 @@ module Services # Service class for handling Wallet-related operations class WalletService < BaseService def initialize(client) - super(client) + super @resource_class = BridgeApi::Wallet end @@ -28,4 +28,4 @@ def list(options = {}) end end end -end \ No newline at end of file +end diff --git a/lib/bridge_api/services/webhook_service.rb b/lib/bridge_api/services/webhook_service.rb index c1074a0..032a2c9 100644 --- a/lib/bridge_api/services/webhook_service.rb +++ b/lib/bridge_api/services/webhook_service.rb @@ -8,7 +8,7 @@ module Services # Service class for handling Webhook-related operations class WebhookService < BaseService def initialize(client) - super(client) + super @resource_class = BridgeApi::Webhook end @@ -28,4 +28,4 @@ def list(options = {}) end end end -end \ No newline at end of file +end From eb884c5d6b47d246c681afdee3f7473732299aff Mon Sep 17 00:00:00 2001 From: Bolo Michelin <72155+bolom@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:31:38 -0400 Subject: [PATCH 09/11] Remove unnecessary require statements in wallet_service.rb Eliminated unused require_relative statements for base_resource and client to clean up the WalletService implementation. --- lib/bridge_api/services/wallet_service.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/bridge_api/services/wallet_service.rb b/lib/bridge_api/services/wallet_service.rb index b013df1..0d249fa 100644 --- a/lib/bridge_api/services/wallet_service.rb +++ b/lib/bridge_api/services/wallet_service.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require_relative '../base_resource' -require_relative '../client' - module BridgeApi module Services # Service class for handling Wallet-related operations From 698e38eb7414228a7f62f7a1fdfadafcf9d7cf7b Mon Sep 17 00:00:00 2001 From: Bolo Michelin <72155+bolom@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:40:06 -0400 Subject: [PATCH 10/11] Refactor dynamic attribute access to base resource Moved the dynamic method handling for attribute access (method_missing and respond_to_missing?) from individual resource classes to BaseResource. This centralizes the logic, reduces code duplication, and ensures consistent behavior across all resource classes. --- lib/bridge_api/base_resource.rb | 16 ++++++++++++++++ lib/bridge_api/resources/customer.rb | 12 ------------ lib/bridge_api/resources/kyc_link.rb | 12 ------------ lib/bridge_api/resources/reward_rate.rb | 12 ------------ lib/bridge_api/resources/total_balance.rb | 13 ------------- lib/bridge_api/resources/transaction_history.rb | 12 ------------ lib/bridge_api/resources/virtual_account.rb | 12 ------------ lib/bridge_api/resources/wallet.rb | 12 ------------ lib/bridge_api/resources/webhook.rb | 12 ------------ lib/bridge_api/resources/webhook_event.rb | 12 ------------ .../resources/webhook_event_delivery_log.rb | 12 ------------ 11 files changed, 16 insertions(+), 121 deletions(-) diff --git a/lib/bridge_api/base_resource.rb b/lib/bridge_api/base_resource.rb index 9bbe1a2..deac78f 100644 --- a/lib/bridge_api/base_resource.rb +++ b/lib/bridge_api/base_resource.rb @@ -121,11 +121,27 @@ def set_attribute(key, value) @unsaved_values.add(key) end + # Dynamic method handling for all attributes in @values + # Only allows attribute access without arguments, raises error if arguments are provided + def method_missing(method_name, *args) + if args.empty? && @values.key?(method_name) + @values[method_name] + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @values.key?(method_name) || super + end + # Convert values to appropriate types when possible def convert_value(value) case value when Array value.map { |v| convert_value(v) } + when Hash + value.each_with_object({}) { |(k, v), hash| hash[k.to_sym] = convert_value(v) } else value end diff --git a/lib/bridge_api/resources/customer.rb b/lib/bridge_api/resources/customer.rb index 7382e2c..8c24475 100644 --- a/lib/bridge_api/resources/customer.rb +++ b/lib/bridge_api/resources/customer.rb @@ -79,18 +79,6 @@ def create_virtual_account(client, params, idempotency_key: nil) self.class.create_customer_virtual_account(client, id, params, idempotency_key: idempotency_key) end - # Dynamic method handling for all attributes in @values - def method_missing(method_name, *args) - if @values.key?(method_name) - @values[method_name] - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - @values.key?(method_name) || super - end private diff --git a/lib/bridge_api/resources/kyc_link.rb b/lib/bridge_api/resources/kyc_link.rb index 41d4456..486cb4e 100644 --- a/lib/bridge_api/resources/kyc_link.rb +++ b/lib/bridge_api/resources/kyc_link.rb @@ -69,18 +69,6 @@ def created_at parse_datetime(@values[:created_at]) end - # Dynamic method handling for all attributes in @values - def method_missing(method_name, *args) - if @values.key?(method_name) - @values[method_name] - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - @values.key?(method_name) || super - end private diff --git a/lib/bridge_api/resources/reward_rate.rb b/lib/bridge_api/resources/reward_rate.rb index 86beae6..9946800 100644 --- a/lib/bridge_api/resources/reward_rate.rb +++ b/lib/bridge_api/resources/reward_rate.rb @@ -20,18 +20,6 @@ def expires_at parse_datetime(@values[:expires_at]) end - # Dynamic method handling for all attributes in @values - def method_missing(method_name, *args) - if @values.key?(method_name) - @values[method_name] - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - @values.key?(method_name) || super - end private diff --git a/lib/bridge_api/resources/total_balance.rb b/lib/bridge_api/resources/total_balance.rb index 7752445..9fdb40b 100644 --- a/lib/bridge_api/resources/total_balance.rb +++ b/lib/bridge_api/resources/total_balance.rb @@ -25,19 +25,6 @@ def chain def contract_address @values[:contract_address] end - - # Dynamic method handling for all attributes in @values - def method_missing(method_name, *args) - if @values.key?(method_name) - @values[method_name] - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - @values.key?(method_name) || super - end end end end diff --git a/lib/bridge_api/resources/transaction_history.rb b/lib/bridge_api/resources/transaction_history.rb index 266be11..943d8c8 100644 --- a/lib/bridge_api/resources/transaction_history.rb +++ b/lib/bridge_api/resources/transaction_history.rb @@ -36,18 +36,6 @@ def destination @values[:destination] end - # Dynamic method handling for all attributes in @values - def method_missing(method_name, *args) - if @values.key?(method_name) - @values[method_name] - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - @values.key?(method_name) || super - end private diff --git a/lib/bridge_api/resources/virtual_account.rb b/lib/bridge_api/resources/virtual_account.rb index 4c0dd48..faae576 100644 --- a/lib/bridge_api/resources/virtual_account.rb +++ b/lib/bridge_api/resources/virtual_account.rb @@ -52,18 +52,6 @@ def balances @values[:balances] || [] end - # Dynamic method handling for all attributes in @values - def method_missing(method_name, *args) - if @values.key?(method_name) - @values[method_name] - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - @values.key?(method_name) || super - end private diff --git a/lib/bridge_api/resources/wallet.rb b/lib/bridge_api/resources/wallet.rb index 3e82c1a..f55eb5f 100644 --- a/lib/bridge_api/resources/wallet.rb +++ b/lib/bridge_api/resources/wallet.rb @@ -55,18 +55,6 @@ def self.get_for_customer(client, customer_id, wallet_id) client.get_customer_wallet(customer_id, wallet_id) end - # Dynamic method handling for all attributes in @values - def method_missing(method_name, *args) - if @values.key?(method_name) - @values[method_name] - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - @values.key?(method_name) || super - end private diff --git a/lib/bridge_api/resources/webhook.rb b/lib/bridge_api/resources/webhook.rb index d508b1d..ce9fa37 100644 --- a/lib/bridge_api/resources/webhook.rb +++ b/lib/bridge_api/resources/webhook.rb @@ -49,18 +49,6 @@ def created_at parse_datetime(@values[:created_at]) end - # Dynamic method handling for all attributes in @values - def method_missing(method_name, *args) - if @values.key?(method_name) - @values[method_name] - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - @values.key?(method_name) || super - end private diff --git a/lib/bridge_api/resources/webhook_event.rb b/lib/bridge_api/resources/webhook_event.rb index 6c2cfbc..3668191 100644 --- a/lib/bridge_api/resources/webhook_event.rb +++ b/lib/bridge_api/resources/webhook_event.rb @@ -52,18 +52,6 @@ def event_created_at parse_datetime(@values[:event_created_at]) end - # Dynamic method handling for all attributes in @values - def method_missing(method_name, *args) - if @values.key?(method_name) - @values[method_name] - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - @values.key?(method_name) || super - end private diff --git a/lib/bridge_api/resources/webhook_event_delivery_log.rb b/lib/bridge_api/resources/webhook_event_delivery_log.rb index 3c4d058..b5b3be9 100644 --- a/lib/bridge_api/resources/webhook_event_delivery_log.rb +++ b/lib/bridge_api/resources/webhook_event_delivery_log.rb @@ -24,18 +24,6 @@ def created_at parse_datetime(@values[:created_at]) end - # Dynamic method handling for all attributes in @values - def method_missing(method_name, *args) - if @values.key?(method_name) - @values[method_name] - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - @values.key?(method_name) || super - end private From 94c0f5604b566e4c8c7be775fd4f0c59a1dc0b70 Mon Sep 17 00:00:00 2001 From: Bolo Michelin <72155+bolom@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:00:36 -0400 Subject: [PATCH 11/11] Fix require path and update webhook retrieval logic Changed require_relative from '../base_resource' to 'base_service' to correct the dependency path. Refactored the get method to call retrieve as a class method on @resource_class, passing @client and webhook_id, instead of instantiating a new resource object. --- lib/bridge_api/services/webhook_service.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/bridge_api/services/webhook_service.rb b/lib/bridge_api/services/webhook_service.rb index 032a2c9..050a469 100644 --- a/lib/bridge_api/services/webhook_service.rb +++ b/lib/bridge_api/services/webhook_service.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../base_resource' +require_relative 'base_service' require_relative '../client' module BridgeApi @@ -16,8 +16,7 @@ def initialize(client) # @param webhook_id [String] The ID of the webhook # @return [BridgeApi::Webhook] The webhook object def get(webhook_id) - resource = @resource_class.new(@client) - resource.retrieve(webhook_id) + @resource_class.retrieve(@client, webhook_id) end # List all webhooks