From 3894ceafff7731747c09b20af38e7ac71e650dff Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 14 Mar 2025 01:59:17 +0100 Subject: [PATCH 01/26] Add paystack open api spec as git submodule --- .gitmodules | 3 +++ openapi | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 openapi diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ee21143 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "openapi"] + path = openapi + url = git@github.com:PaystackOSS/openapi.git diff --git a/openapi b/openapi new file mode 160000 index 0000000..375cd00 --- /dev/null +++ b/openapi @@ -0,0 +1 @@ +Subproject commit 375cd003585cc796f0ddcebf109787949a034d23 From ad1f7147ff1a5264107ac1ecbb44e225e1d2ad18 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 14 Mar 2025 02:03:39 +0100 Subject: [PATCH 02/26] Add script to parse open api specification and generate api methods --- .gitignore | 1 - .rubocop.yml | 4 + Gemfile | 2 + bin/sync_openapi.rb | 245 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 1 deletion(-) create mode 100755 bin/sync_openapi.rb diff --git a/.gitignore b/.gitignore index c902e13..8f252b2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,3 @@ Gemfile.lock /pkg/ /spec/reports/ /tmp/ - diff --git a/.rubocop.yml b/.rubocop.yml index 5cf9949..fabca65 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -10,6 +10,10 @@ Metrics/AbcSize: Max: 25 Metrics/MethodLength: Max: 25 +Metrics/ModuleLength: + Max: 250 +Metrics/ParameterLists: + Enabled: false Minitest/MultipleAssertions: Enabled: false Naming/FileName: diff --git a/Gemfile b/Gemfile index e456a51..47b6415 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,9 @@ gemspec gem 'rake', '~> 13.0' group :development do + gem 'debug', '~> 1.10' gem 'minitest', '~> 5.16' + gem 'openapi_parser', '~> 2.2', require: true gem 'rubocop', '~> 1.21' gem 'rubocop-minitest' gem 'rubocop-rake' diff --git a/bin/sync_openapi.rb b/bin/sync_openapi.rb new file mode 100755 index 0000000..b95b9f6 --- /dev/null +++ b/bin/sync_openapi.rb @@ -0,0 +1,245 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'openapi_parser' +require 'fileutils' +require 'paystack_gateway' +require 'debug' + +class OpenApiGenerator + SDK_ROOT = File.expand_path('..', __dir__) + LIB_DIR = File.join(SDK_ROOT, 'lib', 'paystack_gateway') + OPENAPI_SPEC = File.join(SDK_ROOT, 'openapi', 'base', 'paystack.yaml') + INDENT = ' ' + + def initialize + @document = OpenAPIParser.parse(YAML.load_file(OPENAPI_SPEC), strict_reference_validation: true) + end + + def generate + api_methods_by_api_module_name.each do |api_module_name, api_methods| + puts "Processing #{api_module_name}" + + if existing_api_modules.include?(api_module_name) + puts "Updating existing module: #{api_module_name}" + else + puts "Creating new module: #{api_module_name}" + end + + create_new_module(api_module_name, api_methods) + end + end + + private + + def api_methods_by_api_module_name + @api_methods_by_api_module_name ||= + @document.paths.path.each_with_object({}) do |(path, path_item), paths_by_api_module| + %w[get post put patch delete].each do |http_method| + operation = path_item.operation(http_method) + next if !operation + + api_module_name = operation.tags.first.remove(' ') + next if !api_module_name + + paths_by_api_module[api_module_name] ||= [] + paths_by_api_module[api_module_name] << { path:, http_method:, operation: } + end + end + end + + def existing_api_modules + @existing_api_modules ||= PaystackGateway.api_modules.to_set { |api_module| api_module.name.split('::').last } + end + + def create_new_module(api_module_name, api_methods) + file_path = File.join(LIB_DIR, "#{api_module_name.underscore}.rb") + puts "Creating new module file: #{file_path}" + + content = api_module_content(api_module_name, api_methods) + File.write(file_path, content) + end + + def api_module_content(api_module_name, api_methods) + <<~RUBY + # frozen_string_literal: true + + module PaystackGateway + # = #{tags_by_name.dig(api_module_name, 'x-product-name')&.chomp} + # #{tags_by_name.dig(api_module_name, 'description')&.chomp} + # https://paystack.com/docs/api/#{api_module_name.parameterize} + module #{api_module_name} + include PaystackGateway::RequestModule + + #{api_methods.map { |info| api_method_composition(api_module_name, info) }.join("\n").chomp} + end + end + RUBY + end + + def tags_by_name + @tags_by_name ||= @document.raw_schema['tags'].index_by { _1['name'] } + end + + def api_method_composition(api_module_name, api_method_info) + api_method_info => { path:, http_method:, operation: } + + api_method_name = api_method_name(api_module_name, operation) + <<~RUBY.chomp + #{api_method_response_class_content(api_method_name, operation)} + + #{api_method_error_class_content(api_method_name)} + + #{api_method_content(api_method_name, operation, http_method, path)} + RUBY + end + + def api_method_response_class_content(api_method_name, operation) + responses = operation.responses.response + success_response = responses[responses.keys.find { _1.match?(/\A2..\z/) }] + + definition = "#{INDENT * 2}# Successful response from calling ##{api_method_name}.\n" + definition += "#{INDENT * 2}class #{"#{api_method_name}_response".camelize} < PaystackGateway::Response" + + if success_response + "#{definition}\n#{INDENT * 2}end" + else + "#{definition}; end" + end + end + + def api_method_error_class_content(api_method_name) + definition = "#{INDENT * 2}# Error response from ##{api_method_name}.\n" + definition + "#{INDENT * 2}class #{"#{api_method_name}_error".camelize} < ApiError; end" + end + + def api_method_content(api_method_name, operation, http_method, path) + <<-RUBY + #{api_method_definition_name_and_parameters(api_method_name, operation)} + use_connection do |connection| + connection.#{http_method}( + #{api_method_definition_path(path)},#{api_method_definition_request_params(operation)} + ) + end + end + RUBY + end + + def api_method_definition_name_and_parameters(api_method_name, operation) + api_method_parameters(operation) => { required:, optional: } + + method_args = [ + *required.map { |param| "#{param}:" }, + *optional.map { |param| "#{param}: nil" }, + ] + + definition = "api_method def self.#{api_method_name}(" + + if method_args.length > 5 + while (line_arg = method_args.shift).present? + definition += "\n#{INDENT * 3}#{line_arg}" + definition += ',' if method_args.any? + end + definition + "\n#{INDENT * 2})" + else + definition + "#{method_args.join(', ')})" + end + end + + def api_method_definition_path(path) + # "/transaction/verify/{reference}" -> "/transaction/verify/#{reference}" + interpolated_path = path.gsub(/{([^}]+)}/, "\#{\\1}") + + interpolated_path == path ? "'#{interpolated_path}'" : "\"#{interpolated_path}\"" + end + + def api_method_definition_request_params(operation) + api_method_path_and_query_parameters(operation) => { query_params: } + api_method_request_body_parameters(operation) => { required:, optional: } + params = required + optional + query_params + + return if params.none? + + definition = "\n#{INDENT * 5}{" + + if params.length > 5 + while (line_param = params.shift).present? + definition += "\n#{INDENT * 6}#{line_param}:," + end + + definition + "\n#{INDENT * 5}}.compact," + else + "#{definition} #{params.map { |param| "#{param}:" }.join(', ')} }.compact," + end + end + + def api_method_parameters(operation) + path_and_query_params = api_method_path_and_query_parameters(operation) + body_params = api_method_request_body_parameters(operation) + + required = path_and_query_params[:path_params] + body_params[:required] + optional = body_params[:optional] + path_and_query_params[:query_params] + + { required:, optional: } + end + + def api_method_path_and_query_parameters(operation) + path_params = [] + query_params = [] + + path_params, query_params = operation.parameters.partition { _1.in == 'path' } if operation.parameters + + { + path_params: path_params.map(&:name), + query_params: query_params.map(&:name), + } + end + + def api_method_request_body_parameters(operation) + required = [] + optional = [] + + if operation.request_body + schema = operation.request_body.content['application/json'].schema + + if schema.type == 'array' + schema.items.properties.map { |name, _| required << [name, schema.items.properties[name]] } + else + schema_data = + if schema.all_of.present? + [schema.all_of.map(&:properties).flatten, schema.all_of.map(&:required).flatten] + else + [schema.properties, schema.required] + end + schema_data => [schema_properties, schema_required] + + required, optional = schema_properties.partition { |name, _| schema_required&.include?(name) } + end + end + + { + required: required.map(&:first), + optional: optional.map(&:first), + } + end + + def api_method_name(api_module_name, operation) + operation + .operation_id + .delete_prefix( + case api_module_name.to_sym + when :DedicatedVirtualAccount + 'dedicatedAccount' + when :TransferRecipient + 'transferrecipient' + else + api_module_name.camelize(:lower) + end, + ) + .delete_prefix('_') + .underscore + end +end + +generator = OpenApiGenerator.new +generator.generate From e832f9adec2bfb23b3c4398306c6577fce165257 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 14 Mar 2025 02:12:53 +0100 Subject: [PATCH 03/26] Remove class initializer --- bin/sync_openapi.rb | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/bin/sync_openapi.rb b/bin/sync_openapi.rb index b95b9f6..08f5393 100755 --- a/bin/sync_openapi.rb +++ b/bin/sync_openapi.rb @@ -7,15 +7,6 @@ require 'debug' class OpenApiGenerator - SDK_ROOT = File.expand_path('..', __dir__) - LIB_DIR = File.join(SDK_ROOT, 'lib', 'paystack_gateway') - OPENAPI_SPEC = File.join(SDK_ROOT, 'openapi', 'base', 'paystack.yaml') - INDENT = ' ' - - def initialize - @document = OpenAPIParser.parse(YAML.load_file(OPENAPI_SPEC), strict_reference_validation: true) - end - def generate api_methods_by_api_module_name.each do |api_module_name, api_methods| puts "Processing #{api_module_name}" @@ -32,9 +23,14 @@ def generate private + SDK_ROOT = File.expand_path('..', __dir__) + LIB_DIR = File.join(SDK_ROOT, 'lib', 'paystack_gateway') + OPENAPI_SPEC = File.join(SDK_ROOT, 'openapi', 'base', 'paystack.yaml') + INDENT = ' ' + def api_methods_by_api_module_name @api_methods_by_api_module_name ||= - @document.paths.path.each_with_object({}) do |(path, path_item), paths_by_api_module| + document.paths.path.each_with_object({}) do |(path, path_item), paths_by_api_module| %w[get post put patch delete].each do |http_method| operation = path_item.operation(http_method) next if !operation @@ -48,10 +44,6 @@ def api_methods_by_api_module_name end end - def existing_api_modules - @existing_api_modules ||= PaystackGateway.api_modules.to_set { |api_module| api_module.name.split('::').last } - end - def create_new_module(api_module_name, api_methods) file_path = File.join(LIB_DIR, "#{api_module_name.underscore}.rb") puts "Creating new module file: #{file_path}" @@ -77,10 +69,6 @@ module #{api_module_name} RUBY end - def tags_by_name - @tags_by_name ||= @document.raw_schema['tags'].index_by { _1['name'] } - end - def api_method_composition(api_module_name, api_method_info) api_method_info => { path:, http_method:, operation: } @@ -239,7 +227,18 @@ def api_method_name(api_module_name, operation) .delete_prefix('_') .underscore end + + def existing_api_modules + @existing_api_modules ||= PaystackGateway.api_modules.to_set { |api_module| api_module.name.split('::').last } + end + + def tags_by_name + @tags_by_name ||= @document.raw_schema['tags'].index_by { _1['name'] } + end + + def document + @document ||= OpenAPIParser.parse(YAML.load_file(OPENAPI_SPEC), strict_reference_validation: true) + end end -generator = OpenApiGenerator.new -generator.generate +OpenApiGenerator.new.generate From b4e94d7d8f82e74e2e75e765c1f93e6f883800df Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 14 Mar 2025 02:22:33 +0100 Subject: [PATCH 04/26] Remove unsused fileutils requirement --- bin/sync_openapi.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/sync_openapi.rb b/bin/sync_openapi.rb index 08f5393..7d6c84d 100755 --- a/bin/sync_openapi.rb +++ b/bin/sync_openapi.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require 'openapi_parser' -require 'fileutils' require 'paystack_gateway' require 'debug' From bfb757844bdb82c3f0d88a84abe867b18134a2c3 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 14 Mar 2025 12:30:17 +0100 Subject: [PATCH 05/26] Add delegation for required keys and method parameter documentation --- .rubocop.yml | 2 + bin/sync_openapi.rb | 183 ++++++++++++++++++++++++++++++++------------ 2 files changed, 138 insertions(+), 47 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index fabca65..785a5fd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,6 +6,8 @@ AllCops: TargetRubyVersion: 3.3 NewCops: enable +Layout/MultilineMethodCallIndentation: + EnforcedStyle: indented_relative_to_receiver Metrics/AbcSize: Max: 25 Metrics/MethodLength: diff --git a/bin/sync_openapi.rb b/bin/sync_openapi.rb index 7d6c84d..0e40306 100755 --- a/bin/sync_openapi.rb +++ b/bin/sync_openapi.rb @@ -83,12 +83,25 @@ def api_method_composition(api_module_name, api_method_info) def api_method_response_class_content(api_method_name, operation) responses = operation.responses.response - success_response = responses[responses.keys.find { _1.match?(/\A2..\z/) }] definition = "#{INDENT * 2}# Successful response from calling ##{api_method_name}.\n" definition += "#{INDENT * 2}class #{"#{api_method_name}_response".camelize} < PaystackGateway::Response" - if success_response + if (success_response = responses[responses.keys.find { _1.match?(/\A2..\z/) }]) && + (required_data_keys = success_response.content['application/json'].schema.properties['data']&.required) + + required_data_keys -= %w[status message data meta] + if required_data_keys.length > 3 + definition += "\n#{INDENT * 3}delegate :#{required_data_keys.shift}," + + while (line_key = required_data_keys.shift).present? + definition += "\n#{INDENT * 3}#{' ' * 'delegate'.length} :#{line_key}," + end + definition += ' to: :data' + else + definition += "\n#{INDENT * 3}delegate #{required_data_keys.map { |key| ":#{key}" }.join(', ')}, to: :data" + end + "#{definition}\n#{INDENT * 2}end" else "#{definition}; end" @@ -102,6 +115,7 @@ def api_method_error_class_content(api_method_name) def api_method_content(api_method_name, operation, http_method, path) <<-RUBY + #{api_method_definition_docstring(operation, http_method, path)} #{api_method_definition_name_and_parameters(api_method_name, operation)} use_connection do |connection| connection.#{http_method}( @@ -112,13 +126,47 @@ def api_method_content(api_method_name, operation, http_method, path) RUBY end - def api_method_definition_name_and_parameters(api_method_name, operation) - api_method_parameters(operation) => { required:, optional: } + def api_method_definition_docstring(operation, http_method, path) + definition = "# #{operation.summary}: #{http_method.upcase} #{path}" + definition += "\n#{INDENT * 2}# #{operation.description}" if operation.description.present? + + api_method_parameters(operation).each do |param| + definition += "\n#{INDENT * 2}# @param #{param[:name]} [#{param[:type]}]" + definition += ' (required)' if param[:required] + + if (description = param[:description]&.squish) + definition += wrapped_text(description, "\n#{INDENT * 2}##{INDENT * 3} ") + end + + next if !(object_properties = param[:object_properties]) + + object_properties.each do |props| + definition += "\n#{INDENT * 2}##{INDENT * 4}@option #{param[:name]} [#{props[:type]}] :#{props[:name]}" + definition += wrapped_text(props[:description], "\n#{INDENT * 2}##{INDENT * 7} ") + end + end + + definition + end + + def wrapped_text(text, prefix = nil) + wrapped = '' + text_words = text.split + + until text_words.none? + line = '' + line += " #{text_words.shift}" until text_words.none? || line.length >= 80 + + wrapped += "#{prefix}#{line}" + end + + wrapped + end - method_args = [ - *required.map { |param| "#{param}:" }, - *optional.map { |param| "#{param}: nil" }, - ] + def api_method_definition_name_and_parameters(api_method_name, operation) + method_args = api_method_parameters(operation).map do |param| + param[:required] ? "#{param[:name]}:" : "#{param[:name]}: nil" + end definition = "api_method def self.#{api_method_name}(" @@ -141,10 +189,9 @@ def api_method_definition_path(path) end def api_method_definition_request_params(operation) - api_method_path_and_query_parameters(operation) => { query_params: } - api_method_request_body_parameters(operation) => { required:, optional: } - params = required + optional + query_params - + params = api_method_parameters(operation) + .reject { |param| param[:in] == 'path' } + .map { |param| param[:name] } return if params.none? definition = "\n#{INDENT * 5}{" @@ -161,53 +208,95 @@ def api_method_definition_request_params(operation) end def api_method_parameters(operation) - path_and_query_params = api_method_path_and_query_parameters(operation) - body_params = api_method_request_body_parameters(operation) - - required = path_and_query_params[:path_params] + body_params[:required] - optional = body_params[:optional] + path_and_query_params[:query_params] - - { required:, optional: } + @api_method_parameters ||= {} + @api_method_parameters[operation] ||= [ + *api_method_path_and_query_parameters(operation), + *api_method_request_body_parameters(operation), + ].sort_by { |param| param[:required] ? 0 : 1 } end def api_method_path_and_query_parameters(operation) - path_params = [] - query_params = [] + return [] if !operation.parameters + + operation.parameters.map do |param| + { + name: param.name, + in: param.in, + required: param.in == 'path', + description: param.description, + type: schema_type(param.schema), + } + end + end - path_params, query_params = operation.parameters.partition { _1.in == 'path' } if operation.parameters + def api_method_request_body_parameters(operation) + return [] if !operation.request_body - { - path_params: path_params.map(&:name), - query_params: query_params.map(&:name), - } + schema = operation.request_body.content['application/json'].schema + if schema.type == 'array' + schema_array_properties(schema) + else + schema_object_properties(schema) + end end - def api_method_request_body_parameters(operation) - required = [] - optional = [] + def schema_array_properties(schema) + schema.items.properties.map do |name, p_schema| + { + name:, + in: 'body', + required: true, + description: schema_description(p_schema), + type: "Array<#{schema_type(p_schema.items)}>", + object_properties: schema_object_properties(p_schema.items), + } + end + end - if operation.request_body - schema = operation.request_body.content['application/json'].schema + def schema_object_properties(schema) + if schema.all_of.present? + schema_properties = schema.all_of.map(&:properties).reduce(&:merge) + schema_required = schema.all_of.flat_map(&:required).compact.presence + else + schema_properties = schema.properties + schema_required = schema.required + end - if schema.type == 'array' - schema.items.properties.map { |name, _| required << [name, schema.items.properties[name]] } + schema_properties&.map do |name, p_schema| + { + name:, + in: 'body', + required: schema_required ? schema_required.include?(name) : true, + description: schema_description(p_schema), + type: schema_type(p_schema), + object_properties: schema_object_properties(p_schema), + } + end + end + + def schema_description(schema) + schema.description || schema.items&.description + end + + def schema_type(schema) + case schema.type + when 'array' + "Array<#{schema_type(schema.items)}>" + when 'string' + if schema.format == 'date-time' + 'Time' + elsif schema.enum + schema.enum.map { |v| "\"#{v}\"" }.join(', ') else - schema_data = - if schema.all_of.present? - [schema.all_of.map(&:properties).flatten, schema.all_of.map(&:required).flatten] - else - [schema.properties, schema.required] - end - schema_data => [schema_properties, schema_required] - - required, optional = schema_properties.partition { |name, _| schema_required&.include?(name) } + 'String' end + when 'object' + 'Hash' + when 'integer', 'boolean', 'number' + schema.type.capitalize + else + raise "Unhandled schema type: #{schema.type}" end - - { - required: required.map(&:first), - optional: optional.map(&:first), - } end def api_method_name(api_module_name, operation) From aceaa20fb175d4640341f52b22255a51dd5eec83 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 14 Mar 2025 13:24:20 +0100 Subject: [PATCH 06/26] Fix api module docstring --- bin/sync_openapi.rb | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/bin/sync_openapi.rb b/bin/sync_openapi.rb index 0e40306..e5a893e 100755 --- a/bin/sync_openapi.rb +++ b/bin/sync_openapi.rb @@ -56,9 +56,7 @@ def api_module_content(api_module_name, api_methods) # frozen_string_literal: true module PaystackGateway - # = #{tags_by_name.dig(api_module_name, 'x-product-name')&.chomp} - # #{tags_by_name.dig(api_module_name, 'description')&.chomp} - # https://paystack.com/docs/api/#{api_module_name.parameterize} + #{api_module_docstring(api_module_name)} module #{api_module_name} include PaystackGateway::RequestModule @@ -68,6 +66,21 @@ module #{api_module_name} RUBY end + def api_module_docstring(api_module_name) + module_info = tags_by_name[api_module_name] + return if !module_info + + docstring = + if module_info['x-product-name'] + "# = #{module_info['x-product-name'].squish}" + else + "# = #{api_module_name.humanize}" + end + + docstring += "\n#{INDENT * 1}# #{module_info['description'].squish}" if module_info['description'] + docstring + "\n#{INDENT * 1}# https://paystack.com/docs/api/#{api_module_name.parameterize}" + end + def api_method_composition(api_module_name, api_method_info) api_method_info => { path:, http_method:, operation: } @@ -127,26 +140,26 @@ def api_method_content(api_method_name, operation, http_method, path) end def api_method_definition_docstring(operation, http_method, path) - definition = "# #{operation.summary}: #{http_method.upcase} #{path}" - definition += "\n#{INDENT * 2}# #{operation.description}" if operation.description.present? + docstring = "# #{operation.summary}: #{http_method.upcase} #{path}" + docstring += "\n#{INDENT * 2}# #{operation.description}" if operation.description.present? api_method_parameters(operation).each do |param| - definition += "\n#{INDENT * 2}# @param #{param[:name]} [#{param[:type]}]" - definition += ' (required)' if param[:required] + docstring += "\n#{INDENT * 2}# @param #{param[:name]} [#{param[:type]}]" + docstring += ' (required)' if param[:required] if (description = param[:description]&.squish) - definition += wrapped_text(description, "\n#{INDENT * 2}##{INDENT * 3} ") + docstring += wrapped_text(description, "\n#{INDENT * 2}##{INDENT * 3} ") end next if !(object_properties = param[:object_properties]) object_properties.each do |props| - definition += "\n#{INDENT * 2}##{INDENT * 4}@option #{param[:name]} [#{props[:type]}] :#{props[:name]}" - definition += wrapped_text(props[:description], "\n#{INDENT * 2}##{INDENT * 7} ") + docstring += "\n#{INDENT * 2}##{INDENT * 4}@option #{param[:name]} [#{props[:type]}] :#{props[:name]}" + docstring += wrapped_text(props[:description], "\n#{INDENT * 2}##{INDENT * 7} ") end end - definition + docstring end def wrapped_text(text, prefix = nil) @@ -321,7 +334,7 @@ def existing_api_modules end def tags_by_name - @tags_by_name ||= @document.raw_schema['tags'].index_by { _1['name'] } + @tags_by_name ||= @document.raw_schema['tags'].index_by { _1['name'].remove(' ') } end def document From e4777cedd5fa2fa6e0fa2a43572e2673bdabb8bd Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 14 Mar 2025 13:45:01 +0100 Subject: [PATCH 07/26] Add return and raise doc tags --- bin/sync_openapi.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bin/sync_openapi.rb b/bin/sync_openapi.rb index e5a893e..0239979 100755 --- a/bin/sync_openapi.rb +++ b/bin/sync_openapi.rb @@ -128,7 +128,7 @@ def api_method_error_class_content(api_method_name) def api_method_content(api_method_name, operation, http_method, path) <<-RUBY - #{api_method_definition_docstring(operation, http_method, path)} + #{api_method_definition_docstring(api_method_name, operation, http_method, path)} #{api_method_definition_name_and_parameters(api_method_name, operation)} use_connection do |connection| connection.#{http_method}( @@ -139,7 +139,7 @@ def api_method_content(api_method_name, operation, http_method, path) RUBY end - def api_method_definition_docstring(operation, http_method, path) + def api_method_definition_docstring(api_method_name, operation, http_method, path) docstring = "# #{operation.summary}: #{http_method.upcase} #{path}" docstring += "\n#{INDENT * 2}# #{operation.description}" if operation.description.present? @@ -159,6 +159,9 @@ def api_method_definition_docstring(operation, http_method, path) end end + docstring += "\n#{INDENT * 2}# @return [#{"#{api_method_name}_response".camelize}] successful response" + docstring += "\n#{INDENT * 2}# @raise [#{"#{api_method_name}_error".camelize}] if the request fails" + docstring end From e24ba3b1d9df5feb45845001920bf94ce33c3074 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 14 Mar 2025 13:46:19 +0100 Subject: [PATCH 08/26] Handle when no method args --- bin/sync_openapi.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/sync_openapi.rb b/bin/sync_openapi.rb index 0239979..455a094 100755 --- a/bin/sync_openapi.rb +++ b/bin/sync_openapi.rb @@ -184,7 +184,10 @@ def api_method_definition_name_and_parameters(api_method_name, operation) param[:required] ? "#{param[:name]}:" : "#{param[:name]}: nil" end - definition = "api_method def self.#{api_method_name}(" + definition = "api_method def self.#{api_method_name}" + return definition if method_args.none? + + definition += '(' if method_args.length > 5 while (line_arg = method_args.shift).present? From 7987788223016ce0b5ed66d89d321d5c1eba6db6 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 14 Mar 2025 15:49:25 +0100 Subject: [PATCH 09/26] Update documentation, rename script, remove class --- bin/generate_sdk_from_openapi_spec.rb | 363 ++++++++++++++++++++++++++ bin/sync_openapi.rb | 351 ------------------------- 2 files changed, 363 insertions(+), 351 deletions(-) create mode 100755 bin/generate_sdk_from_openapi_spec.rb delete mode 100755 bin/sync_openapi.rb diff --git a/bin/generate_sdk_from_openapi_spec.rb b/bin/generate_sdk_from_openapi_spec.rb new file mode 100755 index 0000000..f0e9160 --- /dev/null +++ b/bin/generate_sdk_from_openapi_spec.rb @@ -0,0 +1,363 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'openapi_parser' +require 'paystack_gateway' +require 'debug' + +SDK_ROOT = File.expand_path('..', __dir__) +LIB_DIR = File.join(SDK_ROOT, 'lib', 'paystack_gateway') +LIB_FILE = File.join(SDK_ROOT, 'lib', 'paystack_gateway.rb') +OPENAPI_SPEC = File.join(SDK_ROOT, 'openapi', 'base', 'paystack.yaml') +INDENT = ' ' +WRAP_LINE_LENGTH = 80 + +def api_methods_by_api_module_name + @api_methods_by_api_module_name ||= + document.paths.path.each_with_object({}) do |(path, path_item), paths_by_api_module| + %w[get post put patch delete].each do |http_method| + operation = path_item.operation(http_method) + next if !operation + + api_module_name = operation.tags.first.remove(' ') + next if !api_module_name + + paths_by_api_module[api_module_name] ||= [] + paths_by_api_module[api_module_name] << { path:, http_method:, operation: } + end + end +end + +def update_required_files(module_names) + file_lines = File.readlines(LIB_FILE) + + comment_start_index = file_lines.find_index { |line| line.include?('# API Modules') } + if !comment_start_index + puts "Could not find '# API Modules' comment in #{LIB_FILE}" + return + end + + insertion_start = comment_start_index + 1 + insertion_start += 1 while insertion_start < file_lines.size && file_lines[insertion_start].start_with?('#') + + insertion_end = insertion_start + insertion_end += 1 while insertion_end < file_lines.size && !file_lines[insertion_end].strip.empty? + + file_lines[insertion_start...insertion_end] = + module_names.map { |module_name| "require 'paystack_gateway/#{module_name.underscore}'\n" }.sort.join + + File.write(LIB_FILE, file_lines.join) + + puts 'Updated paystack_gateway.rb with new API Modules requires.' +end + +def api_module_content(api_module_name, api_methods) + <<~RUBY + # frozen_string_literal: true + + module PaystackGateway + #{api_module_docstring(api_module_name)} + module #{api_module_name} + include PaystackGateway::RequestModule + + #{api_methods.map { |info| api_method_composition(api_module_name, info) }.join("\n").chomp} + end + end + RUBY +end + +def api_module_docstring(api_module_name) + module_info = tags_by_name[api_module_name] + return if !module_info + + docstring = "# https://paystack.com/docs/api/#{api_module_name.parameterize}" + docstring += "\n#{INDENT * 1}#" + docstring += "\n#{INDENT * 1}# #{module_info['x-product-name'].squish || api_module_name.humanize}" + + docstring += "\n#{INDENT * 1}# #{module_info['description'].squish}" if module_info['description'] + docstring +end + +def api_method_composition(api_module_name, api_method_info) + api_method_info => { path:, http_method:, operation: } + + api_method_name = api_method_name(api_module_name, operation) + <<~RUBY.chomp + #{api_method_response_class_content(api_method_name, operation)} + + #{api_method_error_class_content(api_method_name)} + + #{api_method_content(api_method_name, operation, http_method, path)} + RUBY +end + +def api_method_response_class_content(api_method_name, operation) + responses = operation.responses.response + + definition = "#{INDENT * 2}# Successful response from calling ##{api_method_name}.\n" + definition += "#{INDENT * 2}class #{"#{api_method_name}_response".camelize} < PaystackGateway::Response" + + if (success_response = responses[responses.keys.find { _1.match?(/\A2..\z/) }]) && + (required_data_keys = success_response.content['application/json'].schema.properties['data']&.required) + + required_data_keys -= %w[status message data meta] + if required_data_keys.length > 3 + definition += "\n#{INDENT * 3}delegate :#{required_data_keys.shift}," + + while (line_key = required_data_keys.shift).present? + definition += "\n#{INDENT * 3}#{' ' * 'delegate'.length} :#{line_key}," + end + definition += ' to: :data' + else + definition += "\n#{INDENT * 3}delegate #{required_data_keys.map { |key| ":#{key}" }.join(', ')}, to: :data" + end + + "#{definition}\n#{INDENT * 2}end" + else + "#{definition}; end" + end +end + +def api_method_error_class_content(api_method_name) + definition = "#{INDENT * 2}# Error response from ##{api_method_name}.\n" + definition + "#{INDENT * 2}class #{"#{api_method_name}_error".camelize} < ApiError; end" +end + +def api_method_content(api_method_name, operation, http_method, path) + <<-RUBY + #{api_method_definition_docstring(api_method_name, operation, http_method, path)} + #{api_method_definition_name_and_parameters(api_method_name, operation)} + use_connection do |connection| + connection.#{http_method}( + #{api_method_definition_path(path)},#{api_method_definition_request_params(operation)} + ) + end + end + RUBY +end + +def api_method_definition_docstring(api_method_name, operation, http_method, path) + docstring = "# https://paystack.com/docs/api/#{operation.tags.first.parameterize}/##{api_method_name}" + docstring += "\n#{INDENT * 2}# #{operation.summary}: #{http_method.upcase} #{path}" + docstring += "\n#{INDENT * 2}# #{operation.description}" if operation.description.present? + docstring += "\n#{INDENT * 2}#" + + api_method_parameters(operation).each do |param| + docstring += "\n#{INDENT * 2}# @param #{param[:name]} [#{param[:type]}]" + docstring += ' (required)' if param[:required] + + if (description = param[:description]&.squish) + docstring += wrapped_text(description, "\n#{INDENT * 2}##{INDENT * 3} ") + end + + next if !(object_properties = param[:object_properties]) + + object_properties.each do |props| + docstring += "\n#{INDENT * 2}##{INDENT * 1} @option #{param[:name]} [#{props[:type]}] :#{props[:name]}" + docstring += wrapped_text(props[:description], "\n#{INDENT * 2}##{INDENT * 5}") + end + end + + docstring += "\n#{INDENT * 2}# @return [#{"#{api_method_name}_response".camelize}] successful response" + docstring + "\n#{INDENT * 2}# @raise [#{"#{api_method_name}_error".camelize}] if the request fails" +end + +def api_method_definition_name_and_parameters(api_method_name, operation) + method_args = api_method_parameters(operation).map do |param| + param[:required] ? "#{param[:name]}:" : "#{param[:name]}: nil" + end + + definition = "api_method def self.#{api_method_name}" + return definition if method_args.none? + + definition += '(' + + if method_args.length > 5 + while (line_arg = method_args.shift).present? + definition += "\n#{INDENT * 3}#{line_arg}" + definition += ',' if method_args.any? + end + definition + "\n#{INDENT * 2})" + else + definition + "#{method_args.join(', ')})" + end +end + +def api_method_definition_path(path) + # "/transaction/verify/{reference}" -> "/transaction/verify/#{reference}" + interpolated_path = path.gsub(/{([^}]+)}/, "\#{\\1}") + + interpolated_path == path ? "'#{interpolated_path}'" : "\"#{interpolated_path}\"" +end + +def api_method_definition_request_params(operation) + params = api_method_parameters(operation) + .reject { |param| param[:in] == 'path' } + .map { |param| param[:name] } + return if params.none? + + definition = "\n#{INDENT * 5}{" + + if params.length > 5 + while (line_param = params.shift).present? + definition += "\n#{INDENT * 6}#{line_param}:," + end + + definition + "\n#{INDENT * 5}}.compact," + else + "#{definition} #{params.map { |param| "#{param}:" }.join(', ')} }.compact," + end +end + +def api_method_parameters(operation) + @api_method_parameters ||= {} + @api_method_parameters[operation] ||= [ + *api_method_path_and_query_parameters(operation), + *api_method_request_body_parameters(operation), + ].sort_by { |param| param[:required] ? 0 : 1 } +end + +def api_method_path_and_query_parameters(operation) + return [] if !operation.parameters + + operation.parameters.map do |param| + { + name: param.name, + in: param.in, + required: param.in == 'path', + description: param.description, + type: schema_type(param.schema), + } + end +end + +def api_method_request_body_parameters(operation) + return [] if !operation.request_body + + schema = operation.request_body.content['application/json'].schema + if schema.type == 'array' + schema_array_properties(schema) + else + schema_object_properties(schema) + end +end + +def schema_object_properties(schema) + if schema.all_of.present? + schema_properties = schema.all_of.map(&:properties).reduce(&:merge) + schema_required = schema.all_of.flat_map(&:required).compact.presence + else + schema_properties = schema.properties + schema_required = schema.required + end + + schema_properties&.map do |name, p_schema| + { + name:, + in: 'body', + required: schema_required ? schema_required.include?(name) : true, + description: schema_description(p_schema), + type: schema_type(p_schema), + object_properties: schema_object_properties(p_schema), + } + end +end + +def schema_array_properties(schema) + schema.items.properties.map do |name, p_schema| + { + name:, + in: 'body', + required: true, + description: schema_description(p_schema), + type: "Array<#{schema_type(p_schema.items)}>", + object_properties: schema_object_properties(p_schema.items), + } + end +end + +def schema_type(schema) + case schema.type + when 'array' + "Array<#{schema_type(schema.items)}>" + when 'string' + if schema.format == 'date-time' + 'Time' + elsif schema.enum + schema.enum.map { |v| "\"#{v}\"" }.join(', ') + else + 'String' + end + when 'object' + 'Hash' + when 'integer', 'boolean', 'number' + schema.type.capitalize + else + raise "Unhandled schema type: #{schema.type}" + end +end + +def schema_description(schema) + schema.description || schema.items&.description +end + +def api_method_name(api_module_name, operation) + operation + .operation_id + .delete_prefix( + case api_module_name.to_sym + when :DedicatedVirtualAccount + 'dedicatedAccount' + when :TransferRecipient + 'transferrecipient' + else + api_module_name.camelize(:lower) + end, + ) + .delete_prefix('_') + .underscore +end + +def wrapped_text(text, prefix = nil) + wrapped = '' + text_words = text.split + + until text_words.none? + line = '' + line += " #{text_words.shift}" until text_words.none? || line.length >= WRAP_LINE_LENGTH + + wrapped += "#{prefix}#{line}" + end + + wrapped +end + +def existing_api_modules + @existing_api_modules ||= PaystackGateway.api_modules.to_set { |api_module| api_module.name.split('::').last } +end + +def tags_by_name + @tags_by_name ||= @document.raw_schema['tags'].index_by { _1['name'].remove(' ') } +end + +def document + @document ||= OpenAPIParser.parse(YAML.load_file(OPENAPI_SPEC), strict_reference_validation: true) +end + +module_names = api_methods_by_api_module_name.map do |api_module_name, api_methods| + if existing_api_modules.include?(api_module_name) + puts "Updating existing module: #{api_module_name}" + else + puts "Processing new module: #{api_module_name}" + end + + content = api_module_content(api_module_name, api_methods) + + file_path = File.join(LIB_DIR, "#{api_module_name.underscore}.rb") + puts "Writing module content for: #{file_path}" + File.write(file_path, content) + + api_module_name +end + +update_required_files(module_names) diff --git a/bin/sync_openapi.rb b/bin/sync_openapi.rb deleted file mode 100755 index 455a094..0000000 --- a/bin/sync_openapi.rb +++ /dev/null @@ -1,351 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require 'openapi_parser' -require 'paystack_gateway' -require 'debug' - -class OpenApiGenerator - def generate - api_methods_by_api_module_name.each do |api_module_name, api_methods| - puts "Processing #{api_module_name}" - - if existing_api_modules.include?(api_module_name) - puts "Updating existing module: #{api_module_name}" - else - puts "Creating new module: #{api_module_name}" - end - - create_new_module(api_module_name, api_methods) - end - end - - private - - SDK_ROOT = File.expand_path('..', __dir__) - LIB_DIR = File.join(SDK_ROOT, 'lib', 'paystack_gateway') - OPENAPI_SPEC = File.join(SDK_ROOT, 'openapi', 'base', 'paystack.yaml') - INDENT = ' ' - - def api_methods_by_api_module_name - @api_methods_by_api_module_name ||= - document.paths.path.each_with_object({}) do |(path, path_item), paths_by_api_module| - %w[get post put patch delete].each do |http_method| - operation = path_item.operation(http_method) - next if !operation - - api_module_name = operation.tags.first.remove(' ') - next if !api_module_name - - paths_by_api_module[api_module_name] ||= [] - paths_by_api_module[api_module_name] << { path:, http_method:, operation: } - end - end - end - - def create_new_module(api_module_name, api_methods) - file_path = File.join(LIB_DIR, "#{api_module_name.underscore}.rb") - puts "Creating new module file: #{file_path}" - - content = api_module_content(api_module_name, api_methods) - File.write(file_path, content) - end - - def api_module_content(api_module_name, api_methods) - <<~RUBY - # frozen_string_literal: true - - module PaystackGateway - #{api_module_docstring(api_module_name)} - module #{api_module_name} - include PaystackGateway::RequestModule - - #{api_methods.map { |info| api_method_composition(api_module_name, info) }.join("\n").chomp} - end - end - RUBY - end - - def api_module_docstring(api_module_name) - module_info = tags_by_name[api_module_name] - return if !module_info - - docstring = - if module_info['x-product-name'] - "# = #{module_info['x-product-name'].squish}" - else - "# = #{api_module_name.humanize}" - end - - docstring += "\n#{INDENT * 1}# #{module_info['description'].squish}" if module_info['description'] - docstring + "\n#{INDENT * 1}# https://paystack.com/docs/api/#{api_module_name.parameterize}" - end - - def api_method_composition(api_module_name, api_method_info) - api_method_info => { path:, http_method:, operation: } - - api_method_name = api_method_name(api_module_name, operation) - <<~RUBY.chomp - #{api_method_response_class_content(api_method_name, operation)} - - #{api_method_error_class_content(api_method_name)} - - #{api_method_content(api_method_name, operation, http_method, path)} - RUBY - end - - def api_method_response_class_content(api_method_name, operation) - responses = operation.responses.response - - definition = "#{INDENT * 2}# Successful response from calling ##{api_method_name}.\n" - definition += "#{INDENT * 2}class #{"#{api_method_name}_response".camelize} < PaystackGateway::Response" - - if (success_response = responses[responses.keys.find { _1.match?(/\A2..\z/) }]) && - (required_data_keys = success_response.content['application/json'].schema.properties['data']&.required) - - required_data_keys -= %w[status message data meta] - if required_data_keys.length > 3 - definition += "\n#{INDENT * 3}delegate :#{required_data_keys.shift}," - - while (line_key = required_data_keys.shift).present? - definition += "\n#{INDENT * 3}#{' ' * 'delegate'.length} :#{line_key}," - end - definition += ' to: :data' - else - definition += "\n#{INDENT * 3}delegate #{required_data_keys.map { |key| ":#{key}" }.join(', ')}, to: :data" - end - - "#{definition}\n#{INDENT * 2}end" - else - "#{definition}; end" - end - end - - def api_method_error_class_content(api_method_name) - definition = "#{INDENT * 2}# Error response from ##{api_method_name}.\n" - definition + "#{INDENT * 2}class #{"#{api_method_name}_error".camelize} < ApiError; end" - end - - def api_method_content(api_method_name, operation, http_method, path) - <<-RUBY - #{api_method_definition_docstring(api_method_name, operation, http_method, path)} - #{api_method_definition_name_and_parameters(api_method_name, operation)} - use_connection do |connection| - connection.#{http_method}( - #{api_method_definition_path(path)},#{api_method_definition_request_params(operation)} - ) - end - end - RUBY - end - - def api_method_definition_docstring(api_method_name, operation, http_method, path) - docstring = "# #{operation.summary}: #{http_method.upcase} #{path}" - docstring += "\n#{INDENT * 2}# #{operation.description}" if operation.description.present? - - api_method_parameters(operation).each do |param| - docstring += "\n#{INDENT * 2}# @param #{param[:name]} [#{param[:type]}]" - docstring += ' (required)' if param[:required] - - if (description = param[:description]&.squish) - docstring += wrapped_text(description, "\n#{INDENT * 2}##{INDENT * 3} ") - end - - next if !(object_properties = param[:object_properties]) - - object_properties.each do |props| - docstring += "\n#{INDENT * 2}##{INDENT * 4}@option #{param[:name]} [#{props[:type]}] :#{props[:name]}" - docstring += wrapped_text(props[:description], "\n#{INDENT * 2}##{INDENT * 7} ") - end - end - - docstring += "\n#{INDENT * 2}# @return [#{"#{api_method_name}_response".camelize}] successful response" - docstring += "\n#{INDENT * 2}# @raise [#{"#{api_method_name}_error".camelize}] if the request fails" - - docstring - end - - def wrapped_text(text, prefix = nil) - wrapped = '' - text_words = text.split - - until text_words.none? - line = '' - line += " #{text_words.shift}" until text_words.none? || line.length >= 80 - - wrapped += "#{prefix}#{line}" - end - - wrapped - end - - def api_method_definition_name_and_parameters(api_method_name, operation) - method_args = api_method_parameters(operation).map do |param| - param[:required] ? "#{param[:name]}:" : "#{param[:name]}: nil" - end - - definition = "api_method def self.#{api_method_name}" - return definition if method_args.none? - - definition += '(' - - if method_args.length > 5 - while (line_arg = method_args.shift).present? - definition += "\n#{INDENT * 3}#{line_arg}" - definition += ',' if method_args.any? - end - definition + "\n#{INDENT * 2})" - else - definition + "#{method_args.join(', ')})" - end - end - - def api_method_definition_path(path) - # "/transaction/verify/{reference}" -> "/transaction/verify/#{reference}" - interpolated_path = path.gsub(/{([^}]+)}/, "\#{\\1}") - - interpolated_path == path ? "'#{interpolated_path}'" : "\"#{interpolated_path}\"" - end - - def api_method_definition_request_params(operation) - params = api_method_parameters(operation) - .reject { |param| param[:in] == 'path' } - .map { |param| param[:name] } - return if params.none? - - definition = "\n#{INDENT * 5}{" - - if params.length > 5 - while (line_param = params.shift).present? - definition += "\n#{INDENT * 6}#{line_param}:," - end - - definition + "\n#{INDENT * 5}}.compact," - else - "#{definition} #{params.map { |param| "#{param}:" }.join(', ')} }.compact," - end - end - - def api_method_parameters(operation) - @api_method_parameters ||= {} - @api_method_parameters[operation] ||= [ - *api_method_path_and_query_parameters(operation), - *api_method_request_body_parameters(operation), - ].sort_by { |param| param[:required] ? 0 : 1 } - end - - def api_method_path_and_query_parameters(operation) - return [] if !operation.parameters - - operation.parameters.map do |param| - { - name: param.name, - in: param.in, - required: param.in == 'path', - description: param.description, - type: schema_type(param.schema), - } - end - end - - def api_method_request_body_parameters(operation) - return [] if !operation.request_body - - schema = operation.request_body.content['application/json'].schema - if schema.type == 'array' - schema_array_properties(schema) - else - schema_object_properties(schema) - end - end - - def schema_array_properties(schema) - schema.items.properties.map do |name, p_schema| - { - name:, - in: 'body', - required: true, - description: schema_description(p_schema), - type: "Array<#{schema_type(p_schema.items)}>", - object_properties: schema_object_properties(p_schema.items), - } - end - end - - def schema_object_properties(schema) - if schema.all_of.present? - schema_properties = schema.all_of.map(&:properties).reduce(&:merge) - schema_required = schema.all_of.flat_map(&:required).compact.presence - else - schema_properties = schema.properties - schema_required = schema.required - end - - schema_properties&.map do |name, p_schema| - { - name:, - in: 'body', - required: schema_required ? schema_required.include?(name) : true, - description: schema_description(p_schema), - type: schema_type(p_schema), - object_properties: schema_object_properties(p_schema), - } - end - end - - def schema_description(schema) - schema.description || schema.items&.description - end - - def schema_type(schema) - case schema.type - when 'array' - "Array<#{schema_type(schema.items)}>" - when 'string' - if schema.format == 'date-time' - 'Time' - elsif schema.enum - schema.enum.map { |v| "\"#{v}\"" }.join(', ') - else - 'String' - end - when 'object' - 'Hash' - when 'integer', 'boolean', 'number' - schema.type.capitalize - else - raise "Unhandled schema type: #{schema.type}" - end - end - - def api_method_name(api_module_name, operation) - operation - .operation_id - .delete_prefix( - case api_module_name.to_sym - when :DedicatedVirtualAccount - 'dedicatedAccount' - when :TransferRecipient - 'transferrecipient' - else - api_module_name.camelize(:lower) - end, - ) - .delete_prefix('_') - .underscore - end - - def existing_api_modules - @existing_api_modules ||= PaystackGateway.api_modules.to_set { |api_module| api_module.name.split('::').last } - end - - def tags_by_name - @tags_by_name ||= @document.raw_schema['tags'].index_by { _1['name'].remove(' ') } - end - - def document - @document ||= OpenAPIParser.parse(YAML.load_file(OPENAPI_SPEC), strict_reference_validation: true) - end -end - -OpenApiGenerator.new.generate From fc1752d7e7e442fc08318eca463c22e63acc94cb Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 14 Mar 2025 16:20:59 +0100 Subject: [PATCH 10/26] Simplify sdk generation script --- .rubocop.yml | 6 ++- bin/generate_sdk_from_openapi_spec.rb | 59 ++++++++++++++++++--------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 785a5fd..adb2d67 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -9,13 +9,17 @@ AllCops: Layout/MultilineMethodCallIndentation: EnforcedStyle: indented_relative_to_receiver Metrics/AbcSize: - Max: 25 + Max: 28 +Metrics/CyclomaticComplexity: + Max: 10 Metrics/MethodLength: Max: 25 Metrics/ModuleLength: Max: 250 Metrics/ParameterLists: Enabled: false +Metrics/PerceivedComplexity: + Max: 10 Minitest/MultipleAssertions: Enabled: false Naming/FileName: diff --git a/bin/generate_sdk_from_openapi_spec.rb b/bin/generate_sdk_from_openapi_spec.rb index f0e9160..d630fe0 100755 --- a/bin/generate_sdk_from_openapi_spec.rb +++ b/bin/generate_sdk_from_openapi_spec.rb @@ -11,6 +11,7 @@ OPENAPI_SPEC = File.join(SDK_ROOT, 'openapi', 'base', 'paystack.yaml') INDENT = ' ' WRAP_LINE_LENGTH = 80 +TOP_LEVEL_RESPONSE_KEYS = %w[status message data meta].freeze def api_methods_by_api_module_name @api_methods_by_api_module_name ||= @@ -92,40 +93,49 @@ def api_method_composition(api_module_name, api_method_info) end def api_method_response_class_content(api_method_name, operation) + definition = "#{INDENT * 2}# Successful response from calling ##{api_method_name}.\n" \ + "#{INDENT * 2}class #{"#{api_method_name}_response".camelize} < PaystackGateway::Response" + + if (delegate_content = api_method_response_class_delegate_content(operation)) + "#{definition}#{delegate_content}\n#{INDENT * 2}end" + else + "#{definition}; end" + end +end + +def api_method_response_class_delegate_content(operation) responses = operation.responses.response + success_response = responses[responses.keys.find { _1.match?(/\A2..\z/) }] + return if !success_response - definition = "#{INDENT * 2}# Successful response from calling ##{api_method_name}.\n" - definition += "#{INDENT * 2}class #{"#{api_method_name}_response".camelize} < PaystackGateway::Response" + required_data_keys = success_response.content['application/json'].schema.properties['data']&.required || [] - if (success_response = responses[responses.keys.find { _1.match?(/\A2..\z/) }]) && - (required_data_keys = success_response.content['application/json'].schema.properties['data']&.required) + required_data_keys -= TOP_LEVEL_RESPONSE_KEYS + return if required_data_keys.none? - required_data_keys -= %w[status message data meta] - if required_data_keys.length > 3 - definition += "\n#{INDENT * 3}delegate :#{required_data_keys.shift}," + if required_data_keys.length > 3 + definition = "\n#{INDENT * 3}delegate :#{required_data_keys.shift}," - while (line_key = required_data_keys.shift).present? - definition += "\n#{INDENT * 3}#{' ' * 'delegate'.length} :#{line_key}," - end - definition += ' to: :data' - else - definition += "\n#{INDENT * 3}delegate #{required_data_keys.map { |key| ":#{key}" }.join(', ')}, to: :data" + while (line_key = required_data_keys.shift).present? + definition += "\n#{INDENT * 3}#{' ' * 'delegate'.length} :#{line_key}," end - "#{definition}\n#{INDENT * 2}end" + "#{definition} to: :data" else - "#{definition}; end" + "\n#{INDENT * 3}delegate #{required_data_keys.map { |key| ":#{key}" }.join(', ')}, to: :data" end end def api_method_error_class_content(api_method_name) - definition = "#{INDENT * 2}# Error response from ##{api_method_name}.\n" - definition + "#{INDENT * 2}class #{"#{api_method_name}_error".camelize} < ApiError; end" + "#{INDENT * 2}# Error response from ##{api_method_name}.\n" \ + "#{INDENT * 2}class #{"#{api_method_name}_error".camelize} < ApiError; end" end def api_method_content(api_method_name, operation, http_method, path) <<-RUBY - #{api_method_definition_docstring(api_method_name, operation, http_method, path)} + #{api_method_definition_header_docstring(api_method_name, operation, http_method, path)} + #{api_method_definition_params_docstring(operation)} + #{api_method_definition_response_docstring(api_method_name)} #{api_method_definition_name_and_parameters(api_method_name, operation)} use_connection do |connection| connection.#{http_method}( @@ -136,11 +146,15 @@ def api_method_content(api_method_name, operation, http_method, path) RUBY end -def api_method_definition_docstring(api_method_name, operation, http_method, path) +def api_method_definition_header_docstring(api_method_name, operation, http_method, path) docstring = "# https://paystack.com/docs/api/#{operation.tags.first.parameterize}/##{api_method_name}" docstring += "\n#{INDENT * 2}# #{operation.summary}: #{http_method.upcase} #{path}" docstring += "\n#{INDENT * 2}# #{operation.description}" if operation.description.present? - docstring += "\n#{INDENT * 2}#" + docstring +end + +def api_method_definition_params_docstring(operation) + docstring = '#' api_method_parameters(operation).each do |param| docstring += "\n#{INDENT * 2}# @param #{param[:name]} [#{param[:type]}]" @@ -158,6 +172,11 @@ def api_method_definition_docstring(api_method_name, operation, http_method, pat end end + docstring +end + +def api_method_definition_response_docstring(api_method_name) + docstring = '#' docstring += "\n#{INDENT * 2}# @return [#{"#{api_method_name}_response".camelize}] successful response" docstring + "\n#{INDENT * 2}# @raise [#{"#{api_method_name}_error".camelize}] if the request fails" end From 577b3efe6aebd0131f8a157ce1f336082fff615a Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 14 Mar 2025 16:34:35 +0100 Subject: [PATCH 11/26] Ensure method params use snake_case --- bin/generate_sdk_from_openapi_spec.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/generate_sdk_from_openapi_spec.rb b/bin/generate_sdk_from_openapi_spec.rb index d630fe0..5a3e63b 100755 --- a/bin/generate_sdk_from_openapi_spec.rb +++ b/bin/generate_sdk_from_openapi_spec.rb @@ -183,7 +183,8 @@ def api_method_definition_response_docstring(api_method_name) def api_method_definition_name_and_parameters(api_method_name, operation) method_args = api_method_parameters(operation).map do |param| - param[:required] ? "#{param[:name]}:" : "#{param[:name]}: nil" + name = param[:name].underscore + param[:required] ? "#{name}:" : "#{name}: nil" end definition = "api_method def self.#{api_method_name}" @@ -219,7 +220,8 @@ def api_method_definition_request_params(operation) if params.length > 5 while (line_param = params.shift).present? - definition += "\n#{INDENT * 6}#{line_param}:," + definition += "\n#{INDENT * 6}#{line_param}:" \ + "#{line_param == line_param.underscore ? nil : " #{line_param.underscore}"}," end definition + "\n#{INDENT * 5}}.compact," From c3ccfd82957576038124e293ee0a9e7d925e0f84 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 14 Mar 2025 16:36:48 +0100 Subject: [PATCH 12/26] Generate api modules and methods from OpenAPI Spec --- lib/paystack_gateway.rb | 29 +- lib/paystack_gateway/apple_pay.rb | 84 +++ lib/paystack_gateway/balance.rb | 61 ++ lib/paystack_gateway/bank.rb | 134 ++++ lib/paystack_gateway/bulk_charge.rb | 172 +++++ lib/paystack_gateway/charge.rb | 373 +++++++++++ lib/paystack_gateway/customer.rb | 322 ++++++++++ .../dedicated_virtual_account.rb | 343 ++++++++++ lib/paystack_gateway/dispute.rb | 371 +++++++++++ lib/paystack_gateway/integration.rb | 54 ++ lib/paystack_gateway/miscellaneous.rb | 82 ++- lib/paystack_gateway/order.rb | 248 +++++++ lib/paystack_gateway/page.rb | 304 +++++++++ lib/paystack_gateway/payment_request.rb | 430 +++++++++++++ lib/paystack_gateway/plan.rb | 238 +++++++ lib/paystack_gateway/product.rb | 288 +++++++++ lib/paystack_gateway/refund.rb | 136 ++++ lib/paystack_gateway/settlement.rb | 55 ++ lib/paystack_gateway/split.rb | 271 ++++++++ lib/paystack_gateway/storefront.rb | 301 +++++++++ lib/paystack_gateway/subaccount.rb | 256 ++++++++ lib/paystack_gateway/subscription.rb | 241 +++++++ lib/paystack_gateway/terminal.rb | 222 +++++++ lib/paystack_gateway/transaction.rb | 604 ++++++++++++++++++ lib/paystack_gateway/transfer.rb | 358 +++++++++++ lib/paystack_gateway/transfer_recipient.rb | 222 +++++++ 26 files changed, 6196 insertions(+), 3 deletions(-) create mode 100644 lib/paystack_gateway/apple_pay.rb create mode 100644 lib/paystack_gateway/balance.rb create mode 100644 lib/paystack_gateway/bank.rb create mode 100644 lib/paystack_gateway/bulk_charge.rb create mode 100644 lib/paystack_gateway/charge.rb create mode 100644 lib/paystack_gateway/customer.rb create mode 100644 lib/paystack_gateway/dedicated_virtual_account.rb create mode 100644 lib/paystack_gateway/dispute.rb create mode 100644 lib/paystack_gateway/integration.rb create mode 100644 lib/paystack_gateway/order.rb create mode 100644 lib/paystack_gateway/page.rb create mode 100644 lib/paystack_gateway/payment_request.rb create mode 100644 lib/paystack_gateway/plan.rb create mode 100644 lib/paystack_gateway/product.rb create mode 100644 lib/paystack_gateway/refund.rb create mode 100644 lib/paystack_gateway/settlement.rb create mode 100644 lib/paystack_gateway/split.rb create mode 100644 lib/paystack_gateway/storefront.rb create mode 100644 lib/paystack_gateway/subaccount.rb create mode 100644 lib/paystack_gateway/subscription.rb create mode 100644 lib/paystack_gateway/terminal.rb create mode 100644 lib/paystack_gateway/transaction.rb create mode 100644 lib/paystack_gateway/transfer.rb create mode 100644 lib/paystack_gateway/transfer_recipient.rb diff --git a/lib/paystack_gateway.rb b/lib/paystack_gateway.rb index 59dad0c..1610646 100644 --- a/lib/paystack_gateway.rb +++ b/lib/paystack_gateway.rb @@ -8,9 +8,9 @@ require 'paystack_gateway/response' require 'paystack_gateway/transaction_response' +# Old Implementations, will be removed in a future version. require 'paystack_gateway/customers' require 'paystack_gateway/dedicated_virtual_accounts' -require 'paystack_gateway/miscellaneous' require 'paystack_gateway/plans' require 'paystack_gateway/refunds' require 'paystack_gateway/subaccounts' @@ -20,6 +20,33 @@ require 'paystack_gateway/verification' require 'paystack_gateway/webhooks' +# API Modules +require 'paystack_gateway/apple_pay' +require 'paystack_gateway/balance' +require 'paystack_gateway/bank' +require 'paystack_gateway/bulk_charge' +require 'paystack_gateway/charge' +require 'paystack_gateway/customer' +require 'paystack_gateway/dedicated_virtual_account' +require 'paystack_gateway/dispute' +require 'paystack_gateway/integration' +require 'paystack_gateway/miscellaneous' +require 'paystack_gateway/order' +require 'paystack_gateway/page' +require 'paystack_gateway/payment_request' +require 'paystack_gateway/plan' +require 'paystack_gateway/product' +require 'paystack_gateway/refund' +require 'paystack_gateway/settlement' +require 'paystack_gateway/split' +require 'paystack_gateway/storefront' +require 'paystack_gateway/subaccount' +require 'paystack_gateway/subscription' +require 'paystack_gateway/terminal' +require 'paystack_gateway/transaction' +require 'paystack_gateway/transfer' +require 'paystack_gateway/transfer_recipient' + # = PaystackGateway module PaystackGateway class << self diff --git a/lib/paystack_gateway/apple_pay.rb b/lib/paystack_gateway/apple_pay.rb new file mode 100644 index 0000000..19688d9 --- /dev/null +++ b/lib/paystack_gateway/apple_pay.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/applepay + # + # Apple Pay + # A collection of endpoints for managing application's top-level domain or subdomain accepting payment via Apple Pay + module ApplePay + include PaystackGateway::RequestModule + + # Successful response from calling #list_domain. + class ListDomainResponse < PaystackGateway::Response; end + + # Error response from #list_domain. + class ListDomainError < ApiError; end + + # https://paystack.com/docs/api/apple-pay/#list_domain + # List Domains: GET /apple-pay/domain + # Lists all registered domains on your integration + # + # @param use_cursor [Boolean] + # @param next [String] + # @param previous [String] + # + # @return [ListDomainResponse] successful response + # @raise [ListDomainError] if the request fails + api_method def self.list_domain(use_cursor: nil, next: nil, previous: nil) + use_connection do |connection| + connection.get( + '/apple-pay/domain', + { use_cursor:, next:, previous: }.compact, + ) + end + end + + # Successful response from calling #register_domain. + class RegisterDomainResponse < PaystackGateway::Response; end + + # Error response from #register_domain. + class RegisterDomainError < ApiError; end + + # https://paystack.com/docs/api/apple-pay/#register_domain + # Register Domain: POST /apple-pay/domain + # Register a top-level domain or subdomain for your Apple Pay integration. + # + # @param domainName [String] (required) + # The domain or subdomain for your application + # + # @return [RegisterDomainResponse] successful response + # @raise [RegisterDomainError] if the request fails + api_method def self.register_domain(domain_name:) + use_connection do |connection| + connection.post( + '/apple-pay/domain', + { domainName: }.compact, + ) + end + end + + # Successful response from calling #unregister_domain. + class UnregisterDomainResponse < PaystackGateway::Response; end + + # Error response from #unregister_domain. + class UnregisterDomainError < ApiError; end + + # https://paystack.com/docs/api/apple-pay/#unregister_domain + # Unregister Domain: DELETE /apple-pay/domain + # Unregister a top-level domain or subdomain previously used for your Apple Pay integration. + # + # @param domainName [String] (required) + # The domain or subdomain for your application + # + # @return [UnregisterDomainResponse] successful response + # @raise [UnregisterDomainError] if the request fails + api_method def self.unregister_domain(domain_name:) + use_connection do |connection| + connection.delete( + '/apple-pay/domain', + { domainName: }.compact, + ) + end + end + end +end diff --git a/lib/paystack_gateway/balance.rb b/lib/paystack_gateway/balance.rb new file mode 100644 index 0000000..07f2e85 --- /dev/null +++ b/lib/paystack_gateway/balance.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/balance + # + # Balance + # A collection of endpoints gaining insights into the amount on an integration + module Balance + include PaystackGateway::RequestModule + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response; end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/balance/#fetch + # Fetch Balance: GET /balance + # You can only transfer from what you have + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + '/balance', + ) + end + end + + # Successful response from calling #ledger. + class LedgerResponse < PaystackGateway::Response; end + + # Error response from #ledger. + class LedgerError < ApiError; end + + # https://paystack.com/docs/api/balance/#ledger + # Balance Ledger: GET /balance/ledger + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [LedgerResponse] successful response + # @raise [LedgerError] if the request fails + api_method def self.ledger(per_page: nil, page: nil, from: nil, to: nil) + use_connection do |connection| + connection.get( + '/balance/ledger', + { perPage:, page:, from:, to: }.compact, + ) + end + end + end +end diff --git a/lib/paystack_gateway/bank.rb b/lib/paystack_gateway/bank.rb new file mode 100644 index 0000000..27816c6 --- /dev/null +++ b/lib/paystack_gateway/bank.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/bank + # + # Banks + # A collection of endpoints for managing bank details + module Bank + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/bank/#list + # List Banks: GET /bank + # + # @param country [String] + # @param pay_with_bank_transfer [Boolean] + # @param use_cursor [Boolean] + # @param perPage [Integer] + # @param next [String] + # @param previous [String] + # @param gateway [String] + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list( + country: nil, + pay_with_bank_transfer: nil, + use_cursor: nil, + per_page: nil, + next: nil, + previous: nil, + gateway: nil + ) + use_connection do |connection| + connection.get( + '/bank', + { + country:, + pay_with_bank_transfer:, + use_cursor:, + perPage: per_page, + next:, + previous:, + gateway:, + }.compact, + ) + end + end + + # Successful response from calling #resolve_account_number. + class ResolveAccountNumberResponse < PaystackGateway::Response + delegate :account_number, :account_name, :bank_id, to: :data + end + + # Error response from #resolve_account_number. + class ResolveAccountNumberError < ApiError; end + + # https://paystack.com/docs/api/bank/#resolve_account_number + # Resolve Account Number: GET /bank/resolve + # + # @param account_number [Integer] + # @param bank_code [Integer] + # + # @return [ResolveAccountNumberResponse] successful response + # @raise [ResolveAccountNumberError] if the request fails + api_method def self.resolve_account_number(account_number: nil, bank_code: nil) + use_connection do |connection| + connection.get( + '/bank/resolve', + { account_number:, bank_code: }.compact, + ) + end + end + + # Successful response from calling #validate_account_number. + class ValidateAccountNumberResponse < PaystackGateway::Response + delegate :verified, :verificationMessage, to: :data + end + + # Error response from #validate_account_number. + class ValidateAccountNumberError < ApiError; end + + # https://paystack.com/docs/api/bank/#validate_account_number + # Validate Bank Account: POST /bank/validate + # + # @param account_name [String] (required) + # Customer's first and last name registered with their bank + # @param account_number [String] (required) + # Customer's account number + # @param account_type ["personal", "business"] (required) + # The type of the customer's account number + # @param bank_code [String] (required) + # The bank code of the customer’s bank. You can fetch the bank codes by using our + # List Banks endpoint + # @param country_code [String] (required) + # The two digit ISO code of the customer’s bank + # @param document_type ["identityNumber", "passportNumber", "businessRegistrationNumber"] (required) + # Customer’s mode of identity + # @param document_number [String] + # Customer’s mode of identity number + # + # @return [ValidateAccountNumberResponse] successful response + # @raise [ValidateAccountNumberError] if the request fails + api_method def self.validate_account_number( + account_name:, + account_number:, + account_type:, + bank_code:, + country_code:, + document_type:, + document_number: nil + ) + use_connection do |connection| + connection.post( + '/bank/validate', + { + account_name:, + account_number:, + account_type:, + bank_code:, + country_code:, + document_type:, + document_number:, + }.compact, + ) + end + end + end +end diff --git a/lib/paystack_gateway/bulk_charge.rb b/lib/paystack_gateway/bulk_charge.rb new file mode 100644 index 0000000..134de80 --- /dev/null +++ b/lib/paystack_gateway/bulk_charge.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/bulkcharge + # + # Bulk Charges + # A collection of endpoints for creating and managing multiple recurring payments + module BulkCharge + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/bulk-charge/#list + # List Bulk Charge Batches: GET /bulkcharge + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list(per_page: nil, page: nil, from: nil, to: nil) + use_connection do |connection| + connection.get( + '/bulkcharge', + { perPage:, page:, from:, to: }.compact, + ) + end + end + + # Successful response from calling #initiate. + class InitiateResponse < PaystackGateway::Response + delegate :batch_code, + :reference, + :id, + :integration, + :domain, + :total_charges, + :pending_charges, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #initiate. + class InitiateError < ApiError; end + + # https://paystack.com/docs/api/bulk-charge/#initiate + # Initiate Bulk Charge: POST /bulkcharge + # + # @param charges [Array] (required) + # @option charges [String] :authorization + # Customer's card authorization code + # @option charges [String] :amount + # Amount to charge on the authorization + # + # @return [InitiateResponse] successful response + # @raise [InitiateError] if the request fails + api_method def self.initiate(charges:) + use_connection do |connection| + connection.post( + '/bulkcharge', + { charges: }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :batch_code, + :reference, + :id, + :integration, + :domain, + :total_charges, + :pending_charges, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/bulk-charge/#fetch + # Fetch Bulk Charge Batch: GET /bulkcharge/{code} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/bulkcharge/#{code}", + ) + end + end + + # Successful response from calling #charges. + class ChargesResponse < PaystackGateway::Response; end + + # Error response from #charges. + class ChargesError < ApiError; end + + # https://paystack.com/docs/api/bulk-charge/#charges + # Fetch Charges in a Batch: GET /bulkcharge/{code}/charges + # + # @param code [String] (required) + # Batch code + # + # @return [ChargesResponse] successful response + # @raise [ChargesError] if the request fails + api_method def self.charges(code:) + use_connection do |connection| + connection.get( + "/bulkcharge/#{code}/charges", + ) + end + end + + # Successful response from calling #pause. + class PauseResponse < PaystackGateway::Response; end + + # Error response from #pause. + class PauseError < ApiError; end + + # https://paystack.com/docs/api/bulk-charge/#pause + # Pause Bulk Charge Batch: GET /bulkcharge/pause/{code} + # + # @param code [String] (required) + # Batch code + # + # @return [PauseResponse] successful response + # @raise [PauseError] if the request fails + api_method def self.pause(code:) + use_connection do |connection| + connection.get( + "/bulkcharge/pause/#{code}", + ) + end + end + + # Successful response from calling #resume. + class ResumeResponse < PaystackGateway::Response; end + + # Error response from #resume. + class ResumeError < ApiError; end + + # https://paystack.com/docs/api/bulk-charge/#resume + # Resume Bulk Charge Batch: GET /bulkcharge/resume/{code} + # + # @param code [String] (required) + # Batch code + # + # @return [ResumeResponse] successful response + # @raise [ResumeError] if the request fails + api_method def self.resume(code:) + use_connection do |connection| + connection.get( + "/bulkcharge/resume/#{code}", + ) + end + end + end +end diff --git a/lib/paystack_gateway/charge.rb b/lib/paystack_gateway/charge.rb new file mode 100644 index 0000000..2d0e012 --- /dev/null +++ b/lib/paystack_gateway/charge.rb @@ -0,0 +1,373 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/charge + # + # Charges + # A collection of endpoints for configuring and managing the payment channels when initiating a payment + module Charge + include PaystackGateway::RequestModule + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :id, + :domain, + :reference, + :receipt_number, + :amount, + :gateway_response, + :paid_at, + :created_at, + :channel, + :currency, + :ip_address, + :metadata, + :log, + :fees, + :fees_split, + :authorization, + :customer, + :plan, + :split, + :order_id, + :paidAt, + :createdAt, + :requested_amount, + :pos_transaction_data, + :source, + :fees_breakdown, + :connect, + :transaction_date, + :plan_object, + :subaccount, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/charge/#create + # Create Charge: POST /charge + # + # @param email [String] (required) + # Customer's email address + # @param amount [String] (required) + # Amount should be in kobo if currency is NGN, pesewas, if currency is GHS, and cents, + # if currency is ZAR + # @param authorization_code [String] + # An authorization code to charge. + # @param pin [String] + # 4-digit PIN (send with a non-reusable authorization code) + # @param reference [String] + # Unique transaction reference. Only -, .`, = and alphanumeric characters allowed. + # @param birthday [Time] + # The customer's birthday in the format YYYY-MM-DD e.g 2017-05-16 + # @param device_id [String] + # This is the unique identifier of the device a user uses in making payment. Only + # -, .`, = and alphanumeric characters are allowed. + # @param metadata [String] + # Stringified JSON object of custom data + # @param bank [Hash] + # @option bank [String] :code + # Customer's bank code + # @option bank [String] :account_number + # Customer's account number + # @param mobile_money [Hash] + # @option mobile_money [String] :phone + # Customer's phone number + # @option mobile_money [String] :provider + # The telco provider of customer's phone number. This can be fetched from the List + # Bank endpoint + # @param ussd [Hash] + # @option ussd ["737", "919", "822", "966"] :type + # The three-digit USSD code. + # @param eft [Hash] + # @option eft [String] :provider + # The EFT provider + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create( + email:, + amount:, + authorization_code: nil, + pin: nil, + reference: nil, + birthday: nil, + device_id: nil, + metadata: nil, + bank: nil, + mobile_money: nil, + ussd: nil, + eft: nil + ) + use_connection do |connection| + connection.post( + '/charge', + { + email:, + amount:, + authorization_code:, + pin:, + reference:, + birthday:, + device_id:, + metadata:, + bank:, + mobile_money:, + ussd:, + eft:, + }.compact, + ) + end + end + + # Successful response from calling #submit_pin. + class SubmitPinResponse < PaystackGateway::Response + delegate :id, + :domain, + :reference, + :receipt_number, + :amount, + :gateway_response, + :paid_at, + :created_at, + :channel, + :currency, + :ip_address, + :metadata, + :log, + :fees, + :fees_split, + :authorization, + :customer, + :plan, + :split, + :order_id, + :paidAt, + :createdAt, + :requested_amount, + :pos_transaction_data, + :source, + :fees_breakdown, + :connect, + :transaction_date, + :plan_object, + :subaccount, to: :data + end + + # Error response from #submit_pin. + class SubmitPinError < ApiError; end + + # https://paystack.com/docs/api/charge/#submit_pin + # Submit PIN: POST /charge/submit_pin + # + # @param pin [String] (required) + # Customer's PIN + # @param reference [String] (required) + # Transaction reference that requires the PIN + # + # @return [SubmitPinResponse] successful response + # @raise [SubmitPinError] if the request fails + api_method def self.submit_pin(pin:, reference:) + use_connection do |connection| + connection.post( + '/charge/submit_pin', + { pin:, reference: }.compact, + ) + end + end + + # Successful response from calling #submit_otp. + class SubmitOtpResponse < PaystackGateway::Response + delegate :id, + :domain, + :reference, + :receipt_number, + :amount, + :gateway_response, + :paid_at, + :created_at, + :channel, + :currency, + :ip_address, + :metadata, + :log, + :fees, + :fees_split, + :authorization, + :customer, + :plan, + :split, + :order_id, + :paidAt, + :createdAt, + :requested_amount, + :pos_transaction_data, + :source, + :fees_breakdown, + :connect, + :transaction_date, + :plan_object, + :subaccount, to: :data + end + + # Error response from #submit_otp. + class SubmitOtpError < ApiError; end + + # https://paystack.com/docs/api/charge/#submit_otp + # Submit OTP: POST /charge/submit_otp + # + # @param otp [String] (required) + # Customer's OTP + # @param reference [String] (required) + # The reference of the ongoing transaction + # + # @return [SubmitOtpResponse] successful response + # @raise [SubmitOtpError] if the request fails + api_method def self.submit_otp(otp:, reference:) + use_connection do |connection| + connection.post( + '/charge/submit_otp', + { otp:, reference: }.compact, + ) + end + end + + # Successful response from calling #submit_phone. + class SubmitPhoneResponse < PaystackGateway::Response + delegate :reference, :display_text, to: :data + end + + # Error response from #submit_phone. + class SubmitPhoneError < ApiError; end + + # https://paystack.com/docs/api/charge/#submit_phone + # Submit Phone: POST /charge/submit_phone + # + # @param phone [String] (required) + # Customer's mobile number + # @param reference [String] (required) + # The reference of the ongoing transaction + # + # @return [SubmitPhoneResponse] successful response + # @raise [SubmitPhoneError] if the request fails + api_method def self.submit_phone(phone:, reference:) + use_connection do |connection| + connection.post( + '/charge/submit_phone', + { phone:, reference: }.compact, + ) + end + end + + # Successful response from calling #submit_birthday. + class SubmitBirthdayResponse < PaystackGateway::Response + delegate :display_text, to: :data + end + + # Error response from #submit_birthday. + class SubmitBirthdayError < ApiError; end + + # https://paystack.com/docs/api/charge/#submit_birthday + # Submit Birthday: POST /charge/submit_birthday + # + # @param birthday [String] (required) + # Customer's birthday in the format YYYY-MM-DD e.g 2016-09-21 + # @param reference [String] (required) + # The reference of the ongoing transaction + # + # @return [SubmitBirthdayResponse] successful response + # @raise [SubmitBirthdayError] if the request fails + api_method def self.submit_birthday(birthday:, reference:) + use_connection do |connection| + connection.post( + '/charge/submit_birthday', + { birthday:, reference: }.compact, + ) + end + end + + # Successful response from calling #submit_address. + class SubmitAddressResponse < PaystackGateway::Response; end + + # Error response from #submit_address. + class SubmitAddressError < ApiError; end + + # https://paystack.com/docs/api/charge/#submit_address + # Submit Address: POST /charge/submit_address + # + # @param address [String] (required) + # Customer's address + # @param city [String] (required) + # Customer's city + # @param state [String] (required) + # Customer's state + # @param zipcode [String] (required) + # Customer's zipcode + # @param reference [String] (required) + # The reference of the ongoing transaction + # + # @return [SubmitAddressResponse] successful response + # @raise [SubmitAddressError] if the request fails + api_method def self.submit_address(address:, city:, state:, zipcode:, reference:) + use_connection do |connection| + connection.post( + '/charge/submit_address', + { address:, city:, state:, zipcode:, reference: }.compact, + ) + end + end + + # Successful response from calling #check. + class CheckResponse < PaystackGateway::Response + delegate :id, + :domain, + :reference, + :receipt_number, + :amount, + :gateway_response, + :paid_at, + :created_at, + :channel, + :currency, + :ip_address, + :metadata, + :log, + :fees, + :fees_split, + :authorization, + :customer, + :plan, + :split, + :order_id, + :paidAt, + :createdAt, + :requested_amount, + :pos_transaction_data, + :source, + :fees_breakdown, + :connect, + :transaction_date, + :plan_object, + :subaccount, to: :data + end + + # Error response from #check. + class CheckError < ApiError; end + + # https://paystack.com/docs/api/charge/#check + # Check pending charge: GET /charge/{reference} + # + # @param reference [String] (required) + # + # @return [CheckResponse] successful response + # @raise [CheckError] if the request fails + api_method def self.check(reference:) + use_connection do |connection| + connection.get( + "/charge/#{reference}", + ) + end + end + end +end diff --git a/lib/paystack_gateway/customer.rb b/lib/paystack_gateway/customer.rb new file mode 100644 index 0000000..630d660 --- /dev/null +++ b/lib/paystack_gateway/customer.rb @@ -0,0 +1,322 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/customer + # + # Transaction Splits + # A collection of endpoints for creating and managing customers on an integration + module Customer + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/customer/#list + # List Customers: GET /customer + # List customers on your integration + # + # @param use_cursor [Boolean] + # @param next [String] + # @param previous [String] + # @param from [String] + # @param to [String] + # @param perPage [String] + # @param page [String] + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list( + use_cursor: nil, + next: nil, + previous: nil, + from: nil, + to: nil, + per_page: nil, + page: nil + ) + use_connection do |connection| + connection.get( + '/customer', + { + use_cursor:, + next:, + previous:, + from:, + to:, + perPage: per_page, + page:, + }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :transactions, + :subscriptions, + :authorizations, + :email, + :first_name, + :last_name, + :phone, + :integration, + :domain, + :metadata, + :customer_code, + :risk_action, + :id, + :createdAt, + :updatedAt, + :identified, + :identifications, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/customer/#create + # Create Customer: POST /customer + # + # @param email [String] (required) + # Customer's email address + # @param first_name [String] + # Customer's first name + # @param last_name [String] + # Customer's last name + # @param phone [String] + # Customer's phone number + # @param metadata [String] + # Stringified JSON object of custom data + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create(email:, first_name: nil, last_name: nil, phone: nil, metadata: nil) + use_connection do |connection| + connection.post( + '/customer', + { email:, first_name:, last_name:, phone:, metadata: }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :transactions, + :subscriptions, + :authorizations, + :first_name, + :last_name, + :email, + :phone, + :metadata, + :domain, + :customer_code, + :risk_action, + :id, + :integration, + :createdAt, + :updatedAt, + :created_at, + :updated_at, + :total_transactions, + :total_transaction_value, + :dedicated_account, + :dedicated_accounts, + :identified, + :identifications, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/customer/#fetch + # Fetch Customer: GET /customer/{code} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/customer/#{code}", + ) + end + end + + # Successful response from calling #update. + class UpdateResponse < PaystackGateway::Response + delegate :first_name, + :last_name, + :email, + :phone, + :metadata, + :domain, + :customer_code, + :risk_action, + :id, + :integration, + :createdAt, + :updatedAt, + :identified, + :identifications, to: :data + end + + # Error response from #update. + class UpdateError < ApiError; end + + # https://paystack.com/docs/api/customer/#update + # Update Customer: PUT /customer/{code} + # + # @param first_name [String] (required) + # Customer's first name + # @param last_name [String] (required) + # Customer's last name + # @param phone [String] (required) + # Customer's phone number + # @param metadata [String] (required) + # Stringified JSON object of custom data + # + # @return [UpdateResponse] successful response + # @raise [UpdateError] if the request fails + api_method def self.update(first_name:, last_name:, phone:, metadata:) + use_connection do |connection| + connection.put( + "/customer/#{code}", + { first_name:, last_name:, phone:, metadata: }.compact, + ) + end + end + + # Successful response from calling #risk_action. + class RiskActionResponse < PaystackGateway::Response + delegate :transactions, + :subscriptions, + :authorizations, + :first_name, + :last_name, + :email, + :phone, + :metadata, + :domain, + :customer_code, + :risk_action, + :id, + :integration, + :createdAt, + :updatedAt, + :identified, + :identifications, to: :data + end + + # Error response from #risk_action. + class RiskActionError < ApiError; end + + # https://paystack.com/docs/api/customer/#risk_action + # White/blacklist Customer: POST /customer/set_risk_action + # Set customer's risk action by whitelisting or blacklisting the customer + # + # @param customer [String] (required) + # Customer's code, or email address + # @param risk_action [String] + # One of the possible risk actions [ default, allow, deny ]. allow to whitelist. deny + # to blacklist. Customers start with a default risk action. + # + # @return [RiskActionResponse] successful response + # @raise [RiskActionError] if the request fails + api_method def self.risk_action(customer:, risk_action: nil) + use_connection do |connection| + connection.post( + '/customer/set_risk_action', + { customer:, risk_action: }.compact, + ) + end + end + + # Successful response from calling #deactivate_authorization. + class DeactivateAuthorizationResponse < PaystackGateway::Response; end + + # Error response from #deactivate_authorization. + class DeactivateAuthorizationError < ApiError; end + + # https://paystack.com/docs/api/customer/#deactivate_authorization + # Deactivate Authorization: POST /customer/deactivate_authorization + # Deactivate a customer's card + # + # @param authorization_code [String] (required) + # Authorization code to be deactivated + # + # @return [DeactivateAuthorizationResponse] successful response + # @raise [DeactivateAuthorizationError] if the request fails + api_method def self.deactivate_authorization(authorization_code:) + use_connection do |connection| + connection.post( + '/customer/deactivate_authorization', + { authorization_code: }.compact, + ) + end + end + + # Successful response from calling #validate. + class ValidateResponse < PaystackGateway::Response; end + + # Error response from #validate. + class ValidateError < ApiError; end + + # https://paystack.com/docs/api/customer/#validate + # Validate Customer: POST /customer/{code}/identification + # Validate a customer's identity + # + # @param first_name [String] (required) + # Customer's first name + # @param last_name [String] (required) + # Customer's last name + # @param type [String] (required) + # Predefined types of identification. + # @param country [String] (required) + # Two-letter country code of identification issuer + # @param bvn [String] (required) + # Customer's Bank Verification Number + # @param bank_code [String] (required) + # You can get the list of bank codes by calling the List Banks endpoint (https://api.paystack.co/bank). + # @param account_number [String] (required) + # Customer's bank account number. + # @param middle_name [String] + # Customer's middle name + # @param value [String] + # Customer's identification number. Required if type is bvn + # + # @return [ValidateResponse] successful response + # @raise [ValidateError] if the request fails + api_method def self.validate( + first_name:, + last_name:, + type:, + country:, + bvn:, + bank_code:, + account_number:, + middle_name: nil, + value: nil + ) + use_connection do |connection| + connection.post( + "/customer/#{code}/identification", + { + first_name:, + last_name:, + type:, + country:, + bvn:, + bank_code:, + account_number:, + middle_name:, + value:, + }.compact, + ) + end + end + end +end diff --git a/lib/paystack_gateway/dedicated_virtual_account.rb b/lib/paystack_gateway/dedicated_virtual_account.rb new file mode 100644 index 0000000..957ec72 --- /dev/null +++ b/lib/paystack_gateway/dedicated_virtual_account.rb @@ -0,0 +1,343 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/dedicatedvirtualaccount + # + # Dedicated Virtual Accounts + # A collection of endpoints for creating and managing payment accounts for customers + module DedicatedVirtualAccount + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response + delegate :customer, + :bank, + :id, + :account_name, + :account_number, + :created_at, + :updated_at, + :currency, + :split_config, + :active, + :assigned, to: :data + end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/dedicated-virtual-account/#list + # List Dedicated Accounts: GET /dedicated_account + # + # @param account_number [String] + # @param customer [String] + # @param active [Boolean] + # @param currency [String] + # @param provider_slug [String] + # @param bank_id [String] + # @param perPage [String] + # @param page [String] + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list( + account_number: nil, + customer: nil, + active: nil, + currency: nil, + provider_slug: nil, + bank_id: nil, + per_page: nil, + page: nil + ) + use_connection do |connection| + connection.get( + '/dedicated_account', + { + account_number:, + customer:, + active:, + currency:, + provider_slug:, + bank_id:, + perPage: per_page, + page:, + }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :bank, + :account_name, + :account_number, + :assigned, + :currency, + :metadata, + :active, + :id, + :created_at, + :updated_at, + :assignment, + :customer, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/dedicated-virtual-account/#create + # Create Dedicated Account: POST /dedicated_account + # + # @param customer [String] (required) + # Customer ID or code + # @param preferred_bank [String] + # The bank slug for preferred bank. To get a list of available banks, use the List + # Providers endpoint + # @param subaccount [String] + # Subaccount code of the account you want to split the transaction with + # @param split_code [String] + # Split code consisting of the lists of accounts you want to split the transaction + # with + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create(customer:, preferred_bank: nil, subaccount: nil, split_code: nil) + use_connection do |connection| + connection.post( + '/dedicated_account', + { customer:, preferred_bank:, subaccount:, split_code: }.compact, + ) + end + end + + # Successful response from calling #assign. + class AssignResponse < PaystackGateway::Response; end + + # Error response from #assign. + class AssignError < ApiError; end + + # https://paystack.com/docs/api/dedicated-virtual-account/#assign + # Assign Dedicated Account: POST /dedicated_account/assign + # + # @param email [String] (required) + # Customer's email address + # @param first_name [String] (required) + # Customer's first name + # @param last_name [String] (required) + # Customer's last name + # @param phone [String] (required) + # Customer's phone name + # @param preferred_bank [String] (required) + # The bank slug for preferred bank. To get a list of available banks, use the List + # Banks endpoint, passing `pay_with_bank_transfer=true` query parameter + # @param country [String] (required) + # Currently accepts NG only + # @param account_number [String] + # Customer's account number + # @param bvn [String] + # Customer's Bank Verification Number + # @param bank_code [String] + # Customer's bank code + # @param subaccount [String] + # Subaccount code of the account you want to split the transaction with + # @param split_code [String] + # Split code consisting of the lists of accounts you want to split the transaction + # with + # + # @return [AssignResponse] successful response + # @raise [AssignError] if the request fails + api_method def self.assign( + email:, + first_name:, + last_name:, + phone:, + preferred_bank:, + country:, + account_number: nil, + bvn: nil, + bank_code: nil, + subaccount: nil, + split_code: nil + ) + use_connection do |connection| + connection.post( + '/dedicated_account/assign', + { + email:, + first_name:, + last_name:, + phone:, + preferred_bank:, + country:, + account_number:, + bvn:, + bank_code:, + subaccount:, + split_code:, + }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :customer, + :bank, + :id, + :account_name, + :account_number, + :created_at, + :updated_at, + :currency, + :split_config, + :active, + :assigned, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/dedicated-virtual-account/#fetch + # Fetch Dedicated Account: GET /dedicated_account/{account_id} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/dedicated_account/#{account_id}", + ) + end + end + + # Successful response from calling #deactivate. + class DeactivateResponse < PaystackGateway::Response + delegate :bank, + :account_name, + :account_number, + :assigned, + :currency, + :metadata, + :active, + :id, + :created_at, + :updated_at, + :assignment, to: :data + end + + # Error response from #deactivate. + class DeactivateError < ApiError; end + + # https://paystack.com/docs/api/dedicated-virtual-account/#deactivate + # Deactivate Dedicated Account: DELETE /dedicated_account/{account_id} + # + # + # @return [DeactivateResponse] successful response + # @raise [DeactivateError] if the request fails + api_method def self.deactivate + use_connection do |connection| + connection.delete( + "/dedicated_account/#{account_id}", + ) + end + end + + # Successful response from calling #requery. + class RequeryResponse < PaystackGateway::Response; end + + # Error response from #requery. + class RequeryError < ApiError; end + + # https://paystack.com/docs/api/dedicated-virtual-account/#requery + # Requery Dedicated Account: GET /dedicated_account/requery + # + # + # @return [RequeryResponse] successful response + # @raise [RequeryError] if the request fails + api_method def self.requery + use_connection do |connection| + connection.get( + '/dedicated_account/requery', + ) + end + end + + # Successful response from calling #add_split. + class AddSplitResponse < PaystackGateway::Response; end + + # Error response from #add_split. + class AddSplitError < ApiError; end + + # https://paystack.com/docs/api/dedicated-virtual-account/#add_split + # Split Dedicated Account Transaction: POST /dedicated_account/split + # + # @param account_number [String] (required) + # Valid Dedicated virtual account + # @param subaccount [String] + # Subaccount code of the account you want to split the transaction with + # @param split_code [String] + # Split code consisting of the lists of accounts you want to split the transaction + # with + # + # @return [AddSplitResponse] successful response + # @raise [AddSplitError] if the request fails + api_method def self.add_split(account_number:, subaccount: nil, split_code: nil) + use_connection do |connection| + connection.post( + '/dedicated_account/split', + { account_number:, subaccount:, split_code: }.compact, + ) + end + end + + # Successful response from calling #remove_split. + class RemoveSplitResponse < PaystackGateway::Response; end + + # Error response from #remove_split. + class RemoveSplitError < ApiError; end + + # https://paystack.com/docs/api/dedicated-virtual-account/#remove_split + # Remove Split from Dedicated Account: DELETE /dedicated_account/split + # + # @param account_number [String] (required) + # Valid Dedicated virtual account + # @param subaccount [String] + # Subaccount code of the account you want to split the transaction with + # @param split_code [String] + # Split code consisting of the lists of accounts you want to split the transaction + # with + # + # @return [RemoveSplitResponse] successful response + # @raise [RemoveSplitError] if the request fails + api_method def self.remove_split(account_number:, subaccount: nil, split_code: nil) + use_connection do |connection| + connection.delete( + '/dedicated_account/split', + { account_number:, subaccount:, split_code: }.compact, + ) + end + end + + # Successful response from calling #available_providers. + class AvailableProvidersResponse < PaystackGateway::Response; end + + # Error response from #available_providers. + class AvailableProvidersError < ApiError; end + + # https://paystack.com/docs/api/dedicated-virtual-account/#available_providers + # Fetch Bank Providers: GET /dedicated_account/available_providers + # + # + # @return [AvailableProvidersResponse] successful response + # @raise [AvailableProvidersError] if the request fails + api_method def self.available_providers + use_connection do |connection| + connection.get( + '/dedicated_account/available_providers', + ) + end + end + end +end diff --git a/lib/paystack_gateway/dispute.rb b/lib/paystack_gateway/dispute.rb new file mode 100644 index 0000000..4e77390 --- /dev/null +++ b/lib/paystack_gateway/dispute.rb @@ -0,0 +1,371 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/dispute + # + # Disputes + # A collection of endpoints for managing transactions complaint made by customers + module Dispute + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/dispute/#list + # List Disputes: GET /dispute + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param status [String] + # Dispute Status. Acceptable values are awaiting-merchant-feedback, awaiting-bank-feedback, + # pending, resolved + # @param transaction [String] + # Transaction ID + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list( + per_page: nil, + page: nil, + status: nil, + transaction: nil, + from: nil, + to: nil + ) + use_connection do |connection| + connection.get( + '/dispute', + { + perPage: per_page, + page:, + status:, + transaction:, + from:, + to:, + }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :id, + :refund_amount, + :currency, + :resolution, + :domain, + :transaction, + :transaction_reference, + :category, + :customer, + :bin, + :last4, + :dueAt, + :resolvedAt, + :evidence, + :attachments, + :note, + :history, + :messages, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/dispute/#fetch + # Fetch Dispute: GET /dispute/{id} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/dispute/#{id}", + ) + end + end + + # Successful response from calling #update. + class UpdateResponse < PaystackGateway::Response + delegate :id, + :refund_amount, + :currency, + :resolution, + :domain, + :transaction, + :transaction_reference, + :category, + :customer, + :bin, + :last4, + :dueAt, + :resolvedAt, + :evidence, + :attachments, + :note, + :history, + :messages, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #update. + class UpdateError < ApiError; end + + # https://paystack.com/docs/api/dispute/#update + # Update Dispute: PUT /dispute/{id} + # + # @param refund_amount [String] (required) + # The amount to refund, in kobo if currency is NGN, pesewas, if currency is GHS, and + # cents, if currency is ZAR + # @param uploaded_filename [String] + # Filename of attachment returned via response from the Dispute upload URL + # + # @return [UpdateResponse] successful response + # @raise [UpdateError] if the request fails + api_method def self.update(refund_amount:, uploaded_filename: nil) + use_connection do |connection| + connection.put( + "/dispute/#{id}", + { refund_amount:, uploaded_filename: }.compact, + ) + end + end + + # Successful response from calling #upload_url. + class UploadUrlResponse < PaystackGateway::Response + delegate :signedUrl, :fileName, to: :data + end + + # Error response from #upload_url. + class UploadUrlError < ApiError; end + + # https://paystack.com/docs/api/dispute/#upload_url + # Get Upload URL: GET /dispute/{id}/upload_url + # + # @param id [String] (required) + # Dispute ID + # + # @return [UploadUrlResponse] successful response + # @raise [UploadUrlError] if the request fails + api_method def self.upload_url(id:) + use_connection do |connection| + connection.get( + "/dispute/#{id}/upload_url", + ) + end + end + + # Successful response from calling #download. + class DownloadResponse < PaystackGateway::Response + delegate :path, :expiresAt, to: :data + end + + # Error response from #download. + class DownloadError < ApiError; end + + # https://paystack.com/docs/api/dispute/#download + # Export Disputes: GET /dispute/export + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param status [String] + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [DownloadResponse] successful response + # @raise [DownloadError] if the request fails + api_method def self.download(per_page: nil, page: nil, status: nil, from: nil, to: nil) + use_connection do |connection| + connection.get( + '/dispute/export', + { perPage:, page:, status:, from:, to: }.compact, + ) + end + end + + # Successful response from calling #transaction. + class TransactionResponse < PaystackGateway::Response + delegate :history, + :messages, + :currency, + :last4, + :bin, + :transaction_reference, + :merchant_transaction_reference, + :refund_amount, + :domain, + :resolution, + :category, + :note, + :attachments, + :id, + :integration, + :transaction, + :created_by, + :evidence, + :resolvedAt, + :createdAt, + :updatedAt, + :dueAt, to: :data + end + + # Error response from #transaction. + class TransactionError < ApiError; end + + # https://paystack.com/docs/api/dispute/#transaction + # List Transaction Disputes: GET /dispute/transaction/{id} + # + # @param id [String] (required) + # Transaction ID + # + # @return [TransactionResponse] successful response + # @raise [TransactionError] if the request fails + api_method def self.transaction(id:) + use_connection do |connection| + connection.get( + "/dispute/transaction/#{id}", + ) + end + end + + # Successful response from calling #resolve. + class ResolveResponse < PaystackGateway::Response + delegate :currency, + :last4, + :bin, + :transaction_reference, + :merchant_transaction_reference, + :refund_amount, + :domain, + :resolution, + :category, + :note, + :attachments, + :id, + :integration, + :transaction, + :created_by, + :evidence, + :resolvedAt, + :createdAt, + :updatedAt, + :dueAt, to: :data + end + + # Error response from #resolve. + class ResolveError < ApiError; end + + # https://paystack.com/docs/api/dispute/#resolve + # Resolve a Dispute: PUT /dispute/{id}/resolve + # + # @param id [String] (required) + # Dispute ID + # @param resolution [String] (required) + # Dispute resolution. Accepted values, merchant-accepted, declined + # @param message [String] (required) + # Reason for resolving + # @param refund_amount [String] (required) + # The amount to refund, in kobo if currency is NGN, pesewas, if currency is GHS, and + # cents, if currency is ZAR + # @param uploaded_filename [String] (required) + # Filename of attachment returned via response from the Dispute upload URL + # @param evidence [Integer] + # Evidence Id for fraud claims + # + # @return [ResolveResponse] successful response + # @raise [ResolveError] if the request fails + api_method def self.resolve( + id:, + resolution:, + message:, + refund_amount:, + uploaded_filename:, + evidence: nil + ) + use_connection do |connection| + connection.put( + "/dispute/#{id}/resolve", + { resolution:, message:, refund_amount:, uploaded_filename:, evidence: }.compact, + ) + end + end + + # Successful response from calling #evidence. + class EvidenceResponse < PaystackGateway::Response + delegate :customer_email, + :customer_name, + :customer_phone, + :service_details, + :delivery_address, + :delivery_date, + :dispute, + :id, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #evidence. + class EvidenceError < ApiError; end + + # https://paystack.com/docs/api/dispute/#evidence + # Add Evidence: POST /dispute/{id}/evidence + # + # @param id [String] (required) + # Dispute ID + # @param customer_email [String] (required) + # Customer email + # @param customer_name [String] (required) + # Customer name + # @param customer_phone [String] (required) + # Customer mobile number + # @param service_details [String] (required) + # Details of service offered + # @param delivery_address [String] + # Delivery address + # @param delivery_date [Time] + # ISO 8601 representation of delivery date (YYYY-MM-DD) + # + # @return [EvidenceResponse] successful response + # @raise [EvidenceError] if the request fails + api_method def self.evidence( + id:, + customer_email:, + customer_name:, + customer_phone:, + service_details:, + delivery_address: nil, + delivery_date: nil + ) + use_connection do |connection| + connection.post( + "/dispute/#{id}/evidence", + { + customer_email:, + customer_name:, + customer_phone:, + service_details:, + delivery_address:, + delivery_date:, + }.compact, + ) + end + end + end +end diff --git a/lib/paystack_gateway/integration.rb b/lib/paystack_gateway/integration.rb new file mode 100644 index 0000000..66195ed --- /dev/null +++ b/lib/paystack_gateway/integration.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/integration + # + # Integration + # A collection of endpoints for managing some settings on an integration + module Integration + include PaystackGateway::RequestModule + + # Successful response from calling #fetch_payment_session_timeout. + class FetchPaymentSessionTimeoutResponse < PaystackGateway::Response; end + + # Error response from #fetch_payment_session_timeout. + class FetchPaymentSessionTimeoutError < ApiError; end + + # https://paystack.com/docs/api/integration/#fetch_payment_session_timeout + # Fetch Payment Session Timeout: GET /integration/payment_session_timeout + # + # + # @return [FetchPaymentSessionTimeoutResponse] successful response + # @raise [FetchPaymentSessionTimeoutError] if the request fails + api_method def self.fetch_payment_session_timeout + use_connection do |connection| + connection.get( + '/integration/payment_session_timeout', + ) + end + end + + # Successful response from calling #update_payment_session_timeout. + class UpdatePaymentSessionTimeoutResponse < PaystackGateway::Response; end + + # Error response from #update_payment_session_timeout. + class UpdatePaymentSessionTimeoutError < ApiError; end + + # https://paystack.com/docs/api/integration/#update_payment_session_timeout + # Update Payment Session Timeout: PUT /integration/payment_session_timeout + # + # @param timeout [String] (required) + # Time in seconds before a transaction becomes invalid + # + # @return [UpdatePaymentSessionTimeoutResponse] successful response + # @raise [UpdatePaymentSessionTimeoutError] if the request fails + api_method def self.update_payment_session_timeout(timeout:) + use_connection do |connection| + connection.put( + '/integration/payment_session_timeout', + { timeout: }.compact, + ) + end + end + end +end diff --git a/lib/paystack_gateway/miscellaneous.rb b/lib/paystack_gateway/miscellaneous.rb index f8bde87..eabde30 100644 --- a/lib/paystack_gateway/miscellaneous.rb +++ b/lib/paystack_gateway/miscellaneous.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true module PaystackGateway - # Supporting APIs that can be used to provide more details to other APIs - # https://paystack.com/docs/api/#miscellaneous + # https://paystack.com/docs/api/miscellaneous + # + # Miscellaneous + # A collection of endpoints that provides utility functions module Miscellaneous include PaystackGateway::RequestModule @@ -18,10 +20,86 @@ def by_bank_codes = data.index_by(&:code) end # https://paystack.com/docs/api/miscellaneous/#bank + # @deprecated Use #Bank.list instead. api_method def self.list_banks(use_cache: true, pay_with_bank_transfer: false) use_connection(cache_options: use_cache ? {} : nil) do |connection| connection.get('/bank', pay_with_bank_transfer ? { pay_with_bank_transfer: } : {}) end end + + # Successful response from calling #resolve_card_bin. + class ResolveCardBinResponse < PaystackGateway::Response + delegate :bin, + :brand, + :sub_brand, + :country_code, + :country_name, + :card_type, + :bank, + :currency, + :linked_bank_id, to: :data + end + + # Error response from #resolve_card_bin. + class ResolveCardBinError < ApiError; end + + # https://paystack.com/docs/api/miscellaneous/#resolve_card_bin + # Resolve Card BIN: GET /decision/bin/{bin} + # + # @param bin [String] (required) + # + # @return [ResolveCardBinResponse] successful response + # @raise [ResolveCardBinError] if the request fails + api_method def self.resolve_card_bin(bin:) + use_connection do |connection| + connection.get( + "/decision/bin/#{bin}", + ) + end + end + + # Successful response from calling #list_countries. + class ListCountriesResponse < PaystackGateway::Response; end + + # Error response from #list_countries. + class ListCountriesError < ApiError; end + + # https://paystack.com/docs/api/miscellaneous/#list_countries + # List Countries: GET /country + # + # + # @return [ListCountriesResponse] successful response + # @raise [ListCountriesError] if the request fails + api_method def self.list_countries + use_connection do |connection| + connection.get( + '/country', + ) + end + end + + # Successful response from calling #avs. + class AvsResponse < PaystackGateway::Response; end + + # Error response from #avs. + class AvsError < ApiError; end + + # https://paystack.com/docs/api/miscellaneous/#avs + # List States (AVS): GET /address_verification/states + # + # @param type [String] + # @param country [String] + # @param currency [String] + # + # @return [AvsResponse] successful response + # @raise [AvsError] if the request fails + api_method def self.avs(type: nil, country: nil, currency: nil) + use_connection do |connection| + connection.get( + '/address_verification/states', + { type:, country:, currency: }.compact, + ) + end + end end end diff --git a/lib/paystack_gateway/order.rb b/lib/paystack_gateway/order.rb new file mode 100644 index 0000000..376ea6e --- /dev/null +++ b/lib/paystack_gateway/order.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/order + # + # Orders + # A collection of endpoints for creating and managing orders + module Order + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/order/#list + # List Orders: GET /order + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list(per_page: nil, page: nil, from: nil, to: nil) + use_connection do |connection| + connection.get( + '/order', + { perPage:, page:, from:, to: }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :discounts, + :currency, + :shipping_address, + :integration, + :domain, + :email, + :customer, + :amount, + :pay_for_me, + :shipping, + :shipping_fees, + :metadata, + :order_code, + :refunded, + :is_viewed, + :expiration_date, + :id, + :createdAt, + :updatedAt, + :items, + :pay_for_me_code, + :discount_amount, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/order/#create + # Create Order: POST /order + # + # @param email [String] (required) + # The email of the customer placing the order + # @param first_name [String] (required) + # The customer's first name + # @param last_name [String] (required) + # The customer's last name + # @param phone [String] (required) + # The customer's mobile number + # @param currency [String] (required) + # Currency in which amount is set. Allowed values are NGN, GHS, ZAR or USD + # @param items [Array] (required) + # The collection of items that make up the order + # @param shipping [Hash] (required) + # The shipping details of the order + # @option shipping [String] :street_line + # The address of for the delivery + # @option shipping [String] :city + # The city of the delivery address + # @option shipping [String] :state + # The state of the delivery address + # @option shipping [String] :country + # The country of the delivery address + # @option shipping [Integer] :shipping_fee + # The cost of delivery + # @option shipping [String] :delivery_note + # Extra details to be aware of for the delivery + # @param is_gift [Boolean] + # A flag to indicate if the order is for someone else + # @param pay_for_me [Boolean] + # A flag to indicate if the someone else should pay for the order + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create( + email:, + first_name:, + last_name:, + phone:, + currency:, + items:, + shipping:, + is_gift: nil, + pay_for_me: nil + ) + use_connection do |connection| + connection.post( + '/order', + { + email:, + first_name:, + last_name:, + phone:, + currency:, + items:, + shipping:, + is_gift:, + pay_for_me:, + }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :discounts, + :order_code, + :domain, + :currency, + :amount, + :email, + :refunded, + :paid_at, + :shipping_address, + :metadata, + :shipping_fees, + :shipping_method, + :is_viewed, + :expiration_date, + :pay_for_me, + :id, + :integration, + :page, + :customer, + :shipping, + :createdAt, + :updatedAt, + :transaction, + :is_gift, + :payer, + :fully_refunded, + :refunded_amount, + :items, + :discount_amount, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/order/#fetch + # Fetch Order: GET /order/{id} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/order/#{id}", + ) + end + end + + # Successful response from calling #fetch_products. + class FetchProductsResponse < PaystackGateway::Response; end + + # Error response from #fetch_products. + class FetchProductsError < ApiError; end + + # https://paystack.com/docs/api/order/#fetch_products + # Fetch Products Order: GET /order/product/{id} + # + # + # @return [FetchProductsResponse] successful response + # @raise [FetchProductsError] if the request fails + api_method def self.fetch_products + use_connection do |connection| + connection.get( + "/order/product/#{id}", + ) + end + end + + # Successful response from calling #validate_pay_for_me. + class ValidatePayForMeResponse < PaystackGateway::Response + delegate :order_code, + :domain, + :currency, + :amount, + :email, + :refunded, + :paid_at, + :shipping_address, + :metadata, + :shipping_fees, + :shipping_method, + :is_viewed, + :expiration_date, + :pay_for_me, + :id, + :integration, + :transaction, + :page, + :customer, + :shipping, + :createdAt, + :updatedAt, + :payer, to: :data + end + + # Error response from #validate_pay_for_me. + class ValidatePayForMeError < ApiError; end + + # https://paystack.com/docs/api/order/#validate_pay_for_me + # Validate pay for me order: GET /order/{code}/validate + # + # + # @return [ValidatePayForMeResponse] successful response + # @raise [ValidatePayForMeError] if the request fails + api_method def self.validate_pay_for_me + use_connection do |connection| + connection.get( + "/order/#{code}/validate", + ) + end + end + end +end diff --git a/lib/paystack_gateway/page.rb b/lib/paystack_gateway/page.rb new file mode 100644 index 0000000..fe702d6 --- /dev/null +++ b/lib/paystack_gateway/page.rb @@ -0,0 +1,304 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/page + # + # Payment Pages + # A collection of endpoints for creating and managing links for the collection of payment for products + module Page + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/page/#list + # List Pages: GET /page + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list(per_page: nil, page: nil, from: nil, to: nil) + use_connection do |connection| + connection.get( + '/page', + { perPage:, page:, from:, to: }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :name, + :integration, + :domain, + :slug, + :currency, + :type, + :collect_phone, + :active, + :published, + :migrate, + :id, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/page/#create + # Create Page: POST /page + # + # @param name [String] (required) + # Name of page + # @param description [String] + # The description of the page + # @param amount [Integer] + # Amount should be in kobo if currency is NGN, pesewas, if currency is GHS, and cents, + # if currency is ZAR + # @param currency ["NGN", "GHS", "ZAR", "KES", "USD"] + # The transaction currency. Defaults to your integration currency. + # @param slug [String] + # URL slug you would like to be associated with this page. Page will be accessible + # at `https://paystack.com/pay/[slug]` + # @param type ["payment", "subscription", "product", "plan"] + # The type of payment page to create. Defaults to `payment` if no type is specified. + # @param plan [String] + # The ID of the plan to subscribe customers on this payment page to when `type` is + # set to `subscription`. + # @param fixed_amount [Boolean] + # Specifies whether to collect a fixed amount on the payment page. If true, `amount` + # must be passed. + # @param split_code [String] + # The split code of the transaction split. e.g. `SPL_98WF13Eb3w` + # @param metadata [String] + # Stringified JSON object of custom data + # @param redirect_url [String] + # If you would like Paystack to redirect to a URL upon successful payment, specify + # the URL here. + # @param success_message [String] + # A success message to display to the customer after a successful transaction + # @param notification_email [String] + # An email address that will receive transaction notifications for this payment page + # @param collect_phone [Boolean] + # Specify whether to collect phone numbers on the payment page + # @param custom_fields [Array] + # If you would like to accept custom fields, specify them here. + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create( + name:, + description: nil, + amount: nil, + currency: nil, + slug: nil, + type: nil, + plan: nil, + fixed_amount: nil, + split_code: nil, + metadata: nil, + redirect_url: nil, + success_message: nil, + notification_email: nil, + collect_phone: nil, + custom_fields: nil + ) + use_connection do |connection| + connection.post( + '/page', + { + name:, + description:, + amount:, + currency:, + slug:, + type:, + plan:, + fixed_amount:, + split_code:, + metadata:, + redirect_url:, + success_message:, + notification_email:, + collect_phone:, + custom_fields:, + }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :integration, + :domain, + :name, + :description, + :amount, + :currency, + :slug, + :custom_fields, + :type, + :redirect_url, + :success_message, + :collect_phone, + :active, + :published, + :migrate, + :notification_email, + :metadata, + :split_code, + :id, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/page/#fetch + # Fetch Page: GET /page/{id} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/page/#{id}", + ) + end + end + + # Successful response from calling #update. + class UpdateResponse < PaystackGateway::Response + delegate :domain, + :name, + :description, + :amount, + :currency, + :slug, + :custom_fields, + :type, + :redirect_url, + :success_message, + :collect_phone, + :active, + :published, + :migrate, + :notification_email, + :metadata, + :split_code, + :id, + :integration, + :plan, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #update. + class UpdateError < ApiError; end + + # https://paystack.com/docs/api/page/#update + # Update Page: PUT /page/{id} + # + # @param name [String] (required) + # Name of page + # @param description [String] (required) + # The description of the page + # @param amount [Integer] (required) + # Amount should be in kobo if currency is NGN, pesewas, if currency is GHS, and cents, + # if currency is ZAR + # @param active [Boolean] (required) + # Set to false to deactivate page url + # + # @return [UpdateResponse] successful response + # @raise [UpdateError] if the request fails + api_method def self.update(name:, description:, amount:, active:) + use_connection do |connection| + connection.put( + "/page/#{id}", + { name:, description:, amount:, active: }.compact, + ) + end + end + + # Successful response from calling #check_slug_availability. + class CheckSlugAvailabilityResponse < PaystackGateway::Response; end + + # Error response from #check_slug_availability. + class CheckSlugAvailabilityError < ApiError; end + + # https://paystack.com/docs/api/page/#check_slug_availability + # Check Slug Availability: GET /page/check_slug_availability/{slug} + # + # + # @return [CheckSlugAvailabilityResponse] successful response + # @raise [CheckSlugAvailabilityError] if the request fails + api_method def self.check_slug_availability + use_connection do |connection| + connection.get( + "/page/check_slug_availability/#{slug}", + ) + end + end + + # Successful response from calling #add_products. + class AddProductsResponse < PaystackGateway::Response + delegate :integration, + :plan, + :domain, + :name, + :description, + :amount, + :currency, + :slug, + :custom_fields, + :type, + :redirect_url, + :success_message, + :collect_phone, + :active, + :published, + :migrate, + :notification_email, + :metadata, + :split_code, + :id, + :createdAt, + :updatedAt, + :products, to: :data + end + + # Error response from #add_products. + class AddProductsError < ApiError; end + + # https://paystack.com/docs/api/page/#add_products + # Add Products: POST /page/{id}/product + # + # @param product [Array] (required) + # IDs of all products to add to a page + # + # @return [AddProductsResponse] successful response + # @raise [AddProductsError] if the request fails + api_method def self.add_products(product:) + use_connection do |connection| + connection.post( + "/page/#{id}/product", + { product: }.compact, + ) + end + end + end +end diff --git a/lib/paystack_gateway/payment_request.rb b/lib/paystack_gateway/payment_request.rb new file mode 100644 index 0000000..f5f9b9d --- /dev/null +++ b/lib/paystack_gateway/payment_request.rb @@ -0,0 +1,430 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/paymentrequest + # + # Payment Requests + # A collection of endpoints for managing invoices for the payment of goods and services + module PaymentRequest + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/payment-request/#list + # List Payment Request: GET /paymentrequest + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param customer [String] + # Customer ID + # @param status [String] + # Invoice status to filter + # @param currency [String] + # If your integration supports more than one currency, choose the one to filter + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list( + per_page: nil, + page: nil, + customer: nil, + status: nil, + currency: nil, + from: nil, + to: nil + ) + use_connection do |connection| + connection.get( + '/paymentrequest', + { + perPage: per_page, + page:, + customer:, + status:, + currency:, + from:, + to:, + }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :id, + :integration, + :domain, + :amount, + :currency, + :due_date, + :has_invoice, + :invoice_number, + :description, + :line_items, + :tax, + :request_code, + :paid, + :metadata, + :notifications, + :offline_reference, + :customer, + :created_at, + :discount, + :split_code, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/payment-request/#create + # Create Payment Request: POST /paymentrequest + # + # @param customer [String] (required) + # Customer id or code + # @param amount [Integer] + # Payment request amount. Only useful if line items and tax values are ignored. The + # endpoint will throw a friendly warning if neither is available. + # @param currency [String] + # Specify the currency of the invoice. Allowed values are NGN, GHS, ZAR and USD. Defaults + # to NGN + # @param due_date [Time] + # ISO 8601 representation of request due date + # @param description [String] + # A short description of the payment request + # @param line_items [Array] + # Array of line items + # @param tax [Array] + # Array of taxes + # @param send_notification [Boolean] + # Indicates whether Paystack sends an email notification to customer. Defaults to + # true + # @param draft [Boolean] + # Indicate if request should be saved as draft. Defaults to false and overrides send_notification + # @param has_invoice [Boolean] + # Set to true to create a draft invoice (adds an auto incrementing invoice number + # if none is provided) even if there are no line_items or tax passed + # @param invoice_number [Integer] + # Numeric value of invoice. Invoice will start from 1 and auto increment from there. + # This field is to help override whatever value Paystack decides. Auto increment for + # subsequent invoices continue from this point. + # @param split_code [String] + # The split code of the transaction split. + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create( + customer:, + amount: nil, + currency: nil, + due_date: nil, + description: nil, + line_items: nil, + tax: nil, + send_notification: nil, + draft: nil, + has_invoice: nil, + invoice_number: nil, + split_code: nil + ) + use_connection do |connection| + connection.post( + '/paymentrequest', + { + customer:, + amount:, + currency:, + due_date:, + description:, + line_items:, + tax:, + send_notification:, + draft:, + has_invoice:, + invoice_number:, + split_code:, + }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response; end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/payment-request/#fetch + # Fetch Payment Request: GET /paymentrequest/{id} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/paymentrequest/#{id}", + ) + end + end + + # Successful response from calling #update. + class UpdateResponse < PaystackGateway::Response + delegate :id, + :integration, + :domain, + :amount, + :currency, + :due_date, + :has_invoice, + :invoice_number, + :description, + :pdf_url, + :line_items, + :tax, + :request_code, + :paid, + :paid_at, + :metadata, + :notifications, + :offline_reference, + :customer, + :created_at, + :discount, + :split_code, to: :data + end + + # Error response from #update. + class UpdateError < ApiError; end + + # https://paystack.com/docs/api/payment-request/#update + # Update Payment Request: PUT /paymentrequest/{id} + # + # @param customer [String] (required) + # Customer id or code + # @param amount [Integer] (required) + # Payment request amount. Only useful if line items and tax values are ignored. The + # endpoint will throw a friendly warning if neither is available. + # @param currency [String] (required) + # Specify the currency of the invoice. Allowed values are NGN, GHS, ZAR and USD. Defaults + # to NGN + # @param due_date [Time] (required) + # ISO 8601 representation of request due date + # @param description [String] (required) + # A short description of the payment request + # @param line_items [Array] (required) + # Array of line items + # @param tax [Array] (required) + # Array of taxes + # @param send_notification [Boolean] (required) + # Indicates whether Paystack sends an email notification to customer. Defaults to + # true + # @param draft [Boolean] (required) + # Indicate if request should be saved as draft. Defaults to false and overrides send_notification + # @param has_invoice [Boolean] (required) + # Set to true to create a draft invoice (adds an auto incrementing invoice number + # if none is provided) even if there are no line_items or tax passed + # @param invoice_number [Integer] (required) + # Numeric value of invoice. Invoice will start from 1 and auto increment from there. + # This field is to help override whatever value Paystack decides. Auto increment for + # subsequent invoices continue from this point. + # @param split_code [String] (required) + # The split code of the transaction split. + # + # @return [UpdateResponse] successful response + # @raise [UpdateError] if the request fails + api_method def self.update( + customer:, + amount:, + currency:, + due_date:, + description:, + line_items:, + tax:, + send_notification:, + draft:, + has_invoice:, + invoice_number:, + split_code: + ) + use_connection do |connection| + connection.put( + "/paymentrequest/#{id}", + { + customer:, + amount:, + currency:, + due_date:, + description:, + line_items:, + tax:, + send_notification:, + draft:, + has_invoice:, + invoice_number:, + split_code:, + }.compact, + ) + end + end + + # Successful response from calling #verify. + class VerifyResponse < PaystackGateway::Response + delegate :id, + :integration, + :domain, + :amount, + :currency, + :due_date, + :has_invoice, + :invoice_number, + :description, + :pdf_url, + :line_items, + :tax, + :request_code, + :paid, + :paid_at, + :metadata, + :notifications, + :offline_reference, + :customer, + :created_at, + :discount, + :split_code, + :pending_amount, to: :data + end + + # Error response from #verify. + class VerifyError < ApiError; end + + # https://paystack.com/docs/api/payment-request/#verify + # Verify Payment Request: GET /paymentrequest/verify/{id} + # + # + # @return [VerifyResponse] successful response + # @raise [VerifyError] if the request fails + api_method def self.verify + use_connection do |connection| + connection.get( + "/paymentrequest/verify/#{id}", + ) + end + end + + # Successful response from calling #notify. + class NotifyResponse < PaystackGateway::Response; end + + # Error response from #notify. + class NotifyError < ApiError; end + + # https://paystack.com/docs/api/payment-request/#notify + # Send Notification: POST /paymentrequest/notify/{id} + # + # + # @return [NotifyResponse] successful response + # @raise [NotifyError] if the request fails + api_method def self.notify + use_connection do |connection| + connection.post( + "/paymentrequest/notify/#{id}", + ) + end + end + + # Successful response from calling #totals. + class TotalsResponse < PaystackGateway::Response + delegate :pending, :successful, :total, to: :data + end + + # Error response from #totals. + class TotalsError < ApiError; end + + # https://paystack.com/docs/api/payment-request/#totals + # Payment Request Total: GET /paymentrequest/totals + # + # + # @return [TotalsResponse] successful response + # @raise [TotalsError] if the request fails + api_method def self.totals + use_connection do |connection| + connection.get( + '/paymentrequest/totals', + ) + end + end + + # Successful response from calling #finalize. + class FinalizeResponse < PaystackGateway::Response + delegate :id, + :integration, + :domain, + :amount, + :currency, + :due_date, + :has_invoice, + :invoice_number, + :description, + :pdf_url, + :line_items, + :tax, + :request_code, + :paid, + :paid_at, + :metadata, + :notifications, + :offline_reference, + :customer, + :created_at, + :discount, + :split_code, + :pending_amount, to: :data + end + + # Error response from #finalize. + class FinalizeError < ApiError; end + + # https://paystack.com/docs/api/payment-request/#finalize + # Finalize Payment Request: POST /paymentrequest/finalize/{id} + # + # + # @return [FinalizeResponse] successful response + # @raise [FinalizeError] if the request fails + api_method def self.finalize + use_connection do |connection| + connection.post( + "/paymentrequest/finalize/#{id}", + ) + end + end + + # Successful response from calling #archive. + class ArchiveResponse < PaystackGateway::Response; end + + # Error response from #archive. + class ArchiveError < ApiError; end + + # https://paystack.com/docs/api/payment-request/#archive + # Archive Payment Request: POST /paymentrequest/archive/{id} + # + # + # @return [ArchiveResponse] successful response + # @raise [ArchiveError] if the request fails + api_method def self.archive + use_connection do |connection| + connection.post( + "/paymentrequest/archive/#{id}", + ) + end + end + end +end diff --git a/lib/paystack_gateway/plan.rb b/lib/paystack_gateway/plan.rb new file mode 100644 index 0000000..cd639f9 --- /dev/null +++ b/lib/paystack_gateway/plan.rb @@ -0,0 +1,238 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/plan + # + # Plans + # A collection of endpoints for creating and managing recurring payment configuration + module Plan + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/plan/#list + # List Plans: GET /plan + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param interval [String] + # Specify interval of the plan + # @param amount [Integer] + # The amount on the plans to retrieve + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list( + per_page: nil, + page: nil, + interval: nil, + amount: nil, + from: nil, + to: nil + ) + use_connection do |connection| + connection.get( + '/plan', + { + perPage: per_page, + page:, + interval:, + amount:, + from:, + to:, + }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :currency, + :name, + :amount, + :interval, + :integration, + :domain, + :plan_code, + :invoice_limit, + :send_invoices, + :send_sms, + :hosted_page, + :migrate, + :is_archived, + :id, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/plan/#create + # Create Plan: POST /plan + # + # @param name [String] (required) + # Name of plan + # @param amount [Integer] (required) + # Amount should be in kobo if currency is NGN, pesewas, if currency is GHS, and cents, + # if currency is ZAR + # @param interval [String] (required) + # Interval in words. Valid intervals are daily, weekly, monthly,biannually, annually + # @param description [String] + # A description for this plan + # @param send_invoices [Boolean] + # Set to false if you don't want invoices to be sent to your customers + # @param send_sms [Boolean] + # Set to false if you don't want text messages to be sent to your customers + # @param currency [String] + # Currency in which amount is set. Allowed values are NGN, GHS, ZAR or USD + # @param invoice_limit [Integer] + # Number of invoices to raise during subscription to this plan. Can be overridden + # by specifying an invoice_limit while subscribing. + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create( + name:, + amount:, + interval:, + description: nil, + send_invoices: nil, + send_sms: nil, + currency: nil, + invoice_limit: nil + ) + use_connection do |connection| + connection.post( + '/plan', + { + name:, + amount:, + interval:, + description:, + send_invoices:, + send_sms:, + currency:, + invoice_limit:, + }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :subscriptions, + :pages, + :domain, + :name, + :plan_code, + :description, + :amount, + :interval, + :invoice_limit, + :send_invoices, + :send_sms, + :hosted_page, + :hosted_page_url, + :hosted_page_summary, + :currency, + :migrate, + :is_deleted, + :is_archived, + :id, + :integration, + :createdAt, + :updatedAt, + :pages_count, + :subscribers_count, + :subscriptions_count, + :active_subscriptions_count, + :total_revenue, + :subscribers, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/plan/#fetch + # Fetch Plan: GET /plan/{code} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/plan/#{code}", + ) + end + end + + # Successful response from calling #update. + class UpdateResponse < PaystackGateway::Response; end + + # Error response from #update. + class UpdateError < ApiError; end + + # https://paystack.com/docs/api/plan/#update + # Update Plan: PUT /plan/{code} + # + # @param name [String] (required) + # Name of plan + # @param amount [Integer] (required) + # Amount should be in kobo if currency is NGN, pesewas, if currency is GHS, and cents, + # if currency is ZAR + # @param interval [String] (required) + # Interval in words. Valid intervals are daily, weekly, monthly,biannually, annually + # @param description [Boolean] (required) + # A description for this plan + # @param send_invoices [Boolean] (required) + # Set to false if you don't want invoices to be sent to your customers + # @param send_sms [Boolean] (required) + # Set to false if you don't want text messages to be sent to your customers + # @param currency [String] (required) + # Currency in which amount is set. Allowed values are NGN, GHS, ZAR or USD + # @param invoice_limit [Integer] (required) + # Number of invoices to raise during subscription to this plan. Can be overridden + # by specifying an invoice_limit while subscribing. + # + # @return [UpdateResponse] successful response + # @raise [UpdateError] if the request fails + api_method def self.update( + name:, + amount:, + interval:, + description:, + send_invoices:, + send_sms:, + currency:, + invoice_limit: + ) + use_connection do |connection| + connection.put( + "/plan/#{code}", + { + name:, + amount:, + interval:, + description:, + send_invoices:, + send_sms:, + currency:, + invoice_limit:, + }.compact, + ) + end + end + end +end diff --git a/lib/paystack_gateway/product.rb b/lib/paystack_gateway/product.rb new file mode 100644 index 0000000..a6fea2c --- /dev/null +++ b/lib/paystack_gateway/product.rb @@ -0,0 +1,288 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/product + # + # Products + # A collection of endpoints for creating and managing inventories + module Product + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/product/#list + # List Products: GET /product + # + # @param perPage [Integer] + # @param page [Integer] + # @param active [Boolean] + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list(per_page: nil, page: nil, active: nil, from: nil, to: nil) + use_connection do |connection| + connection.get( + '/product', + { perPage:, page:, active:, from:, to: }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :variants_options, + :variants, + :name, + :description, + :currency, + :price, + :quantity, + :type, + :is_shippable, + :unlimited, + :files, + :shipping_fields, + :integration, + :domain, + :metadata, + :slug, + :product_code, + :quantity_sold, + :active, + :deleted_at, + :in_stock, + :minimum_orderable, + :maximum_orderable, + :low_stock_alert, + :id, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/product/#create + # Create Product: POST /product + # + # @param name [String] (required) + # Name of product + # @param description [String] (required) + # The description of the product + # @param price [Integer] (required) + # Price should be in kobo if currency is NGN, pesewas, if currency is GHS, and cents, + # if currency is ZAR + # @param currency [String] (required) + # Currency in which price is set. Allowed values are: NGN, GHS, ZAR or USD + # @param unlimited [Boolean] + # Set to true if the product has unlimited stock. Leave as false if the product has + # limited stock + # @param quantity [Integer] + # Number of products in stock. Use if limited is true + # @param split_code [String] + # The split code if sharing the transaction with partners + # @param metadata [String] + # Stringified JSON object of custom data + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create( + name:, + description:, + price:, + currency:, + unlimited: nil, + quantity: nil, + split_code: nil, + metadata: nil + ) + use_connection do |connection| + connection.post( + '/product', + { + name:, + description:, + price:, + currency:, + unlimited:, + quantity:, + split_code:, + metadata:, + }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :digital_assets, + :integration, + :name, + :description, + :product_code, + :price, + :currency, + :quantity, + :quantity_sold, + :type, + :files, + :file_path, + :is_shippable, + :shipping_fields, + :unlimited, + :domain, + :active, + :features, + :in_stock, + :metadata, + :slug, + :success_message, + :redirect_url, + :split_code, + :notification_emails, + :minimum_orderable, + :maximum_orderable, + :low_stock_alert, + :stock_threshold, + :expires_in, + :id, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/product/#fetch + # Fetch Product: GET /product/{id} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/product/#{id}", + ) + end + end + + # Successful response from calling #update. + class UpdateResponse < PaystackGateway::Response + delegate :name, + :description, + :product_code, + :price, + :currency, + :quantity, + :quantity_sold, + :type, + :files, + :file_path, + :is_shippable, + :shipping_fields, + :unlimited, + :domain, + :active, + :features, + :in_stock, + :metadata, + :slug, + :success_message, + :redirect_url, + :split_code, + :notification_emails, + :minimum_orderable, + :maximum_orderable, + :low_stock_alert, + :stock_threshold, + :expires_in, + :id, + :integration, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #update. + class UpdateError < ApiError; end + + # https://paystack.com/docs/api/product/#update + # Update product: PUT /product/{id} + # + # @param name [String] (required) + # Name of product + # @param description [String] (required) + # The description of the product + # @param price [Integer] (required) + # Price should be in kobo if currency is NGN, pesewas, if currency is GHS, and cents, + # if currency is ZAR + # @param currency [String] (required) + # Currency in which price is set. Allowed values are: NGN, GHS, ZAR or USD + # @param unlimited [Boolean] (required) + # Set to true if the product has unlimited stock. Leave as false if the product has + # limited stock + # @param quantity [Integer] (required) + # Number of products in stock. Use if limited is true + # @param split_code [String] (required) + # The split code if sharing the transaction with partners + # @param metadata [String] (required) + # Stringified JSON object of custom data + # + # @return [UpdateResponse] successful response + # @raise [UpdateError] if the request fails + api_method def self.update( + name:, + description:, + price:, + currency:, + unlimited:, + quantity:, + split_code:, + metadata: + ) + use_connection do |connection| + connection.put( + "/product/#{id}", + { + name:, + description:, + price:, + currency:, + unlimited:, + quantity:, + split_code:, + metadata:, + }.compact, + ) + end + end + + # Successful response from calling #delete. + class DeleteResponse < PaystackGateway::Response; end + + # Error response from #delete. + class DeleteError < ApiError; end + + # https://paystack.com/docs/api/product/#delete + # Delete Product: DELETE /product/{id} + # + # + # @return [DeleteResponse] successful response + # @raise [DeleteError] if the request fails + api_method def self.delete + use_connection do |connection| + connection.delete( + "/product/#{id}", + ) + end + end + end +end diff --git a/lib/paystack_gateway/refund.rb b/lib/paystack_gateway/refund.rb new file mode 100644 index 0000000..ba37ca0 --- /dev/null +++ b/lib/paystack_gateway/refund.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/refund + # + # Refunds + # A collection of endpoints for creating and managing transaction reimbursement + module Refund + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/refund/#list + # List Refunds: GET /refund + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list(per_page: nil, page: nil, from: nil, to: nil) + use_connection do |connection| + connection.get( + '/refund', + { perPage:, page:, from:, to: }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :transaction, + :integration, + :deducted_amount, + :channel, + :merchant_note, + :customer_note, + :refunded_by, + :expected_at, + :currency, + :domain, + :amount, + :fully_deducted, + :id, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/refund/#create + # Create Refund: POST /refund + # + # @param transaction [String] (required) + # Transaction reference or id + # @param amount [Integer] + # Amount ( in kobo if currency is NGN, pesewas, if currency is GHS, and cents, if + # currency is ZAR ) to be refunded to the customer. Amount cannot be more than the + # original transaction amount + # @param currency [String] + # Three-letter ISO currency. Allowed values are NGN, GHS, ZAR or USD + # @param customer_note [String] + # Customer reason + # @param merchant_note [String] + # Merchant reason + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create(transaction:, amount: nil, currency: nil, customer_note: nil, merchant_note: nil) + use_connection do |connection| + connection.post( + '/refund', + { transaction:, amount:, currency:, customer_note:, merchant_note: }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :integration, + :transaction, + :dispute, + :settlement, + :id, + :domain, + :currency, + :amount, + :refunded_at, + :refunded_by, + :customer_note, + :merchant_note, + :deducted_amount, + :fully_deducted, + :createdAt, + :bank_reference, + :transaction_reference, + :reason, + :customer, + :refund_type, + :transaction_amount, + :initiated_by, + :refund_channel, + :session_id, + :collect_account_number, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/refund/#fetch + # Fetch Refund: GET /refund/{id} + # + # @param id [String] (required) + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch(id:) + use_connection do |connection| + connection.get( + "/refund/#{id}", + ) + end + end + end +end diff --git a/lib/paystack_gateway/settlement.rb b/lib/paystack_gateway/settlement.rb new file mode 100644 index 0000000..e60bcb6 --- /dev/null +++ b/lib/paystack_gateway/settlement.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/settlement + # + # Settlements + # A collection of endpoints for gaining insights into payouts + module Settlement + include PaystackGateway::RequestModule + + # Successful response from calling #s_fetch. + class SFetchResponse < PaystackGateway::Response; end + + # Error response from #s_fetch. + class SFetchError < ApiError; end + + # https://paystack.com/docs/api/settlement/#s_fetch + # Fetch Settlements: GET /settlement + # + # @param perPage [Integer] + # @param page [Integer] + # + # @return [SFetchResponse] successful response + # @raise [SFetchError] if the request fails + api_method def self.s_fetch(per_page: nil, page: nil) + use_connection do |connection| + connection.get( + '/settlement', + { perPage:, page: }.compact, + ) + end + end + + # Successful response from calling #s_transaction. + class STransactionResponse < PaystackGateway::Response; end + + # Error response from #s_transaction. + class STransactionError < ApiError; end + + # https://paystack.com/docs/api/settlement/#s_transaction + # Settlement Transactions: GET /settlement/{id}/transaction + # + # @param id [String] (required) + # + # @return [STransactionResponse] successful response + # @raise [STransactionError] if the request fails + api_method def self.s_transaction(id:) + use_connection do |connection| + connection.get( + "/settlement/#{id}/transaction", + ) + end + end + end +end diff --git a/lib/paystack_gateway/split.rb b/lib/paystack_gateway/split.rb new file mode 100644 index 0000000..1ecbf85 --- /dev/null +++ b/lib/paystack_gateway/split.rb @@ -0,0 +1,271 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/split + # + # Transaction Splits + # A collection of endpoints for spliting a transaction and managing the splits + module Split + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/split/#list + # List/Search Splits: GET /split + # + # @param name [String] + # @param active [String] + # @param sort_by [String] + # @param from [String] + # @param to [String] + # @param perPage [String] + # @param page [String] + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list( + name: nil, + active: nil, + sort_by: nil, + from: nil, + to: nil, + per_page: nil, + page: nil + ) + use_connection do |connection| + connection.get( + '/split', + { + name:, + active:, + sort_by:, + from:, + to:, + perPage: per_page, + page:, + }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :id, + :name, + :type, + :currency, + :integration, + :domain, + :split_code, + :active, + :bearer_type, + :bearer_subaccount, + :createdAt, + :updatedAt, + :is_dynamic, + :subaccounts, + :total_subaccounts, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/split/#create + # Create Split: POST /split + # + # @param name [String] (required) + # Name of the transaction split + # @param type ["percentage", "flat"] (required) + # The type of transaction split you want to create. + # @param subaccounts [Array] (required) + # A list of object containing subaccount code and number of shares + # @param currency ["NGN", "GHS", "ZAR", "USD"] (required) + # The transaction currency + # @param bearer_type ["subaccount", "account", "all-proportional", "all"] + # This allows you specify how the transaction charge should be processed + # @param bearer_subaccount [String] + # This is the subaccount code of the customer or partner that would bear the transaction + # charge if you specified subaccount as the bearer type + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create( + name:, + type:, + subaccounts:, + currency:, + bearer_type: nil, + bearer_subaccount: nil + ) + use_connection do |connection| + connection.post( + '/split', + { + name:, + type:, + subaccounts:, + currency:, + bearer_type:, + bearer_subaccount:, + }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :id, + :name, + :type, + :currency, + :integration, + :domain, + :split_code, + :active, + :bearer_type, + :bearer_subaccount, + :createdAt, + :updatedAt, + :is_dynamic, + :subaccounts, + :total_subaccounts, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/split/#fetch + # Fetch Split: GET /split/{id} + # + # @param id [String] (required) + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch(id:) + use_connection do |connection| + connection.get( + "/split/#{id}", + ) + end + end + + # Successful response from calling #update. + class UpdateResponse < PaystackGateway::Response + delegate :id, + :name, + :type, + :currency, + :integration, + :domain, + :split_code, + :active, + :bearer_type, + :bearer_subaccount, + :createdAt, + :updatedAt, + :is_dynamic, + :subaccounts, + :total_subaccounts, to: :data + end + + # Error response from #update. + class UpdateError < ApiError; end + + # https://paystack.com/docs/api/split/#update + # Update Split: PUT /split/{id} + # + # @param id [String] (required) + # @param name [String] (required) + # Name of the transaction split + # @param active [Boolean] (required) + # Toggle status of split. When true, the split is active, else it's inactive + # @param bearer_type ["subaccount", "account", "all-proportional", "all"] (required) + # This allows you specify how the transaction charge should be processed + # @param bearer_subaccount [String] (required) + # This is the subaccount code of the customer or partner that would bear the transaction + # charge if you specified subaccount as the bearer type + # + # @return [UpdateResponse] successful response + # @raise [UpdateError] if the request fails + api_method def self.update(id:, name:, active:, bearer_type:, bearer_subaccount:) + use_connection do |connection| + connection.put( + "/split/#{id}", + { name:, active:, bearer_type:, bearer_subaccount: }.compact, + ) + end + end + + # Successful response from calling #add_subaccount. + class AddSubaccountResponse < PaystackGateway::Response + delegate :id, + :name, + :type, + :currency, + :integration, + :domain, + :split_code, + :active, + :bearer_type, + :bearer_subaccount, + :createdAt, + :updatedAt, + :is_dynamic, + :subaccounts, + :total_subaccounts, to: :data + end + + # Error response from #add_subaccount. + class AddSubaccountError < ApiError; end + + # https://paystack.com/docs/api/split/#add_subaccount + # Add Subaccount to Split: POST /split/{id}/subaccount/add + # + # @param id [String] (required) + # @param subaccount [String] (required) + # Subaccount code of the customer or partner + # @param share [String] (required) + # The percentage or flat quota of the customer or partner + # + # @return [AddSubaccountResponse] successful response + # @raise [AddSubaccountError] if the request fails + api_method def self.add_subaccount(id:, subaccount:, share:) + use_connection do |connection| + connection.post( + "/split/#{id}/subaccount/add", + { subaccount:, share: }.compact, + ) + end + end + + # Successful response from calling #remove_subaccount. + class RemoveSubaccountResponse < PaystackGateway::Response; end + + # Error response from #remove_subaccount. + class RemoveSubaccountError < ApiError; end + + # https://paystack.com/docs/api/split/#remove_subaccount + # Remove Subaccount from split: POST /split/{id}/subaccount/remove + # + # @param id [String] (required) + # @param subaccount [String] (required) + # Subaccount code of the customer or partner + # @param share [String] (required) + # The percentage or flat quota of the customer or partner + # + # @return [RemoveSubaccountResponse] successful response + # @raise [RemoveSubaccountError] if the request fails + api_method def self.remove_subaccount(id:, subaccount:, share:) + use_connection do |connection| + connection.post( + "/split/#{id}/subaccount/remove", + { subaccount:, share: }.compact, + ) + end + end + end +end diff --git a/lib/paystack_gateway/storefront.rb b/lib/paystack_gateway/storefront.rb new file mode 100644 index 0000000..f6b3daa --- /dev/null +++ b/lib/paystack_gateway/storefront.rb @@ -0,0 +1,301 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/storefront + # + # Storefronts + # A collection of endpoints for creating and managing storefronts + module Storefront + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/storefront/#list + # List Storefronts: GET /storefront + # + # @param perPage [Integer] + # @param page [Integer] + # @param status ["active", "inactive"] + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list(per_page: nil, page: nil, status: nil) + use_connection do |connection| + connection.get( + '/storefront', + { perPage:, page:, status: }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :social_media, + :contacts, + :name, + :slug, + :currency, + :welcome_message, + :success_message, + :redirect_url, + :description, + :delivery_note, + :background_color, + :shippable, + :integration, + :domain, + :digital_product_expiry, + :id, + :createdAt, + :updatedAt, + :products, + :shipping_fees, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/storefront/#create + # Create Storefront: POST /storefront + # + # @param name [String] (required) + # Name of the storefront + # @param slug [String] (required) + # A unique identifier to access your store. Once the storefront is created, it can + # be accessed from https://paystack.shop/your-slug + # @param currency [String] (required) + # Currency for prices of products in your storefront. Allowed values are: `NGN`, `GHS`, + # `KES`, `ZAR` or `USD` + # @param description [String] + # The description of the storefront + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create(name:, slug:, currency:, description: nil) + use_connection do |connection| + connection.post( + '/storefront', + { name:, slug:, currency:, description: }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :social_media, + :contacts, + :name, + :slug, + :currency, + :welcome_message, + :success_message, + :redirect_url, + :description, + :delivery_note, + :background_color, + :shippable, + :integration, + :domain, + :digital_product_expiry, + :id, + :createdAt, + :updatedAt, + :products, + :shipping_fees, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/storefront/#fetch + # Fetch Storefront: GET /storefront/{id} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/storefront/#{id}", + ) + end + end + + # Successful response from calling #update. + class UpdateResponse < PaystackGateway::Response; end + + # Error response from #update. + class UpdateError < ApiError; end + + # https://paystack.com/docs/api/storefront/#update + # Update Storefront: PUT /storefront/{id} + # + # @param name [String] (required) + # Name of the storefront + # @param slug [String] (required) + # A unique identifier to access your store. Once the storefront is created, it can + # be accessed from https://paystack.shop/your-slug + # @param description [String] (required) + # The description of the storefront + # + # @return [UpdateResponse] successful response + # @raise [UpdateError] if the request fails + api_method def self.update(name:, slug:, description:) + use_connection do |connection| + connection.put( + "/storefront/#{id}", + { name:, slug:, description: }.compact, + ) + end + end + + # Successful response from calling #delete. + class DeleteResponse < PaystackGateway::Response; end + + # Error response from #delete. + class DeleteError < ApiError; end + + # https://paystack.com/docs/api/storefront/#delete + # Delete Storefront: DELETE /storefront/{id} + # + # + # @return [DeleteResponse] successful response + # @raise [DeleteError] if the request fails + api_method def self.delete + use_connection do |connection| + connection.delete( + "/storefront/#{id}", + ) + end + end + + # Successful response from calling #verify_slug. + class VerifySlugResponse < PaystackGateway::Response; end + + # Error response from #verify_slug. + class VerifySlugError < ApiError; end + + # https://paystack.com/docs/api/storefront/#verify_slug + # Verify Storefront Slug: GET /storefront/verify/{slug} + # + # + # @return [VerifySlugResponse] successful response + # @raise [VerifySlugError] if the request fails + api_method def self.verify_slug + use_connection do |connection| + connection.get( + "/storefront/verify/#{slug}", + ) + end + end + + # Successful response from calling #fetch_orders. + class FetchOrdersResponse < PaystackGateway::Response; end + + # Error response from #fetch_orders. + class FetchOrdersError < ApiError; end + + # https://paystack.com/docs/api/storefront/#fetch_orders + # Fetch Storefront Orders: GET /storefront/{id}/order + # Fetch all orders in your Storefront + # + # @param id [String] (required) + # + # @return [FetchOrdersResponse] successful response + # @raise [FetchOrdersError] if the request fails + api_method def self.fetch_orders(id:) + use_connection do |connection| + connection.get( + "/storefront/#{id}/order", + ) + end + end + + # Successful response from calling #list_products. + class ListProductsResponse < PaystackGateway::Response; end + + # Error response from #list_products. + class ListProductsError < ApiError; end + + # https://paystack.com/docs/api/storefront/#list_products + # List Products in Storefront: GET /storefront/{id}/product + # + # + # @return [ListProductsResponse] successful response + # @raise [ListProductsError] if the request fails + api_method def self.list_products + use_connection do |connection| + connection.get( + "/storefront/#{id}/product", + ) + end + end + + # Successful response from calling #add_products. + class AddProductsResponse < PaystackGateway::Response; end + + # Error response from #add_products. + class AddProductsError < ApiError; end + + # https://paystack.com/docs/api/storefront/#add_products + # Add Products to Storefront: POST /storefront/{id}/product + # + # @param products [Array] (required) + # An array of product IDs + # + # @return [AddProductsResponse] successful response + # @raise [AddProductsError] if the request fails + api_method def self.add_products(products:) + use_connection do |connection| + connection.post( + "/storefront/#{id}/product", + { products: }.compact, + ) + end + end + + # Successful response from calling #publish. + class PublishResponse < PaystackGateway::Response; end + + # Error response from #publish. + class PublishError < ApiError; end + + # https://paystack.com/docs/api/storefront/#publish + # Publish Storefront: POST /storefront/{id}/publish + # + # + # @return [PublishResponse] successful response + # @raise [PublishError] if the request fails + api_method def self.publish + use_connection do |connection| + connection.post( + "/storefront/#{id}/publish", + ) + end + end + + # Successful response from calling #duplicate. + class DuplicateResponse < PaystackGateway::Response; end + + # Error response from #duplicate. + class DuplicateError < ApiError; end + + # https://paystack.com/docs/api/storefront/#duplicate + # Duplicate Storefront: POST /storefront/{id}/duplicate + # + # + # @return [DuplicateResponse] successful response + # @raise [DuplicateError] if the request fails + api_method def self.duplicate + use_connection do |connection| + connection.post( + "/storefront/#{id}/duplicate", + ) + end + end + end +end diff --git a/lib/paystack_gateway/subaccount.rb b/lib/paystack_gateway/subaccount.rb new file mode 100644 index 0000000..3f40a67 --- /dev/null +++ b/lib/paystack_gateway/subaccount.rb @@ -0,0 +1,256 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/subaccount + # + # Subaccounts + # A collection of endpoints for creating and managing accounts for sharing a transaction with + module Subaccount + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/subaccount/#list + # List Subaccounts: GET /subaccount + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list(per_page: nil, page: nil, from: nil, to: nil) + use_connection do |connection| + connection.get( + '/subaccount', + { perPage:, page:, from:, to: }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :business_name, + :description, + :primary_contact_name, + :primary_contact_email, + :primary_contact_phone, + :metadata, + :account_number, + :percentage_charge, + :settlement_bank, + :currency, + :bank, + :integration, + :domain, + :managed_by_integration, + :product, + :subaccount_code, + :is_verified, + :settlement_schedule, + :active, + :migrate, + :id, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/subaccount/#create + # Create Subaccount: POST /subaccount + # + # @param business_name [String] (required) + # Name of business for subaccount + # @param settlement_bank [String] (required) + # Bank code for the bank. You can get the list of Bank Codes by calling the List Banks + # endpoint. + # @param account_number [String] (required) + # Bank account number + # @param percentage_charge [Number] (required) + # Customer's phone number + # @param description [String] + # A description for this subaccount + # @param primary_contact_email [String] + # A contact email for the subaccount + # @param primary_contact_name [String] + # The name of the contact person for this subaccount + # @param primary_contact_phone [String] + # A phone number to call for this subaccount + # @param metadata [String] + # Stringified JSON object of custom data + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create( + business_name:, + settlement_bank:, + account_number:, + percentage_charge:, + description: nil, + primary_contact_email: nil, + primary_contact_name: nil, + primary_contact_phone: nil, + metadata: nil + ) + use_connection do |connection| + connection.post( + '/subaccount', + { + business_name:, + settlement_bank:, + account_number:, + percentage_charge:, + description:, + primary_contact_email:, + primary_contact_name:, + primary_contact_phone:, + metadata:, + }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :integration, + :bank, + :managed_by_integration, + :domain, + :subaccount_code, + :business_name, + :description, + :primary_contact_name, + :primary_contact_email, + :primary_contact_phone, + :metadata, + :percentage_charge, + :is_verified, + :settlement_bank, + :account_number, + :settlement_schedule, + :active, + :migrate, + :currency, + :product, + :id, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/subaccount/#fetch + # Fetch Subaccount: GET /subaccount/{code} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/subaccount/#{code}", + ) + end + end + + # Successful response from calling #update. + class UpdateResponse < PaystackGateway::Response + delegate :domain, + :subaccount_code, + :business_name, + :description, + :primary_contact_name, + :primary_contact_email, + :primary_contact_phone, + :metadata, + :percentage_charge, + :is_verified, + :settlement_bank, + :account_number, + :settlement_schedule, + :active, + :migrate, + :currency, + :product, + :id, + :integration, + :bank, + :managed_by_integration, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #update. + class UpdateError < ApiError; end + + # https://paystack.com/docs/api/subaccount/#update + # Update Subaccount: PUT /subaccount/{code} + # + # @param business_name [String] (required) + # Name of business for subaccount + # @param settlement_bank [String] (required) + # Bank code for the bank. You can get the list of Bank Codes by calling the List Banks + # endpoint. + # @param account_number [String] (required) + # Bank account number + # @param active [Boolean] (required) + # Activate or deactivate a subaccount + # @param percentage_charge [Number] (required) + # Customer's phone number + # @param description [String] (required) + # A description for this subaccount + # @param primary_contact_email [String] (required) + # A contact email for the subaccount + # @param primary_contact_name [String] (required) + # The name of the contact person for this subaccount + # @param primary_contact_phone [String] (required) + # A phone number to call for this subaccount + # @param metadata [String] (required) + # Stringified JSON object of custom data + # + # @return [UpdateResponse] successful response + # @raise [UpdateError] if the request fails + api_method def self.update( + business_name:, + settlement_bank:, + account_number:, + active:, + percentage_charge:, + description:, + primary_contact_email:, + primary_contact_name:, + primary_contact_phone:, + metadata: + ) + use_connection do |connection| + connection.put( + "/subaccount/#{code}", + { + business_name:, + settlement_bank:, + account_number:, + active:, + percentage_charge:, + description:, + primary_contact_email:, + primary_contact_name:, + primary_contact_phone:, + metadata:, + }.compact, + ) + end + end + end +end diff --git a/lib/paystack_gateway/subscription.rb b/lib/paystack_gateway/subscription.rb new file mode 100644 index 0000000..b1a3096 --- /dev/null +++ b/lib/paystack_gateway/subscription.rb @@ -0,0 +1,241 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/subscription + # + # Subscriptions + # A collection of endpoints for creating and managing recurring payments + module Subscription + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/subscription/#list + # List Subscriptions: GET /subscription + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param plan [String] + # Plan ID + # @param customer [String] + # Customer ID + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list( + per_page: nil, + page: nil, + plan: nil, + customer: nil, + from: nil, + to: nil + ) + use_connection do |connection| + connection.get( + '/subscription', + { + perPage: per_page, + page:, + plan:, + customer:, + from:, + to:, + }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :customer, + :plan, + :integration, + :domain, + :start, + :quantity, + :amount, + :authorization, + :invoice_limit, + :split_code, + :subscription_code, + :email_token, + :id, + :cancelledAt, + :createdAt, + :updatedAt, + :cron_expression, + :next_payment_date, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/subscription/#create + # Create Subscription: POST /subscription + # + # @param customer [String] (required) + # Customer's email address or customer code + # @param plan [String] (required) + # Plan code + # @param authorization [String] + # If customer has multiple authorizations, you can set the desired authorization you + # wish to use for this subscription here. If this is not supplied, the customer's + # most recent authorization would be used + # @param start_date [Time] + # Set the date for the first debit. (ISO 8601 format) e.g. 2017-05-16T00:30:13+01:00 + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create(customer:, plan:, authorization: nil, start_date: nil) + use_connection do |connection| + connection.post( + '/subscription', + { customer:, plan:, authorization:, start_date: }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :id, + :domain, + :subscription_code, + :email_token, + :amount, + :cron_expression, + :next_payment_date, + :open_invoice, + :createdAt, + :cancelledAt, + :integration, + :plan, + :authorization, + :customer, + :invoices, + :invoices_history, + :invoice_limit, + :split_code, + :most_recent_invoice, + :payments_count, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/subscription/#fetch + # Fetch Subscription: GET /subscription/{code} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/subscription/#{code}", + ) + end + end + + # Successful response from calling #disable. + class DisableResponse < PaystackGateway::Response; end + + # Error response from #disable. + class DisableError < ApiError; end + + # https://paystack.com/docs/api/subscription/#disable + # Disable Subscription: POST /subscription/disable + # + # @param code [String] (required) + # Subscription code + # @param token [String] (required) + # Email token + # + # @return [DisableResponse] successful response + # @raise [DisableError] if the request fails + api_method def self.disable(code:, token:) + use_connection do |connection| + connection.post( + '/subscription/disable', + { code:, token: }.compact, + ) + end + end + + # Successful response from calling #enable. + class EnableResponse < PaystackGateway::Response; end + + # Error response from #enable. + class EnableError < ApiError; end + + # https://paystack.com/docs/api/subscription/#enable + # Enable Subscription: POST /subscription/enable + # + # @param code [String] (required) + # Subscription code + # @param token [String] (required) + # Email token + # + # @return [EnableResponse] successful response + # @raise [EnableError] if the request fails + api_method def self.enable(code:, token:) + use_connection do |connection| + connection.post( + '/subscription/enable', + { code:, token: }.compact, + ) + end + end + + # Successful response from calling #manage_link. + class ManageLinkResponse < PaystackGateway::Response; end + + # Error response from #manage_link. + class ManageLinkError < ApiError; end + + # https://paystack.com/docs/api/subscription/#manage_link + # Generate Update Subscription Link: GET /subscription/{code}/manage/link + # + # @param code [String] (required) + # + # @return [ManageLinkResponse] successful response + # @raise [ManageLinkError] if the request fails + api_method def self.manage_link(code:) + use_connection do |connection| + connection.get( + "/subscription/#{code}/manage/link", + ) + end + end + + # Successful response from calling #manage_email. + class ManageEmailResponse < PaystackGateway::Response; end + + # Error response from #manage_email. + class ManageEmailError < ApiError; end + + # https://paystack.com/docs/api/subscription/#manage_email + # Send Update Subscription Link: POST /subscription/{code}/manage/email + # + # @param code [String] (required) + # + # @return [ManageEmailResponse] successful response + # @raise [ManageEmailError] if the request fails + api_method def self.manage_email(code:) + use_connection do |connection| + connection.post( + "/subscription/#{code}/manage/email", + ) + end + end + end +end diff --git a/lib/paystack_gateway/terminal.rb b/lib/paystack_gateway/terminal.rb new file mode 100644 index 0000000..e0ca517 --- /dev/null +++ b/lib/paystack_gateway/terminal.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/terminal + # + # Terminal + # A collection of endpoints for building delightful in-person payment experiences + module Terminal + include PaystackGateway::RequestModule + + # Successful response from calling #send_event. + class SendEventResponse < PaystackGateway::Response; end + + # Error response from #send_event. + class SendEventError < ApiError; end + + # https://paystack.com/docs/api/terminal/#send_event + # Send Event: POST /terminal/{id}/event + # Send an event from your application to the Paystack Terminal + # + # @param id [String] (required) + # @param type ["invoice", "transaction"] (required) + # The type of event to push + # @param action ["process", "view", "print"] (required) + # The action the Terminal needs to perform. For the invoice type, the action can either + # be process or view. For the transaction type, the action can either be process or + # print. + # @param data [Hash] (required) + # The parameters needed to perform the specified action + # @option data [Integer] :id + # The invoice or transaction ID you want to push to the Terminal + # @option data [String] :reference + # The offline_reference from the Payment Request response + # + # @return [SendEventResponse] successful response + # @raise [SendEventError] if the request fails + api_method def self.send_event(id:, type:, action:, data:) + use_connection do |connection| + connection.post( + "/terminal/#{id}/event", + { type:, action:, data: }.compact, + ) + end + end + + # Successful response from calling #fetch_event_status. + class FetchEventStatusResponse < PaystackGateway::Response; end + + # Error response from #fetch_event_status. + class FetchEventStatusError < ApiError; end + + # https://paystack.com/docs/api/terminal/#fetch_event_status + # Fetch Event Status: GET /terminal/{terminal_id}/event/{event_id} + # Check the status of an event sent to the Terminal + # + # @param terminal_id [String] (required) + # @param event_id [String] (required) + # + # @return [FetchEventStatusResponse] successful response + # @raise [FetchEventStatusError] if the request fails + api_method def self.fetch_event_status(terminal_id:, event_id:) + use_connection do |connection| + connection.get( + "/terminal/#{terminal_id}/event/#{event_id}", + ) + end + end + + # Successful response from calling #fetch_terminal_status. + class FetchTerminalStatusResponse < PaystackGateway::Response + delegate :online, :available, to: :data + end + + # Error response from #fetch_terminal_status. + class FetchTerminalStatusError < ApiError; end + + # https://paystack.com/docs/api/terminal/#fetch_terminal_status + # Fetch Terminal Status: GET /terminal/{terminal_id}/presence + # Check the availiability of a Terminal before sending an event to it + # + # @param terminal_id [String] (required) + # + # @return [FetchTerminalStatusResponse] successful response + # @raise [FetchTerminalStatusError] if the request fails + api_method def self.fetch_terminal_status(terminal_id:) + use_connection do |connection| + connection.get( + "/terminal/#{terminal_id}/presence", + ) + end + end + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/terminal/#list + # List Terminals: GET /terminal + # List the Terminals available on your integration + # + # @param next [String] + # @param previous [String] + # @param per_page [String] + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list(next: nil, previous: nil, per_page: nil) + use_connection do |connection| + connection.get( + '/terminal', + { next:, previous:, per_page: }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :id, + :serial_number, + :device_make, + :terminal_id, + :integration, + :domain, + :name, + :address, + :split_code, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/terminal/#fetch + # Fetch Terminal: GET /terminal/{terminal_id} + # Get the details of a Terminal + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/terminal/#{terminal_id}", + ) + end + end + + # Successful response from calling #update. + class UpdateResponse < PaystackGateway::Response; end + + # Error response from #update. + class UpdateError < ApiError; end + + # https://paystack.com/docs/api/terminal/#update + # Update Terminal: PUT /terminal/{terminal_id} + # + # @param name [String] (required) + # The new name for the Terminal + # @param address [String] (required) + # The new address for the Terminal + # + # @return [UpdateResponse] successful response + # @raise [UpdateError] if the request fails + api_method def self.update(name:, address:) + use_connection do |connection| + connection.put( + "/terminal/#{terminal_id}", + { name:, address: }.compact, + ) + end + end + + # Successful response from calling #commission. + class CommissionResponse < PaystackGateway::Response; end + + # Error response from #commission. + class CommissionError < ApiError; end + + # https://paystack.com/docs/api/terminal/#commission + # Commission Terminal: POST /terminal/commission_device + # Activate your debug device by linking it to your integration + # + # @param serial_number [String] (required) + # Device Serial Number + # + # @return [CommissionResponse] successful response + # @raise [CommissionError] if the request fails + api_method def self.commission(serial_number:) + use_connection do |connection| + connection.post( + '/terminal/commission_device', + { serial_number: }.compact, + ) + end + end + + # Successful response from calling #decommission. + class DecommissionResponse < PaystackGateway::Response; end + + # Error response from #decommission. + class DecommissionError < ApiError; end + + # https://paystack.com/docs/api/terminal/#decommission + # Decommission Terminal: POST /terminal/decommission_device + # Unlink your debug device from your integration + # + # @param serial_number [String] (required) + # Device Serial Number + # + # @return [DecommissionResponse] successful response + # @raise [DecommissionError] if the request fails + api_method def self.decommission(serial_number:) + use_connection do |connection| + connection.post( + '/terminal/decommission_device', + { serial_number: }.compact, + ) + end + end + end +end diff --git a/lib/paystack_gateway/transaction.rb b/lib/paystack_gateway/transaction.rb new file mode 100644 index 0000000..0e88cd5 --- /dev/null +++ b/lib/paystack_gateway/transaction.rb @@ -0,0 +1,604 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/transaction + # + # Transactions + # A collection of endpoints for managing payments + module Transaction + include PaystackGateway::RequestModule + + # Successful response from calling #initialize. + class InitializeResponse < PaystackGateway::Response + delegate :authorization_url, :access_code, :reference, to: :data + end + + # Error response from #initialize. + class InitializeError < ApiError; end + + # https://paystack.com/docs/api/transaction/#initialize + # Initialize Transaction: POST /transaction/initialize + # Create a new transaction + # + # @param email [String] (required) + # Customer's email address + # @param amount [Integer] (required) + # Amount should be in smallest denomination of the currency. For example, kobo, if + # currency is NGN, pesewas, if currency is GHS, and cents, if currency is ZAR + # @param currency ["GHS", "KES", "NGN", "ZAR", "USD"] + # List of all support currencies + # @param reference [String] + # Unique transaction reference. Only -, ., = and alphanumeric characters allowed. + # @param channels [Array<"card", "bank", "ussd", "qr", "eft", "mobile_money", "bank_transfer">] + # An array of payment channels to control what channels you want to make available + # to the user to make a payment with + # @param callback_url [String] + # Fully qualified url, e.g. https://example.com/ to redirect your customers to after + # a successful payment. Use this to override the callback url provided on the dashboard + # for this transaction + # @param plan [String] + # If transaction is to create a subscription to a predefined plan, provide plan code + # here. This would invalidate the value provided in amount + # @param invoice_limit [Integer] + # Number of times to charge customer during subscription to plan + # @param split_code [String] + # The split code of the transaction split + # @param split [Hash] + # Split configuration for transactions + # @option split [String] :name + # Name of the transaction split + # @option split ["percentage", "flat"] :type + # The type of transaction split you want to create. + # @option split [Array] :subaccounts + # A list of object containing subaccount code and number of shares + # @option split ["NGN", "GHS", "ZAR", "USD"] :currency + # The transaction currency + # @option split ["subaccount", "account", "all-proportional", "all"] :bearer_type + # This allows you specify how the transaction charge should be processed + # @option split [String] :bearer_subaccount + # This is the subaccount code of the customer or partner that would bear the transaction + # charge if you specified subaccount as the bearer type + # @param subaccount [String] + # The code for the subaccount that owns the payment + # @param transaction_charge [String] + # A flat fee to charge the subaccount for a transaction. This overrides the split + # percentage set when the subaccount was created + # @param bearer ["account", "subaccount"] + # The bearer of the transaction charge + # @param label [String] + # Used to replace the email address shown on the Checkout + # @param metadata [String] + # Stringified JSON object of custom data + # + # @return [InitializeResponse] successful response + # @raise [InitializeError] if the request fails + api_method def self.initialize( + email:, + amount:, + currency: nil, + reference: nil, + channels: nil, + callback_url: nil, + plan: nil, + invoice_limit: nil, + split_code: nil, + split: nil, + subaccount: nil, + transaction_charge: nil, + bearer: nil, + label: nil, + metadata: nil + ) + use_connection do |connection| + connection.post( + '/transaction/initialize', + { + email:, + amount:, + currency:, + reference:, + channels:, + callback_url:, + plan:, + invoice_limit:, + split_code:, + split:, + subaccount:, + transaction_charge:, + bearer:, + label:, + metadata:, + }.compact, + ) + end + end + + # Successful response from calling #verify. + class VerifyResponse < PaystackGateway::Response + delegate :id, + :domain, + :reference, + :receipt_number, + :amount, + :gateway_response, + :paid_at, + :created_at, + :channel, + :currency, + :ip_address, + :metadata, + :log, + :fees, + :fees_split, + :authorization, + :customer, + :plan, + :split, + :order_id, + :paidAt, + :createdAt, + :requested_amount, + :pos_transaction_data, + :source, + :fees_breakdown, + :connect, + :transaction_date, + :plan_object, + :subaccount, to: :data + end + + # Error response from #verify. + class VerifyError < ApiError; end + + # https://paystack.com/docs/api/transaction/#verify + # Verify Transaction: GET /transaction/verify/{reference} + # Verify a previously initiated transaction using it's reference + # + # @param reference [String] (required) + # The transaction reference to verify + # + # @return [VerifyResponse] successful response + # @raise [VerifyError] if the request fails + api_method def self.verify(reference:) + use_connection do |connection| + connection.get( + "/transaction/verify/#{reference}", + ) + end + end + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/transaction/#list + # List Transactions: GET /transaction + # List transactions that has occurred on your integration + # + # @param use_cursor [Boolean] + # A flag to indicate if cursor based pagination should be used + # @param next [String] + # An alphanumeric value returned for every cursor based retrieval, used to retrieve + # the next set of data + # @param previous [String] + # An alphanumeric value returned for every cursor based retrieval, used to retrieve + # the previous set of data + # @param per_page [Integer] + # The number of records to fetch per request + # @param page [Integer] + # Used to indicate the offeset to retrieve data from + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # @param channel ["card", "pos", "bank", "dedicated_nuban", "ussd", "bank_transfer"] + # The payment method the customer used to complete the transaction + # @param terminal_id [String] + # The terminal ID to filter all transactions from a terminal + # @param customer_code [String] + # The customer code to filter all transactions from a customer + # @param amount [Integer] + # Filter transactions by a certain amount + # @param status ["success", "failed", "abandoned", "reversed"] + # Filter transaction by status + # @param source ["merchantApi", "checkout", "pos", "virtualTerminal"] + # The origin of the payment + # @param subaccount_code [String] + # Filter transaction by subaccount code + # @param split_code [String] + # Filter transaction by split code + # @param settlement [Integer] + # The settlement ID to filter for settled transactions + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list( + use_cursor: nil, + next: nil, + previous: nil, + per_page: nil, + page: nil, + from: nil, + to: nil, + channel: nil, + terminal_id: nil, + customer_code: nil, + amount: nil, + status: nil, + source: nil, + subaccount_code: nil, + split_code: nil, + settlement: nil + ) + use_connection do |connection| + connection.get( + '/transaction', + { + use_cursor:, + next:, + previous:, + per_page:, + page:, + from:, + to:, + channel:, + terminal_id:, + customer_code:, + amount:, + status:, + source:, + subaccount_code:, + split_code:, + settlement:, + }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :id, + :domain, + :reference, + :receipt_number, + :amount, + :gateway_response, + :helpdesk_link, + :paid_at, + :created_at, + :channel, + :currency, + :ip_address, + :metadata, + :log, + :fees, + :fees_split, + :authorization, + :customer, + :plan, + :subaccount, + :split, + :order_id, + :paidAt, + :createdAt, + :requested_amount, + :pos_transaction_data, + :source, + :fees_breakdown, + :connect, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/transaction/#fetch + # Fetch Transaction: GET /transaction/{id} + # Fetch a transaction to get its details + # + # @param id [String] (required) + # The ID of the transaction to fetch + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch(id:) + use_connection do |connection| + connection.get( + "/transaction/#{id}", + ) + end + end + + # Successful response from calling #timeline. + class TimelineResponse < PaystackGateway::Response; end + + # Error response from #timeline. + class TimelineError < ApiError; end + + # https://paystack.com/docs/api/transaction/#timeline + # Fetch Transaction Timeline: GET /transaction/timeline/{id} + # Get the details about the lifecycle of a transaction from initiation to completion + # + # @param id [Integer] (required) + # + # @return [TimelineResponse] successful response + # @raise [TimelineError] if the request fails + api_method def self.timeline(id:) + use_connection do |connection| + connection.get( + "/transaction/timeline/#{id}", + ) + end + end + + # Successful response from calling #totals. + class TotalsResponse < PaystackGateway::Response + delegate :total_transactions, + :total_volume, + :total_volume_by_currency, + :pending_transfers, + :pending_transfers_by_currency, to: :data + end + + # Error response from #totals. + class TotalsError < ApiError; end + + # https://paystack.com/docs/api/transaction/#totals + # Transaction Totals: GET /transaction/totals + # Get the total amount of all transactions + # + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [TotalsResponse] successful response + # @raise [TotalsError] if the request fails + api_method def self.totals(from: nil, to: nil) + use_connection do |connection| + connection.get( + '/transaction/totals', + { from:, to: }.compact, + ) + end + end + + # Successful response from calling #download. + class DownloadResponse < PaystackGateway::Response + delegate :path, :expiresAt, to: :data + end + + # Error response from #download. + class DownloadError < ApiError; end + + # https://paystack.com/docs/api/transaction/#download + # Export Transactions: GET /transaction/export + # + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # @param status ["success", "failed", "abandoned", "reversed", "all"] + # Filter by the status of the transaction + # @param customer [String] + # Filter by customer code + # @param subaccount_code [String] + # Filter by subaccount code + # @param settlement [Integer] + # Filter by the settlement ID + # + # @return [DownloadResponse] successful response + # @raise [DownloadError] if the request fails + api_method def self.download( + from: nil, + to: nil, + status: nil, + customer: nil, + subaccount_code: nil, + settlement: nil + ) + use_connection do |connection| + connection.get( + '/transaction/export', + { + from:, + to:, + status:, + customer:, + subaccount_code:, + settlement:, + }.compact, + ) + end + end + + # Successful response from calling #charge_authorization. + class ChargeAuthorizationResponse < PaystackGateway::Response + delegate :amount, + :currency, + :transaction_date, + :reference, + :domain, + :redirect_url, + :metadata, + :gateway_response, + :channel, + :fees, + :authorization, + :customer, to: :data + end + + # Error response from #charge_authorization. + class ChargeAuthorizationError < ApiError; end + + # https://paystack.com/docs/api/transaction/#charge_authorization + # Charge Authorization: POST /transaction/charge_authorization + # + # @param email [String] (required) + # Customer's email address + # @param amount [Integer] (required) + # Amount in the lower denomination of your currency + # @param authorization_code [String] (required) + # Valid authorization code to charge + # @param reference [String] + # Unique transaction reference. Only -, ., = and alphanumeric characters allowed. + # @param currency ["GHS", "KES", "NGN", "ZAR", "USD"] + # List of all support currencies + # @param split_code [String] + # The split code of the transaction split + # @param subaccount [String] + # The code for the subaccount that owns the payment + # @param transaction_charge [String] + # A flat fee to charge the subaccount for a transaction. This overrides the split + # percentage set when the subaccount was created + # @param bearer ["account", "subaccount"] + # The bearer of the transaction charge + # @param metadata [String] + # Stringified JSON object of custom data + # @param queue [Boolean] + # If you are making a scheduled charge call, it is a good idea to queue them so the + # processing system does not get overloaded causing transaction processing errors. + # + # @return [ChargeAuthorizationResponse] successful response + # @raise [ChargeAuthorizationError] if the request fails + api_method def self.charge_authorization( + email:, + amount:, + authorization_code:, + reference: nil, + currency: nil, + split_code: nil, + subaccount: nil, + transaction_charge: nil, + bearer: nil, + metadata: nil, + queue: nil + ) + use_connection do |connection| + connection.post( + '/transaction/charge_authorization', + { + email:, + amount:, + authorization_code:, + reference:, + currency:, + split_code:, + subaccount:, + transaction_charge:, + bearer:, + metadata:, + queue:, + }.compact, + ) + end + end + + # Successful response from calling #partial_debit. + class PartialDebitResponse < PaystackGateway::Response + delegate :amount, + :currency, + :transaction_date, + :reference, + :domain, + :gateway_response, + :channel, + :ip_address, + :log, + :fees, + :authorization, + :customer, + :metadata, + :plan, + :requested_amount, + :id, to: :data + end + + # Error response from #partial_debit. + class PartialDebitError < ApiError; end + + # https://paystack.com/docs/api/transaction/#partial_debit + # Partial Debit: POST /transaction/partial_debit + # + # @param email [String] (required) + # Customer's email address + # @param amount [Integer] (required) + # Specified in the lowest denomination of your currency + # @param authorization_code [String] (required) + # Valid authorization code to charge + # @param currency ["GHS", "KES", "NGN", "ZAR", "USD"] (required) + # List of all support currencies + # @param at_least [String] + # Minimum amount to charge + # @param reference [String] + # Unique transaction reference. Only -, ., = and alphanumeric characters allowed. + # + # @return [PartialDebitResponse] successful response + # @raise [PartialDebitError] if the request fails + api_method def self.partial_debit( + email:, + amount:, + authorization_code:, + currency:, + at_least: nil, + reference: nil + ) + use_connection do |connection| + connection.post( + '/transaction/partial_debit', + { + email:, + amount:, + authorization_code:, + currency:, + at_least:, + reference:, + }.compact, + ) + end + end + + # Successful response from calling #event. + class EventResponse < PaystackGateway::Response; end + + # Error response from #event. + class EventError < ApiError; end + + # https://paystack.com/docs/api/transaction/#event + # Get Transaction Event: GET /transaction/{id}/event + # + # @param id [Integer] (required) + # + # @return [EventResponse] successful response + # @raise [EventError] if the request fails + api_method def self.event(id:) + use_connection do |connection| + connection.get( + "/transaction/#{id}/event", + ) + end + end + + # Successful response from calling #session. + class SessionResponse < PaystackGateway::Response; end + + # Error response from #session. + class SessionError < ApiError; end + + # https://paystack.com/docs/api/transaction/#session + # Get Transaction Session: GET /transaction/{id}/session + # + # @param id [Integer] (required) + # + # @return [SessionResponse] successful response + # @raise [SessionError] if the request fails + api_method def self.session(id:) + use_connection do |connection| + connection.get( + "/transaction/#{id}/session", + ) + end + end + end +end diff --git a/lib/paystack_gateway/transfer.rb b/lib/paystack_gateway/transfer.rb new file mode 100644 index 0000000..101a49d --- /dev/null +++ b/lib/paystack_gateway/transfer.rb @@ -0,0 +1,358 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/transfer + # + # Transfers + # A collection of endpoints for automating sending money to beneficiaries + module Transfer + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/transfer/#list + # List Transfers: GET /transfer + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param status [String] + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list(per_page: nil, page: nil, status: nil, from: nil, to: nil) + use_connection do |connection| + connection.get( + '/transfer', + { perPage:, page:, status:, from:, to: }.compact, + ) + end + end + + # Successful response from calling #initiate. + class InitiateResponse < PaystackGateway::Response + delegate :transfersessionid, + :domain, + :amount, + :currency, + :reference, + :source, + :source_details, + :reason, + :failures, + :transfer_code, + :titan_code, + :transferred_at, + :id, + :integration, + :request, + :recipient, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #initiate. + class InitiateError < ApiError; end + + # https://paystack.com/docs/api/transfer/#initiate + # Initiate Transfer: POST /transfer + # + # @param source [String] (required) + # Where should we transfer from? Only balance is allowed for now + # @param amount [String] (required) + # Amount to transfer in kobo if currency is NGN and pesewas if currency is GHS. + # @param recipient [String] (required) + # The transfer recipient's code + # @param reason [String] + # The reason or narration for the transfer. + # @param currency [String] + # Specify the currency of the transfer. Defaults to NGN. + # @param reference [String] + # If specified, the field should be a unique identifier (in lowercase) for the object. + # Only -,_ and alphanumeric characters are allowed. + # + # @return [InitiateResponse] successful response + # @raise [InitiateError] if the request fails + api_method def self.initiate( + source:, + amount:, + recipient:, + reason: nil, + currency: nil, + reference: nil + ) + use_connection do |connection| + connection.post( + '/transfer', + { + source:, + amount:, + recipient:, + reason:, + currency:, + reference:, + }.compact, + ) + end + end + + # Successful response from calling #finalize. + class FinalizeResponse < PaystackGateway::Response; end + + # Error response from #finalize. + class FinalizeError < ApiError; end + + # https://paystack.com/docs/api/transfer/#finalize + # Finalize Transfer: POST /transfer/finalize_transfer + # + # @param transfer_code [String] (required) + # The transfer code you want to finalize + # @param otp [String] (required) + # OTP sent to business phone to verify transfer + # + # @return [FinalizeResponse] successful response + # @raise [FinalizeError] if the request fails + api_method def self.finalize(transfer_code:, otp:) + use_connection do |connection| + connection.post( + '/transfer/finalize_transfer', + { transfer_code:, otp: }.compact, + ) + end + end + + # Successful response from calling #bulk. + class BulkResponse < PaystackGateway::Response; end + + # Error response from #bulk. + class BulkError < ApiError; end + + # https://paystack.com/docs/api/transfer/#bulk + # Initiate Bulk Transfer: POST /transfer/bulk + # + # @param source [String] (required) + # Where should we transfer from? Only balance is allowed for now + # @param transfers [Array] (required) + # A list of transfer object. Each object should contain amount, recipient, and reference + # + # @return [BulkResponse] successful response + # @raise [BulkError] if the request fails + api_method def self.bulk(source:, transfers:) + use_connection do |connection| + connection.post( + '/transfer/bulk', + { source:, transfers: }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :amount, + :createdAt, + :currency, + :domain, + :failures, + :id, + :integration, + :reason, + :reference, + :source, + :source_details, + :titan_code, + :transfer_code, + :request, + :transferred_at, + :updatedAt, + :recipient, + :session, + :fee_charged, + :fees_breakdown, + :gateway_response, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/transfer/#fetch + # Fetch Transfer: GET /transfer/{code} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/transfer/#{code}", + ) + end + end + + # Successful response from calling #verify. + class VerifyResponse < PaystackGateway::Response + delegate :amount, + :createdAt, + :currency, + :domain, + :failures, + :id, + :integration, + :reason, + :reference, + :source, + :source_details, + :titan_code, + :transfer_code, + :transferred_at, + :updatedAt, + :recipient, + :session, + :gateway_response, to: :data + end + + # Error response from #verify. + class VerifyError < ApiError; end + + # https://paystack.com/docs/api/transfer/#verify + # Verify Transfer: GET /transfer/verify/{reference} + # + # @param reference [String] (required) + # + # @return [VerifyResponse] successful response + # @raise [VerifyError] if the request fails + api_method def self.verify(reference:) + use_connection do |connection| + connection.get( + "/transfer/verify/#{reference}", + ) + end + end + + # Successful response from calling #download. + class DownloadResponse < PaystackGateway::Response; end + + # Error response from #download. + class DownloadError < ApiError; end + + # https://paystack.com/docs/api/transfer/#download + # Export Transfers: GET /transfer/export + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param status [String] + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [DownloadResponse] successful response + # @raise [DownloadError] if the request fails + api_method def self.download(per_page: nil, page: nil, status: nil, from: nil, to: nil) + use_connection do |connection| + connection.get( + '/transfer/export', + { perPage:, page:, status:, from:, to: }.compact, + ) + end + end + + # Successful response from calling #resend_otp. + class ResendOtpResponse < PaystackGateway::Response; end + + # Error response from #resend_otp. + class ResendOtpError < ApiError; end + + # https://paystack.com/docs/api/transfer/#resend_otp + # Resend OTP for Transfer: POST /transfer/resend_otp + # + # @param transfer_code [String] (required) + # The transfer code that requires an OTP validation + # @param reason [String] (required) + # Either resend_otp or transfer + # + # @return [ResendOtpResponse] successful response + # @raise [ResendOtpError] if the request fails + api_method def self.resend_otp(transfer_code:, reason:) + use_connection do |connection| + connection.post( + '/transfer/resend_otp', + { transfer_code:, reason: }.compact, + ) + end + end + + # Successful response from calling #disable_otp. + class DisableOtpResponse < PaystackGateway::Response; end + + # Error response from #disable_otp. + class DisableOtpError < ApiError; end + + # https://paystack.com/docs/api/transfer/#disable_otp + # Disable OTP requirement for Transfers: POST /transfer/disable_otp + # + # + # @return [DisableOtpResponse] successful response + # @raise [DisableOtpError] if the request fails + api_method def self.disable_otp + use_connection do |connection| + connection.post( + '/transfer/disable_otp', + ) + end + end + + # Successful response from calling #disable_otp_finalize. + class DisableOtpFinalizeResponse < PaystackGateway::Response; end + + # Error response from #disable_otp_finalize. + class DisableOtpFinalizeError < ApiError; end + + # https://paystack.com/docs/api/transfer/#disable_otp_finalize + # Finalize Disabling of OTP requirement for Transfers: POST /transfer/disable_otp_finalize + # + # @param otp [String] (required) + # OTP sent to business phone to verify disabling OTP requirement + # + # @return [DisableOtpFinalizeResponse] successful response + # @raise [DisableOtpFinalizeError] if the request fails + api_method def self.disable_otp_finalize(otp:) + use_connection do |connection| + connection.post( + '/transfer/disable_otp_finalize', + { otp: }.compact, + ) + end + end + + # Successful response from calling #enable_otp. + class EnableOtpResponse < PaystackGateway::Response; end + + # Error response from #enable_otp. + class EnableOtpError < ApiError; end + + # https://paystack.com/docs/api/transfer/#enable_otp + # Enable OTP requirement for Transfers: POST /transfer/enable_otp + # + # + # @return [EnableOtpResponse] successful response + # @raise [EnableOtpError] if the request fails + api_method def self.enable_otp + use_connection do |connection| + connection.post( + '/transfer/enable_otp', + ) + end + end + end +end diff --git a/lib/paystack_gateway/transfer_recipient.rb b/lib/paystack_gateway/transfer_recipient.rb new file mode 100644 index 0000000..9e36b60 --- /dev/null +++ b/lib/paystack_gateway/transfer_recipient.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +module PaystackGateway + # https://paystack.com/docs/api/transferrecipient + # + # Transfer Recipients + # A collection of endpoints for creating and managing beneficiaries that you send money to + module TransferRecipient + include PaystackGateway::RequestModule + + # Successful response from calling #list. + class ListResponse < PaystackGateway::Response; end + + # Error response from #list. + class ListError < ApiError; end + + # https://paystack.com/docs/api/transfer-recipient/#list + # List Transfer Recipients: GET /transferrecipient + # + # @param perPage [Integer] + # Number of records to fetch per page + # @param page [Integer] + # The section to retrieve + # @param from [Time] + # The start date + # @param to [Time] + # The end date + # + # @return [ListResponse] successful response + # @raise [ListError] if the request fails + api_method def self.list(per_page: nil, page: nil, from: nil, to: nil) + use_connection do |connection| + connection.get( + '/transferrecipient', + { perPage:, page:, from:, to: }.compact, + ) + end + end + + # Successful response from calling #create. + class CreateResponse < PaystackGateway::Response + delegate :active, + :createdAt, + :currency, + :description, + :domain, + :email, + :id, + :integration, + :metadata, + :name, + :recipient_code, + :type, + :updatedAt, + :is_deleted, + :isDeleted, + :details, to: :data + end + + # Error response from #create. + class CreateError < ApiError; end + + # https://paystack.com/docs/api/transfer-recipient/#create + # Create Transfer Recipient: POST /transferrecipient + # + # @param type [String] (required) + # Recipient Type (Only nuban at this time) + # @param name [String] (required) + # Recipient's name + # @param account_number [String] (required) + # Recipient's bank account number + # @param bank_code [String] (required) + # Recipient's bank code. You can get the list of Bank Codes by calling the List Banks + # endpoint + # @param description [String] + # A description for this recipient + # @param currency [String] + # Currency for the account receiving the transfer + # @param authorization_code [String] + # An authorization code from a previous transaction + # @param metadata [String] + # Stringified JSON object of custom data + # + # @return [CreateResponse] successful response + # @raise [CreateError] if the request fails + api_method def self.create( + type:, + name:, + account_number:, + bank_code:, + description: nil, + currency: nil, + authorization_code: nil, + metadata: nil + ) + use_connection do |connection| + connection.post( + '/transferrecipient', + { + type:, + name:, + account_number:, + bank_code:, + description:, + currency:, + authorization_code:, + metadata:, + }.compact, + ) + end + end + + # Successful response from calling #bulk. + class BulkResponse < PaystackGateway::Response + delegate :success, :errors, to: :data + end + + # Error response from #bulk. + class BulkError < ApiError; end + + # https://paystack.com/docs/api/transfer-recipient/#bulk + # Bulk Create Transfer Recipient: POST /transferrecipient/bulk + # + # @param batch [Array] (required) + # A list of transfer recipient object. Each object should contain type, name, and + # bank_code. Any Create Transfer Recipient param can also be passed. + # + # @return [BulkResponse] successful response + # @raise [BulkError] if the request fails + api_method def self.bulk(batch:) + use_connection do |connection| + connection.post( + '/transferrecipient/bulk', + { batch: }.compact, + ) + end + end + + # Successful response from calling #fetch. + class FetchResponse < PaystackGateway::Response + delegate :integration, + :domain, + :type, + :currency, + :name, + :details, + :description, + :metadata, + :recipient_code, + :active, + :recipient_account, + :institution_code, + :email, + :id, + :isDeleted, + :createdAt, + :updatedAt, to: :data + end + + # Error response from #fetch. + class FetchError < ApiError; end + + # https://paystack.com/docs/api/transfer-recipient/#fetch + # Fetch Transfer recipient: GET /transferrecipient/{code} + # + # + # @return [FetchResponse] successful response + # @raise [FetchError] if the request fails + api_method def self.fetch + use_connection do |connection| + connection.get( + "/transferrecipient/#{code}", + ) + end + end + + # Successful response from calling #update. + class UpdateResponse < PaystackGateway::Response; end + + # Error response from #update. + class UpdateError < ApiError; end + + # https://paystack.com/docs/api/transfer-recipient/#update + # Update Transfer recipient: PUT /transferrecipient/{code} + # + # @param name [String] (required) + # Recipient's name + # @param email [String] (required) + # Recipient's email address + # + # @return [UpdateResponse] successful response + # @raise [UpdateError] if the request fails + api_method def self.update(name:, email:) + use_connection do |connection| + connection.put( + "/transferrecipient/#{code}", + { name:, email: }.compact, + ) + end + end + + # Successful response from calling #delete. + class DeleteResponse < PaystackGateway::Response; end + + # Error response from #delete. + class DeleteError < ApiError; end + + # https://paystack.com/docs/api/transfer-recipient/#delete + # Delete Transfer Recipient: DELETE /transferrecipient/{code} + # + # + # @return [DeleteResponse] successful response + # @raise [DeleteError] if the request fails + api_method def self.delete + use_connection do |connection| + connection.delete( + "/transferrecipient/#{code}", + ) + end + end + end +end From d1dde30a93aae060ad26804ed5a6463d8b16c31e Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 01:09:50 +0100 Subject: [PATCH 13/26] Update camelcase request params --- lib/paystack_gateway/apple_pay.rb | 4 ++-- lib/paystack_gateway/balance.rb | 2 +- lib/paystack_gateway/bulk_charge.rb | 2 +- lib/paystack_gateway/dispute.rb | 2 +- lib/paystack_gateway/order.rb | 2 +- lib/paystack_gateway/page.rb | 2 +- lib/paystack_gateway/product.rb | 2 +- lib/paystack_gateway/refund.rb | 2 +- lib/paystack_gateway/settlement.rb | 2 +- lib/paystack_gateway/storefront.rb | 2 +- lib/paystack_gateway/subaccount.rb | 2 +- lib/paystack_gateway/transfer.rb | 4 ++-- lib/paystack_gateway/transfer_recipient.rb | 2 +- 13 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/paystack_gateway/apple_pay.rb b/lib/paystack_gateway/apple_pay.rb index 19688d9..5c3b265 100644 --- a/lib/paystack_gateway/apple_pay.rb +++ b/lib/paystack_gateway/apple_pay.rb @@ -52,7 +52,7 @@ class RegisterDomainError < ApiError; end use_connection do |connection| connection.post( '/apple-pay/domain', - { domainName: }.compact, + { domainName: domain_name }.compact, ) end end @@ -76,7 +76,7 @@ class UnregisterDomainError < ApiError; end use_connection do |connection| connection.delete( '/apple-pay/domain', - { domainName: }.compact, + { domainName: domain_name }.compact, ) end end diff --git a/lib/paystack_gateway/balance.rb b/lib/paystack_gateway/balance.rb index 07f2e85..4761cb0 100644 --- a/lib/paystack_gateway/balance.rb +++ b/lib/paystack_gateway/balance.rb @@ -53,7 +53,7 @@ class LedgerError < ApiError; end use_connection do |connection| connection.get( '/balance/ledger', - { perPage:, page:, from:, to: }.compact, + { perPage: per_page, page:, from:, to: }.compact, ) end end diff --git a/lib/paystack_gateway/bulk_charge.rb b/lib/paystack_gateway/bulk_charge.rb index 134de80..53e93cf 100644 --- a/lib/paystack_gateway/bulk_charge.rb +++ b/lib/paystack_gateway/bulk_charge.rb @@ -32,7 +32,7 @@ class ListError < ApiError; end use_connection do |connection| connection.get( '/bulkcharge', - { perPage:, page:, from:, to: }.compact, + { perPage: per_page, page:, from:, to: }.compact, ) end end diff --git a/lib/paystack_gateway/dispute.rb b/lib/paystack_gateway/dispute.rb index 4e77390..dc4863d 100644 --- a/lib/paystack_gateway/dispute.rb +++ b/lib/paystack_gateway/dispute.rb @@ -195,7 +195,7 @@ class DownloadError < ApiError; end use_connection do |connection| connection.get( '/dispute/export', - { perPage:, page:, status:, from:, to: }.compact, + { perPage: per_page, page:, status:, from:, to: }.compact, ) end end diff --git a/lib/paystack_gateway/order.rb b/lib/paystack_gateway/order.rb index 376ea6e..d572d12 100644 --- a/lib/paystack_gateway/order.rb +++ b/lib/paystack_gateway/order.rb @@ -32,7 +32,7 @@ class ListError < ApiError; end use_connection do |connection| connection.get( '/order', - { perPage:, page:, from:, to: }.compact, + { perPage: per_page, page:, from:, to: }.compact, ) end end diff --git a/lib/paystack_gateway/page.rb b/lib/paystack_gateway/page.rb index fe702d6..d399bc9 100644 --- a/lib/paystack_gateway/page.rb +++ b/lib/paystack_gateway/page.rb @@ -32,7 +32,7 @@ class ListError < ApiError; end use_connection do |connection| connection.get( '/page', - { perPage:, page:, from:, to: }.compact, + { perPage: per_page, page:, from:, to: }.compact, ) end end diff --git a/lib/paystack_gateway/product.rb b/lib/paystack_gateway/product.rb index a6fea2c..8d7cec5 100644 --- a/lib/paystack_gateway/product.rb +++ b/lib/paystack_gateway/product.rb @@ -31,7 +31,7 @@ class ListError < ApiError; end use_connection do |connection| connection.get( '/product', - { perPage:, page:, active:, from:, to: }.compact, + { perPage: per_page, page:, active:, from:, to: }.compact, ) end end diff --git a/lib/paystack_gateway/refund.rb b/lib/paystack_gateway/refund.rb index ba37ca0..2827e7a 100644 --- a/lib/paystack_gateway/refund.rb +++ b/lib/paystack_gateway/refund.rb @@ -32,7 +32,7 @@ class ListError < ApiError; end use_connection do |connection| connection.get( '/refund', - { perPage:, page:, from:, to: }.compact, + { perPage: per_page, page:, from:, to: }.compact, ) end end diff --git a/lib/paystack_gateway/settlement.rb b/lib/paystack_gateway/settlement.rb index e60bcb6..fb85f96 100644 --- a/lib/paystack_gateway/settlement.rb +++ b/lib/paystack_gateway/settlement.rb @@ -26,7 +26,7 @@ class SFetchError < ApiError; end use_connection do |connection| connection.get( '/settlement', - { perPage:, page: }.compact, + { perPage: per_page, page: }.compact, ) end end diff --git a/lib/paystack_gateway/storefront.rb b/lib/paystack_gateway/storefront.rb index f6b3daa..168e039 100644 --- a/lib/paystack_gateway/storefront.rb +++ b/lib/paystack_gateway/storefront.rb @@ -27,7 +27,7 @@ class ListError < ApiError; end use_connection do |connection| connection.get( '/storefront', - { perPage:, page:, status: }.compact, + { perPage: per_page, page:, status: }.compact, ) end end diff --git a/lib/paystack_gateway/subaccount.rb b/lib/paystack_gateway/subaccount.rb index 3f40a67..60ca6f6 100644 --- a/lib/paystack_gateway/subaccount.rb +++ b/lib/paystack_gateway/subaccount.rb @@ -32,7 +32,7 @@ class ListError < ApiError; end use_connection do |connection| connection.get( '/subaccount', - { perPage:, page:, from:, to: }.compact, + { perPage: per_page, page:, from:, to: }.compact, ) end end diff --git a/lib/paystack_gateway/transfer.rb b/lib/paystack_gateway/transfer.rb index 101a49d..0d46ec0 100644 --- a/lib/paystack_gateway/transfer.rb +++ b/lib/paystack_gateway/transfer.rb @@ -33,7 +33,7 @@ class ListError < ApiError; end use_connection do |connection| connection.get( '/transfer', - { perPage:, page:, status:, from:, to: }.compact, + { perPage: per_page, page:, status:, from:, to: }.compact, ) end end @@ -262,7 +262,7 @@ class DownloadError < ApiError; end use_connection do |connection| connection.get( '/transfer/export', - { perPage:, page:, status:, from:, to: }.compact, + { perPage: per_page, page:, status:, from:, to: }.compact, ) end end diff --git a/lib/paystack_gateway/transfer_recipient.rb b/lib/paystack_gateway/transfer_recipient.rb index 9e36b60..ce1615e 100644 --- a/lib/paystack_gateway/transfer_recipient.rb +++ b/lib/paystack_gateway/transfer_recipient.rb @@ -32,7 +32,7 @@ class ListError < ApiError; end use_connection do |connection| connection.get( '/transferrecipient', - { perPage:, page:, from:, to: }.compact, + { perPage: per_page, page:, from:, to: }.compact, ) end end From d16a5ac28e503313d9ff3e825fbcfde700f9f636 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 01:10:23 +0100 Subject: [PATCH 14/26] Update script to define entire module body in a single method --- bin/generate_sdk_from_openapi_spec.rb | 329 +++++++++++--------------- 1 file changed, 138 insertions(+), 191 deletions(-) diff --git a/bin/generate_sdk_from_openapi_spec.rb b/bin/generate_sdk_from_openapi_spec.rb index 5a3e63b..79fbcd8 100755 --- a/bin/generate_sdk_from_openapi_spec.rb +++ b/bin/generate_sdk_from_openapi_spec.rb @@ -13,221 +13,133 @@ WRAP_LINE_LENGTH = 80 TOP_LEVEL_RESPONSE_KEYS = %w[status message data meta].freeze -def api_methods_by_api_module_name - @api_methods_by_api_module_name ||= - document.paths.path.each_with_object({}) do |(path, path_item), paths_by_api_module| - %w[get post put patch delete].each do |http_method| - operation = path_item.operation(http_method) - next if !operation - - api_module_name = operation.tags.first.remove(' ') - next if !api_module_name - - paths_by_api_module[api_module_name] ||= [] - paths_by_api_module[api_module_name] << { path:, http_method:, operation: } - end - end -end - -def update_required_files(module_names) - file_lines = File.readlines(LIB_FILE) - - comment_start_index = file_lines.find_index { |line| line.include?('# API Modules') } - if !comment_start_index - puts "Could not find '# API Modules' comment in #{LIB_FILE}" - return - end - - insertion_start = comment_start_index + 1 - insertion_start += 1 while insertion_start < file_lines.size && file_lines[insertion_start].start_with?('#') - - insertion_end = insertion_start - insertion_end += 1 while insertion_end < file_lines.size && !file_lines[insertion_end].strip.empty? - - file_lines[insertion_start...insertion_end] = - module_names.map { |module_name| "require 'paystack_gateway/#{module_name.underscore}'\n" }.sort.join - - File.write(LIB_FILE, file_lines.join) - - puts 'Updated paystack_gateway.rb with new API Modules requires.' -end - -def api_module_content(api_module_name, api_methods) +def api_module_content(api_module_name, api_methods) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity + module_info = tags_by_name[api_module_name] <<~RUBY # frozen_string_literal: true module PaystackGateway - #{api_module_docstring(api_module_name)} + # https://paystack.com/docs/api/#{api_module_name.parameterize} + # + # #{module_info['x-product-name'].squish || api_module_name.humanize} + # #{module_info['description'].squish} module #{api_module_name} include PaystackGateway::RequestModule - #{api_methods.map { |info| api_method_composition(api_module_name, info) }.join("\n").chomp} + #{ + api_methods.map do |api_method_info| # rubocop:disable Metrics/BlockLength + api_method_info => { path:, http_method:, operation: } + + api_method_name = api_method_name(api_module_name, operation) + <<~RUBY.chomp + #{INDENT * 2}# Successful response from calling ##{api_method_name}. + #{INDENT * 2}class #{"#{api_method_name}_response".camelize} < PaystackGateway::Response#{ + if (data_keys = api_response_data_keys(operation)).any? + if data_keys.length > 3 + "\n#{INDENT * 3}delegate #{data_keys.map { |key| ":#{key}," }.join("\n#{INDENT * 3} #{' ' * 'delegate'.length}")} to: :data\n#{INDENT * 2}end" + else + "\n#{INDENT * 3}delegate #{data_keys.map { |key| ":#{key}" }.join(', ')}, to: :data\n#{INDENT * 2}end" + end + else + '; end' + end + } + + #{INDENT * 2}# Error response from ##{api_method_name}. + #{INDENT * 2}class #{"#{api_method_name}_error".camelize} < ApiError; end + + #{INDENT * 2}# https://paystack.com/docs/api/#{operation.tags.first.parameterize}/##{api_method_name} + #{INDENT * 2}# #{operation.summary}: #{http_method.upcase} #{path}#{ + operation.description ? "\n#{INDENT * 2}# #{operation.description}" : '' + } + #{INDENT * 2}##{ + docstring = '' + api_method_parameters(operation).each do |param| + docstring += "\n#{INDENT * 2}# @param #{param[:name]} [#{param[:type]}]" + docstring += ' (required)' if param[:required] + + if (description = param[:description]&.squish) + docstring += wrapped_text(description, "\n#{INDENT * 2}##{INDENT * 3} ") + end + + next if !(object_properties = param[:object_properties]) + + object_properties.each do |props| + docstring += "\n#{INDENT * 2}##{INDENT * 1} @option #{param[:name]} [#{props[:type]}] :#{props[:name]}" + docstring += wrapped_text(props[:description], "\n#{INDENT * 2}##{INDENT * 5}").to_s + end + end + + docstring + } + #{INDENT * 2}# + #{INDENT * 2}# @return [#{"#{api_method_name}_response".camelize}] successful response + #{INDENT * 2}# @raise [#{"#{api_method_name}_error".camelize}] if the request fails + #{INDENT * 2}api_method def self.#{api_method_name}#{ + if (method_args = api_method_args(operation)).any? + if method_args.length > 5 + "(#{method_args.map { |arg| "\n#{INDENT * 3}#{arg}" }.join(',')}\n#{INDENT * 2})" + else + "(#{method_args.join(', ')})" + end + end + } + #{INDENT * 2} use_connection do |connection| + #{INDENT * 2} connection.#{http_method}( + #{INDENT * 2} #{ + if path == (interpolated = path.gsub(/{([^}]+)}/, "\#{\\1}")) + "'#{path}'" + else + "\"#{interpolated}\"" + end + },#{ + if (request_args = api_request_args(operation)).any? + if request_args.length > 5 + "\n#{INDENT * 5}{#{request_args.map { |arg| "\n#{INDENT * 6}#{arg}," }.join}\n#{INDENT * 5}}.compact," + else + "\n#{INDENT * 5}{ #{request_args.join(', ')} }.compact," + end + end + } + #{INDENT * 2} ) + #{INDENT * 2} end + #{INDENT * 2}end + + RUBY + end.join("\n").chomp + } end end RUBY end -def api_module_docstring(api_module_name) - module_info = tags_by_name[api_module_name] - return if !module_info - - docstring = "# https://paystack.com/docs/api/#{api_module_name.parameterize}" - docstring += "\n#{INDENT * 1}#" - docstring += "\n#{INDENT * 1}# #{module_info['x-product-name'].squish || api_module_name.humanize}" - - docstring += "\n#{INDENT * 1}# #{module_info['description'].squish}" if module_info['description'] - docstring -end - -def api_method_composition(api_module_name, api_method_info) - api_method_info => { path:, http_method:, operation: } - - api_method_name = api_method_name(api_module_name, operation) - <<~RUBY.chomp - #{api_method_response_class_content(api_method_name, operation)} - - #{api_method_error_class_content(api_method_name)} - - #{api_method_content(api_method_name, operation, http_method, path)} - RUBY -end - -def api_method_response_class_content(api_method_name, operation) - definition = "#{INDENT * 2}# Successful response from calling ##{api_method_name}.\n" \ - "#{INDENT * 2}class #{"#{api_method_name}_response".camelize} < PaystackGateway::Response" - - if (delegate_content = api_method_response_class_delegate_content(operation)) - "#{definition}#{delegate_content}\n#{INDENT * 2}end" - else - "#{definition}; end" - end -end - -def api_method_response_class_delegate_content(operation) +def api_response_data_keys(operation) responses = operation.responses.response success_response = responses[responses.keys.find { _1.match?(/\A2..\z/) }] - return if !success_response + return [] if !success_response required_data_keys = success_response.content['application/json'].schema.properties['data']&.required || [] - - required_data_keys -= TOP_LEVEL_RESPONSE_KEYS - return if required_data_keys.none? - - if required_data_keys.length > 3 - definition = "\n#{INDENT * 3}delegate :#{required_data_keys.shift}," - - while (line_key = required_data_keys.shift).present? - definition += "\n#{INDENT * 3}#{' ' * 'delegate'.length} :#{line_key}," - end - - "#{definition} to: :data" - else - "\n#{INDENT * 3}delegate #{required_data_keys.map { |key| ":#{key}" }.join(', ')}, to: :data" - end -end - -def api_method_error_class_content(api_method_name) - "#{INDENT * 2}# Error response from ##{api_method_name}.\n" \ - "#{INDENT * 2}class #{"#{api_method_name}_error".camelize} < ApiError; end" + required_data_keys - TOP_LEVEL_RESPONSE_KEYS end -def api_method_content(api_method_name, operation, http_method, path) - <<-RUBY - #{api_method_definition_header_docstring(api_method_name, operation, http_method, path)} - #{api_method_definition_params_docstring(operation)} - #{api_method_definition_response_docstring(api_method_name)} - #{api_method_definition_name_and_parameters(api_method_name, operation)} - use_connection do |connection| - connection.#{http_method}( - #{api_method_definition_path(path)},#{api_method_definition_request_params(operation)} - ) - end - end - RUBY -end - -def api_method_definition_header_docstring(api_method_name, operation, http_method, path) - docstring = "# https://paystack.com/docs/api/#{operation.tags.first.parameterize}/##{api_method_name}" - docstring += "\n#{INDENT * 2}# #{operation.summary}: #{http_method.upcase} #{path}" - docstring += "\n#{INDENT * 2}# #{operation.description}" if operation.description.present? - docstring -end - -def api_method_definition_params_docstring(operation) - docstring = '#' - - api_method_parameters(operation).each do |param| - docstring += "\n#{INDENT * 2}# @param #{param[:name]} [#{param[:type]}]" - docstring += ' (required)' if param[:required] - - if (description = param[:description]&.squish) - docstring += wrapped_text(description, "\n#{INDENT * 2}##{INDENT * 3} ") - end - - next if !(object_properties = param[:object_properties]) - - object_properties.each do |props| - docstring += "\n#{INDENT * 2}##{INDENT * 1} @option #{param[:name]} [#{props[:type]}] :#{props[:name]}" - docstring += wrapped_text(props[:description], "\n#{INDENT * 2}##{INDENT * 5}") - end - end - - docstring -end - -def api_method_definition_response_docstring(api_method_name) - docstring = '#' - docstring += "\n#{INDENT * 2}# @return [#{"#{api_method_name}_response".camelize}] successful response" - docstring + "\n#{INDENT * 2}# @raise [#{"#{api_method_name}_error".camelize}] if the request fails" -end - -def api_method_definition_name_and_parameters(api_method_name, operation) - method_args = api_method_parameters(operation).map do |param| +def api_method_args(operation) + api_method_parameters(operation).map do |param| name = param[:name].underscore param[:required] ? "#{name}:" : "#{name}: nil" end - - definition = "api_method def self.#{api_method_name}" - return definition if method_args.none? - - definition += '(' - - if method_args.length > 5 - while (line_arg = method_args.shift).present? - definition += "\n#{INDENT * 3}#{line_arg}" - definition += ',' if method_args.any? - end - definition + "\n#{INDENT * 2})" - else - definition + "#{method_args.join(', ')})" - end -end - -def api_method_definition_path(path) - # "/transaction/verify/{reference}" -> "/transaction/verify/#{reference}" - interpolated_path = path.gsub(/{([^}]+)}/, "\#{\\1}") - - interpolated_path == path ? "'#{interpolated_path}'" : "\"#{interpolated_path}\"" end -def api_method_definition_request_params(operation) - params = api_method_parameters(operation) - .reject { |param| param[:in] == 'path' } - .map { |param| param[:name] } - return if params.none? - - definition = "\n#{INDENT * 5}{" +def api_request_args(operation) + api_method_parameters(operation) + .filter_map do |param| + next if param[:in] == 'path' - if params.length > 5 - while (line_param = params.shift).present? - definition += "\n#{INDENT * 6}#{line_param}:" \ - "#{line_param == line_param.underscore ? nil : " #{line_param.underscore}"}," + if param[:name] == param[:name].underscore + "#{param[:name]}:" + else + "#{param[:name]}: #{param[:name].underscore}" + end end - - definition + "\n#{INDENT * 5}}.compact," - else - "#{definition} #{params.map { |param| "#{param}:" }.join(', ')} }.compact," - end end def api_method_parameters(operation) @@ -365,6 +277,22 @@ def document @document ||= OpenAPIParser.parse(YAML.load_file(OPENAPI_SPEC), strict_reference_validation: true) end +def api_methods_by_api_module_name + @api_methods_by_api_module_name ||= + document.paths.path.each_with_object({}) do |(path, path_item), paths_by_api_module| + %w[get post put patch delete].each do |http_method| + operation = path_item.operation(http_method) + next if !operation + + api_module_name = operation.tags.first.remove(' ') + next if !api_module_name + + paths_by_api_module[api_module_name] ||= [] + paths_by_api_module[api_module_name] << { path:, http_method:, operation: } + end + end +end + module_names = api_methods_by_api_module_name.map do |api_module_name, api_methods| if existing_api_modules.include?(api_module_name) puts "Updating existing module: #{api_module_name}" @@ -381,4 +309,23 @@ def document api_module_name end -update_required_files(module_names) +lib_file_lines = File.readlines(LIB_FILE) + +comment_start_index = lib_file_lines.find_index { |line| line.include?('# API Modules') } +if !comment_start_index + puts "Could not find '# API Modules' comment in #{LIB_FILE}" + exit 1 +end + +insertion_start = comment_start_index + 1 +insertion_start += 1 while insertion_start < lib_file_lines.size && lib_file_lines[insertion_start].start_with?('#') + +insertion_end = insertion_start +insertion_end += 1 while insertion_end < lib_file_lines.size && !lib_file_lines[insertion_end].strip.empty? + +lib_file_lines[insertion_start...insertion_end] = + module_names.map { |module_name| "require 'paystack_gateway/#{module_name.underscore}'\n" }.sort.join + +File.write(LIB_FILE, lib_file_lines.join) + +puts 'Updated paystack_gateway.rb with new API Modules requires.' From 7c28b8b434d0d8e8996c8004bca324e5974081d3 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 01:26:51 +0100 Subject: [PATCH 15/26] Remove redefined id in delegate --- lib/paystack_gateway/transfers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/paystack_gateway/transfers.rb b/lib/paystack_gateway/transfers.rb index f3df718..ceb2113 100644 --- a/lib/paystack_gateway/transfers.rb +++ b/lib/paystack_gateway/transfers.rb @@ -9,7 +9,7 @@ module Transfers class InitiateTransferResponse < PaystackGateway::Response include TransactionResponse - delegate :transfer_code, :id, :transferred_at, to: :data + delegate :transfer_code, :transferred_at, to: :data def transaction_completed_at transferred_at || super From f3da172b4668ab0f81e7a3b5e4144bf959335956 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 01:33:52 +0100 Subject: [PATCH 16/26] Mark old modules as deprecated --- lib/paystack_gateway.rb | 2 ++ lib/paystack_gateway/customers.rb | 2 ++ lib/paystack_gateway/dedicated_virtual_accounts.rb | 4 +++- lib/paystack_gateway/plans.rb | 2 ++ lib/paystack_gateway/refunds.rb | 2 ++ lib/paystack_gateway/subaccounts.rb | 2 ++ lib/paystack_gateway/transactions.rb | 2 ++ lib/paystack_gateway/transfer_recipients.rb | 2 ++ lib/paystack_gateway/transfers.rb | 2 ++ lib/paystack_gateway/verification.rb | 2 ++ 10 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/paystack_gateway.rb b/lib/paystack_gateway.rb index 1610646..537aba1 100644 --- a/lib/paystack_gateway.rb +++ b/lib/paystack_gateway.rb @@ -18,6 +18,8 @@ require 'paystack_gateway/transfer_recipients' require 'paystack_gateway/transfers' require 'paystack_gateway/verification' + +# Webhooks require 'paystack_gateway/webhooks' # API Modules diff --git a/lib/paystack_gateway/customers.rb b/lib/paystack_gateway/customers.rb index 2fb01fd..2b5a5a7 100644 --- a/lib/paystack_gateway/customers.rb +++ b/lib/paystack_gateway/customers.rb @@ -2,6 +2,8 @@ module PaystackGateway # Create and manage customers https://paystack.com/docs/api/customer/ + # + # @deprecated Use PaystackGateway::Customer instead. module Customers include PaystackGateway::RequestModule diff --git a/lib/paystack_gateway/dedicated_virtual_accounts.rb b/lib/paystack_gateway/dedicated_virtual_accounts.rb index 042e370..3d14c68 100644 --- a/lib/paystack_gateway/dedicated_virtual_accounts.rb +++ b/lib/paystack_gateway/dedicated_virtual_accounts.rb @@ -3,7 +3,9 @@ module PaystackGateway # The Dedicated Virtual Account API enables Nigerian merchants to manage unique # payment accounts of their customers. - # https://paystack.com/docs/api/dedicated-virtual-account/ + # https://paystack.com/docs/api/dedicated-virtual-account + # + # @deprecated Use PaystackGateway::DedicatedVirtualAccount instead. module DedicatedVirtualAccounts include PaystackGateway::RequestModule diff --git a/lib/paystack_gateway/plans.rb b/lib/paystack_gateway/plans.rb index 2438e69..4e65696 100644 --- a/lib/paystack_gateway/plans.rb +++ b/lib/paystack_gateway/plans.rb @@ -3,6 +3,8 @@ module PaystackGateway # Create and manage installment payment options # https://paystack.com/docs/api/plan/#create + # + # @deprecated Use PaystackGateway::Plan instead. module Plans include PaystackGateway::RequestModule diff --git a/lib/paystack_gateway/refunds.rb b/lib/paystack_gateway/refunds.rb index cfd4223..1b9fb33 100644 --- a/lib/paystack_gateway/refunds.rb +++ b/lib/paystack_gateway/refunds.rb @@ -3,6 +3,8 @@ module PaystackGateway # Create and manage transaction refunds. # https://paystack.com/docs/api/refund + # + # @deprecated Use PaystackGateway::Refund instead. module Refunds include PaystackGateway::RequestModule diff --git a/lib/paystack_gateway/subaccounts.rb b/lib/paystack_gateway/subaccounts.rb index d03c4f1..23f477b 100644 --- a/lib/paystack_gateway/subaccounts.rb +++ b/lib/paystack_gateway/subaccounts.rb @@ -2,6 +2,8 @@ module PaystackGateway # Create and manage subaccounts https://paystack.com/docs/api/subaccount/ + # + # @deprecated Use PaystackGateway::Subaccount instead. module Subaccounts include PaystackGateway::RequestModule diff --git a/lib/paystack_gateway/transactions.rb b/lib/paystack_gateway/transactions.rb index 3b6abb4..cfdd6fe 100644 --- a/lib/paystack_gateway/transactions.rb +++ b/lib/paystack_gateway/transactions.rb @@ -2,6 +2,8 @@ module PaystackGateway # Create and manage payments https://paystack.com/docs/api/#transaction + # + # @deprecated Use PaystackGateway::Transaction instead. module Transactions include PaystackGateway::RequestModule diff --git a/lib/paystack_gateway/transfer_recipients.rb b/lib/paystack_gateway/transfer_recipients.rb index 4d11346..33f5c7e 100644 --- a/lib/paystack_gateway/transfer_recipients.rb +++ b/lib/paystack_gateway/transfer_recipients.rb @@ -3,6 +3,8 @@ module PaystackGateway # Create and manage beneficiaries that you send money to # https://paystack.com/docs/api/#transfer-recipient + # + # @deprecated Use PaystackGateway::TransferRecipient instead. module TransferRecipients include PaystackGateway::RequestModule diff --git a/lib/paystack_gateway/transfers.rb b/lib/paystack_gateway/transfers.rb index ceb2113..88568b9 100644 --- a/lib/paystack_gateway/transfers.rb +++ b/lib/paystack_gateway/transfers.rb @@ -2,6 +2,8 @@ module PaystackGateway # Automate sending money https://paystack.com/docs/api/#transfer + # + # @deprecated Use PaystackGateway::Transfer instead. module Transfers include PaystackGateway::RequestModule diff --git a/lib/paystack_gateway/verification.rb b/lib/paystack_gateway/verification.rb index 1489ce4..119bf3d 100644 --- a/lib/paystack_gateway/verification.rb +++ b/lib/paystack_gateway/verification.rb @@ -2,6 +2,8 @@ module PaystackGateway # Perform KYC processes https://paystack.com/docs/api/#verification + # + # @deprecated Use PaystackGateway::Bank instead. module Verification include PaystackGateway::RequestModule From 36469c2befb8daa967fdccccd9ab2e0189605e25 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 03:51:48 +0100 Subject: [PATCH 17/26] rename Transaction#initialize to Transaction#initialize_transaction due to reserved method name --- bin/generate_sdk_from_openapi_spec.rb | 36 ++++++++++++++++----------- lib/paystack_gateway.rb | 9 +------ lib/paystack_gateway/configuration.rb | 13 +++++++++- lib/paystack_gateway/transaction.rb | 16 ++++++------ 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/bin/generate_sdk_from_openapi_spec.rb b/bin/generate_sdk_from_openapi_spec.rb index 79fbcd8..6696324 100755 --- a/bin/generate_sdk_from_openapi_spec.rb +++ b/bin/generate_sdk_from_openapi_spec.rb @@ -235,20 +235,28 @@ def schema_description(schema) end def api_method_name(api_module_name, operation) - operation - .operation_id - .delete_prefix( - case api_module_name.to_sym - when :DedicatedVirtualAccount - 'dedicatedAccount' - when :TransferRecipient - 'transferrecipient' - else - api_module_name.camelize(:lower) - end, - ) - .delete_prefix('_') - .underscore + name = operation + .operation_id + .delete_prefix( + case api_module_name.to_sym + when :DedicatedVirtualAccount + 'dedicatedAccount' + when :TransferRecipient + 'transferrecipient' + else + api_module_name.camelize(:lower) + end, + ) + .delete_prefix('_') + .underscore + + # initialize is a reserved word in Ruby + case [api_module_name, name] + when %w[Transaction initialize] + 'initialize_transaction' + else + name + end end def wrapped_text(text, prefix = nil) diff --git a/lib/paystack_gateway.rb b/lib/paystack_gateway.rb index 537aba1..3cec94d 100644 --- a/lib/paystack_gateway.rb +++ b/lib/paystack_gateway.rb @@ -6,9 +6,9 @@ require 'paystack_gateway/current' require 'paystack_gateway/request_module' require 'paystack_gateway/response' -require 'paystack_gateway/transaction_response' # Old Implementations, will be removed in a future version. +require 'paystack_gateway/transaction_response' require 'paystack_gateway/customers' require 'paystack_gateway/dedicated_virtual_accounts' require 'paystack_gateway/plans' @@ -52,13 +52,6 @@ # = PaystackGateway module PaystackGateway class << self - attr_writer :config - - delegate :secret_key, :logger, :logging_options, :log_filter, to: :config - - def config = @config ||= Configuration.new - def configure = yield(config) - def api_modules constants.filter_map do |const_name| const = const_get(const_name) diff --git a/lib/paystack_gateway/configuration.rb b/lib/paystack_gateway/configuration.rb index f7aa841..d0e29cc 100644 --- a/lib/paystack_gateway/configuration.rb +++ b/lib/paystack_gateway/configuration.rb @@ -7,11 +7,22 @@ module PaystackGateway # Encapsulates the configuration options for PaystackGateway including the # secret key, logger, logging_options, and log filter. class Configuration - attr_accessor :secret_key, :logger, :logging_options, :log_filter + attr_accessor :secret_key, :logger, :logging_options, :log_filter, :use_extensions def initialize @logger = Logger.new($stdout) @log_filter = lambda(&:dup) + @use_extensions = true end end + + class << self + attr_writer :config + + delegate :secret_key, :logger, :logging_options, :log_filter, :use_extensions, + to: :config + + def config = @config ||= Configuration.new + def configure = yield(config) + end end diff --git a/lib/paystack_gateway/transaction.rb b/lib/paystack_gateway/transaction.rb index 0e88cd5..a7f2602 100644 --- a/lib/paystack_gateway/transaction.rb +++ b/lib/paystack_gateway/transaction.rb @@ -8,15 +8,15 @@ module PaystackGateway module Transaction include PaystackGateway::RequestModule - # Successful response from calling #initialize. - class InitializeResponse < PaystackGateway::Response + # Successful response from calling #initialize_transaction. + class InitializeTransactionResponse < PaystackGateway::Response delegate :authorization_url, :access_code, :reference, to: :data end - # Error response from #initialize. - class InitializeError < ApiError; end + # Error response from #initialize_transaction. + class InitializeTransactionError < ApiError; end - # https://paystack.com/docs/api/transaction/#initialize + # https://paystack.com/docs/api/transaction/#initialize_transaction # Initialize Transaction: POST /transaction/initialize # Create a new transaction # @@ -70,9 +70,9 @@ class InitializeError < ApiError; end # @param metadata [String] # Stringified JSON object of custom data # - # @return [InitializeResponse] successful response - # @raise [InitializeError] if the request fails - api_method def self.initialize( + # @return [InitializeTransactionResponse] successful response + # @raise [InitializeTransactionError] if the request fails + api_method def self.initialize_transaction( email:, amount:, currency: nil, From 73a53e135791a032afe965bbeb5fd4e80e5e37c8 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 03:53:39 +0100 Subject: [PATCH 18/26] Add extensions for responses and errors --- lib/paystack_gateway.rb | 7 +++ .../extensions/customer_extensions.rb | 21 +++++++ .../extensions/plan_extensions.rb | 51 +++++++++++++++++ .../extensions/refund_extensions.rb | 48 ++++++++++++++++ .../extensions/transaction_extensions.rb | 57 +++++++++++++++++++ .../transaction_response_extension.rb | 31 ++++++++++ .../extensions/transfer_extensions.rb | 50 ++++++++++++++++ 7 files changed, 265 insertions(+) create mode 100644 lib/paystack_gateway/extensions/customer_extensions.rb create mode 100644 lib/paystack_gateway/extensions/plan_extensions.rb create mode 100644 lib/paystack_gateway/extensions/refund_extensions.rb create mode 100644 lib/paystack_gateway/extensions/transaction_extensions.rb create mode 100644 lib/paystack_gateway/extensions/transaction_response_extension.rb create mode 100644 lib/paystack_gateway/extensions/transfer_extensions.rb diff --git a/lib/paystack_gateway.rb b/lib/paystack_gateway.rb index 3cec94d..e6eced6 100644 --- a/lib/paystack_gateway.rb +++ b/lib/paystack_gateway.rb @@ -49,6 +49,13 @@ require 'paystack_gateway/transfer' require 'paystack_gateway/transfer_recipient' +# Extensions +require 'paystack_gateway/extensions/customer_extensions' +require 'paystack_gateway/extensions/plan_extensions' +require 'paystack_gateway/extensions/refund_extensions' +require 'paystack_gateway/extensions/transaction_extensions' +require 'paystack_gateway/extensions/transfer_extensions' + # = PaystackGateway module PaystackGateway class << self diff --git a/lib/paystack_gateway/extensions/customer_extensions.rb b/lib/paystack_gateway/extensions/customer_extensions.rb new file mode 100644 index 0000000..d913860 --- /dev/null +++ b/lib/paystack_gateway/extensions/customer_extensions.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module PaystackGateway + module Extensions + # Helpers for PaystackGateway::Customer + module CustomerExtensions + # Helpers for PaystackGateway::Customer::FetchResponse + module FetchResponseExtension + def active_subscriptions = subscriptions.select { _1.status == 'active' } + def active_subscription_codes = active_subscriptions.map(&:subscription_code) + def reusable_authorizations = authorizations.select(&:reusable) + end + end + end +end + +if PaystackGateway.use_extensions + PaystackGateway::Customer::FetchResponse.include( + PaystackGateway::Extensions::CustomerExtensions::FetchResponseExtension, + ) +end diff --git a/lib/paystack_gateway/extensions/plan_extensions.rb b/lib/paystack_gateway/extensions/plan_extensions.rb new file mode 100644 index 0000000..c4488ec --- /dev/null +++ b/lib/paystack_gateway/extensions/plan_extensions.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module PaystackGateway + module Extensions + # Helpers for PaystackGateway::Plan + module PlanExtensions + # Helpers for PaystackGateway::Plan::CreateResponse + module CreateResponseExtension + def plan_id = data.id + end + + # Helpers for PaystackGateway::Plan::ListResponse + module ListResponseExtension + def active_plans = data.select { |plan| !plan.is_deleted && !plan.is_archived } + + def find_active_plan_by_name(name) + active_plans.sort_by { -Time.parse(_1.createdAt).to_i }.find { _1.name == name } + end + end + + # Helpers for PaystackGateway::Plan::FetchResponse + module FetchResponseExtension + def active_subscriptions = subscriptions.select { _1.status.to_sym == :active } + + def active_subscription_codes(email: nil) + subscriptions = + if email + active_subscriptions.select { _1.customer.email.casecmp?(email) } + else + active_subscriptions + end + subscriptions.map(&:subscription_code) + end + end + end + end +end + +if PaystackGateway.use_extensions + PaystackGateway::Plan::ListResponse.include( + PaystackGateway::Extensions::PlanExtensions::ListResponseExtension, + ) + + PaystackGateway::Plan::CreateResponse.include( + PaystackGateway::Extensions::PlanExtensions::CreateResponseExtension, + ) + + PaystackGateway::Plans::FetchPlanResponse.include( + PaystackGateway::Extensions::PlanExtensions::FetchResponseExtension, + ) +end diff --git a/lib/paystack_gateway/extensions/refund_extensions.rb b/lib/paystack_gateway/extensions/refund_extensions.rb new file mode 100644 index 0000000..837e7ee --- /dev/null +++ b/lib/paystack_gateway/extensions/refund_extensions.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require_relative 'transaction_response_extension' + +module PaystackGateway + module Extensions + # Helpers for PaystackGateway::Refund + module RefundExtensions + # Helpers for PaystackGateway::Refund::ListResponse + module ListResponseExtension + def pending_or_successful + filtered = data.select { _1.status&.to_sym.in?(%i[processed pending processing]) } + + ListResponse.new({ **self, data: filtered }) + end + + def with_amount(amount) + filtered = data.select { _1.amount == amount * 100 } + + ListResponse.new({ **self, data: filtered }) + end + end + end + + # Common helpers for responses from refunds endpoints + module TransactionRefundResponseExtension + def refund_success? = transaction_status == :processed + def refund_failed? = transaction_status == :failed + def refund_pending? = transaction_status.in?(%i[pending processing]) + end + end +end + +if PaystackGateway.use_extensions + PaystackGateway::Refund::ListResponse.include( + PaystackGateway::Extensions::RefundExtensions::ListResponseExtension, + ) + + PaystackGateway::Refund::CreateResponse.include( + PaystackGateway::Extensions::TransactionResponseExtension, + PaystackGateway::Extensions::TransactionRefundResponseExtension, + ) + + PaystackGateway::Refund::FetchResponse.include( + PaystackGateway::Extensions::TransactionResponseExtension, + PaystackGateway::Extensions::TransactionRefundResponseExtension, + ) +end diff --git a/lib/paystack_gateway/extensions/transaction_extensions.rb b/lib/paystack_gateway/extensions/transaction_extensions.rb new file mode 100644 index 0000000..cb87683 --- /dev/null +++ b/lib/paystack_gateway/extensions/transaction_extensions.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require_relative 'transaction_response_extension' + +module PaystackGateway + module Extensions + # Helpers for PaystackGateway::Transaction + module TransactionExtensions + # Helpers for PaystackGateway::Transaction::InitializeTransactionResponse + module InitializeTransactionResponseExtension + def payment_url + authorization_url + end + end + + # Helpers for PaystackGateway::Transaction::InitializeTransactionError + module InitializeTransactionErrorExtension + def cancellable? = network_error? + end + + # Helpers for PaystackGateway::Transaction::VerifyResponse + module VerifyResponseExtension + def transaction_completed_at = paid_at || super + end + + # Helpers for PaystackGateway::Transaction::VerifyError + module VerifyErrorExtension + def transaction_not_found? + return false if !response_body + + response_body[:status] == false && response_body[:message].match?(/transaction reference not found/i) + end + end + end + end +end + +if PaystackGateway.use_extensions + PaystackGateway::Transaction::InitializeTransactionResponse.include( + PaystackGateway::Extensions::TransactionExtensions::InitializeTransactionResponseExtension, + ) + PaystackGateway::Transaction::InitializeTransactionError.include( + PaystackGateway::Extensions::TransactionExtensions::InitializeTransactionErrorExtension, + ) + + PaystackGateway::Transaction::VerifyResponse.include( + PaystackGateway::Extensions::TransactionExtensions::VerifyResponseExtension, + PaystackGateway::Extensions::TransactionResponseExtension, + ) + PaystackGateway::Transaction::VerifyError.include( + PaystackGateway::Extensions::TransactionExtensions::VerifyErrorExtension, + ) + + PaystackGateway::Transaction::ChargeAuthorizationResponse.include( + PaystackGateway::Extensions::TransactionResponseExtension, + ) +end diff --git a/lib/paystack_gateway/extensions/transaction_response_extension.rb b/lib/paystack_gateway/extensions/transaction_response_extension.rb new file mode 100644 index 0000000..0345578 --- /dev/null +++ b/lib/paystack_gateway/extensions/transaction_response_extension.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'bigdecimal' + +module PaystackGateway + module Extensions + # Helpers for various responses around transactions. + module TransactionResponseExtension + def transaction_success? = %i[success reversed reversal_pending].include?(transaction_status) + def transaction_abandoned? = transaction_status == :abandoned + def transaction_failed? = transaction_status == :failed + def transaction_pending? = %i[pending ongoing].include?(transaction_status) + + def transaction_status = data.status.to_sym + def transaction_amount_in_major_units = amount / BigDecimal(100) + def transaction_completed_at = data[:updatedAt] + + def subaccount_amount_in_major_units + return if !subaccount || !fees_split + + fees_split.subaccount / BigDecimal(100) + end + + def failure_reason + return if !transaction_failed? && !transaction_abandoned? + + data.gateway_response || transaction_status || message + end + end + end +end diff --git a/lib/paystack_gateway/extensions/transfer_extensions.rb b/lib/paystack_gateway/extensions/transfer_extensions.rb new file mode 100644 index 0000000..a86a770 --- /dev/null +++ b/lib/paystack_gateway/extensions/transfer_extensions.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require_relative 'transaction_response_extension' + +module PaystackGateway + module Extensions + # Helpers for PaystackGateway::Transfer + module TransferExtensions + # Helpers for PaystackGateway::Transfer::InitiateResponse + module InitiateResponseExtension + def transaction_completed_at = transferred_at || super + end + + # Helpers for PaystackGateway::Transfer::InitiateError + module InitiateErrorExtension + def transaction_failed? = response_body[:status] == false && http_code == 400 + def failure_reason = response_body[:message] + end + + # Helpers for PaystackGateway::Transfer::VerifyResponse + module VerifyResponseExtension + def transaction_completed_at = transferred_at || super + end + + # Helpers for PaystackGateway::Transfer::VerifyError + module VerifyErrorExtension + def transaction_not_found? + response_body[:status] == false && response_body[:message].match?(/transfer not found/i) + end + end + end + end +end + +if PaystackGateway.use_extensions + PaystackGateway::Transfer::InitiateResponse.include( + PaystackGateway::Extensions::TransferExtensions::InitiateResponseExtension, + PaystackGateway::Extensions::TransactionResponseExtension, + ) + PaystackGateway::Transfer::InitiateError.include( + PaystackGateway::Extensions::TransferExtensions::InitiateErrorExtension, + ) + + PaystackGateway::Transfer::VerifyResponse.include( + PaystackGateway::Extensions::TransferExtensions::VerifyResponseExtension, + ) + PaystackGateway::Transfer::VerifyError.include( + PaystackGateway::Extensions::TransferExtensions::VerifyErrorExtension, + ) +end From 8cfa5f8e23f411d4ca3b60e239bf2de4fa385008 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 04:17:11 +0100 Subject: [PATCH 19/26] Full response is only logged in debug mode --- lib/paystack_gateway/configuration.rb | 2 +- lib/paystack_gateway/request_module.rb | 2 +- test/paystack_gateway/request_module_test.rb | 19 +++++++++++-------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/paystack_gateway/configuration.rb b/lib/paystack_gateway/configuration.rb index d0e29cc..ab683c4 100644 --- a/lib/paystack_gateway/configuration.rb +++ b/lib/paystack_gateway/configuration.rb @@ -10,7 +10,7 @@ class Configuration attr_accessor :secret_key, :logger, :logging_options, :log_filter, :use_extensions def initialize - @logger = Logger.new($stdout) + @logger = Logger.new($stdout, level: :info) @log_filter = lambda(&:dup) @use_extensions = true end diff --git a/lib/paystack_gateway/request_module.rb b/lib/paystack_gateway/request_module.rb index e85c140..bacbd7b 100644 --- a/lib/paystack_gateway/request_module.rb +++ b/lib/paystack_gateway/request_module.rb @@ -74,7 +74,7 @@ def decorate_api_methods(method_names = api_methods) def handle_error(error) PaystackGateway.logger.error "#{Current.qualified_api_method_name}: #{error.message}" - PaystackGateway.logger.error JSON.pretty_generate(filtered_response(error.response) || {}) if error.response + PaystackGateway.logger.debug { JSON.pretty_generate(filtered_response(error.response) || {}) } if error.response raise Current.error_class.new( "Paystack error: #{error.message}, status: #{error.response_status}, response: #{error.response_body}", diff --git a/test/paystack_gateway/request_module_test.rb b/test/paystack_gateway/request_module_test.rb index e38cb86..a14414d 100644 --- a/test/paystack_gateway/request_module_test.rb +++ b/test/paystack_gateway/request_module_test.rb @@ -58,20 +58,23 @@ def test_api_methods_are_decorated_with_current_attributes def test_api_methods_are_decorated_with_error_handling logger_error_mock = Minitest::Mock.new logger_error_mock.expect(:call, nil, [/RequestModuleTest::MockRequestModule#mock_api_error_method:.*404/]) - logger_error_mock.expect( + + logger_debug_mock = Minitest::Mock.new + logger_debug_mock.expect( :call, nil, - [/"request_method":.*"request_url":.*"request_headers":.*"response_headers":.*"response_body":/m], - ) + ) { [/"request_method":.*"request_url":.*"request_headers":.*"response_headers":.*"response_body":/m] } PaystackGateway.logger.stub(:error, logger_error_mock) do - error = assert_raises MockRequestModule::MockApiErrorMethodError do - VCR.use_cassette 'mock_failure' do - MockRequestModule.mock_api_error_method + PaystackGateway.logger.stub(:debug, logger_debug_mock) do + error = assert_raises MockRequestModule::MockApiErrorMethodError do + VCR.use_cassette 'mock_failure' do + MockRequestModule.mock_api_error_method + end end - end - assert_match(/Paystack error:.*server.*responded.*404.*status:.*response:/, error.message) + assert_match(/Paystack error:.*server.*responded.*404.*status:.*response:/, error.message) + end end logger_error_mock.verify From 82d71b78f5a3f8e3d0e3e70bd2df20e78f1e341a Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 18:13:32 +0100 Subject: [PATCH 20/26] Use block for more efficient logging --- lib/paystack_gateway/request_module.rb | 2 +- test/paystack_gateway/request_module_test.rb | 25 ++++++++++---------- test/test_helper.rb | 14 +++++------ 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/lib/paystack_gateway/request_module.rb b/lib/paystack_gateway/request_module.rb index bacbd7b..fb9b623 100644 --- a/lib/paystack_gateway/request_module.rb +++ b/lib/paystack_gateway/request_module.rb @@ -73,7 +73,7 @@ def decorate_api_methods(method_names = api_methods) end def handle_error(error) - PaystackGateway.logger.error "#{Current.qualified_api_method_name}: #{error.message}" + PaystackGateway.logger.error { "#{Current.qualified_api_method_name}: #{error.message}" } PaystackGateway.logger.debug { JSON.pretty_generate(filtered_response(error.response) || {}) } if error.response raise Current.error_class.new( diff --git a/test/paystack_gateway/request_module_test.rb b/test/paystack_gateway/request_module_test.rb index a14414d..e883352 100644 --- a/test/paystack_gateway/request_module_test.rb +++ b/test/paystack_gateway/request_module_test.rb @@ -56,17 +56,18 @@ def test_api_methods_are_decorated_with_current_attributes end def test_api_methods_are_decorated_with_error_handling - logger_error_mock = Minitest::Mock.new - logger_error_mock.expect(:call, nil, [/RequestModuleTest::MockRequestModule#mock_api_error_method:.*404/]) - - logger_debug_mock = Minitest::Mock.new - logger_debug_mock.expect( - :call, - nil, - ) { [/"request_method":.*"request_url":.*"request_headers":.*"response_headers":.*"response_body":/m] } - - PaystackGateway.logger.stub(:error, logger_error_mock) do - PaystackGateway.logger.stub(:debug, logger_debug_mock) do + assert_message_received( + PaystackGateway.logger, :error, + block_matcher: ->(&blk) { blk.call.match?(/RequestModuleTest::MockRequestModule#mock_api_error_method:.*404/) }, + ) do + assert_message_received( + PaystackGateway.logger, :debug, + block_matcher: lambda { |&blk| + blk.call.match?( + /"request_method":.*"request_url":.*"request_headers":.*"response_headers":.*"response_body":/m, + ) + }, + ) do error = assert_raises MockRequestModule::MockApiErrorMethodError do VCR.use_cassette 'mock_failure' do MockRequestModule.mock_api_error_method @@ -76,7 +77,5 @@ def test_api_methods_are_decorated_with_error_handling assert_match(/Paystack error:.*server.*responded.*404.*status:.*response:/, error.message) end end - - logger_error_mock.verify end end diff --git a/test/test_helper.rb b/test/test_helper.rb index a9badd4..36bceec 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -26,17 +26,17 @@ module Assertions # assert_message(SomeService, :some_method, *args, **kwargs) do # SomeService.some_method(*args, **kwargs) # end - def assert_message_received(receiver, message, return_val = nil, args = [], **, &) + def assert_message_received(receiver, message, args = [], return_val: nil, block_matcher: nil, **, &) mock = Minitest::Mock.new - mock.expect(:call, return_val, args, **) + mock.expect(:call, return_val, args, **, &block_matcher) receiver.stub(message, mock, &) - begin - assert_mock mock - rescue MockExpectationError - raise Minitest::Assertion, "Expected #{receiver} to receive #{message} with #{args.inspect}, but it did not." - end + assert_mock mock + rescue MockExpectationError => e + raise Minitest::Assertion, "Expected #{receiver} to receive #{message} " \ + "with #{args.inspect}, but it did not.\n" \ + "Instead, #{e.message}" end end end From 1f46ff8d16a4553090c7eab18a21a2897407e5e2 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 18:55:00 +0100 Subject: [PATCH 21/26] Add helper for expecting multiple messages --- .github/workflows/main.yml | 2 +- test/paystack_gateway/request_module_test.rb | 14 ++++---- test/test_helper.rb | 35 +++++++++++++++++++- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 12ea855..f14a38f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: ruby: - - '3.3.5' + - '3.3.7' steps: - uses: actions/checkout@v4 diff --git a/test/paystack_gateway/request_module_test.rb b/test/paystack_gateway/request_module_test.rb index e883352..d03153b 100644 --- a/test/paystack_gateway/request_module_test.rb +++ b/test/paystack_gateway/request_module_test.rb @@ -26,19 +26,19 @@ def test_module_lists_api_methods end def test_api_methods_make_requests_and_return_responses - logger_info_mock = Minitest::Mock.new - logger_info_mock.expect(:call, nil, ['request']) - logger_info_mock.expect(:call, nil, ['response']) - - PaystackGateway.logger.stub(:info, logger_info_mock) do + assert_messages_received( + PaystackGateway.logger, :info, + [ + { block_matcher: ->(&blk) { blk.call.match?(/request/) } }, + { block_matcher: ->(&blk) { blk.call.match?(/response/) }}, + ], + ) do VCR.use_cassette 'miscellaneous/country_success' do response = MockRequestModule.mock_api_method assert_instance_of MockRequestModule::MockApiMethodResponse, response end end - - logger_info_mock.verify end def test_api_methods_are_decorated_with_current_attributes diff --git a/test/test_helper.rb b/test/test_helper.rb index 36bceec..d358339 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -19,11 +19,12 @@ module Minitest module Assertions + # Fails unless +receiver+ received message +message+, optionally with +args+ # (and +kwargs+). The message is stubbed to return +return_val+ # # @example Asserting a method call with a block - # assert_message(SomeService, :some_method, *args, **kwargs) do + # assert_message_received(SomeService, :some_method, *args, **kwargs) do # SomeService.some_method(*args, **kwargs) # end def assert_message_received(receiver, message, args = [], return_val: nil, block_matcher: nil, **, &) @@ -38,5 +39,37 @@ def assert_message_received(receiver, message, args = [], return_val: nil, block "with #{args.inspect}, but it did not.\n" \ "Instead, #{e.message}" end + + # Fails unless +receiver+ received message +message+ multiple times + # with the specified expectations. + # + # @example Asserting multiple calls to a method + # assert_messages_received(SomeService, :some_method, [ + # { args: [arg1], return_val: val1 }, + # { args: [arg2], return_val: val2 } + # ]) do + # SomeService.some_method(arg1) + # SomeService.some_method(arg2) + # end + def assert_messages_received(receiver, message, expectations, &) + mock = Minitest::Mock.new + + expectations.each do |expectation| + args = expectation[:args] || [] + kwargs = expectation[:kwargs] || {} + return_val = expectation[:return_val] + block_matcher = expectation[:block_matcher] + + mock.expect(:call, return_val, args, **kwargs, &block_matcher) + end + + receiver.stub(message, mock, &) + + assert_mock mock + rescue MockExpectationError => e + raise Minitest::Assertion, "Expected #{receiver} to receive #{message} " \ + "multiple times with specified arguments, but expectations were not met.\n" \ + "Error: #{e.message}" + end end end From 787a39e280c593bca88181cbd55aceb901296364 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 19:01:01 +0100 Subject: [PATCH 22/26] Fix linting issues --- test/paystack_gateway/request_module_test.rb | 2 +- test/test_helper.rb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/paystack_gateway/request_module_test.rb b/test/paystack_gateway/request_module_test.rb index d03153b..718a3dd 100644 --- a/test/paystack_gateway/request_module_test.rb +++ b/test/paystack_gateway/request_module_test.rb @@ -30,7 +30,7 @@ def test_api_methods_make_requests_and_return_responses PaystackGateway.logger, :info, [ { block_matcher: ->(&blk) { blk.call.match?(/request/) } }, - { block_matcher: ->(&blk) { blk.call.match?(/response/) }}, + { block_matcher: ->(&blk) { blk.call.match?(/response/) } }, ], ) do VCR.use_cassette 'miscellaneous/country_success' do diff --git a/test/test_helper.rb b/test/test_helper.rb index d358339..0d2f19c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -19,7 +19,6 @@ module Minitest module Assertions - # Fails unless +receiver+ received message +message+, optionally with +args+ # (and +kwargs+). The message is stubbed to return +return_val+ # From 65781339b5f28171c15cf0a8a0cd3b0ebfe6f456 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 20:33:05 +0100 Subject: [PATCH 23/26] Fix rubocop warnings --- .rubocop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index adb2d67..c51a20e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,4 @@ -require: +plugins: - rubocop-minitest - rubocop-rake From b7bcb0b4f0cad23d0634db9d9ff6c702958a758d Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 20:33:36 +0100 Subject: [PATCH 24/26] Add lint and test scripts --- bin/lint | 2 ++ bin/test | 1 + 2 files changed, 3 insertions(+) create mode 100755 bin/lint create mode 100755 bin/test diff --git a/bin/lint b/bin/lint new file mode 100755 index 0000000..229f25f --- /dev/null +++ b/bin/lint @@ -0,0 +1,2 @@ +bundle exec rubocop + diff --git a/bin/test b/bin/test new file mode 100755 index 0000000..7f34a1c --- /dev/null +++ b/bin/test @@ -0,0 +1 @@ +bundle exec rake test From a6315ac61b74ecfbf5bea8c39172e288a4ab6adc Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 20:34:03 +0100 Subject: [PATCH 25/26] Fix customer fetch endpoint --- lib/paystack_gateway/customer.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/paystack_gateway/customer.rb b/lib/paystack_gateway/customer.rb index 630d660..ba8c918 100644 --- a/lib/paystack_gateway/customer.rb +++ b/lib/paystack_gateway/customer.rb @@ -135,10 +135,11 @@ class FetchError < ApiError; end # https://paystack.com/docs/api/customer/#fetch # Fetch Customer: GET /customer/{code} # + # @param code [String] (required) # # @return [FetchResponse] successful response # @raise [FetchError] if the request fails - api_method def self.fetch + api_method def self.fetch(code:) use_connection do |connection| connection.get( "/customer/#{code}", From 7cdd93dcd868ca685c239e339f84be6207e6d70b Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sun, 30 Mar 2025 20:35:04 +0100 Subject: [PATCH 26/26] Update readme with information on using open API spec --- README.md | 183 ++++++++++++++++++++++++------------------------------ 1 file changed, 81 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 2f75263..e6cd991 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,10 @@ To use the PaystackGateway gem, you need to configure it with your Paystack secr The configuration options are - `secret_key`: Your paystack api key used to authorize requests -- `logger`: Your ruby Logger. Default is Logger.new($stdout) +- `logger`: Your ruby Logger. Default is `Logger.new($stdout)` - `logging_options`: Options passed to [Faraday logger middleware](https://github.com/lostisland/faraday/blob/main/lib/faraday/response/logger.rb). Default is `{ headers: false }` - `log_filter`: Filter used when logging headers and body. +- `use_extensions`: Enable extension modules that add useful helper methods to response and error objects. Default is `true`. ```ruby # config/initializers/paystack_gateway.rb @@ -41,30 +42,78 @@ end ## Usage -### Calling API endpoints -Once configured, you can start using the various API modules and methods provided by the gem. They are designed to mirror how the api methods are grouped and listed on the [Paystack API](https://paystack.com/docs/api/). +Once configured, you can begin utilising the various API modules and methods provided by the gem. +```irb +:001 > r = PaystackGateway::Customer.fetch(code: 'test@example.com') + I, [2025-03-30T19:53:06.951015 #29623] INFO -- : request: GET https://api.paystack.co/customer/test@example.com + I, [2025-03-30T19:53:07.486206 #29623] INFO -- : response: Status 200 + => + {:status=>true, + ... +:002 > r.class + => PaystackGateway::Customer::FetchResponse +:003 > r.customer_code + => "CUS_xsrozmbt8g1oear" +``` -Here's an example creating a customer using the [/customer/create endpoint](https://paystack.com/docs/api/customer/#create). +### API Organisation -```ruby -response = PaystackGateway::Customers.create_customer( - email: 'test@example.com', - first_name: 'Test', - last_name: 'User', -) +The code is generated directly from [Paystack's OpenAPI specification](https://github.com/PaystackOSS/openapi), ensuring that it mirrors Paystack’s API organisation accurately. Refer to the API documentation for detailed schemas and available options for each endpoint. + +- **API Modules**: Each API tag in the Paystack documentation becomes a module under `PaystackGateway`. For example, the Transaction API is accessible via `PaystackGateway::Transaction`. +- **API Methods**: Each operation within a tag is implemented as a method in the corresponding module. For instance, to verify a transaction, you call `PaystackGateway::Transaction.verify`. + +### Parameters + +All API parameters are implemented as method arguments: + +- **Path parameters**: Required parameters in the URL path (e.g., `reference` in `/transaction/verify/{reference}`) +- **Query parameters**: Optional parameters for GET requests (e.g., `from`, `to` in `/transaction/totals`) +- **Request body parameters**: Parameters sent in the request body for POST/PUT requests + +Required parameters are clearly marked in the method signatures, while optional parameters typically default to `nil`. + +### Responses + +Each API method returns a specific response object. For example, when you call `PaystackGateway::Transaction.verify`, you will receive either: + +- `PaystackGateway::Transaction::VerifyResponse`, which indicates a successful call. +- `PaystackGateway::Transaction::VerifyError`, which is raised if the call fails. + +Paystack responses usually include the main payload nested in the `data` field of the response body. To simplify access, the response objects automatically delegate known attributes from this field, allowing you to reference them directly. + +For instance, here’s how you can fetch a customer using the [/customer/{code} endpoint](https://paystack.com/docs/api/customer/#fetch): + +```ruby +response = PaystackGateway::Customer.fetch(code: 'CUS_xsrozmbt8g1oear') + +# An example of the original response body: +# {:status=>true, +# :message=>"Customer retrieved", +# :data=> +# {"email"=>"test@example.com", +# "phone"=>"+2348011111111", +# "customer_code"=>"CUS_xsrozmbt8g1oear", +# "id"=>203316808, +# ... + +# You can access the attributes directly: response.id # => 203316808 response.customer_code # => "CUS_xsrozmbt8g1oear" + +# Alternatively, you can access them via the data field: +response.data.id # => 203316808 +response.data.customer_code # => "CUS_xsrozmbt8g1oear" ``` ### Error Handling -Whenever a network error occurs or the called endpoint responds with an error response, a `PaystackGateway::ApiError`(or a subclass of it) is raised that can be handled in your calling code. -Here's an example initializing a transaction using the [/transaction/initialize endpoint](https://paystack.com/docs/api/transaction/#initialize) +Whenever a network error occurs or the called endpoint returns an error response, a `PaystackGateway::ApiError` (or one of its subclasses) is raised, which you can handle in your code. For example, initialising a transaction using the [/transaction/initialize endpoint](https://paystack.com/docs/api/transaction/#initialize) might be done as follows: ```ruby begin - response = PaystackGateway::Transactions.initialize_transaction( + response = PaystackGateway::Transaction.initialize_transaction( email: 'test@example.com', amount: 1000, reference: 'test_reference', @@ -87,91 +136,21 @@ Some endpoints currently make use of caching: - Miscellaneous#list_banks - Verifications#resolve_account_number -Caching works using an [ActiveSupport::Cache::FileStore](https://api.rubyonrails.org/classes/ActiveSupport/Cache/FileStore.html) cache. The default caching period is 7 days and the cache data is stored on the file system at `ENV['TMPDIR']` or `/tmp/cache`. - - -## API Modules and Methods -> Refer to the [Paystack API documentation](https://paystack.com/docs/api) for details on all the available endpoints and their usage. - -### Implemented Modules and Methods - -Below is a complete list of the API modules and methods that are currently implemented. - -I invite you to collaborate on this project! If you need to use any of the unimplemented API methods or modules, or if you want to make modifications to the defaults or the level of configuration available in the currently implemented API methods, please feel free to raise a pull request (PR). See [Contributing](#contributing) and [Code of Conduct](#code-of-conduct) below - -- [x] [Transactions](https://paystack.com/docs/api/transaction/) - - [x] [Initialize Transaction](https://paystack.com/docs/api/transaction/#initialize) - - [x] [Verify Transaction](https://paystack.com/docs/api/transaction/#verify) - - [ ] [List Transactions](https://paystack.com/docs/api/transaction/#list) - - [ ] [Fetch Transaction](https://paystack.com/docs/api/transaction/#fetch) - - [x] [Charge Authorization](https://paystack.com/docs/api/transaction/#charge-authorization) - - [ ] [View Transaction Timeline](https://paystack.com/docs/api/transaction/#view-timeline) - - [ ] [Transaction Totals](https://paystack.com/docs/api/transaction/#totals) - - [ ] [Export Transactions](https://paystack.com/docs/api/transaction/#export) - - [ ] [Partial Debit](https://paystack.com/docs/api/transaction/#partial-debit) - -- [ ] [Customers](https://paystack.com/docs/api/customer/) - - [x] [Create Customer](https://paystack.com/docs/api/customer/#create) - - [ ] [List Customers](https://paystack.com/docs/api/customer/#list) - - [x] [Fetch Customer](https://paystack.com/docs/api/customer/#fetch) - - [ ] [Update Customer](https://paystack.com/docs/api/customer/#update) - - [ ] [Validate Customer](https://paystack.com/docs/api/customer/#validate) - - [ ] [Whitelist/Blacklist Customer](https://paystack.com/docs/api/customer/#whitelist-blacklist) - - [ ] [Deactivate Authorization](https://paystack.com/docs/api/customer/#deactivate-authorization) - -- [x] [Dedicated Virtual Accounts](https://paystack.com/docs/api/dedicated-virtual-account/) - - [x] [Create Dedicated Virtual Account](https://paystack.com/docs/api/dedicated-virtual-account/#create) - - [x] [Assign Dedicated Virtual Account](https://paystack.com/docs/api/dedicated-virtual-account/#assign) - - [ ] [List Dedicated Accounts](https://paystack.com/docs/api/dedicated-virtual-account/#list) - - [ ] [Fetch Dedicated Account](https://paystack.com/docs/api/dedicated-virtual-account/#fetch) - - [x] [Requery Dedicated Account](https://paystack.com/docs/api/dedicated-virtual-account/#requery) - - [ ] [Deactivate Dedicated Account](https://paystack.com/docs/api/dedicated-virtual-account/#deactivate) - - [x] [Split Dedicated Account Transaction](https://paystack.com/docs/api/dedicated-virtual-account/#add-split) - - [ ] [Remove Split from Dedicated Account](https://paystack.com/docs/api/dedicated-virtual-account/#remove-split) - - [ ] [Fetch Bank Providers](https://paystack.com/docs/api/dedicated-virtual-account/#providers) - -- [ ] [Subaccounts](https://paystack.com/docs/api/subaccount/) - - [x] [Create Subaccount](https://paystack.com/docs/api/subaccount/#create) - - [ ] [List Subaccounts](https://paystack.com/docs/api/subaccount/#list) - - [ ] [Fetch Subaccount](https://paystack.com/docs/api/subaccount/#fetch) - - [x] [Update Subaccount](https://paystack.com/docs/api/subaccount/#update) - -- [x] [Plans](https://paystack.com/docs/api/plan/) - - [x] [Create Plan](https://paystack.com/docs/api/plan/#create) - - [x] [List Plans](https://paystack.com/docs/api/plan/#list) - - [x] [Fetch Plan](https://paystack.com/docs/api/plan/#fetch) - - [x] [Update Plan](https://paystack.com/docs/api/plan/#update) - -- [x] [Transfer Recipients](https://paystack.com/docs/api/transfer-recipient/) - - [x] [Create Transfer Recipient](https://paystack.com/docs/api/transfer-recipient/#create) - - [ ] [Bulk Create Transfer Recipient](https://paystack.com/docs/api/transfer-recipient/#bulk) - - [ ] [List Transfer Recipients](https://paystack.com/docs/api/transfer-recipient/#list) - - [ ] [Fetch Transfer Recipient](https://paystack.com/docs/api/transfer-recipient/#fetch) - - [ ] [Update Transfer Recipient](https://paystack.com/docs/api/transfer-recipient/#update) - - [ ] [Delete Transfer Recipient](https://paystack.com/docs/api/transfer-recipient/#delete) - -- [x] [Transfers](https://paystack.com/docs/api/transfer/) - - [x] [Initiate Transfer](https://paystack.com/docs/api/transfer/#initiate) - - [ ] [Finalize Transfer](https://paystack.com/docs/api/transfer/#finalize) - - [ ] [Initiate Bulk Transfer](https://paystack.com/docs/api/transfer/#bulk) - - [ ] [List Transfers](https://paystack.com/docs/api/transfer/#list) - - [ ] [Fetch Transfer](https://paystack.com/docs/api/transfer/#fetch) - - [x] [Verify Transfer](https://paystack.com/docs/api/transfer/#verify) - -- [x] [Refunds](https://paystack.com/docs/api/refund/) - - [x] [Create Refund](https://paystack.com/docs/api/refund/#create) - - [x] [List Refunds](https://paystack.com/docs/api/refund/#list) - - [x] [Fetch Refund](https://paystack.com/docs/api/refund/#fetch) - -- [x] [Verification](https://paystack.com/docs/api/verification/) - - [x] [Resolve Account Number](https://paystack.com/docs/api/verification/#resolve-account) - - [ ] [Validate Account](https://paystack.com/docs/api/verification/#validate-account) - - [ ] [Resolve Card BIN](https://paystack.com/docs/api/verification/#resolve-card) - -- [x] [Miscellaneous](https://paystack.com/docs/api/miscellaneous/) - - [x] [List Banks](https://paystack.com/docs/api/miscellaneous/#bank) - - [ ] [List/Search Countries](https://paystack.com/docs/api/miscellaneous/#country) - - [ ] [List States (AVS)](https://paystack.com/docs/api/miscellaneous/#avs-states) +Caching works using an [ActiveSupport::Cache::FileStore](https://api.rubyonrails.org/classes/ActiveSupport/Cache/FileStore.html) cache. The default caching period is 7 days and the cache data is stored on the file system at `ENV['TMPDIR']` or `/tmp/cache`. + +### Extensions + +PaystackGateway includes extension modules that offer additional helper methods on both response and error objects. These modules are enabled by default, so you can immediately benefit from simpler data access and enhanced error handling. + +If you prefer to opt out of these enhancements, you can update your configuration as follows: + +```ruby +PaystackGateway.configure do |config| + config.use_extensions = false +end +``` + +For additional details on the available extensions and helper methods, please refer directly to the source code in the [extensions directory](https://github.com/darthrighteous/paystack-gateway/tree/main/lib/paystack_gateway/extensions). ## Development @@ -184,7 +163,7 @@ I invite you to collaborate on this project! If you need to use any of the unimp ### Setting up -After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. ### Running the tests and linter @@ -193,9 +172,9 @@ Minitest is used for unit tests. Rubocop is used to enforce the ruby style. To run the complete set of tests and linter run the following: ```bash -$ bundle install -$ bundle exec rake test -$ bundle exec rubocop +$ bin/setup +$ bin/test +$ bin/lint ``` ## Contributing