From c095430312cbe1b52b0f230c60126763638ea1ba Mon Sep 17 00:00:00 2001 From: Shepherd Yaw Morttey Date: Sun, 21 Sep 2025 20:53:46 +0000 Subject: [PATCH 1/3] feat(charges): add Mobile Money channel via Charges API Add PaystackSdk::Resources::Charges with: POST /charge for Mobile Money initiation (mobile_money.phone, mobile_money.provider) POST /charge/submit_otp for OTP/voucher flows (e.g., Vodafone) Validate payloads (email, amount, currency, reference) and provider codes (mtn, atl, vod, mpesa, orange, wave) Expose client.charges Add specs for Charges resource and client accessor No breaking changes. Mobile Money status verification continues via /transaction/verify/{reference} and webhook handling remains app-level. Also, Refactor code to standardize string delimiters and improve readability - No functional changes were made; this is purely a formatting improvement. --- Gemfile | 2 +- Rakefile | 6 +- bin/console | 6 +- lib/paystack_sdk.rb | 10 +- lib/paystack_sdk/client.rb | 29 ++- lib/paystack_sdk/resources/banks.rb | 6 +- lib/paystack_sdk/resources/base.rb | 8 +- lib/paystack_sdk/resources/charges.rb | 95 ++++++++ lib/paystack_sdk/resources/customers.rb | 54 ++--- lib/paystack_sdk/resources/transactions.rb | 90 +++---- .../resources/transfer_recipients.rb | 18 +- lib/paystack_sdk/resources/transfers.rb | 20 +- lib/paystack_sdk/resources/verification.rb | 16 +- lib/paystack_sdk/response.rb | 48 ++-- lib/paystack_sdk/utils/connection_utils.rb | 12 +- lib/paystack_sdk/validations.rb | 74 +++--- lib/paystack_sdk/version.rb | 2 +- paystack_sdk.gemspec | 36 +-- spec/client_spec.rb | 39 +-- spec/paystack_sdk_spec.rb | 2 +- spec/resources/banks_spec.rb | 26 +- spec/resources/charges_spec.rb | 83 +++++++ spec/resources/customers_spec.rb | 224 +++++++++--------- spec/resources/transactions_spec.rb | 178 +++++++------- spec/resources/transfer_recipients_spec.rb | 64 ++--- spec/resources/transfers_spec.rb | 62 ++--- spec/resources/verification_spec.rb | 78 +++--- spec/spec_helper.rb | 6 +- 28 files changed, 747 insertions(+), 547 deletions(-) create mode 100644 lib/paystack_sdk/resources/charges.rb create mode 100644 spec/resources/charges_spec.rb diff --git a/Gemfile b/Gemfile index bbc5e79..1c44ac3 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ # frozen_string_literal: true -source "https://rubygems.org" +source 'https://rubygems.org' # Specify your gem's dependencies in paystack_sdk.gemspec gemspec diff --git a/Rakefile b/Rakefile index df40677..7d38ee8 100644 --- a/Rakefile +++ b/Rakefile @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "bundler/gem_tasks" -require "rspec/core/rake_task" +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) -require "standard/rake" +require 'standard/rake' task default: %i[spec standard] diff --git a/bin/console b/bin/console index fc775ad..3e28b27 100755 --- a/bin/console +++ b/bin/console @@ -1,11 +1,11 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require "bundler/setup" -require "paystack_sdk" +require 'bundler/setup' +require 'paystack_sdk' # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. -require "irb" +require 'irb' IRB.start(__FILE__) diff --git a/lib/paystack_sdk.rb b/lib/paystack_sdk.rb index 6e2e02a..a3750d5 100644 --- a/lib/paystack_sdk.rb +++ b/lib/paystack_sdk.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require "faraday" -require_relative "paystack_sdk/version" -require_relative "paystack_sdk/client" +require 'faraday' +require_relative 'paystack_sdk/version' +require_relative 'paystack_sdk/client' module PaystackSdk # Base error class for all Paystack SDK errors. @@ -54,7 +54,7 @@ class APIError < Error; end # Raised when authentication fails class AuthenticationError < APIError - def initialize(message = "Invalid API key or authentication failed") + def initialize(message = 'Invalid API key or authentication failed') super end end @@ -83,7 +83,7 @@ def initialize(retry_after) class ServerError < APIError attr_reader :status_code - def initialize(status_code, message = "An error occurred on the Paystack server") + def initialize(status_code, message = 'An error occurred on the Paystack server') @status_code = status_code super("#{message} (Status: #{status_code})") end diff --git a/lib/paystack_sdk/client.rb b/lib/paystack_sdk/client.rb index 514a61d..951c996 100644 --- a/lib/paystack_sdk/client.rb +++ b/lib/paystack_sdk/client.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -require_relative "resources/transactions" -require_relative "resources/customers" -require_relative "resources/transfer_recipients" -require_relative "resources/transfers" -require_relative "resources/banks" -require_relative "resources/verification" -require_relative "utils/connection_utils" +require_relative 'resources/transactions' +require_relative 'resources/customers' +require_relative 'resources/transfer_recipients' +require_relative 'resources/transfers' +require_relative 'resources/banks' +require_relative 'resources/verification' +require_relative 'resources/charges' +require_relative 'utils/connection_utils' module PaystackSdk # The `Client` class serves as the main entry point for interacting with the Paystack API. @@ -121,5 +122,19 @@ def banks def verification @verification ||= Resources::Verification.new(@connection) end + + # Provides access to the `Charges` resource. + # + # @return [PaystackSdk::Resources::Charges] An instance of the + # `Charges` resource. + # + # @example + # ```ruby + # charges = client.charges + # response = charges.mobile_money(payload) + # ``` + def charges + @charges ||= Resources::Charges.new(@connection) + end end end diff --git a/lib/paystack_sdk/resources/banks.rb b/lib/paystack_sdk/resources/banks.rb index bc924d0..fd3117f 100644 --- a/lib/paystack_sdk/resources/banks.rb +++ b/lib/paystack_sdk/resources/banks.rb @@ -1,4 +1,4 @@ -require_relative "../validations" +require_relative '../validations' module PaystackSdk module Resources @@ -9,12 +9,12 @@ def list(query = {}) if query.key?(:currency) validate_allowed_values!( value: query[:currency], - name: "currency", + name: 'currency', allowed_values: %w[NGN GHS ZAR KES USD] ) end - handle_response(@connection.get("/bank", query)) + handle_response(@connection.get('/bank', query)) end end end diff --git a/lib/paystack_sdk/resources/base.rb b/lib/paystack_sdk/resources/base.rb index 2625c89..d2e0ef4 100644 --- a/lib/paystack_sdk/resources/base.rb +++ b/lib/paystack_sdk/resources/base.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require_relative "../response" -require_relative "../client" -require_relative "../validations" -require_relative "../utils/connection_utils" +require_relative '../response' +require_relative '../client' +require_relative '../validations' +require_relative '../utils/connection_utils' module PaystackSdk module Resources diff --git a/lib/paystack_sdk/resources/charges.rb b/lib/paystack_sdk/resources/charges.rb new file mode 100644 index 0000000..1f8763f --- /dev/null +++ b/lib/paystack_sdk/resources/charges.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require_relative 'base' + +module PaystackSdk + module Resources + # The `Charges` resource exposes helpers for initiating and managing charges + # through alternative payment channels such as Mobile Money. + # + # At the moment the SDK focuses on supporting the Mobile Money channel which + # requires posting to the `/charge` endpoint with the customer's email, + # amount, currency, and the provider specific `mobile_money` payload. + class Charges < PaystackSdk::Resources::Base + MOBILE_MONEY_PROVIDERS = %w[mtn atl vod mpesa orange wave].freeze + + # Initiates a Mobile Money payment. + # + # @param payload [Hash] The payload containing charge details. + # @option payload [String] :email Customer's email address (required) + # @option payload [Integer] :amount Amount in the lowest currency unit (required) + # @option payload [String] :currency ISO currency code (default: GHS) + # @option payload [String] :reference Optional reference supplied by the merchant + # @option payload [String] :callback_url Optional callback URL for Paystack to redirect to + # @option payload [Hash] :metadata Optional metadata to attach to the transaction + # @option payload [Hash] :mobile_money The mobile money details (required) + # - :phone [String] Customer's mobile money phone number (required) + # - :provider [String] Mobile money provider code (required) + # + # @return [PaystackSdk::Response] The wrapped API response. + # @raise [PaystackSdk::ValidationError] If the payload is invalid. + def mobile_money(payload) + validate_mobile_money_payload!(payload) + + response = @connection.post('/charge', payload) + handle_response(response) + end + + # Submits an OTP for authorising a pending Mobile Money charge (e.g. Vodafone). + # + # @param payload [Hash] Payload containing the OTP and charge reference. + # @option payload [String] :otp The OTP supplied by the customer (required) + # @option payload [String] :reference The charge reference returned from initiation (required) + # + # @return [PaystackSdk::Response] The wrapped API response. + # @raise [PaystackSdk::ValidationError] If the payload is invalid. + def submit_otp(payload) + validate_fields!( + payload: payload, + validations: { + otp: { type: :string, required: true }, + reference: { type: :reference, required: true } + } + ) + + response = @connection.post('/charge/submit_otp', payload) + handle_response(response) + end + + private + + def validate_mobile_money_payload!(payload) + validate_fields!( + payload: payload, + validations: { + email: { type: :email, required: true }, + amount: { type: :positive_integer, required: true }, + currency: { type: :currency, required: false }, + reference: { type: :reference, required: false }, + callback_url: { required: false }, + metadata: { required: false }, + mobile_money: { required: true } + } + ) + + mobile_money = payload[:mobile_money] || payload['mobile_money'] + validate_hash!(input: mobile_money, name: 'mobile_money') + + phone = mobile_money[:phone] || mobile_money['phone'] + validate_presence!(value: phone, name: 'mobile_money phone') + + provider = mobile_money[:provider] || mobile_money['provider'] + validate_mobile_money_provider!(provider) + end + + def validate_mobile_money_provider!(provider) + validate_allowed_values!( + value: provider&.downcase, + allowed_values: MOBILE_MONEY_PROVIDERS, + name: 'mobile_money provider', + allow_nil: false + ) + end + end + end +end diff --git a/lib/paystack_sdk/resources/customers.rb b/lib/paystack_sdk/resources/customers.rb index 9fecdb9..14f00fd 100644 --- a/lib/paystack_sdk/resources/customers.rb +++ b/lib/paystack_sdk/resources/customers.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "base" +require_relative 'base' module PaystackSdk module Resources @@ -45,14 +45,14 @@ def create(payload) validate_fields!( payload: payload, validations: { - email: {type: :email, required: true}, - first_name: {type: :string, required: false}, - last_name: {type: :string, required: false}, - phone: {type: :string, required: false} + email: { type: :email, required: true }, + first_name: { type: :string, required: false }, + last_name: { type: :string, required: false }, + phone: { type: :string, required: false } } ) - response = @connection.post("customer", payload) + response = @connection.post('customer', payload) handle_response(response) end @@ -65,19 +65,15 @@ def create(payload) # @return [PaystackSdk::Response] The response from the Paystack API. # @raise [PaystackSdk::Error] If the API request fails. def list(per_page: 50, page: 1, **params) - validate_positive_integer!(value: per_page, name: "per_page", allow_nil: true) - validate_positive_integer!(value: page, name: "page", allow_nil: true) + validate_positive_integer!(value: per_page, name: 'per_page', allow_nil: true) + validate_positive_integer!(value: page, name: 'page', allow_nil: true) - if params[:from] - validate_date_format!(date_str: params[:from], name: "from") - end + validate_date_format!(date_str: params[:from], name: 'from') if params[:from] - if params[:to] - validate_date_format!(date_str: params[:to], name: "to") - end + validate_date_format!(date_str: params[:to], name: 'to') if params[:to] - query_params = {perPage: per_page, page: page}.merge(params) - response = @connection.get("customer", query_params) + query_params = { perPage: per_page, page: page }.merge(params) + response = @connection.get('customer', query_params) handle_response(response) end @@ -87,7 +83,7 @@ def list(per_page: 50, page: 1, **params) # @return [PaystackSdk::Response] The response from the Paystack API. # @raise [PaystackSdk::Error] If the parameter is invalid or the API request fails. def fetch(email_or_code) - validate_presence!(value: email_or_code, name: "email_or_code") + validate_presence!(value: email_or_code, name: 'email_or_code') response = @connection.get("customer/#{email_or_code}") handle_response(response) end @@ -103,8 +99,8 @@ def fetch(email_or_code) # @return [PaystackSdk::Response] The response from the Paystack API. # @raise [PaystackSdk::Error] If the parameters are invalid or the API request fails. def update(code, payload) - validate_presence!(value: code, name: "code") - validate_hash!(input: payload, name: "payload") + validate_presence!(value: code, name: 'code') + validate_hash!(input: payload, name: 'payload') response = @connection.put("customer/#{code}", payload) handle_response(response) @@ -124,14 +120,14 @@ def update(code, payload) # @return [PaystackSdk::Response] The response from the Paystack API. # @raise [PaystackSdk::Error] If the parameters are invalid or the API request fails. def validate(code, payload) - validate_presence!(value: code, name: "code") + validate_presence!(value: code, name: 'code') validate_fields!( payload: payload, validations: { - country: {type: :string, required: true}, - type: {type: :string, required: true}, - account_number: {type: :string, required: true}, - bank_code: {type: :string, required: true} + country: { type: :string, required: true }, + type: { type: :string, required: true }, + account_number: { type: :string, required: true }, + bank_code: { type: :string, required: true } } ) @@ -161,12 +157,12 @@ def set_risk_action(payload) validate_fields!( payload: payload, validations: { - customer: {type: :string, required: true}, - risk_action: {type: :inclusion, required: true, allowed_values: %w[default allow deny]} + customer: { type: :string, required: true }, + risk_action: { type: :inclusion, required: true, allowed_values: %w[default allow deny] } } ) - response = @connection.post("customer/set_risk_action", payload) + response = @connection.post('customer/set_risk_action', payload) handle_response(response) end @@ -180,11 +176,11 @@ def deactivate_authorization(payload) validate_fields!( payload: payload, validations: { - authorization_code: {type: :string, required: true} + authorization_code: { type: :string, required: true } } ) - response = @connection.post("customer/deactivate_authorization", payload) + response = @connection.post('customer/deactivate_authorization', payload) handle_response(response) end end diff --git a/lib/paystack_sdk/resources/transactions.rb b/lib/paystack_sdk/resources/transactions.rb index ac6918d..18e98eb 100644 --- a/lib/paystack_sdk/resources/transactions.rb +++ b/lib/paystack_sdk/resources/transactions.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "base" +require_relative 'base' module PaystackSdk module Resources @@ -57,15 +57,15 @@ def initiate(payload) validate_fields!( payload: payload, validations: { - email: {type: :email, required: true}, - amount: {type: :positive_integer, required: true}, - currency: {type: :currency, required: false}, - reference: {type: :reference, required: false}, - callback_url: {required: false} + email: { type: :email, required: true }, + amount: { type: :positive_integer, required: true }, + currency: { type: :currency, required: false }, + reference: { type: :reference, required: false }, + callback_url: { required: false } } ) - response = @connection.post("/transaction/initialize", payload) + response = @connection.post('/transaction/initialize', payload) handle_response(response) end @@ -78,7 +78,7 @@ def initiate(payload) # @example # response = transactions.verify(reference: "transaction_reference") def verify(reference:) - validate_presence!(value: reference, name: "Reference") + validate_presence!(value: reference, name: 'Reference') response = @connection.get("/transaction/verify/#{reference}") handle_response(response) @@ -104,26 +104,26 @@ def verify(reference:) # response = transactions.list(per_page: 10, from: "2023-01-01", to: "2023-12-31", status: "success") def list(per_page: 50, page: 1, **params) # Create a combined parameter hash for validation - all_params = {per_page: per_page, page: page}.merge(params) + all_params = { per_page: per_page, page: page }.merge(params) # Validate parameters validate_fields!( payload: all_params, validations: { - per_page: {type: :positive_integer, required: false}, - page: {type: :positive_integer, required: false}, - from: {type: :date, required: false}, - to: {type: :date, required: false}, - status: {type: :inclusion, allowed_values: %w[failed success abandoned], required: false}, - customer: {type: :positive_integer, required: false}, - amount: {type: :positive_integer, required: false}, - currency: {type: :currency, required: false} + per_page: { type: :positive_integer, required: false }, + page: { type: :positive_integer, required: false }, + from: { type: :date, required: false }, + to: { type: :date, required: false }, + status: { type: :inclusion, allowed_values: %w[failed success abandoned], required: false }, + customer: { type: :positive_integer, required: false }, + amount: { type: :positive_integer, required: false }, + currency: { type: :currency, required: false } } ) # Prepare request parameters - request_params = {perPage: per_page, page: page}.merge(params) - response = @connection.get("/transaction", request_params) + request_params = { perPage: per_page, page: page }.merge(params) + response = @connection.get('/transaction', request_params) handle_response(response) end @@ -136,7 +136,7 @@ def list(per_page: 50, page: 1, **params) # @example # response = transactions.fetch("12345") def fetch(transaction_id) - validate_presence!(value: transaction_id, name: "Transaction ID") + validate_presence!(value: transaction_id, name: 'Transaction ID') response = @connection.get("/transaction/#{transaction_id}") handle_response(response) @@ -157,12 +157,12 @@ def totals(**params) validate_fields!( payload: params, validations: { - from: {type: :date, required: false}, - to: {type: :date, required: false} + from: { type: :date, required: false }, + to: { type: :date, required: false } } ) - response = @connection.get("/transaction/totals", params) + response = @connection.get('/transaction/totals', params) handle_response(response) end @@ -188,18 +188,18 @@ def export(**params) validate_fields!( payload: params, validations: { - from: {type: :date, required: false}, - to: {type: :date, required: false}, - status: {type: :inclusion, allowed_values: %w[failed success abandoned], required: false}, - currency: {type: :currency, required: false}, - amount: {type: :positive_integer, required: false}, - payment_page: {type: :positive_integer, required: false}, - customer: {type: :positive_integer, required: false}, - settlement: {type: :positive_integer, required: false} + from: { type: :date, required: false }, + to: { type: :date, required: false }, + status: { type: :inclusion, allowed_values: %w[failed success abandoned], required: false }, + currency: { type: :currency, required: false }, + amount: { type: :positive_integer, required: false }, + payment_page: { type: :positive_integer, required: false }, + customer: { type: :positive_integer, required: false }, + settlement: { type: :positive_integer, required: false } } ) - response = @connection.get("/transaction/export", params) + response = @connection.get('/transaction/export', params) handle_response(response) end @@ -227,15 +227,15 @@ def charge_authorization(payload) validate_fields!( payload: payload, validations: { - authorization_code: {required: true}, - email: {type: :email, required: true}, - amount: {type: :positive_integer, required: true}, - reference: {type: :reference, required: false}, - currency: {type: :currency, required: false} + authorization_code: { required: true }, + email: { type: :email, required: true }, + amount: { type: :positive_integer, required: true }, + reference: { type: :reference, required: false }, + currency: { type: :currency, required: false } } ) - response = @connection.post("/transaction/charge_authorization", payload) + response = @connection.post('/transaction/charge_authorization', payload) handle_response(response) end @@ -264,15 +264,15 @@ def partial_debit(payload) validate_fields!( payload: payload, validations: { - authorization_code: {required: true}, - currency: {type: :currency, required: true}, - amount: {type: :positive_integer, required: true}, - email: {type: :email, required: true}, - reference: {type: :reference, required: false} + authorization_code: { required: true }, + currency: { type: :currency, required: true }, + amount: { type: :positive_integer, required: true }, + email: { type: :email, required: true }, + reference: { type: :reference, required: false } } ) - response = @connection.post("/transaction/partial_debit", payload) + response = @connection.post('/transaction/partial_debit', payload) handle_response(response) end @@ -287,7 +287,7 @@ def partial_debit(payload) # # OR # response = transactions.timeline("ref_123456789") def timeline(id_or_reference) - validate_presence!(value: id_or_reference, name: "Transaction ID or Reference") + validate_presence!(value: id_or_reference, name: 'Transaction ID or Reference') response = @connection.get("/transaction/timeline/#{id_or_reference}") handle_response(response) diff --git a/lib/paystack_sdk/resources/transfer_recipients.rb b/lib/paystack_sdk/resources/transfer_recipients.rb index 8f755e7..d58560f 100644 --- a/lib/paystack_sdk/resources/transfer_recipients.rb +++ b/lib/paystack_sdk/resources/transfer_recipients.rb @@ -1,4 +1,4 @@ -require_relative "../validations" +require_relative '../validations' module PaystackSdk module Resources @@ -6,40 +6,40 @@ class TransferRecipients < Base # Create a transfer recipient # @see https://paystack.com/docs/api/transfer-recipient/#create def create(params) - validate_hash!(input: params, name: "TransferRecipient params") + validate_hash!(input: params, name: 'TransferRecipient params') validate_required_params!( payload: params, required_params: %i[type name account_number bank_code], - operation_name: "Create Transfer Recipient" + operation_name: 'Create Transfer Recipient' ) - handle_response(@connection.post("/transferrecipient", params)) + handle_response(@connection.post('/transferrecipient', params)) end # List transfer recipients # @see https://paystack.com/docs/api/transfer-recipient/#list def list(query = {}) - handle_response(@connection.get("/transferrecipient", query)) + handle_response(@connection.get('/transferrecipient', query)) end # Fetch a transfer recipient # @see https://paystack.com/docs/api/transfer-recipient/#fetch def fetch(recipient_code:) - validate_presence!(value: recipient_code, name: "recipient_code") + validate_presence!(value: recipient_code, name: 'recipient_code') handle_response(@connection.get("/transferrecipient/#{recipient_code}")) end # Update a transfer recipient # @see https://paystack.com/docs/api/transfer-recipient/#update def update(recipient_code:, params:) - validate_presence!(value: recipient_code, name: "recipient_code") - validate_hash!(input: params, name: "Update TransferRecipient params") + validate_presence!(value: recipient_code, name: 'recipient_code') + validate_hash!(input: params, name: 'Update TransferRecipient params') handle_response(@connection.put("/transferrecipient/#{recipient_code}", params)) end # Delete a transfer recipient # @see https://paystack.com/docs/api/transfer-recipient/#delete def delete(recipient_code:) - validate_presence!(value: recipient_code, name: "recipient_code") + validate_presence!(value: recipient_code, name: 'recipient_code') handle_response(@connection.delete("/transferrecipient/#{recipient_code}")) end end diff --git a/lib/paystack_sdk/resources/transfers.rb b/lib/paystack_sdk/resources/transfers.rb index ca844c5..37c7689 100644 --- a/lib/paystack_sdk/resources/transfers.rb +++ b/lib/paystack_sdk/resources/transfers.rb @@ -1,4 +1,4 @@ -require_relative "../validations" +require_relative '../validations' module PaystackSdk module Resources @@ -6,40 +6,40 @@ class Transfers < Base # Create a transfer # @see https://paystack.com/docs/api/transfer/#initiate def create(params) - validate_hash!(input: params, name: "Transfer params") + validate_hash!(input: params, name: 'Transfer params') validate_required_params!( payload: params, required_params: %i[source amount recipient], - operation_name: "Create Transfer" + operation_name: 'Create Transfer' ) - handle_response(@connection.post("/transfer", params)) + handle_response(@connection.post('/transfer', params)) end # List transfers # @see https://paystack.com/docs/api/transfer/#list def list(query = {}) - handle_response(@connection.get("/transfer", query)) + handle_response(@connection.get('/transfer', query)) end # Fetch a transfer # @see https://paystack.com/docs/api/transfer/#fetch def fetch(id:) - validate_presence!(value: id, name: "transfer id") + validate_presence!(value: id, name: 'transfer id') handle_response(@connection.get("/transfer/#{id}")) end # Finalize a transfer (OTP) # @see https://paystack.com/docs/api/transfer/#finalize def finalize(transfer_code:, otp:) - validate_presence!(value: transfer_code, name: "transfer_code") - validate_presence!(value: otp, name: "otp") - handle_response(@connection.post("/transfer/finalize_transfer", {transfer_code: transfer_code, otp: otp})) + validate_presence!(value: transfer_code, name: 'transfer_code') + validate_presence!(value: otp, name: 'otp') + handle_response(@connection.post('/transfer/finalize_transfer', { transfer_code: transfer_code, otp: otp })) end # Verify a transfer # @see https://paystack.com/docs/api/transfer/#verify def verify(reference:) - validate_presence!(value: reference, name: "reference") + validate_presence!(value: reference, name: 'reference') handle_response(@connection.get("/transfer/verify/#{reference}")) end end diff --git a/lib/paystack_sdk/resources/verification.rb b/lib/paystack_sdk/resources/verification.rb index fdd8502..945fe67 100644 --- a/lib/paystack_sdk/resources/verification.rb +++ b/lib/paystack_sdk/resources/verification.rb @@ -1,5 +1,5 @@ -require_relative "../validations" -require_relative "base" +require_relative '../validations' +require_relative 'base' module PaystackSdk module Resources @@ -7,15 +7,15 @@ class Verification < Base # Resolve Bank Account # @see https://paystack.com/docs/api/verification/#resolve-bank-account def resolve_account(account_number:, bank_code:) - validate_presence!(value: account_number, name: "account_number") - validate_presence!(value: bank_code, name: "bank_code") - handle_response(@connection.get("/bank/resolve", {account_number: account_number, bank_code: bank_code})) + validate_presence!(value: account_number, name: 'account_number') + validate_presence!(value: bank_code, name: 'bank_code') + handle_response(@connection.get('/bank/resolve', { account_number: account_number, bank_code: bank_code })) end # Resolve Card BIN # @see https://paystack.com/docs/api/verification/#resolve-card-bin def resolve_card_bin(bin) - validate_presence!(value: bin, name: "bin") + validate_presence!(value: bin, name: 'bin') handle_response(@connection.get("/decision/bin/#{bin}")) end @@ -27,9 +27,9 @@ def validate_account(params) validate_required_params!( payload: params, required_params: %i[account_number account_name account_type bank_code country_code document_type], - operation_name: "Validate Account" + operation_name: 'Validate Account' ) - handle_response(@connection.post("/bank/validate", params)) + handle_response(@connection.post('/bank/validate', params)) end end end diff --git a/lib/paystack_sdk/response.rb b/lib/paystack_sdk/response.rb index 87fdba8..0ce2554 100644 --- a/lib/paystack_sdk/response.rb +++ b/lib/paystack_sdk/response.rb @@ -82,22 +82,20 @@ def initialize(response) when 400..499 # Client errors - return unsuccessful response for user to handle @success = false - @error_message = @api_message || "Client error" + @error_message = @api_message || 'Client error' # Still raise for authentication issues as these are usually config problems - if @status_code == 401 - raise AuthenticationError.new(@api_message || "Authentication failed") - end + raise AuthenticationError.new(@api_message || 'Authentication failed') if @status_code == 401 when 429 # Rate limiting - raise as users need to implement retry logic - retry_after = response.headers["Retry-After"] + retry_after = response.headers['Retry-After'] raise RateLimitError.new(retry_after || 30) when 500..599 # Server errors - raise as these indicate Paystack infrastructure issues raise ServerError.new(@status_code, @api_message) else @success = false - @error_message = @api_message || "Unknown error" + @error_message = @api_message || 'Unknown error' end elsif response.is_a?(Response) @success = response.success? @@ -211,7 +209,7 @@ def key?(key) # @yield [key, value] For hashes, passes each key-value pair # @yield [value] For arrays, passes each item # @return [Response, Enumerator] Self for chaining or Enumerator if no block given - def each(&block) + def each return enum_for(:each) unless block_given? if @raw_data.is_a?(Hash) @@ -231,7 +229,7 @@ def each(&block) # @return [Integer] The number of items # @!method empty? # @return [Boolean] Whether the collection is empty - [:size, :length, :count, :empty?].each do |method_name| + %i[size length count empty?].each do |method_name| define_method(method_name) do @raw_data.send(method_name) if @raw_data.respond_to?(method_name) end @@ -242,9 +240,10 @@ def each(&block) # @return [Object, Response] The first item, wrapped if necessary # @!method last # @return [Object, Response] The last item, wrapped if necessary - [:first, :last].each do |method_name| + %i[first last].each do |method_name| define_method(method_name) do return nil unless @raw_data.is_a?(Array) + wrap_value(@raw_data.send(method_name)) end end @@ -257,21 +256,19 @@ def each(&block) # @param body [Hash] The response body # @return [String] The extracted identifier or "unknown" def extract_identifier(body) - return "unknown" unless body.is_a?(Hash) + return 'unknown' unless body.is_a?(Hash) # First try to get identifier from the message - message = body["message"].to_s.downcase - if message =~ /with (id|code|reference|email): ([^\s]+)/i - return $2 - end + message = body['message'].to_s.downcase + return ::Regexp.last_match(2) if message =~ /with (id|code|reference|email): ([^\s]+)/i # If not found in message, try to extract from error code - if body["code"]&.match?(/^(transaction|customer)_/) - parts = body["code"].to_s.split("_") - return parts.last if parts.last != "not_found" + if body['code']&.match?(/^(transaction|customer)_/) + parts = body['code'].to_s.split('_') + return parts.last if parts.last != 'not_found' end - "unknown" + 'unknown' end # Extract the API message from the response body @@ -279,7 +276,7 @@ def extract_identifier(body) # @param body [Hash, nil] The response body # @return [String, nil] The API message if present def extract_api_message(body) - body["message"] if body.is_a?(Hash) && body["message"] + body['message'] if body.is_a?(Hash) && body['message'] end # Extract the data from the response body @@ -288,7 +285,8 @@ def extract_api_message(body) # @return [Hash, Array, nil] The data from the response def extract_data_from_body(body) return body unless body.is_a?(Hash) - body["data"] || body + + body['data'] || body end # Wrap value in Response if needed @@ -310,14 +308,14 @@ def wrap_value(value) end def determine_resource_type - return "Unknown" unless @body.is_a?(Hash) && @body["code"] + return 'Unknown' unless @body.is_a?(Hash) && @body['code'] - if @body["code"].include?("_") - parts = @body["code"].to_s.split("_") - return parts.last if parts.last != "not_found" + if @body['code'].include?('_') + parts = @body['code'].to_s.split('_') + return parts.last if parts.last != 'not_found' end - "unknown" + 'unknown' end end end diff --git a/lib/paystack_sdk/utils/connection_utils.rb b/lib/paystack_sdk/utils/connection_utils.rb index 242ed6a..f3b8ff6 100644 --- a/lib/paystack_sdk/utils/connection_utils.rb +++ b/lib/paystack_sdk/utils/connection_utils.rb @@ -7,7 +7,7 @@ module Utils # and resource classes. module ConnectionUtils # The base URL for the Paystack API. - BASE_URL = "https://api.paystack.co" + BASE_URL = 'https://api.paystack.co' # Initializes a connection based on the provided parameters. # @@ -22,8 +22,8 @@ def initialize_connection(connection = nil, secret_key: nil) create_connection(secret_key:) else # Try to get API key from environment variable - env_secret_key = ENV["PAYSTACK_SECRET_KEY"] - raise AuthenticationError, "No connection or API key provided" unless env_secret_key + env_secret_key = ENV['PAYSTACK_SECRET_KEY'] + raise AuthenticationError, 'No connection or API key provided' unless env_secret_key create_connection(secret_key: env_secret_key) end @@ -37,9 +37,9 @@ def create_connection(secret_key:) Faraday.new(url: BASE_URL) do |conn| conn.request :json conn.response :json, content_type: /\bjson$/ - conn.headers["Authorization"] = "Bearer #{secret_key}" - conn.headers["Content-Type"] = "application/json" - conn.headers["User-Agent"] = "paystack_sdk/#{PaystackSdk::VERSION}" + conn.headers['Authorization'] = "Bearer #{secret_key}" + conn.headers['Content-Type'] = 'application/json' + conn.headers['User-Agent'] = "paystack_sdk/#{PaystackSdk::VERSION}" conn.adapter Faraday.default_adapter end end diff --git a/lib/paystack_sdk/validations.rb b/lib/paystack_sdk/validations.rb index 9a1276d..d277ddb 100644 --- a/lib/paystack_sdk/validations.rb +++ b/lib/paystack_sdk/validations.rb @@ -36,10 +36,10 @@ module Validations # @param input [Object] The input to validate # @param name [String] Name of the parameter for error messages # @raise [PaystackSdk::InvalidFormatError] If input is not a hash - def validate_hash!(input:, name: "Payload") - unless input.is_a?(Hash) - raise PaystackSdk::InvalidFormatError.new(name, "Hash") - end + def validate_hash!(input:, name: 'Payload') + return if input.is_a?(Hash) + + raise PaystackSdk::InvalidFormatError.new(name, 'Hash') end # Validates that required parameters are present in a payload. @@ -48,15 +48,15 @@ def validate_hash!(input:, name: "Payload") # @param required_params [Array] List of required parameter keys # @param operation_name [String] Name of the operation for error messages # @raise [PaystackSdk::MissingParamError] If any required parameters are missing - def validate_required_params!(payload:, required_params:, operation_name: "Operation") + def validate_required_params!(payload:, required_params:, operation_name: 'Operation') missing_params = required_params.select do |param| !payload.key?(param) && !payload.key?(param.to_s) end - unless missing_params.empty? - param = missing_params.first - raise PaystackSdk::MissingParamError.new(param) - end + return if missing_params.empty? + + param = missing_params.first + raise PaystackSdk::MissingParamError.new(param) end # Validates that a value is present (not nil or empty). @@ -64,10 +64,10 @@ def validate_required_params!(payload:, required_params:, operation_name: "Opera # @param value [Object] The value to validate # @param name [String] Name of the parameter for error messages # @raise [PaystackSdk::MissingParamError] If value is nil or empty - def validate_presence!(value:, name: "Parameter") - if value.nil? || (value.respond_to?(:empty?) && value.empty?) - raise PaystackSdk::MissingParamError.new(name) - end + def validate_presence!(value:, name: 'Parameter') + return unless value.nil? || (value.respond_to?(:empty?) && value.empty?) + + raise PaystackSdk::MissingParamError.new(name) end # Validates that a number is a positive integer. @@ -77,11 +77,11 @@ def validate_presence!(value:, name: "Parameter") # @param allow_nil [Boolean] Whether nil values are allowed # @raise [PaystackSdk::InvalidValueError] If value is not a positive integer # @raise [PaystackSdk::MissingParamError] If value is nil and not allowed - def validate_positive_integer!(value:, name: "Parameter", allow_nil: true) + def validate_positive_integer!(value:, name: 'Parameter', allow_nil: true) if value.nil? raise PaystackSdk::MissingParamError.new(name) unless allow_nil elsif !value.is_a?(Integer) || value < 1 - raise PaystackSdk::InvalidValueError.new(name, "must be a positive integer") + raise PaystackSdk::InvalidValueError.new(name, 'must be a positive integer') end end @@ -90,10 +90,10 @@ def validate_positive_integer!(value:, name: "Parameter", allow_nil: true) # @param reference [String] The reference to validate # @param name [String] Name of the parameter for error messages # @raise [PaystackSdk::InvalidFormatError] If reference format is invalid - def validate_reference_format!(reference:, name: "Reference") - unless reference.to_s.match?(/^[a-zA-Z0-9._=-]+$/) - raise PaystackSdk::InvalidFormatError.new(name, "alphanumeric characters and the following: -, ., =") - end + def validate_reference_format!(reference:, name: 'Reference') + return if reference.to_s.match?(/^[a-zA-Z0-9._=-]+$/) + + raise PaystackSdk::InvalidFormatError.new(name, 'alphanumeric characters and the following: -, ., =') end # Validates a date string format. @@ -103,16 +103,17 @@ def validate_reference_format!(reference:, name: "Reference") # @param allow_nil [Boolean] Whether nil values are allowed # @raise [PaystackSdk::InvalidFormatError] If date format is invalid # @raise [PaystackSdk::MissingParamError] If date is nil and not allowed - def validate_date_format!(date_str:, name: "Date", allow_nil: true) + def validate_date_format!(date_str:, name: 'Date', allow_nil: true) if date_str.nil? raise PaystackSdk::MissingParamError.new(name) unless allow_nil + return end begin Date.parse(date_str.to_s) rescue Date::Error - raise PaystackSdk::InvalidFormatError.new(name, "YYYY-MM-DD or ISO8601") + raise PaystackSdk::InvalidFormatError.new(name, 'YYYY-MM-DD or ISO8601') end end @@ -133,16 +134,17 @@ def validate_date_format!(date_str:, name: "Date", allow_nil: true) # name: "risk_action" # ) # ``` - def validate_allowed_values!(value:, allowed_values:, name: "Parameter", allow_nil: true) + def validate_allowed_values!(value:, allowed_values:, name: 'Parameter', allow_nil: true) if value.nil? raise PaystackSdk::MissingParamError.new(name) unless allow_nil + return end - unless allowed_values.include?(value) - allowed_list = allowed_values.join(", ") - raise PaystackSdk::InvalidValueError.new(name, "must be one of: #{allowed_list}") - end + return if allowed_values.include?(value) + + allowed_list = allowed_values.join(', ') + raise PaystackSdk::InvalidValueError.new(name, "must be one of: #{allowed_list}") end # Validates an email format. @@ -151,15 +153,16 @@ def validate_allowed_values!(value:, allowed_values:, name: "Parameter", allow_n # @param name [String] Name of the parameter for error messages # @param allow_nil [Boolean] Whether nil values are allowed # @raise [PaystackSdk::Error] If email format is invalid - def validate_email!(email:, name: "Email", allow_nil: false) + def validate_email!(email:, name: 'Email', allow_nil: false) if email.nil? raise PaystackSdk::MissingParamError.new(name) unless allow_nil + return end - unless email.to_s.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/) - raise PaystackSdk::InvalidFormatError.new(name, "valid email address") - end + return if email.to_s.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/) + + raise PaystackSdk::InvalidFormatError.new(name, 'valid email address') end # Validates a currency code format. @@ -168,15 +171,16 @@ def validate_email!(email:, name: "Email", allow_nil: false) # @param name [String] Name of the parameter for error messages # @param allow_nil [Boolean] Whether nil values are allowed # @raise [PaystackSdk::Error] If currency format is invalid - def validate_currency!(currency:, name: "Currency", allow_nil: true) + def validate_currency!(currency:, name: 'Currency', allow_nil: true) if currency.nil? raise PaystackSdk::MissingParamError.new(name) unless allow_nil + return end - unless currency.to_s.match?(/\A[A-Z]{3}\z/) - raise PaystackSdk::InvalidFormatError.new(name, "3-letter ISO code (e.g., NGN, USD, GHS)") - end + return if currency.to_s.match?(/\A[A-Z]{3}\z/) + + raise PaystackSdk::InvalidFormatError.new(name, '3-letter ISO code (e.g., NGN, USD, GHS)') end # Validates multiple fields at once. @@ -198,7 +202,7 @@ def validate_currency!(currency:, name: "Currency", allow_nil: true) # ) # ``` def validate_fields!(payload:, validations:) - validate_hash!(input: payload, name: "Payload") + validate_hash!(input: payload, name: 'Payload') # First check required fields required_fields = validations.select { |_, opts| opts[:required] }.keys diff --git a/lib/paystack_sdk/version.rb b/lib/paystack_sdk/version.rb index b266eeb..00c6365 100644 --- a/lib/paystack_sdk/version.rb +++ b/lib/paystack_sdk/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module PaystackSdk - VERSION = "0.1.0" + VERSION = '0.1.0' end diff --git a/paystack_sdk.gemspec b/paystack_sdk.gemspec index dbee9af..0561f02 100644 --- a/paystack_sdk.gemspec +++ b/paystack_sdk.gemspec @@ -1,12 +1,12 @@ # frozen_string_literal: true -require_relative "lib/paystack_sdk/version" +require_relative 'lib/paystack_sdk/version' Gem::Specification.new do |spec| - spec.name = "paystack_sdk" + spec.name = 'paystack_sdk' spec.version = PaystackSdk::VERSION - spec.authors = ["Maxwell Nana Forson (theLazyProgrammer)"] - spec.email = ["nanaforsonjnr@gmail.com"] + spec.authors = ['Maxwell Nana Forson (theLazyProgrammer)'] + spec.email = ['nanaforsonjnr@gmail.com'] spec.summary = "A Ruby SDK for integrating with Paystack's payment gateway API." spec.description = <<~EOS @@ -16,13 +16,13 @@ Gem::Specification.new do |spec| applications. With support for various endpoints, this SDK simplifies tasks such as initiating transactions, verifying payments, managing customers, and more. EOS - spec.homepage = "https://github.com/nanafox/paystack_sdk" - spec.license = "MIT" - spec.required_ruby_version = ">= 3.2.2" + spec.homepage = 'https://github.com/nanafox/paystack_sdk' + spec.license = 'MIT' + spec.required_ruby_version = '>= 3.2.2' - spec.metadata["allowed_push_host"] = "https://rubygems.org" - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["changelog_uri"] = "https://github.com/nanafox/paystack_sdk/blob/main/CHANGELOG.md" + spec.metadata['allowed_push_host'] = 'https://rubygems.org' + spec.metadata['homepage_uri'] = spec.homepage + spec.metadata['changelog_uri'] = 'https://github.com/nanafox/paystack_sdk/blob/main/CHANGELOG.md' # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. @@ -33,17 +33,17 @@ Gem::Specification.new do |spec| f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) end end - spec.bindir = "exe" + spec.bindir = 'exe' spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] + spec.require_paths = ['lib'] # Dependencies - spec.add_dependency "faraday", "~> 2.13.1" - spec.add_development_dependency "rspec", "~> 3.13" - spec.add_development_dependency "standard", "~> 1.49.0" - spec.add_development_dependency "irb", "~> 1.15.1" - spec.add_development_dependency "rake", "~> 13.2.1" - spec.add_development_dependency "debug", "~> 1.9.0" + spec.add_dependency 'faraday', '~> 2.13.1' + spec.add_development_dependency 'debug', '~> 1.9.0' + spec.add_development_dependency 'irb', '~> 1.15.1' + spec.add_development_dependency 'rake', '~> 13.2.1' + spec.add_development_dependency 'rspec', '~> 3.13' + spec.add_development_dependency 'standard', '~> 1.49.0' # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 5396297..bb8f57a 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -1,44 +1,53 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Client do - let(:secret_key) { "sk_test_xxx" } + let(:secret_key) { 'sk_test_xxx' } let(:connection_double) { instance_double(Faraday::Connection) } let(:client) do allow(Faraday).to receive(:new).and_return(connection_double) described_class.new(secret_key: secret_key) end - describe "#initialize" do - it "initializes a new client with the given API key" do + describe '#initialize' do + it 'initializes a new client with the given API key' do expect(client).to be_a(PaystackSdk::Client) end - it "sets the correct base URL" do - allow(connection_double).to receive(:url_prefix).and_return(URI("https://api.paystack.co")) - expect(client.instance_variable_get(:@connection).url_prefix.to_s.chomp("/")).to eq("https://api.paystack.co") + it 'sets the correct base URL' do + allow(connection_double).to receive(:url_prefix).and_return(URI('https://api.paystack.co')) + expect(client.instance_variable_get(:@connection).url_prefix.to_s.chomp('/')).to eq('https://api.paystack.co') end - it "sets the correct headers" do + it 'sets the correct headers' do headers = { - "Authorization" => "Bearer #{secret_key}", - "Content-Type" => "application/json", - "User-Agent" => "paystack_sdk/#{PaystackSdk::VERSION}" + 'Authorization' => "Bearer #{secret_key}", + 'Content-Type' => 'application/json', + 'User-Agent' => "paystack_sdk/#{PaystackSdk::VERSION}" } allow(connection_double).to receive(:headers).and_return(headers) connection_headers = client.instance_variable_get(:@connection).headers - expect(connection_headers["Authorization"]).to eq("Bearer #{secret_key}") - expect(connection_headers["Content-Type"]).to eq("application/json") - expect(connection_headers["User-Agent"]).to eq("paystack_sdk/#{PaystackSdk::VERSION}") + expect(connection_headers['Authorization']).to eq("Bearer #{secret_key}") + expect(connection_headers['Content-Type']).to eq('application/json') + expect(connection_headers['User-Agent']).to eq("paystack_sdk/#{PaystackSdk::VERSION}") end end - describe "#transactions" do - it "returns an instance of Transactions resource" do + describe '#transactions' do + it 'returns an instance of Transactions resource' do transactions_double = instance_double(PaystackSdk::Resources::Transactions) allow(PaystackSdk::Resources::Transactions).to receive(:new).and_return(transactions_double) expect(client.transactions).to eq(transactions_double) end end + + describe '#charges' do + it 'returns an instance of Charges resource' do + charges_double = instance_double(PaystackSdk::Resources::Charges) + allow(PaystackSdk::Resources::Charges).to receive(:new).and_return(charges_double) + + expect(client.charges).to eq(charges_double) + end + end end diff --git a/spec/paystack_sdk_spec.rb b/spec/paystack_sdk_spec.rb index ddfa9ae..f10616d 100644 --- a/spec/paystack_sdk_spec.rb +++ b/spec/paystack_sdk_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe PaystackSdk do - it "has a version number" do + it 'has a version number' do expect(PaystackSdk::VERSION).not_to be nil end end diff --git a/spec/resources/banks_spec.rb b/spec/resources/banks_spec.rb index d5b9a33..cc09087 100644 --- a/spec/resources/banks_spec.rb +++ b/spec/resources/banks_spec.rb @@ -1,32 +1,32 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Resources::Banks do - let(:connection) { instance_double("PaystackSdk::Connection") } + let(:connection) { instance_double('PaystackSdk::Connection') } let(:banks) { described_class.new(connection) } - describe "#list" do - it "lists banks and wraps response" do - response_double = double("Response", success?: true) + describe '#list' do + it 'lists banks and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:get) - .with("/bank", {}) + .with('/bank', {}) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) banks.list end - it "passes query params to the API" do - response_double = double("Response", success?: true) + it 'passes query params to the API' do + response_double = double('Response', success?: true) expect(connection).to receive(:get) - .with("/bank", {currency: "NGN"}) + .with('/bank', { currency: 'NGN' }) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - banks.list(currency: "NGN") + banks.list(currency: 'NGN') end - it "raises error for invalid currency" do - expect { - banks.list(currency: "INVALID") - }.to raise_error(PaystackSdk::InvalidValueError, /currency/) + it 'raises error for invalid currency' do + expect do + banks.list(currency: 'INVALID') + end.to raise_error(PaystackSdk::InvalidValueError, /currency/) end end end diff --git a/spec/resources/charges_spec.rb b/spec/resources/charges_spec.rb new file mode 100644 index 0000000..5521770 --- /dev/null +++ b/spec/resources/charges_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +RSpec.describe PaystackSdk::Resources::Charges do + let(:connection) { instance_double('PaystackSdk::Connection') } + let(:charges) { described_class.new(connection) } + + describe '#mobile_money' do + let(:payload) do + { + email: 'customer@email.com', + amount: 10_000, + currency: 'GHS', + mobile_money: { + phone: '0551234987', + provider: 'mtn' + } + } + end + + it 'creates a mobile money charge' do + response_double = double('Response', success?: true, status: 'pay_offline') + expect(connection).to receive(:post) + .with('/charge', payload) + .and_return(response_double) + expect(PaystackSdk::Response).to receive(:new).with(response_double) + .and_return(response_double) + + response = charges.mobile_money(payload) + + expect(response.success?).to be true + expect(response.status).to eq('pay_offline') + end + + it 'accepts provider codes in different cases' do + payload[:mobile_money][:provider] = 'MTN' + response_double = double('Response', success?: true) + expect(connection).to receive(:post) + .with('/charge', payload) + .and_return(response_double) + expect(PaystackSdk::Response).to receive(:new).with(response_double) + .and_return(response_double) + + expect { charges.mobile_money(payload) }.not_to raise_error + end + + it 'raises an error when mobile_money provider is invalid' do + payload[:mobile_money][:provider] = 'invalid' + + expect { charges.mobile_money(payload) } + .to raise_error(PaystackSdk::InvalidValueError, /mobile_money provider/) + end + + it 'raises an error when required mobile_money details are missing' do + payload[:mobile_money].delete(:phone) + + expect { charges.mobile_money(payload) } + .to raise_error(PaystackSdk::MissingParamError, /mobile_money phone/) + end + end + + describe '#submit_otp' do + let(:otp_payload) { { otp: '123456', reference: 'r13havfcdt7btcm' } } + + it 'submits the otp for a charge' do + response_double = double('Response', success?: true, status: 'success') + expect(connection).to receive(:post) + .with('/charge/submit_otp', otp_payload) + .and_return(response_double) + expect(PaystackSdk::Response).to receive(:new).with(response_double) + .and_return(response_double) + + response = charges.submit_otp(otp_payload) + expect(response.status).to eq('success') + end + + it 'raises an error when otp is missing' do + otp_payload.delete(:otp) + + expect { charges.submit_otp(otp_payload) } + .to raise_error(PaystackSdk::MissingParamError, /otp/) + end + end +end diff --git a/spec/resources/customers_spec.rb b/spec/resources/customers_spec.rb index 8764222..aee2272 100644 --- a/spec/resources/customers_spec.rb +++ b/spec/resources/customers_spec.rb @@ -1,101 +1,101 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Resources::Customers do - let(:connection) { instance_double("PaystackSdk::Connection") } + let(:connection) { instance_double('PaystackSdk::Connection') } let(:customers) { described_class.new(connection) } - describe "#create" do + describe '#create' do let(:params) do { - email: "customer@email.com", - first_name: "Zero", - last_name: "Sum", - phone: "+2348123456789" + email: 'customer@email.com', + first_name: 'Zero', + last_name: 'Sum', + phone: '+2348123456789' } end - context "with successful response" do - it "creates a new customer" do - response_double = double("Response", success?: true, - data: double("Data", customer_code: "CUS_xr58yrr2ujlft9k")) + context 'with successful response' do + it 'creates a new customer' do + response_double = double('Response', success?: true, + data: double('Data', customer_code: 'CUS_xr58yrr2ujlft9k')) expect(connection).to receive(:post) - .with("customer", params) + .with('customer', params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = customers.create(params) expect(response).to be_success - expect(response.data.customer_code).to eq("CUS_xr58yrr2ujlft9k") + expect(response.data.customer_code).to eq('CUS_xr58yrr2ujlft9k') end end - context "with failed response" do - it "returns an unsuccessful response" do - response_double = double("Response", success?: false, failed?: true, - error_message: "Customer creation failed") + context 'with failed response' do + it 'returns an unsuccessful response' do + response_double = double('Response', success?: false, failed?: true, + error_message: 'Customer creation failed') expect(connection).to receive(:post) - .with("customer", params) + .with('customer', params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = customers.create(params) expect(response.success?).to be false expect(response.failed?).to be true - expect(response.error_message).to eq("Customer creation failed") + expect(response.error_message).to eq('Customer creation failed') end end - context "with validation errors" do - it "raises MissingParamError when email is missing" do + context 'with validation errors' do + it 'raises MissingParamError when email is missing' do invalid_params = params.except(:email) expect { customers.create(invalid_params) }.to raise_error( PaystackSdk::MissingParamError, - "Missing required parameter: email" + 'Missing required parameter: email' ) end - it "raises InvalidFormatError when email format is invalid" do - invalid_params = params.merge(email: "invalid-email") + it 'raises InvalidFormatError when email format is invalid' do + invalid_params = params.merge(email: 'invalid-email') expect { customers.create(invalid_params) }.to raise_error( PaystackSdk::InvalidFormatError, - "Invalid format for Email. Expected format: valid email address" + 'Invalid format for Email. Expected format: valid email address' ) end end end - describe "#list" do - context "with successful response" do - it "returns a list of customers" do - response_double = double("Response", success?: true, - data: double("Data", first: double("Customer", customer_code: "CUS_xr58yrr2ujlft9k"))) + describe '#list' do + context 'with successful response' do + it 'returns a list of customers' do + response_double = double('Response', success?: true, + data: double('Data', first: double('Customer', customer_code: 'CUS_xr58yrr2ujlft9k'))) expect(connection).to receive(:get) - .with("customer", hash_including(perPage: 50, page: 1)) + .with('customer', hash_including(perPage: 50, page: 1)) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = customers.list expect(response.success?).to be true - expect(response.data.first.customer_code).to eq("CUS_xr58yrr2ujlft9k") + expect(response.data.first.customer_code).to eq('CUS_xr58yrr2ujlft9k') end end end - describe "#fetch" do - let(:email_or_code) { "CUS_xr58yrr2ujlft9k" } + describe '#fetch' do + let(:email_or_code) { 'CUS_xr58yrr2ujlft9k' } - context "with successful response" do - it "fetches customer details" do - response_double = double("Response", success?: true, - data: double("Data", customer_code: email_or_code)) + context 'with successful response' do + it 'fetches customer details' do + response_double = double('Response', success?: true, + data: double('Data', customer_code: email_or_code)) expect(connection).to receive(:get) .with("customer/#{email_or_code}") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = customers.fetch(email_or_code) expect(response.success?).to be true @@ -103,97 +103,97 @@ end end - context "with not found error" do - it "returns an unsuccessful response" do - response_double = double("Response", success?: false, failed?: true, - error_message: "Customer code/email specified is invalid") + context 'with not found error' do + it 'returns an unsuccessful response' do + response_double = double('Response', success?: false, failed?: true, + error_message: 'Customer code/email specified is invalid') expect(connection).to receive(:get) .with("customer/#{email_or_code}") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = customers.fetch(email_or_code) expect(response.success?).to be false expect(response.failed?).to be true - expect(response.error_message).to eq("Customer code/email specified is invalid") + expect(response.error_message).to eq('Customer code/email specified is invalid') end end - context "with validation errors" do - it "raises MissingParamError when email_or_code is nil" do + context 'with validation errors' do + it 'raises MissingParamError when email_or_code is nil' do expect { customers.fetch(nil) }.to raise_error( PaystackSdk::MissingParamError, - "Missing required parameter: email_or_code" + 'Missing required parameter: email_or_code' ) end end end - describe "#update" do - let(:code) { "CUS_xr58yrr2ujlft9k" } + describe '#update' do + let(:code) { 'CUS_xr58yrr2ujlft9k' } let(:params) do { - first_name: "John", - last_name: "Doe", - phone: "+2348123456789" + first_name: 'John', + last_name: 'Doe', + phone: '+2348123456789' } end - context "with successful response" do + context 'with successful response' do before do allow(connection).to receive(:put) .with("customer/#{code}", params) .and_return(Faraday::Response.new(status: 200, body: { - "status" => true, - "message" => "Customer updated", - "data" => params.merge("customer_code" => code) - })) + 'status' => true, + 'message' => 'Customer updated', + 'data' => params.merge('customer_code' => code) + })) end - it "updates customer details" do + it 'updates customer details' do response = customers.update(code, params) expect(response.success?).to be true - expect(response.data.first_name).to eq("John") + expect(response.data.first_name).to eq('John') end end - context "with validation errors" do - it "raises InvalidFormatError when payload is not a hash" do - expect { customers.update(code, "invalid") }.to raise_error( + context 'with validation errors' do + it 'raises InvalidFormatError when payload is not a hash' do + expect { customers.update(code, 'invalid') }.to raise_error( PaystackSdk::InvalidFormatError, - "Invalid format for payload. Expected format: Hash" + 'Invalid format for payload. Expected format: Hash' ) end - it "raises MissingParamError when code is missing" do + it 'raises MissingParamError when code is missing' do expect { customers.update(nil, params) }.to raise_error( PaystackSdk::MissingParamError, - "Missing required parameter: code" + 'Missing required parameter: code' ) end end end - describe "#validate" do - let(:code) { "CUS_xr58yrr2ujlft9k" } + describe '#validate' do + let(:code) { 'CUS_xr58yrr2ujlft9k' } let(:params) do { - country: "NG", - type: "bank_account", - account_number: "0123456789", - bvn: "20012345677", - bank_code: "007", - first_name: "John", - last_name: "Doe" + country: 'NG', + type: 'bank_account', + account_number: '0123456789', + bvn: '20012345677', + bank_code: '007', + first_name: 'John', + last_name: 'Doe' } end - context "with successful response" do - it "validates a customer successfully" do + context 'with successful response' do + it 'validates a customer successfully' do response_body = { - "status" => true, - "message" => "Customer Identification in progress" + 'status' => true, + 'message' => 'Customer Identification in progress' } faraday_response = Faraday::Response.new(status: 202, body: response_body) @@ -205,100 +205,100 @@ response = customers.validate(code, params) expect(response).to be_success - expect(response.message).to eq("Customer Identification in progress") + expect(response.message).to eq('Customer Identification in progress') end end - context "with validation errors" do - it "raises an error when required parameters are missing" do + context 'with validation errors' do + it 'raises an error when required parameters are missing' do invalid_params = params.reject { |k| k == :type } expect { customers.validate(code, invalid_params) } .to raise_error(PaystackSdk::Error, /type/i) end - it "raises an error when code is nil" do + it 'raises an error when code is nil' do expect { customers.validate(nil, params) } - .to raise_error(PaystackSdk::MissingParamError, "Missing required parameter: code") + .to raise_error(PaystackSdk::MissingParamError, 'Missing required parameter: code') end end end - describe "#set_risk_action" do + describe '#set_risk_action' do let(:params) do { - customer: "CUS_xr58yrr2ujlft9k", - risk_action: "allow" + customer: 'CUS_xr58yrr2ujlft9k', + risk_action: 'allow' } end - context "with successful response" do - it "sets risk action successfully" do + context 'with successful response' do + it 'sets risk action successfully' do response_body = { - "status" => true, - "message" => "Customer updated", - "data" => { - "customer_code" => "CUS_xr58yrr2ujlft9k", - "risk_action" => "allow" + 'status' => true, + 'message' => 'Customer updated', + 'data' => { + 'customer_code' => 'CUS_xr58yrr2ujlft9k', + 'risk_action' => 'allow' } } faraday_response = Faraday::Response.new(status: 200, body: response_body) expect(connection).to receive(:post) - .with("customer/set_risk_action", params) + .with('customer/set_risk_action', params) .and_return(faraday_response) response = customers.set_risk_action(params) expect(response).to be_success - expect(response.risk_action).to eq("allow") + expect(response.risk_action).to eq('allow') end end - context "with validation errors" do - it "raises an error when customer is missing" do + context 'with validation errors' do + it 'raises an error when customer is missing' do invalid_params = params.reject { |k| k == :customer } expect { customers.set_risk_action(invalid_params) } .to raise_error(PaystackSdk::Error, /customer/i) end - it "raises an error when risk_action is invalid" do - invalid_params = params.merge(risk_action: "invalid") + it 'raises an error when risk_action is invalid' do + invalid_params = params.merge(risk_action: 'invalid') expect { customers.set_risk_action(invalid_params) } .to raise_error(PaystackSdk::InvalidValueError, /risk_action/i) end end end - describe "#deactivate_authorization" do + describe '#deactivate_authorization' do let(:params) do { - authorization_code: "AUTH_72btv547" + authorization_code: 'AUTH_72btv547' } end - context "with successful response" do - it "deactivates authorization successfully" do + context 'with successful response' do + it 'deactivates authorization successfully' do response_body = { - "status" => true, - "message" => "Authorization has been deactivated" + 'status' => true, + 'message' => 'Authorization has been deactivated' } faraday_response = Faraday::Response.new(status: 200, body: response_body) expect(connection).to receive(:post) - .with("customer/deactivate_authorization", params) + .with('customer/deactivate_authorization', params) .and_return(faraday_response) response = customers.deactivate_authorization(params) expect(response).to be_success - expect(response.message).to eq("Authorization has been deactivated") + expect(response.message).to eq('Authorization has been deactivated') end end - context "with validation errors" do - it "raises an error when authorization_code is missing" do + context 'with validation errors' do + it 'raises an error when authorization_code is missing' do expect { customers.deactivate_authorization({}) } .to raise_error(PaystackSdk::Error, /authorization_code/i) end diff --git a/spec/resources/transactions_spec.rb b/spec/resources/transactions_spec.rb index 36cfed4..4c0d138 100644 --- a/spec/resources/transactions_spec.rb +++ b/spec/resources/transactions_spec.rb @@ -1,99 +1,99 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Resources::Transactions do - let(:connection) { instance_double("PaystackSdk::Connection") } + let(:connection) { instance_double('PaystackSdk::Connection') } let(:transactions) { described_class.new(connection) } - describe "#initiate" do + describe '#initiate' do let(:params) do { - email: "customer@email.com", - amount: 10000, - currency: "GHS" + email: 'customer@email.com', + amount: 10_000, + currency: 'GHS' } end - context "with successful response" do - it "initializes a transaction" do - response_double = double("Response", success?: true, - data: double("Data", authorization_url: "https://checkout.paystack.com/abc123"), - authorization_url: "https://checkout.paystack.com/abc123") + context 'with successful response' do + it 'initializes a transaction' do + response_double = double('Response', success?: true, + data: double('Data', authorization_url: 'https://checkout.paystack.com/abc123'), + authorization_url: 'https://checkout.paystack.com/abc123') expect(connection).to receive(:post) - .with("/transaction/initialize", params) + .with('/transaction/initialize', params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = transactions.initiate(params) expect(response.success?).to be true - expect(response.data.authorization_url).to eq("https://checkout.paystack.com/abc123") - expect(response.authorization_url).to eq("https://checkout.paystack.com/abc123") + expect(response.data.authorization_url).to eq('https://checkout.paystack.com/abc123') + expect(response.authorization_url).to eq('https://checkout.paystack.com/abc123') end end - context "with failed response" do - it "returns an unsuccessful response" do - response_double = double("Response", success?: false, failed?: true, - error_message: "Transaction initialization failed") + context 'with failed response' do + it 'returns an unsuccessful response' do + response_double = double('Response', success?: false, failed?: true, + error_message: 'Transaction initialization failed') expect(connection).to receive(:post) - .with("/transaction/initialize", params) + .with('/transaction/initialize', params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = transactions.initiate(params) expect(response.success?).to be false expect(response.failed?).to be true - expect(response.error_message).to eq("Transaction initialization failed") + expect(response.error_message).to eq('Transaction initialization failed') end end - context "with validation errors" do - it "raises MissingParamError when email is missing" do + context 'with validation errors' do + it 'raises MissingParamError when email is missing' do invalid_params = params.except(:email) expect { transactions.initiate(invalid_params) }.to raise_error( PaystackSdk::MissingParamError, - "Missing required parameter: email" + 'Missing required parameter: email' ) end - it "raises MissingParamError when amount is missing" do + it 'raises MissingParamError when amount is missing' do invalid_params = params.except(:amount) expect { transactions.initiate(invalid_params) }.to raise_error( PaystackSdk::MissingParamError, - "Missing required parameter: amount" + 'Missing required parameter: amount' ) end - it "raises InvalidFormatError when email format is invalid" do - invalid_params = params.merge(email: "invalid-email") + it 'raises InvalidFormatError when email format is invalid' do + invalid_params = params.merge(email: 'invalid-email') expect { transactions.initiate(invalid_params) }.to raise_error( PaystackSdk::InvalidFormatError, - "Invalid format for Email. Expected format: valid email address" + 'Invalid format for Email. Expected format: valid email address' ) end - it "raises InvalidFormatError when currency format is invalid" do - invalid_params = params.merge(currency: "INVALID") + it 'raises InvalidFormatError when currency format is invalid' do + invalid_params = params.merge(currency: 'INVALID') expect { transactions.initiate(invalid_params) }.to raise_error( PaystackSdk::InvalidFormatError, - "Invalid format for currency. Expected format: 3-letter ISO code (e.g., NGN, USD, GHS)" + 'Invalid format for currency. Expected format: 3-letter ISO code (e.g., NGN, USD, GHS)' ) end end end - describe "#verify" do - let(:reference) { "ref_123" } + describe '#verify' do + let(:reference) { 'ref_123' } - context "with successful response" do - it "verifies a transaction" do - response_double = double("Response", success?: true, status: true) + context 'with successful response' do + it 'verifies a transaction' do + response_double = double('Response', success?: true, status: true) expect(connection).to receive(:get) .with("/transaction/verify/#{reference}") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = transactions.verify(reference: reference) expect(response.success?).to be true @@ -101,132 +101,132 @@ end end - context "with not found error" do - it "returns an unsuccessful response" do - response_double = double("Response", success?: false, failed?: true, - error_message: "Transaction reference not found.") + context 'with not found error' do + it 'returns an unsuccessful response' do + response_double = double('Response', success?: false, failed?: true, + error_message: 'Transaction reference not found.') expect(connection).to receive(:get) .with("/transaction/verify/#{reference}") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = transactions.verify(reference: reference) expect(response.success?).to be false expect(response.failed?).to be true - expect(response.error_message).to eq("Transaction reference not found.") + expect(response.error_message).to eq('Transaction reference not found.') end end - context "with validation errors" do - it "raises MissingParamError when reference is missing" do + context 'with validation errors' do + it 'raises MissingParamError when reference is missing' do expect { transactions.verify(reference: nil) }.to raise_error( PaystackSdk::MissingParamError, - "Missing required parameter: Reference" + 'Missing required parameter: Reference' ) end end end - describe "#charge_authorization" do + describe '#charge_authorization' do let(:payload) do { - authorization_code: "AUTH_72btv547", - email: "customer@email.com", - amount: 10000 + authorization_code: 'AUTH_72btv547', + email: 'customer@email.com', + amount: 10_000 } end - context "with successful response" do - it "charges the authorization" do - response_double = double("Response", success?: true, status: "success", - api_message: "Charge attempted", amount: 35247, reference: "0m7frfnr47ezyxl", - authorization: double("Authorization", authorization_code: "AUTH_uh8bcl3zbn"), - customer: double("Customer", email: "customer@email.com")) + context 'with successful response' do + it 'charges the authorization' do + response_double = double('Response', success?: true, status: 'success', + api_message: 'Charge attempted', amount: 35_247, reference: '0m7frfnr47ezyxl', + authorization: double('Authorization', authorization_code: 'AUTH_uh8bcl3zbn'), + customer: double('Customer', email: 'customer@email.com')) expect(connection).to receive(:post) - .with("/transaction/charge_authorization", payload) + .with('/transaction/charge_authorization', payload) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = transactions.charge_authorization(payload) expect(response.success?).to be true - expect(response.status).to be "success" - expect(response.api_message).to eq("Charge attempted") - expect(response.amount).to eq(35247) - expect(response.reference).to eq("0m7frfnr47ezyxl") - expect(response.authorization.authorization_code).to eq("AUTH_uh8bcl3zbn") - expect(response.customer.email).to eq("customer@email.com") + expect(response.status).to be 'success' + expect(response.api_message).to eq('Charge attempted') + expect(response.amount).to eq(35_247) + expect(response.reference).to eq('0m7frfnr47ezyxl') + expect(response.authorization.authorization_code).to eq('AUTH_uh8bcl3zbn') + expect(response.customer.email).to eq('customer@email.com') end end - context "with validation errors" do - it "raises MissingParamError when authorization_code is missing" do + context 'with validation errors' do + it 'raises MissingParamError when authorization_code is missing' do invalid_payload = payload.except(:authorization_code) expect { transactions.charge_authorization(invalid_payload) }.to raise_error( PaystackSdk::MissingParamError, - "Missing required parameter: authorization_code" + 'Missing required parameter: authorization_code' ) end - it "raises MissingParamError when email is missing" do + it 'raises MissingParamError when email is missing' do invalid_payload = payload.except(:email) expect { transactions.charge_authorization(invalid_payload) }.to raise_error( PaystackSdk::MissingParamError, - "Missing required parameter: email" + 'Missing required parameter: email' ) end - it "raises MissingParamError when amount is missing" do + it 'raises MissingParamError when amount is missing' do invalid_payload = payload.except(:amount) expect { transactions.charge_authorization(invalid_payload) }.to raise_error( PaystackSdk::MissingParamError, - "Missing required parameter: amount" + 'Missing required parameter: amount' ) end end end - describe "#partial_debit" do + describe '#partial_debit' do let(:payload) do { - authorization_code: "AUTH_72btv547", - currency: "NGN", - amount: 10000, - email: "customer@email.com" + authorization_code: 'AUTH_72btv547', + currency: 'NGN', + amount: 10_000, + email: 'customer@email.com' } end - context "with successful response" do - it "performs a partial debit" do - response_double = double("Response", success?: true, status: "success") + context 'with successful response' do + it 'performs a partial debit' do + response_double = double('Response', success?: true, status: 'success') expect(connection).to receive(:post) - .with("/transaction/partial_debit", payload) + .with('/transaction/partial_debit', payload) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = transactions.partial_debit(payload) expect(response.success?).to be true - expect(response.status).to eq("success") + expect(response.status).to eq('success') end end - context "with validation errors" do - it "raises MissingParamError when authorization_code is missing" do + context 'with validation errors' do + it 'raises MissingParamError when authorization_code is missing' do invalid_payload = payload.except(:authorization_code) expect { transactions.partial_debit(invalid_payload) }.to raise_error( PaystackSdk::MissingParamError, - "Missing required parameter: authorization_code" + 'Missing required parameter: authorization_code' ) end - it "raises InvalidFormatError when currency format is invalid" do - invalid_payload = payload.merge(currency: "INVALID") + it 'raises InvalidFormatError when currency format is invalid' do + invalid_payload = payload.merge(currency: 'INVALID') expect { transactions.partial_debit(invalid_payload) }.to raise_error( PaystackSdk::InvalidFormatError, - "Invalid format for currency. Expected format: 3-letter ISO code (e.g., NGN, USD, GHS)" + 'Invalid format for currency. Expected format: 3-letter ISO code (e.g., NGN, USD, GHS)' ) end end diff --git a/spec/resources/transfer_recipients_spec.rb b/spec/resources/transfer_recipients_spec.rb index 07cf38f..1b2a203 100644 --- a/spec/resources/transfer_recipients_spec.rb +++ b/spec/resources/transfer_recipients_spec.rb @@ -1,74 +1,74 @@ RSpec.describe PaystackSdk::Resources::TransferRecipients do - let(:connection) { instance_double("PaystackSdk::Connection") } + let(:connection) { instance_double('PaystackSdk::Connection') } let(:recipients) { described_class.new(connection) } let(:params) do { - type: "nuban", - name: "Jane Doe", - account_number: "0001234567", - bank_code: "058", - currency: "NGN" + type: 'nuban', + name: 'Jane Doe', + account_number: '0001234567', + bank_code: '058', + currency: 'NGN' } end - describe "#create" do - it "creates a transfer recipient and wraps response" do - response_double = double("Response", success?: true) + describe '#create' do + it 'creates a transfer recipient and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:post) - .with("/transferrecipient", params) + .with('/transferrecipient', params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) recipients.create(params) end - it "raises error for missing required params" do - expect { + it 'raises error for missing required params' do + expect do recipients.create({}) - }.to raise_error(PaystackSdk::MissingParamError) + end.to raise_error(PaystackSdk::MissingParamError) end end - describe "#list" do - it "lists transfer recipients and wraps response" do - response_double = double("Response", success?: true) + describe '#list' do + it 'lists transfer recipients and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:get) - .with("/transferrecipient", {}) + .with('/transferrecipient', {}) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) recipients.list end end - describe "#fetch" do - it "fetches a transfer recipient and wraps response" do - response_double = double("Response", success?: true) + describe '#fetch' do + it 'fetches a transfer recipient and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:get) - .with("/transferrecipient/RCP_123") + .with('/transferrecipient/RCP_123') .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - recipients.fetch(recipient_code: "RCP_123") + recipients.fetch(recipient_code: 'RCP_123') end end - describe "#update" do - it "updates a transfer recipient and wraps response" do - response_double = double("Response", success?: true) + describe '#update' do + it 'updates a transfer recipient and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:put) - .with("/transferrecipient/RCP_123", {name: "New Name"}) + .with('/transferrecipient/RCP_123', { name: 'New Name' }) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - recipients.update(recipient_code: "RCP_123", params: {name: "New Name"}) + recipients.update(recipient_code: 'RCP_123', params: { name: 'New Name' }) end end - describe "#delete" do - it "deletes a transfer recipient and wraps response" do - response_double = double("Response", success?: true) + describe '#delete' do + it 'deletes a transfer recipient and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:delete) - .with("/transferrecipient/RCP_123") + .with('/transferrecipient/RCP_123') .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - recipients.delete(recipient_code: "RCP_123") + recipients.delete(recipient_code: 'RCP_123') end end end diff --git a/spec/resources/transfers_spec.rb b/spec/resources/transfers_spec.rb index 6b5713e..6e77cf2 100644 --- a/spec/resources/transfers_spec.rb +++ b/spec/resources/transfers_spec.rb @@ -1,75 +1,75 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Resources::Transfers do - let(:connection) { instance_double("PaystackSdk::Connection") } + let(:connection) { instance_double('PaystackSdk::Connection') } let(:transfers) { described_class.new(connection) } let(:params) do { - source: "balance", - amount: 50000, - recipient: "RCP_1234567890", - reason: "Test transfer" + source: 'balance', + amount: 50_000, + recipient: 'RCP_1234567890', + reason: 'Test transfer' } end - describe "#create" do - it "creates a transfer and wraps response" do - response_double = double("Response", success?: true) + describe '#create' do + it 'creates a transfer and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:post) - .with("/transfer", params) + .with('/transfer', params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) transfers.create(params) end - it "raises error for missing required params" do - expect { + it 'raises error for missing required params' do + expect do transfers.create({}) - }.to raise_error(PaystackSdk::MissingParamError) + end.to raise_error(PaystackSdk::MissingParamError) end end - describe "#list" do - it "lists transfers and wraps response" do - response_double = double("Response", success?: true) + describe '#list' do + it 'lists transfers and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:get) - .with("/transfer", {}) + .with('/transfer', {}) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) transfers.list end end - describe "#fetch" do - it "fetches a transfer and wraps response" do - response_double = double("Response", success?: true) + describe '#fetch' do + it 'fetches a transfer and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:get) - .with("/transfer/12345") + .with('/transfer/12345') .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - transfers.fetch(id: "12345") + transfers.fetch(id: '12345') end end - describe "#finalize" do - it "finalizes a transfer and wraps response" do - response_double = double("Response", success?: true) + describe '#finalize' do + it 'finalizes a transfer and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:post) - .with("/transfer/finalize_transfer", {transfer_code: "TRF_abc", otp: "123456"}) + .with('/transfer/finalize_transfer', { transfer_code: 'TRF_abc', otp: '123456' }) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - transfers.finalize(transfer_code: "TRF_abc", otp: "123456") + transfers.finalize(transfer_code: 'TRF_abc', otp: '123456') end end - describe "#verify" do - it "verifies a transfer and wraps response" do - response_double = double("Response", success?: true) + describe '#verify' do + it 'verifies a transfer and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:get) - .with("/transfer/verify/abc123") + .with('/transfer/verify/abc123') .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - transfers.verify(reference: "abc123") + transfers.verify(reference: 'abc123') end end end diff --git a/spec/resources/verification_spec.rb b/spec/resources/verification_spec.rb index 6c76a04..119f8e4 100644 --- a/spec/resources/verification_spec.rb +++ b/spec/resources/verification_spec.rb @@ -1,87 +1,87 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Resources::Verification do - let(:connection) { instance_double("PaystackSdk::Connection") } + let(:connection) { instance_double('PaystackSdk::Connection') } let(:verification) { described_class.new(connection) } - describe "#resolve_account" do - it "resolves a bank account and wraps response" do - response_double = double("Response", success?: true) + describe '#resolve_account' do + it 'resolves a bank account and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:get) - .with("/bank/resolve", {account_number: "0001234567", bank_code: "058"}) + .with('/bank/resolve', { account_number: '0001234567', bank_code: '058' }) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - verification.resolve_account(account_number: "0001234567", bank_code: "058") + verification.resolve_account(account_number: '0001234567', bank_code: '058') end - it "raises error for missing account_number" do - expect { - verification.resolve_account(account_number: nil, bank_code: "058") - }.to raise_error(PaystackSdk::MissingParamError, /account_number/) + it 'raises error for missing account_number' do + expect do + verification.resolve_account(account_number: nil, bank_code: '058') + end.to raise_error(PaystackSdk::MissingParamError, /account_number/) end - it "raises error for missing bank_code" do - expect { - verification.resolve_account(account_number: "0001234567", bank_code: nil) - }.to raise_error(PaystackSdk::MissingParamError, /bank_code/) + it 'raises error for missing bank_code' do + expect do + verification.resolve_account(account_number: '0001234567', bank_code: nil) + end.to raise_error(PaystackSdk::MissingParamError, /bank_code/) end end - describe "#resolve_card_bin" do - it "resolves a card bin and wraps response" do - response_double = double("Response", success?: true) + describe '#resolve_card_bin' do + it 'resolves a card bin and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:get) - .with("/decision/bin/539983") + .with('/decision/bin/539983') .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - verification.resolve_card_bin("539983") + verification.resolve_card_bin('539983') end - it "raises error for missing bin" do - expect { + it 'raises error for missing bin' do + expect do verification.resolve_card_bin(nil) - }.to raise_error(PaystackSdk::MissingParamError, /bin/) + end.to raise_error(PaystackSdk::MissingParamError, /bin/) end end - describe "#validate_account" do + describe '#validate_account' do let(:required_params) do { - account_number: "0001234567", - account_name: "Jane Doe", - account_type: "personal", - bank_code: "058", - country_code: "NG", - document_type: "identityNumber" + account_number: '0001234567', + account_name: 'Jane Doe', + account_type: 'personal', + bank_code: '058', + country_code: 'NG', + document_type: 'identityNumber' } end - it "validates an account and wraps response" do - response_double = double("Response", success?: true) + it 'validates an account and wraps response' do + response_double = double('Response', success?: true) expect(connection).to receive(:post) - .with("/bank/validate", required_params) + .with('/bank/validate', required_params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) verification.validate_account(required_params) end - it "raises error for missing required fields" do + it 'raises error for missing required fields' do %i[account_number account_name account_type bank_code country_code document_type].each do |field| params = required_params.dup params.delete(field) - expect { + expect do verification.validate_account(params) - }.to raise_error(PaystackSdk::MissingParamError, /#{field}/) + end.to raise_error(PaystackSdk::MissingParamError, /#{field}/) end end - it "validates an account with all params and wraps response" do + it 'validates an account with all params and wraps response' do all_params = required_params.merge( - document_number: "1234567890123" + document_number: '1234567890123' ) - response_double = double("Response", success?: true) + response_double = double('Response', success?: true) expect(connection).to receive(:post) - .with("/bank/validate", all_params) + .with('/bank/validate', all_params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) verification.validate_account(all_params) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a2e3fd8..5c8fbf2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require "debug" -require_relative "../lib/paystack_sdk" +require 'debug' +require_relative '../lib/paystack_sdk' RSpec.configure do |config| # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = ".rspec_status" + config.example_status_persistence_file_path = '.rspec_status' # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! From 0cee5ee83b4adb91f4106a951dcba24f206db77b Mon Sep 17 00:00:00 2001 From: Shepherd Yaw Morttey Date: Mon, 22 Sep 2025 21:08:18 +0000 Subject: [PATCH 2/3] Refactor: Standardize string delimiters across the codebase to conform with standard rb instead of rubocop --- Gemfile | 2 +- Rakefile | 6 +- lib/paystack_sdk.rb | 10 +- lib/paystack_sdk/client.rb | 16 +- lib/paystack_sdk/resources/banks.rb | 6 +- lib/paystack_sdk/resources/base.rb | 8 +- lib/paystack_sdk/resources/charges.rb | 36 +-- lib/paystack_sdk/resources/customers.rb | 50 ++-- lib/paystack_sdk/resources/transactions.rb | 90 +++---- .../resources/transfer_recipients.rb | 18 +- lib/paystack_sdk/resources/transfers.rb | 20 +- lib/paystack_sdk/resources/verification.rb | 16 +- lib/paystack_sdk/response.rb | 34 +-- lib/paystack_sdk/utils/connection_utils.rb | 12 +- lib/paystack_sdk/validations.rb | 34 +-- lib/paystack_sdk/version.rb | 2 +- paystack_sdk.gemspec | 36 +-- spec/client_spec.rb | 34 +-- spec/paystack_sdk_spec.rb | 2 +- spec/resources/banks_spec.rb | 22 +- spec/resources/charges_spec.rb | 54 ++--- spec/resources/customers_spec.rb | 224 +++++++++--------- spec/resources/transactions_spec.rb | 170 ++++++------- spec/resources/transfer_recipients_spec.rb | 60 ++--- spec/resources/transfers_spec.rb | 56 ++--- spec/resources/verification_spec.rb | 62 ++--- spec/spec_helper.rb | 6 +- 27 files changed, 543 insertions(+), 543 deletions(-) diff --git a/Gemfile b/Gemfile index 1c44ac3..bbc5e79 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ # frozen_string_literal: true -source 'https://rubygems.org' +source "https://rubygems.org" # Specify your gem's dependencies in paystack_sdk.gemspec gemspec diff --git a/Rakefile b/Rakefile index 7d38ee8..df40677 100644 --- a/Rakefile +++ b/Rakefile @@ -1,10 +1,10 @@ # frozen_string_literal: true -require 'bundler/gem_tasks' -require 'rspec/core/rake_task' +require "bundler/gem_tasks" +require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) -require 'standard/rake' +require "standard/rake" task default: %i[spec standard] diff --git a/lib/paystack_sdk.rb b/lib/paystack_sdk.rb index a3750d5..6e2e02a 100644 --- a/lib/paystack_sdk.rb +++ b/lib/paystack_sdk.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'faraday' -require_relative 'paystack_sdk/version' -require_relative 'paystack_sdk/client' +require "faraday" +require_relative "paystack_sdk/version" +require_relative "paystack_sdk/client" module PaystackSdk # Base error class for all Paystack SDK errors. @@ -54,7 +54,7 @@ class APIError < Error; end # Raised when authentication fails class AuthenticationError < APIError - def initialize(message = 'Invalid API key or authentication failed') + def initialize(message = "Invalid API key or authentication failed") super end end @@ -83,7 +83,7 @@ def initialize(retry_after) class ServerError < APIError attr_reader :status_code - def initialize(status_code, message = 'An error occurred on the Paystack server') + def initialize(status_code, message = "An error occurred on the Paystack server") @status_code = status_code super("#{message} (Status: #{status_code})") end diff --git a/lib/paystack_sdk/client.rb b/lib/paystack_sdk/client.rb index 951c996..1de59e1 100644 --- a/lib/paystack_sdk/client.rb +++ b/lib/paystack_sdk/client.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -require_relative 'resources/transactions' -require_relative 'resources/customers' -require_relative 'resources/transfer_recipients' -require_relative 'resources/transfers' -require_relative 'resources/banks' -require_relative 'resources/verification' -require_relative 'resources/charges' -require_relative 'utils/connection_utils' +require_relative "resources/transactions" +require_relative "resources/customers" +require_relative "resources/transfer_recipients" +require_relative "resources/transfers" +require_relative "resources/banks" +require_relative "resources/verification" +require_relative "resources/charges" +require_relative "utils/connection_utils" module PaystackSdk # The `Client` class serves as the main entry point for interacting with the Paystack API. diff --git a/lib/paystack_sdk/resources/banks.rb b/lib/paystack_sdk/resources/banks.rb index fd3117f..bc924d0 100644 --- a/lib/paystack_sdk/resources/banks.rb +++ b/lib/paystack_sdk/resources/banks.rb @@ -1,4 +1,4 @@ -require_relative '../validations' +require_relative "../validations" module PaystackSdk module Resources @@ -9,12 +9,12 @@ def list(query = {}) if query.key?(:currency) validate_allowed_values!( value: query[:currency], - name: 'currency', + name: "currency", allowed_values: %w[NGN GHS ZAR KES USD] ) end - handle_response(@connection.get('/bank', query)) + handle_response(@connection.get("/bank", query)) end end end diff --git a/lib/paystack_sdk/resources/base.rb b/lib/paystack_sdk/resources/base.rb index d2e0ef4..2625c89 100644 --- a/lib/paystack_sdk/resources/base.rb +++ b/lib/paystack_sdk/resources/base.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require_relative '../response' -require_relative '../client' -require_relative '../validations' -require_relative '../utils/connection_utils' +require_relative "../response" +require_relative "../client" +require_relative "../validations" +require_relative "../utils/connection_utils" module PaystackSdk module Resources diff --git a/lib/paystack_sdk/resources/charges.rb b/lib/paystack_sdk/resources/charges.rb index 1f8763f..b1dea8e 100644 --- a/lib/paystack_sdk/resources/charges.rb +++ b/lib/paystack_sdk/resources/charges.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'base' +require_relative "base" module PaystackSdk module Resources @@ -31,7 +31,7 @@ class Charges < PaystackSdk::Resources::Base def mobile_money(payload) validate_mobile_money_payload!(payload) - response = @connection.post('/charge', payload) + response = @connection.post("/charge", payload) handle_response(response) end @@ -47,12 +47,12 @@ def submit_otp(payload) validate_fields!( payload: payload, validations: { - otp: { type: :string, required: true }, - reference: { type: :reference, required: true } + otp: {type: :string, required: true}, + reference: {type: :reference, required: true} } ) - response = @connection.post('/charge/submit_otp', payload) + response = @connection.post("/charge/submit_otp", payload) handle_response(response) end @@ -62,23 +62,23 @@ def validate_mobile_money_payload!(payload) validate_fields!( payload: payload, validations: { - email: { type: :email, required: true }, - amount: { type: :positive_integer, required: true }, - currency: { type: :currency, required: false }, - reference: { type: :reference, required: false }, - callback_url: { required: false }, - metadata: { required: false }, - mobile_money: { required: true } + email: {type: :email, required: true}, + amount: {type: :positive_integer, required: true}, + currency: {type: :currency, required: false}, + reference: {type: :reference, required: false}, + callback_url: {required: false}, + metadata: {required: false}, + mobile_money: {required: true} } ) - mobile_money = payload[:mobile_money] || payload['mobile_money'] - validate_hash!(input: mobile_money, name: 'mobile_money') + mobile_money = payload[:mobile_money] || payload["mobile_money"] + validate_hash!(input: mobile_money, name: "mobile_money") - phone = mobile_money[:phone] || mobile_money['phone'] - validate_presence!(value: phone, name: 'mobile_money phone') + phone = mobile_money[:phone] || mobile_money["phone"] + validate_presence!(value: phone, name: "mobile_money phone") - provider = mobile_money[:provider] || mobile_money['provider'] + provider = mobile_money[:provider] || mobile_money["provider"] validate_mobile_money_provider!(provider) end @@ -86,7 +86,7 @@ def validate_mobile_money_provider!(provider) validate_allowed_values!( value: provider&.downcase, allowed_values: MOBILE_MONEY_PROVIDERS, - name: 'mobile_money provider', + name: "mobile_money provider", allow_nil: false ) end diff --git a/lib/paystack_sdk/resources/customers.rb b/lib/paystack_sdk/resources/customers.rb index 14f00fd..c112fa4 100644 --- a/lib/paystack_sdk/resources/customers.rb +++ b/lib/paystack_sdk/resources/customers.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'base' +require_relative "base" module PaystackSdk module Resources @@ -45,14 +45,14 @@ def create(payload) validate_fields!( payload: payload, validations: { - email: { type: :email, required: true }, - first_name: { type: :string, required: false }, - last_name: { type: :string, required: false }, - phone: { type: :string, required: false } + email: {type: :email, required: true}, + first_name: {type: :string, required: false}, + last_name: {type: :string, required: false}, + phone: {type: :string, required: false} } ) - response = @connection.post('customer', payload) + response = @connection.post("customer", payload) handle_response(response) end @@ -65,15 +65,15 @@ def create(payload) # @return [PaystackSdk::Response] The response from the Paystack API. # @raise [PaystackSdk::Error] If the API request fails. def list(per_page: 50, page: 1, **params) - validate_positive_integer!(value: per_page, name: 'per_page', allow_nil: true) - validate_positive_integer!(value: page, name: 'page', allow_nil: true) + validate_positive_integer!(value: per_page, name: "per_page", allow_nil: true) + validate_positive_integer!(value: page, name: "page", allow_nil: true) - validate_date_format!(date_str: params[:from], name: 'from') if params[:from] + validate_date_format!(date_str: params[:from], name: "from") if params[:from] - validate_date_format!(date_str: params[:to], name: 'to') if params[:to] + validate_date_format!(date_str: params[:to], name: "to") if params[:to] - query_params = { perPage: per_page, page: page }.merge(params) - response = @connection.get('customer', query_params) + query_params = {perPage: per_page, page: page}.merge(params) + response = @connection.get("customer", query_params) handle_response(response) end @@ -83,7 +83,7 @@ def list(per_page: 50, page: 1, **params) # @return [PaystackSdk::Response] The response from the Paystack API. # @raise [PaystackSdk::Error] If the parameter is invalid or the API request fails. def fetch(email_or_code) - validate_presence!(value: email_or_code, name: 'email_or_code') + validate_presence!(value: email_or_code, name: "email_or_code") response = @connection.get("customer/#{email_or_code}") handle_response(response) end @@ -99,8 +99,8 @@ def fetch(email_or_code) # @return [PaystackSdk::Response] The response from the Paystack API. # @raise [PaystackSdk::Error] If the parameters are invalid or the API request fails. def update(code, payload) - validate_presence!(value: code, name: 'code') - validate_hash!(input: payload, name: 'payload') + validate_presence!(value: code, name: "code") + validate_hash!(input: payload, name: "payload") response = @connection.put("customer/#{code}", payload) handle_response(response) @@ -120,14 +120,14 @@ def update(code, payload) # @return [PaystackSdk::Response] The response from the Paystack API. # @raise [PaystackSdk::Error] If the parameters are invalid or the API request fails. def validate(code, payload) - validate_presence!(value: code, name: 'code') + validate_presence!(value: code, name: "code") validate_fields!( payload: payload, validations: { - country: { type: :string, required: true }, - type: { type: :string, required: true }, - account_number: { type: :string, required: true }, - bank_code: { type: :string, required: true } + country: {type: :string, required: true}, + type: {type: :string, required: true}, + account_number: {type: :string, required: true}, + bank_code: {type: :string, required: true} } ) @@ -157,12 +157,12 @@ def set_risk_action(payload) validate_fields!( payload: payload, validations: { - customer: { type: :string, required: true }, - risk_action: { type: :inclusion, required: true, allowed_values: %w[default allow deny] } + customer: {type: :string, required: true}, + risk_action: {type: :inclusion, required: true, allowed_values: %w[default allow deny]} } ) - response = @connection.post('customer/set_risk_action', payload) + response = @connection.post("customer/set_risk_action", payload) handle_response(response) end @@ -176,11 +176,11 @@ def deactivate_authorization(payload) validate_fields!( payload: payload, validations: { - authorization_code: { type: :string, required: true } + authorization_code: {type: :string, required: true} } ) - response = @connection.post('customer/deactivate_authorization', payload) + response = @connection.post("customer/deactivate_authorization", payload) handle_response(response) end end diff --git a/lib/paystack_sdk/resources/transactions.rb b/lib/paystack_sdk/resources/transactions.rb index 18e98eb..ac6918d 100644 --- a/lib/paystack_sdk/resources/transactions.rb +++ b/lib/paystack_sdk/resources/transactions.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'base' +require_relative "base" module PaystackSdk module Resources @@ -57,15 +57,15 @@ def initiate(payload) validate_fields!( payload: payload, validations: { - email: { type: :email, required: true }, - amount: { type: :positive_integer, required: true }, - currency: { type: :currency, required: false }, - reference: { type: :reference, required: false }, - callback_url: { required: false } + email: {type: :email, required: true}, + amount: {type: :positive_integer, required: true}, + currency: {type: :currency, required: false}, + reference: {type: :reference, required: false}, + callback_url: {required: false} } ) - response = @connection.post('/transaction/initialize', payload) + response = @connection.post("/transaction/initialize", payload) handle_response(response) end @@ -78,7 +78,7 @@ def initiate(payload) # @example # response = transactions.verify(reference: "transaction_reference") def verify(reference:) - validate_presence!(value: reference, name: 'Reference') + validate_presence!(value: reference, name: "Reference") response = @connection.get("/transaction/verify/#{reference}") handle_response(response) @@ -104,26 +104,26 @@ def verify(reference:) # response = transactions.list(per_page: 10, from: "2023-01-01", to: "2023-12-31", status: "success") def list(per_page: 50, page: 1, **params) # Create a combined parameter hash for validation - all_params = { per_page: per_page, page: page }.merge(params) + all_params = {per_page: per_page, page: page}.merge(params) # Validate parameters validate_fields!( payload: all_params, validations: { - per_page: { type: :positive_integer, required: false }, - page: { type: :positive_integer, required: false }, - from: { type: :date, required: false }, - to: { type: :date, required: false }, - status: { type: :inclusion, allowed_values: %w[failed success abandoned], required: false }, - customer: { type: :positive_integer, required: false }, - amount: { type: :positive_integer, required: false }, - currency: { type: :currency, required: false } + per_page: {type: :positive_integer, required: false}, + page: {type: :positive_integer, required: false}, + from: {type: :date, required: false}, + to: {type: :date, required: false}, + status: {type: :inclusion, allowed_values: %w[failed success abandoned], required: false}, + customer: {type: :positive_integer, required: false}, + amount: {type: :positive_integer, required: false}, + currency: {type: :currency, required: false} } ) # Prepare request parameters - request_params = { perPage: per_page, page: page }.merge(params) - response = @connection.get('/transaction', request_params) + request_params = {perPage: per_page, page: page}.merge(params) + response = @connection.get("/transaction", request_params) handle_response(response) end @@ -136,7 +136,7 @@ def list(per_page: 50, page: 1, **params) # @example # response = transactions.fetch("12345") def fetch(transaction_id) - validate_presence!(value: transaction_id, name: 'Transaction ID') + validate_presence!(value: transaction_id, name: "Transaction ID") response = @connection.get("/transaction/#{transaction_id}") handle_response(response) @@ -157,12 +157,12 @@ def totals(**params) validate_fields!( payload: params, validations: { - from: { type: :date, required: false }, - to: { type: :date, required: false } + from: {type: :date, required: false}, + to: {type: :date, required: false} } ) - response = @connection.get('/transaction/totals', params) + response = @connection.get("/transaction/totals", params) handle_response(response) end @@ -188,18 +188,18 @@ def export(**params) validate_fields!( payload: params, validations: { - from: { type: :date, required: false }, - to: { type: :date, required: false }, - status: { type: :inclusion, allowed_values: %w[failed success abandoned], required: false }, - currency: { type: :currency, required: false }, - amount: { type: :positive_integer, required: false }, - payment_page: { type: :positive_integer, required: false }, - customer: { type: :positive_integer, required: false }, - settlement: { type: :positive_integer, required: false } + from: {type: :date, required: false}, + to: {type: :date, required: false}, + status: {type: :inclusion, allowed_values: %w[failed success abandoned], required: false}, + currency: {type: :currency, required: false}, + amount: {type: :positive_integer, required: false}, + payment_page: {type: :positive_integer, required: false}, + customer: {type: :positive_integer, required: false}, + settlement: {type: :positive_integer, required: false} } ) - response = @connection.get('/transaction/export', params) + response = @connection.get("/transaction/export", params) handle_response(response) end @@ -227,15 +227,15 @@ def charge_authorization(payload) validate_fields!( payload: payload, validations: { - authorization_code: { required: true }, - email: { type: :email, required: true }, - amount: { type: :positive_integer, required: true }, - reference: { type: :reference, required: false }, - currency: { type: :currency, required: false } + authorization_code: {required: true}, + email: {type: :email, required: true}, + amount: {type: :positive_integer, required: true}, + reference: {type: :reference, required: false}, + currency: {type: :currency, required: false} } ) - response = @connection.post('/transaction/charge_authorization', payload) + response = @connection.post("/transaction/charge_authorization", payload) handle_response(response) end @@ -264,15 +264,15 @@ def partial_debit(payload) validate_fields!( payload: payload, validations: { - authorization_code: { required: true }, - currency: { type: :currency, required: true }, - amount: { type: :positive_integer, required: true }, - email: { type: :email, required: true }, - reference: { type: :reference, required: false } + authorization_code: {required: true}, + currency: {type: :currency, required: true}, + amount: {type: :positive_integer, required: true}, + email: {type: :email, required: true}, + reference: {type: :reference, required: false} } ) - response = @connection.post('/transaction/partial_debit', payload) + response = @connection.post("/transaction/partial_debit", payload) handle_response(response) end @@ -287,7 +287,7 @@ def partial_debit(payload) # # OR # response = transactions.timeline("ref_123456789") def timeline(id_or_reference) - validate_presence!(value: id_or_reference, name: 'Transaction ID or Reference') + validate_presence!(value: id_or_reference, name: "Transaction ID or Reference") response = @connection.get("/transaction/timeline/#{id_or_reference}") handle_response(response) diff --git a/lib/paystack_sdk/resources/transfer_recipients.rb b/lib/paystack_sdk/resources/transfer_recipients.rb index d58560f..8f755e7 100644 --- a/lib/paystack_sdk/resources/transfer_recipients.rb +++ b/lib/paystack_sdk/resources/transfer_recipients.rb @@ -1,4 +1,4 @@ -require_relative '../validations' +require_relative "../validations" module PaystackSdk module Resources @@ -6,40 +6,40 @@ class TransferRecipients < Base # Create a transfer recipient # @see https://paystack.com/docs/api/transfer-recipient/#create def create(params) - validate_hash!(input: params, name: 'TransferRecipient params') + validate_hash!(input: params, name: "TransferRecipient params") validate_required_params!( payload: params, required_params: %i[type name account_number bank_code], - operation_name: 'Create Transfer Recipient' + operation_name: "Create Transfer Recipient" ) - handle_response(@connection.post('/transferrecipient', params)) + handle_response(@connection.post("/transferrecipient", params)) end # List transfer recipients # @see https://paystack.com/docs/api/transfer-recipient/#list def list(query = {}) - handle_response(@connection.get('/transferrecipient', query)) + handle_response(@connection.get("/transferrecipient", query)) end # Fetch a transfer recipient # @see https://paystack.com/docs/api/transfer-recipient/#fetch def fetch(recipient_code:) - validate_presence!(value: recipient_code, name: 'recipient_code') + validate_presence!(value: recipient_code, name: "recipient_code") handle_response(@connection.get("/transferrecipient/#{recipient_code}")) end # Update a transfer recipient # @see https://paystack.com/docs/api/transfer-recipient/#update def update(recipient_code:, params:) - validate_presence!(value: recipient_code, name: 'recipient_code') - validate_hash!(input: params, name: 'Update TransferRecipient params') + validate_presence!(value: recipient_code, name: "recipient_code") + validate_hash!(input: params, name: "Update TransferRecipient params") handle_response(@connection.put("/transferrecipient/#{recipient_code}", params)) end # Delete a transfer recipient # @see https://paystack.com/docs/api/transfer-recipient/#delete def delete(recipient_code:) - validate_presence!(value: recipient_code, name: 'recipient_code') + validate_presence!(value: recipient_code, name: "recipient_code") handle_response(@connection.delete("/transferrecipient/#{recipient_code}")) end end diff --git a/lib/paystack_sdk/resources/transfers.rb b/lib/paystack_sdk/resources/transfers.rb index 37c7689..ca844c5 100644 --- a/lib/paystack_sdk/resources/transfers.rb +++ b/lib/paystack_sdk/resources/transfers.rb @@ -1,4 +1,4 @@ -require_relative '../validations' +require_relative "../validations" module PaystackSdk module Resources @@ -6,40 +6,40 @@ class Transfers < Base # Create a transfer # @see https://paystack.com/docs/api/transfer/#initiate def create(params) - validate_hash!(input: params, name: 'Transfer params') + validate_hash!(input: params, name: "Transfer params") validate_required_params!( payload: params, required_params: %i[source amount recipient], - operation_name: 'Create Transfer' + operation_name: "Create Transfer" ) - handle_response(@connection.post('/transfer', params)) + handle_response(@connection.post("/transfer", params)) end # List transfers # @see https://paystack.com/docs/api/transfer/#list def list(query = {}) - handle_response(@connection.get('/transfer', query)) + handle_response(@connection.get("/transfer", query)) end # Fetch a transfer # @see https://paystack.com/docs/api/transfer/#fetch def fetch(id:) - validate_presence!(value: id, name: 'transfer id') + validate_presence!(value: id, name: "transfer id") handle_response(@connection.get("/transfer/#{id}")) end # Finalize a transfer (OTP) # @see https://paystack.com/docs/api/transfer/#finalize def finalize(transfer_code:, otp:) - validate_presence!(value: transfer_code, name: 'transfer_code') - validate_presence!(value: otp, name: 'otp') - handle_response(@connection.post('/transfer/finalize_transfer', { transfer_code: transfer_code, otp: otp })) + validate_presence!(value: transfer_code, name: "transfer_code") + validate_presence!(value: otp, name: "otp") + handle_response(@connection.post("/transfer/finalize_transfer", {transfer_code: transfer_code, otp: otp})) end # Verify a transfer # @see https://paystack.com/docs/api/transfer/#verify def verify(reference:) - validate_presence!(value: reference, name: 'reference') + validate_presence!(value: reference, name: "reference") handle_response(@connection.get("/transfer/verify/#{reference}")) end end diff --git a/lib/paystack_sdk/resources/verification.rb b/lib/paystack_sdk/resources/verification.rb index 945fe67..fdd8502 100644 --- a/lib/paystack_sdk/resources/verification.rb +++ b/lib/paystack_sdk/resources/verification.rb @@ -1,5 +1,5 @@ -require_relative '../validations' -require_relative 'base' +require_relative "../validations" +require_relative "base" module PaystackSdk module Resources @@ -7,15 +7,15 @@ class Verification < Base # Resolve Bank Account # @see https://paystack.com/docs/api/verification/#resolve-bank-account def resolve_account(account_number:, bank_code:) - validate_presence!(value: account_number, name: 'account_number') - validate_presence!(value: bank_code, name: 'bank_code') - handle_response(@connection.get('/bank/resolve', { account_number: account_number, bank_code: bank_code })) + validate_presence!(value: account_number, name: "account_number") + validate_presence!(value: bank_code, name: "bank_code") + handle_response(@connection.get("/bank/resolve", {account_number: account_number, bank_code: bank_code})) end # Resolve Card BIN # @see https://paystack.com/docs/api/verification/#resolve-card-bin def resolve_card_bin(bin) - validate_presence!(value: bin, name: 'bin') + validate_presence!(value: bin, name: "bin") handle_response(@connection.get("/decision/bin/#{bin}")) end @@ -27,9 +27,9 @@ def validate_account(params) validate_required_params!( payload: params, required_params: %i[account_number account_name account_type bank_code country_code document_type], - operation_name: 'Validate Account' + operation_name: "Validate Account" ) - handle_response(@connection.post('/bank/validate', params)) + handle_response(@connection.post("/bank/validate", params)) end end end diff --git a/lib/paystack_sdk/response.rb b/lib/paystack_sdk/response.rb index 0ce2554..9aaf979 100644 --- a/lib/paystack_sdk/response.rb +++ b/lib/paystack_sdk/response.rb @@ -82,20 +82,20 @@ def initialize(response) when 400..499 # Client errors - return unsuccessful response for user to handle @success = false - @error_message = @api_message || 'Client error' + @error_message = @api_message || "Client error" # Still raise for authentication issues as these are usually config problems - raise AuthenticationError.new(@api_message || 'Authentication failed') if @status_code == 401 + raise AuthenticationError.new(@api_message || "Authentication failed") if @status_code == 401 when 429 # Rate limiting - raise as users need to implement retry logic - retry_after = response.headers['Retry-After'] + retry_after = response.headers["Retry-After"] raise RateLimitError.new(retry_after || 30) when 500..599 # Server errors - raise as these indicate Paystack infrastructure issues raise ServerError.new(@status_code, @api_message) else @success = false - @error_message = @api_message || 'Unknown error' + @error_message = @api_message || "Unknown error" end elsif response.is_a?(Response) @success = response.success? @@ -256,19 +256,19 @@ def each # @param body [Hash] The response body # @return [String] The extracted identifier or "unknown" def extract_identifier(body) - return 'unknown' unless body.is_a?(Hash) + return "unknown" unless body.is_a?(Hash) # First try to get identifier from the message - message = body['message'].to_s.downcase + message = body["message"].to_s.downcase return ::Regexp.last_match(2) if message =~ /with (id|code|reference|email): ([^\s]+)/i # If not found in message, try to extract from error code - if body['code']&.match?(/^(transaction|customer)_/) - parts = body['code'].to_s.split('_') - return parts.last if parts.last != 'not_found' + if body["code"]&.match?(/^(transaction|customer)_/) + parts = body["code"].to_s.split("_") + return parts.last if parts.last != "not_found" end - 'unknown' + "unknown" end # Extract the API message from the response body @@ -276,7 +276,7 @@ def extract_identifier(body) # @param body [Hash, nil] The response body # @return [String, nil] The API message if present def extract_api_message(body) - body['message'] if body.is_a?(Hash) && body['message'] + body["message"] if body.is_a?(Hash) && body["message"] end # Extract the data from the response body @@ -286,7 +286,7 @@ def extract_api_message(body) def extract_data_from_body(body) return body unless body.is_a?(Hash) - body['data'] || body + body["data"] || body end # Wrap value in Response if needed @@ -308,14 +308,14 @@ def wrap_value(value) end def determine_resource_type - return 'Unknown' unless @body.is_a?(Hash) && @body['code'] + return "Unknown" unless @body.is_a?(Hash) && @body["code"] - if @body['code'].include?('_') - parts = @body['code'].to_s.split('_') - return parts.last if parts.last != 'not_found' + if @body["code"].include?("_") + parts = @body["code"].to_s.split("_") + return parts.last if parts.last != "not_found" end - 'unknown' + "unknown" end end end diff --git a/lib/paystack_sdk/utils/connection_utils.rb b/lib/paystack_sdk/utils/connection_utils.rb index f3b8ff6..242ed6a 100644 --- a/lib/paystack_sdk/utils/connection_utils.rb +++ b/lib/paystack_sdk/utils/connection_utils.rb @@ -7,7 +7,7 @@ module Utils # and resource classes. module ConnectionUtils # The base URL for the Paystack API. - BASE_URL = 'https://api.paystack.co' + BASE_URL = "https://api.paystack.co" # Initializes a connection based on the provided parameters. # @@ -22,8 +22,8 @@ def initialize_connection(connection = nil, secret_key: nil) create_connection(secret_key:) else # Try to get API key from environment variable - env_secret_key = ENV['PAYSTACK_SECRET_KEY'] - raise AuthenticationError, 'No connection or API key provided' unless env_secret_key + env_secret_key = ENV["PAYSTACK_SECRET_KEY"] + raise AuthenticationError, "No connection or API key provided" unless env_secret_key create_connection(secret_key: env_secret_key) end @@ -37,9 +37,9 @@ def create_connection(secret_key:) Faraday.new(url: BASE_URL) do |conn| conn.request :json conn.response :json, content_type: /\bjson$/ - conn.headers['Authorization'] = "Bearer #{secret_key}" - conn.headers['Content-Type'] = 'application/json' - conn.headers['User-Agent'] = "paystack_sdk/#{PaystackSdk::VERSION}" + conn.headers["Authorization"] = "Bearer #{secret_key}" + conn.headers["Content-Type"] = "application/json" + conn.headers["User-Agent"] = "paystack_sdk/#{PaystackSdk::VERSION}" conn.adapter Faraday.default_adapter end end diff --git a/lib/paystack_sdk/validations.rb b/lib/paystack_sdk/validations.rb index d277ddb..d1f63e7 100644 --- a/lib/paystack_sdk/validations.rb +++ b/lib/paystack_sdk/validations.rb @@ -36,10 +36,10 @@ module Validations # @param input [Object] The input to validate # @param name [String] Name of the parameter for error messages # @raise [PaystackSdk::InvalidFormatError] If input is not a hash - def validate_hash!(input:, name: 'Payload') + def validate_hash!(input:, name: "Payload") return if input.is_a?(Hash) - raise PaystackSdk::InvalidFormatError.new(name, 'Hash') + raise PaystackSdk::InvalidFormatError.new(name, "Hash") end # Validates that required parameters are present in a payload. @@ -48,7 +48,7 @@ def validate_hash!(input:, name: 'Payload') # @param required_params [Array] List of required parameter keys # @param operation_name [String] Name of the operation for error messages # @raise [PaystackSdk::MissingParamError] If any required parameters are missing - def validate_required_params!(payload:, required_params:, operation_name: 'Operation') + def validate_required_params!(payload:, required_params:, operation_name: "Operation") missing_params = required_params.select do |param| !payload.key?(param) && !payload.key?(param.to_s) end @@ -64,7 +64,7 @@ def validate_required_params!(payload:, required_params:, operation_name: 'Opera # @param value [Object] The value to validate # @param name [String] Name of the parameter for error messages # @raise [PaystackSdk::MissingParamError] If value is nil or empty - def validate_presence!(value:, name: 'Parameter') + def validate_presence!(value:, name: "Parameter") return unless value.nil? || (value.respond_to?(:empty?) && value.empty?) raise PaystackSdk::MissingParamError.new(name) @@ -77,11 +77,11 @@ def validate_presence!(value:, name: 'Parameter') # @param allow_nil [Boolean] Whether nil values are allowed # @raise [PaystackSdk::InvalidValueError] If value is not a positive integer # @raise [PaystackSdk::MissingParamError] If value is nil and not allowed - def validate_positive_integer!(value:, name: 'Parameter', allow_nil: true) + def validate_positive_integer!(value:, name: "Parameter", allow_nil: true) if value.nil? raise PaystackSdk::MissingParamError.new(name) unless allow_nil elsif !value.is_a?(Integer) || value < 1 - raise PaystackSdk::InvalidValueError.new(name, 'must be a positive integer') + raise PaystackSdk::InvalidValueError.new(name, "must be a positive integer") end end @@ -90,10 +90,10 @@ def validate_positive_integer!(value:, name: 'Parameter', allow_nil: true) # @param reference [String] The reference to validate # @param name [String] Name of the parameter for error messages # @raise [PaystackSdk::InvalidFormatError] If reference format is invalid - def validate_reference_format!(reference:, name: 'Reference') + def validate_reference_format!(reference:, name: "Reference") return if reference.to_s.match?(/^[a-zA-Z0-9._=-]+$/) - raise PaystackSdk::InvalidFormatError.new(name, 'alphanumeric characters and the following: -, ., =') + raise PaystackSdk::InvalidFormatError.new(name, "alphanumeric characters and the following: -, ., =") end # Validates a date string format. @@ -103,7 +103,7 @@ def validate_reference_format!(reference:, name: 'Reference') # @param allow_nil [Boolean] Whether nil values are allowed # @raise [PaystackSdk::InvalidFormatError] If date format is invalid # @raise [PaystackSdk::MissingParamError] If date is nil and not allowed - def validate_date_format!(date_str:, name: 'Date', allow_nil: true) + def validate_date_format!(date_str:, name: "Date", allow_nil: true) if date_str.nil? raise PaystackSdk::MissingParamError.new(name) unless allow_nil @@ -113,7 +113,7 @@ def validate_date_format!(date_str:, name: 'Date', allow_nil: true) begin Date.parse(date_str.to_s) rescue Date::Error - raise PaystackSdk::InvalidFormatError.new(name, 'YYYY-MM-DD or ISO8601') + raise PaystackSdk::InvalidFormatError.new(name, "YYYY-MM-DD or ISO8601") end end @@ -134,7 +134,7 @@ def validate_date_format!(date_str:, name: 'Date', allow_nil: true) # name: "risk_action" # ) # ``` - def validate_allowed_values!(value:, allowed_values:, name: 'Parameter', allow_nil: true) + def validate_allowed_values!(value:, allowed_values:, name: "Parameter", allow_nil: true) if value.nil? raise PaystackSdk::MissingParamError.new(name) unless allow_nil @@ -143,7 +143,7 @@ def validate_allowed_values!(value:, allowed_values:, name: 'Parameter', allow_n return if allowed_values.include?(value) - allowed_list = allowed_values.join(', ') + allowed_list = allowed_values.join(", ") raise PaystackSdk::InvalidValueError.new(name, "must be one of: #{allowed_list}") end @@ -153,7 +153,7 @@ def validate_allowed_values!(value:, allowed_values:, name: 'Parameter', allow_n # @param name [String] Name of the parameter for error messages # @param allow_nil [Boolean] Whether nil values are allowed # @raise [PaystackSdk::Error] If email format is invalid - def validate_email!(email:, name: 'Email', allow_nil: false) + def validate_email!(email:, name: "Email", allow_nil: false) if email.nil? raise PaystackSdk::MissingParamError.new(name) unless allow_nil @@ -162,7 +162,7 @@ def validate_email!(email:, name: 'Email', allow_nil: false) return if email.to_s.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/) - raise PaystackSdk::InvalidFormatError.new(name, 'valid email address') + raise PaystackSdk::InvalidFormatError.new(name, "valid email address") end # Validates a currency code format. @@ -171,7 +171,7 @@ def validate_email!(email:, name: 'Email', allow_nil: false) # @param name [String] Name of the parameter for error messages # @param allow_nil [Boolean] Whether nil values are allowed # @raise [PaystackSdk::Error] If currency format is invalid - def validate_currency!(currency:, name: 'Currency', allow_nil: true) + def validate_currency!(currency:, name: "Currency", allow_nil: true) if currency.nil? raise PaystackSdk::MissingParamError.new(name) unless allow_nil @@ -180,7 +180,7 @@ def validate_currency!(currency:, name: 'Currency', allow_nil: true) return if currency.to_s.match?(/\A[A-Z]{3}\z/) - raise PaystackSdk::InvalidFormatError.new(name, '3-letter ISO code (e.g., NGN, USD, GHS)') + raise PaystackSdk::InvalidFormatError.new(name, "3-letter ISO code (e.g., NGN, USD, GHS)") end # Validates multiple fields at once. @@ -202,7 +202,7 @@ def validate_currency!(currency:, name: 'Currency', allow_nil: true) # ) # ``` def validate_fields!(payload:, validations:) - validate_hash!(input: payload, name: 'Payload') + validate_hash!(input: payload, name: "Payload") # First check required fields required_fields = validations.select { |_, opts| opts[:required] }.keys diff --git a/lib/paystack_sdk/version.rb b/lib/paystack_sdk/version.rb index 00c6365..b266eeb 100644 --- a/lib/paystack_sdk/version.rb +++ b/lib/paystack_sdk/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module PaystackSdk - VERSION = '0.1.0' + VERSION = "0.1.0" end diff --git a/paystack_sdk.gemspec b/paystack_sdk.gemspec index 0561f02..9a4d08e 100644 --- a/paystack_sdk.gemspec +++ b/paystack_sdk.gemspec @@ -1,12 +1,12 @@ # frozen_string_literal: true -require_relative 'lib/paystack_sdk/version' +require_relative "lib/paystack_sdk/version" Gem::Specification.new do |spec| - spec.name = 'paystack_sdk' + spec.name = "paystack_sdk" spec.version = PaystackSdk::VERSION - spec.authors = ['Maxwell Nana Forson (theLazyProgrammer)'] - spec.email = ['nanaforsonjnr@gmail.com'] + spec.authors = ["Maxwell Nana Forson (theLazyProgrammer)"] + spec.email = ["nanaforsonjnr@gmail.com"] spec.summary = "A Ruby SDK for integrating with Paystack's payment gateway API." spec.description = <<~EOS @@ -16,13 +16,13 @@ Gem::Specification.new do |spec| applications. With support for various endpoints, this SDK simplifies tasks such as initiating transactions, verifying payments, managing customers, and more. EOS - spec.homepage = 'https://github.com/nanafox/paystack_sdk' - spec.license = 'MIT' - spec.required_ruby_version = '>= 3.2.2' + spec.homepage = "https://github.com/nanafox/paystack_sdk" + spec.license = "MIT" + spec.required_ruby_version = ">= 3.2.2" - spec.metadata['allowed_push_host'] = 'https://rubygems.org' - spec.metadata['homepage_uri'] = spec.homepage - spec.metadata['changelog_uri'] = 'https://github.com/nanafox/paystack_sdk/blob/main/CHANGELOG.md' + spec.metadata["allowed_push_host"] = "https://rubygems.org" + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["changelog_uri"] = "https://github.com/nanafox/paystack_sdk/blob/main/CHANGELOG.md" # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. @@ -33,17 +33,17 @@ Gem::Specification.new do |spec| f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) end end - spec.bindir = 'exe' + spec.bindir = "exe" spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } - spec.require_paths = ['lib'] + spec.require_paths = ["lib"] # Dependencies - spec.add_dependency 'faraday', '~> 2.13.1' - spec.add_development_dependency 'debug', '~> 1.9.0' - spec.add_development_dependency 'irb', '~> 1.15.1' - spec.add_development_dependency 'rake', '~> 13.2.1' - spec.add_development_dependency 'rspec', '~> 3.13' - spec.add_development_dependency 'standard', '~> 1.49.0' + spec.add_dependency "faraday", "~> 2.13.1" + spec.add_development_dependency "debug", "~> 1.9.0" + spec.add_development_dependency "irb", "~> 1.15.1" + spec.add_development_dependency "rake", "~> 13.2.1" + spec.add_development_dependency "rspec", "~> 3.13" + spec.add_development_dependency "standard", "~> 1.49.0" # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html diff --git a/spec/client_spec.rb b/spec/client_spec.rb index bb8f57a..3eb97d8 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -1,40 +1,40 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Client do - let(:secret_key) { 'sk_test_xxx' } + let(:secret_key) { "sk_test_xxx" } let(:connection_double) { instance_double(Faraday::Connection) } let(:client) do allow(Faraday).to receive(:new).and_return(connection_double) described_class.new(secret_key: secret_key) end - describe '#initialize' do - it 'initializes a new client with the given API key' do + describe "#initialize" do + it "initializes a new client with the given API key" do expect(client).to be_a(PaystackSdk::Client) end - it 'sets the correct base URL' do - allow(connection_double).to receive(:url_prefix).and_return(URI('https://api.paystack.co')) - expect(client.instance_variable_get(:@connection).url_prefix.to_s.chomp('/')).to eq('https://api.paystack.co') + it "sets the correct base URL" do + allow(connection_double).to receive(:url_prefix).and_return(URI("https://api.paystack.co")) + expect(client.instance_variable_get(:@connection).url_prefix.to_s.chomp("/")).to eq("https://api.paystack.co") end - it 'sets the correct headers' do + it "sets the correct headers" do headers = { - 'Authorization' => "Bearer #{secret_key}", - 'Content-Type' => 'application/json', - 'User-Agent' => "paystack_sdk/#{PaystackSdk::VERSION}" + "Authorization" => "Bearer #{secret_key}", + "Content-Type" => "application/json", + "User-Agent" => "paystack_sdk/#{PaystackSdk::VERSION}" } allow(connection_double).to receive(:headers).and_return(headers) connection_headers = client.instance_variable_get(:@connection).headers - expect(connection_headers['Authorization']).to eq("Bearer #{secret_key}") - expect(connection_headers['Content-Type']).to eq('application/json') - expect(connection_headers['User-Agent']).to eq("paystack_sdk/#{PaystackSdk::VERSION}") + expect(connection_headers["Authorization"]).to eq("Bearer #{secret_key}") + expect(connection_headers["Content-Type"]).to eq("application/json") + expect(connection_headers["User-Agent"]).to eq("paystack_sdk/#{PaystackSdk::VERSION}") end end - describe '#transactions' do - it 'returns an instance of Transactions resource' do + describe "#transactions" do + it "returns an instance of Transactions resource" do transactions_double = instance_double(PaystackSdk::Resources::Transactions) allow(PaystackSdk::Resources::Transactions).to receive(:new).and_return(transactions_double) @@ -42,8 +42,8 @@ end end - describe '#charges' do - it 'returns an instance of Charges resource' do + describe "#charges" do + it "returns an instance of Charges resource" do charges_double = instance_double(PaystackSdk::Resources::Charges) allow(PaystackSdk::Resources::Charges).to receive(:new).and_return(charges_double) diff --git a/spec/paystack_sdk_spec.rb b/spec/paystack_sdk_spec.rb index f10616d..ddfa9ae 100644 --- a/spec/paystack_sdk_spec.rb +++ b/spec/paystack_sdk_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe PaystackSdk do - it 'has a version number' do + it "has a version number" do expect(PaystackSdk::VERSION).not_to be nil end end diff --git a/spec/resources/banks_spec.rb b/spec/resources/banks_spec.rb index cc09087..73a9ba7 100644 --- a/spec/resources/banks_spec.rb +++ b/spec/resources/banks_spec.rb @@ -1,31 +1,31 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Resources::Banks do - let(:connection) { instance_double('PaystackSdk::Connection') } + let(:connection) { instance_double("PaystackSdk::Connection") } let(:banks) { described_class.new(connection) } - describe '#list' do - it 'lists banks and wraps response' do - response_double = double('Response', success?: true) + describe "#list" do + it "lists banks and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:get) - .with('/bank', {}) + .with("/bank", {}) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) banks.list end - it 'passes query params to the API' do - response_double = double('Response', success?: true) + it "passes query params to the API" do + response_double = double("Response", success?: true) expect(connection).to receive(:get) - .with('/bank', { currency: 'NGN' }) + .with("/bank", {currency: "NGN"}) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - banks.list(currency: 'NGN') + banks.list(currency: "NGN") end - it 'raises error for invalid currency' do + it "raises error for invalid currency" do expect do - banks.list(currency: 'INVALID') + banks.list(currency: "INVALID") end.to raise_error(PaystackSdk::InvalidValueError, /currency/) end end diff --git a/spec/resources/charges_spec.rb b/spec/resources/charges_spec.rb index 5521770..3a03a28 100644 --- a/spec/resources/charges_spec.rb +++ b/spec/resources/charges_spec.rb @@ -1,56 +1,56 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Resources::Charges do - let(:connection) { instance_double('PaystackSdk::Connection') } + let(:connection) { instance_double("PaystackSdk::Connection") } let(:charges) { described_class.new(connection) } - describe '#mobile_money' do + describe "#mobile_money" do let(:payload) do { - email: 'customer@email.com', + email: "customer@email.com", amount: 10_000, - currency: 'GHS', + currency: "GHS", mobile_money: { - phone: '0551234987', - provider: 'mtn' + phone: "0551234987", + provider: "mtn" } } end - it 'creates a mobile money charge' do - response_double = double('Response', success?: true, status: 'pay_offline') + it "creates a mobile money charge" do + response_double = double("Response", success?: true, status: "pay_offline") expect(connection).to receive(:post) - .with('/charge', payload) + .with("/charge", payload) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = charges.mobile_money(payload) expect(response.success?).to be true - expect(response.status).to eq('pay_offline') + expect(response.status).to eq("pay_offline") end - it 'accepts provider codes in different cases' do - payload[:mobile_money][:provider] = 'MTN' - response_double = double('Response', success?: true) + it "accepts provider codes in different cases" do + payload[:mobile_money][:provider] = "MTN" + response_double = double("Response", success?: true) expect(connection).to receive(:post) - .with('/charge', payload) + .with("/charge", payload) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) expect { charges.mobile_money(payload) }.not_to raise_error end - it 'raises an error when mobile_money provider is invalid' do - payload[:mobile_money][:provider] = 'invalid' + it "raises an error when mobile_money provider is invalid" do + payload[:mobile_money][:provider] = "invalid" expect { charges.mobile_money(payload) } .to raise_error(PaystackSdk::InvalidValueError, /mobile_money provider/) end - it 'raises an error when required mobile_money details are missing' do + it "raises an error when required mobile_money details are missing" do payload[:mobile_money].delete(:phone) expect { charges.mobile_money(payload) } @@ -58,22 +58,22 @@ end end - describe '#submit_otp' do - let(:otp_payload) { { otp: '123456', reference: 'r13havfcdt7btcm' } } + describe "#submit_otp" do + let(:otp_payload) { {otp: "123456", reference: "r13havfcdt7btcm"} } - it 'submits the otp for a charge' do - response_double = double('Response', success?: true, status: 'success') + it "submits the otp for a charge" do + response_double = double("Response", success?: true, status: "success") expect(connection).to receive(:post) - .with('/charge/submit_otp', otp_payload) + .with("/charge/submit_otp", otp_payload) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = charges.submit_otp(otp_payload) - expect(response.status).to eq('success') + expect(response.status).to eq("success") end - it 'raises an error when otp is missing' do + it "raises an error when otp is missing" do otp_payload.delete(:otp) expect { charges.submit_otp(otp_payload) } diff --git a/spec/resources/customers_spec.rb b/spec/resources/customers_spec.rb index aee2272..8764222 100644 --- a/spec/resources/customers_spec.rb +++ b/spec/resources/customers_spec.rb @@ -1,101 +1,101 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Resources::Customers do - let(:connection) { instance_double('PaystackSdk::Connection') } + let(:connection) { instance_double("PaystackSdk::Connection") } let(:customers) { described_class.new(connection) } - describe '#create' do + describe "#create" do let(:params) do { - email: 'customer@email.com', - first_name: 'Zero', - last_name: 'Sum', - phone: '+2348123456789' + email: "customer@email.com", + first_name: "Zero", + last_name: "Sum", + phone: "+2348123456789" } end - context 'with successful response' do - it 'creates a new customer' do - response_double = double('Response', success?: true, - data: double('Data', customer_code: 'CUS_xr58yrr2ujlft9k')) + context "with successful response" do + it "creates a new customer" do + response_double = double("Response", success?: true, + data: double("Data", customer_code: "CUS_xr58yrr2ujlft9k")) expect(connection).to receive(:post) - .with('customer', params) + .with("customer", params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = customers.create(params) expect(response).to be_success - expect(response.data.customer_code).to eq('CUS_xr58yrr2ujlft9k') + expect(response.data.customer_code).to eq("CUS_xr58yrr2ujlft9k") end end - context 'with failed response' do - it 'returns an unsuccessful response' do - response_double = double('Response', success?: false, failed?: true, - error_message: 'Customer creation failed') + context "with failed response" do + it "returns an unsuccessful response" do + response_double = double("Response", success?: false, failed?: true, + error_message: "Customer creation failed") expect(connection).to receive(:post) - .with('customer', params) + .with("customer", params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = customers.create(params) expect(response.success?).to be false expect(response.failed?).to be true - expect(response.error_message).to eq('Customer creation failed') + expect(response.error_message).to eq("Customer creation failed") end end - context 'with validation errors' do - it 'raises MissingParamError when email is missing' do + context "with validation errors" do + it "raises MissingParamError when email is missing" do invalid_params = params.except(:email) expect { customers.create(invalid_params) }.to raise_error( PaystackSdk::MissingParamError, - 'Missing required parameter: email' + "Missing required parameter: email" ) end - it 'raises InvalidFormatError when email format is invalid' do - invalid_params = params.merge(email: 'invalid-email') + it "raises InvalidFormatError when email format is invalid" do + invalid_params = params.merge(email: "invalid-email") expect { customers.create(invalid_params) }.to raise_error( PaystackSdk::InvalidFormatError, - 'Invalid format for Email. Expected format: valid email address' + "Invalid format for Email. Expected format: valid email address" ) end end end - describe '#list' do - context 'with successful response' do - it 'returns a list of customers' do - response_double = double('Response', success?: true, - data: double('Data', first: double('Customer', customer_code: 'CUS_xr58yrr2ujlft9k'))) + describe "#list" do + context "with successful response" do + it "returns a list of customers" do + response_double = double("Response", success?: true, + data: double("Data", first: double("Customer", customer_code: "CUS_xr58yrr2ujlft9k"))) expect(connection).to receive(:get) - .with('customer', hash_including(perPage: 50, page: 1)) + .with("customer", hash_including(perPage: 50, page: 1)) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = customers.list expect(response.success?).to be true - expect(response.data.first.customer_code).to eq('CUS_xr58yrr2ujlft9k') + expect(response.data.first.customer_code).to eq("CUS_xr58yrr2ujlft9k") end end end - describe '#fetch' do - let(:email_or_code) { 'CUS_xr58yrr2ujlft9k' } + describe "#fetch" do + let(:email_or_code) { "CUS_xr58yrr2ujlft9k" } - context 'with successful response' do - it 'fetches customer details' do - response_double = double('Response', success?: true, - data: double('Data', customer_code: email_or_code)) + context "with successful response" do + it "fetches customer details" do + response_double = double("Response", success?: true, + data: double("Data", customer_code: email_or_code)) expect(connection).to receive(:get) .with("customer/#{email_or_code}") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = customers.fetch(email_or_code) expect(response.success?).to be true @@ -103,97 +103,97 @@ end end - context 'with not found error' do - it 'returns an unsuccessful response' do - response_double = double('Response', success?: false, failed?: true, - error_message: 'Customer code/email specified is invalid') + context "with not found error" do + it "returns an unsuccessful response" do + response_double = double("Response", success?: false, failed?: true, + error_message: "Customer code/email specified is invalid") expect(connection).to receive(:get) .with("customer/#{email_or_code}") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = customers.fetch(email_or_code) expect(response.success?).to be false expect(response.failed?).to be true - expect(response.error_message).to eq('Customer code/email specified is invalid') + expect(response.error_message).to eq("Customer code/email specified is invalid") end end - context 'with validation errors' do - it 'raises MissingParamError when email_or_code is nil' do + context "with validation errors" do + it "raises MissingParamError when email_or_code is nil" do expect { customers.fetch(nil) }.to raise_error( PaystackSdk::MissingParamError, - 'Missing required parameter: email_or_code' + "Missing required parameter: email_or_code" ) end end end - describe '#update' do - let(:code) { 'CUS_xr58yrr2ujlft9k' } + describe "#update" do + let(:code) { "CUS_xr58yrr2ujlft9k" } let(:params) do { - first_name: 'John', - last_name: 'Doe', - phone: '+2348123456789' + first_name: "John", + last_name: "Doe", + phone: "+2348123456789" } end - context 'with successful response' do + context "with successful response" do before do allow(connection).to receive(:put) .with("customer/#{code}", params) .and_return(Faraday::Response.new(status: 200, body: { - 'status' => true, - 'message' => 'Customer updated', - 'data' => params.merge('customer_code' => code) - })) + "status" => true, + "message" => "Customer updated", + "data" => params.merge("customer_code" => code) + })) end - it 'updates customer details' do + it "updates customer details" do response = customers.update(code, params) expect(response.success?).to be true - expect(response.data.first_name).to eq('John') + expect(response.data.first_name).to eq("John") end end - context 'with validation errors' do - it 'raises InvalidFormatError when payload is not a hash' do - expect { customers.update(code, 'invalid') }.to raise_error( + context "with validation errors" do + it "raises InvalidFormatError when payload is not a hash" do + expect { customers.update(code, "invalid") }.to raise_error( PaystackSdk::InvalidFormatError, - 'Invalid format for payload. Expected format: Hash' + "Invalid format for payload. Expected format: Hash" ) end - it 'raises MissingParamError when code is missing' do + it "raises MissingParamError when code is missing" do expect { customers.update(nil, params) }.to raise_error( PaystackSdk::MissingParamError, - 'Missing required parameter: code' + "Missing required parameter: code" ) end end end - describe '#validate' do - let(:code) { 'CUS_xr58yrr2ujlft9k' } + describe "#validate" do + let(:code) { "CUS_xr58yrr2ujlft9k" } let(:params) do { - country: 'NG', - type: 'bank_account', - account_number: '0123456789', - bvn: '20012345677', - bank_code: '007', - first_name: 'John', - last_name: 'Doe' + country: "NG", + type: "bank_account", + account_number: "0123456789", + bvn: "20012345677", + bank_code: "007", + first_name: "John", + last_name: "Doe" } end - context 'with successful response' do - it 'validates a customer successfully' do + context "with successful response" do + it "validates a customer successfully" do response_body = { - 'status' => true, - 'message' => 'Customer Identification in progress' + "status" => true, + "message" => "Customer Identification in progress" } faraday_response = Faraday::Response.new(status: 202, body: response_body) @@ -205,100 +205,100 @@ response = customers.validate(code, params) expect(response).to be_success - expect(response.message).to eq('Customer Identification in progress') + expect(response.message).to eq("Customer Identification in progress") end end - context 'with validation errors' do - it 'raises an error when required parameters are missing' do + context "with validation errors" do + it "raises an error when required parameters are missing" do invalid_params = params.reject { |k| k == :type } expect { customers.validate(code, invalid_params) } .to raise_error(PaystackSdk::Error, /type/i) end - it 'raises an error when code is nil' do + it "raises an error when code is nil" do expect { customers.validate(nil, params) } - .to raise_error(PaystackSdk::MissingParamError, 'Missing required parameter: code') + .to raise_error(PaystackSdk::MissingParamError, "Missing required parameter: code") end end end - describe '#set_risk_action' do + describe "#set_risk_action" do let(:params) do { - customer: 'CUS_xr58yrr2ujlft9k', - risk_action: 'allow' + customer: "CUS_xr58yrr2ujlft9k", + risk_action: "allow" } end - context 'with successful response' do - it 'sets risk action successfully' do + context "with successful response" do + it "sets risk action successfully" do response_body = { - 'status' => true, - 'message' => 'Customer updated', - 'data' => { - 'customer_code' => 'CUS_xr58yrr2ujlft9k', - 'risk_action' => 'allow' + "status" => true, + "message" => "Customer updated", + "data" => { + "customer_code" => "CUS_xr58yrr2ujlft9k", + "risk_action" => "allow" } } faraday_response = Faraday::Response.new(status: 200, body: response_body) expect(connection).to receive(:post) - .with('customer/set_risk_action', params) + .with("customer/set_risk_action", params) .and_return(faraday_response) response = customers.set_risk_action(params) expect(response).to be_success - expect(response.risk_action).to eq('allow') + expect(response.risk_action).to eq("allow") end end - context 'with validation errors' do - it 'raises an error when customer is missing' do + context "with validation errors" do + it "raises an error when customer is missing" do invalid_params = params.reject { |k| k == :customer } expect { customers.set_risk_action(invalid_params) } .to raise_error(PaystackSdk::Error, /customer/i) end - it 'raises an error when risk_action is invalid' do - invalid_params = params.merge(risk_action: 'invalid') + it "raises an error when risk_action is invalid" do + invalid_params = params.merge(risk_action: "invalid") expect { customers.set_risk_action(invalid_params) } .to raise_error(PaystackSdk::InvalidValueError, /risk_action/i) end end end - describe '#deactivate_authorization' do + describe "#deactivate_authorization" do let(:params) do { - authorization_code: 'AUTH_72btv547' + authorization_code: "AUTH_72btv547" } end - context 'with successful response' do - it 'deactivates authorization successfully' do + context "with successful response" do + it "deactivates authorization successfully" do response_body = { - 'status' => true, - 'message' => 'Authorization has been deactivated' + "status" => true, + "message" => "Authorization has been deactivated" } faraday_response = Faraday::Response.new(status: 200, body: response_body) expect(connection).to receive(:post) - .with('customer/deactivate_authorization', params) + .with("customer/deactivate_authorization", params) .and_return(faraday_response) response = customers.deactivate_authorization(params) expect(response).to be_success - expect(response.message).to eq('Authorization has been deactivated') + expect(response.message).to eq("Authorization has been deactivated") end end - context 'with validation errors' do - it 'raises an error when authorization_code is missing' do + context "with validation errors" do + it "raises an error when authorization_code is missing" do expect { customers.deactivate_authorization({}) } .to raise_error(PaystackSdk::Error, /authorization_code/i) end diff --git a/spec/resources/transactions_spec.rb b/spec/resources/transactions_spec.rb index 4c0d138..3d8796e 100644 --- a/spec/resources/transactions_spec.rb +++ b/spec/resources/transactions_spec.rb @@ -1,99 +1,99 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Resources::Transactions do - let(:connection) { instance_double('PaystackSdk::Connection') } + let(:connection) { instance_double("PaystackSdk::Connection") } let(:transactions) { described_class.new(connection) } - describe '#initiate' do + describe "#initiate" do let(:params) do { - email: 'customer@email.com', + email: "customer@email.com", amount: 10_000, - currency: 'GHS' + currency: "GHS" } end - context 'with successful response' do - it 'initializes a transaction' do - response_double = double('Response', success?: true, - data: double('Data', authorization_url: 'https://checkout.paystack.com/abc123'), - authorization_url: 'https://checkout.paystack.com/abc123') + context "with successful response" do + it "initializes a transaction" do + response_double = double("Response", success?: true, + data: double("Data", authorization_url: "https://checkout.paystack.com/abc123"), + authorization_url: "https://checkout.paystack.com/abc123") expect(connection).to receive(:post) - .with('/transaction/initialize', params) + .with("/transaction/initialize", params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = transactions.initiate(params) expect(response.success?).to be true - expect(response.data.authorization_url).to eq('https://checkout.paystack.com/abc123') - expect(response.authorization_url).to eq('https://checkout.paystack.com/abc123') + expect(response.data.authorization_url).to eq("https://checkout.paystack.com/abc123") + expect(response.authorization_url).to eq("https://checkout.paystack.com/abc123") end end - context 'with failed response' do - it 'returns an unsuccessful response' do - response_double = double('Response', success?: false, failed?: true, - error_message: 'Transaction initialization failed') + context "with failed response" do + it "returns an unsuccessful response" do + response_double = double("Response", success?: false, failed?: true, + error_message: "Transaction initialization failed") expect(connection).to receive(:post) - .with('/transaction/initialize', params) + .with("/transaction/initialize", params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = transactions.initiate(params) expect(response.success?).to be false expect(response.failed?).to be true - expect(response.error_message).to eq('Transaction initialization failed') + expect(response.error_message).to eq("Transaction initialization failed") end end - context 'with validation errors' do - it 'raises MissingParamError when email is missing' do + context "with validation errors" do + it "raises MissingParamError when email is missing" do invalid_params = params.except(:email) expect { transactions.initiate(invalid_params) }.to raise_error( PaystackSdk::MissingParamError, - 'Missing required parameter: email' + "Missing required parameter: email" ) end - it 'raises MissingParamError when amount is missing' do + it "raises MissingParamError when amount is missing" do invalid_params = params.except(:amount) expect { transactions.initiate(invalid_params) }.to raise_error( PaystackSdk::MissingParamError, - 'Missing required parameter: amount' + "Missing required parameter: amount" ) end - it 'raises InvalidFormatError when email format is invalid' do - invalid_params = params.merge(email: 'invalid-email') + it "raises InvalidFormatError when email format is invalid" do + invalid_params = params.merge(email: "invalid-email") expect { transactions.initiate(invalid_params) }.to raise_error( PaystackSdk::InvalidFormatError, - 'Invalid format for Email. Expected format: valid email address' + "Invalid format for Email. Expected format: valid email address" ) end - it 'raises InvalidFormatError when currency format is invalid' do - invalid_params = params.merge(currency: 'INVALID') + it "raises InvalidFormatError when currency format is invalid" do + invalid_params = params.merge(currency: "INVALID") expect { transactions.initiate(invalid_params) }.to raise_error( PaystackSdk::InvalidFormatError, - 'Invalid format for currency. Expected format: 3-letter ISO code (e.g., NGN, USD, GHS)' + "Invalid format for currency. Expected format: 3-letter ISO code (e.g., NGN, USD, GHS)" ) end end end - describe '#verify' do - let(:reference) { 'ref_123' } + describe "#verify" do + let(:reference) { "ref_123" } - context 'with successful response' do - it 'verifies a transaction' do - response_double = double('Response', success?: true, status: true) + context "with successful response" do + it "verifies a transaction" do + response_double = double("Response", success?: true, status: true) expect(connection).to receive(:get) .with("/transaction/verify/#{reference}") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = transactions.verify(reference: reference) expect(response.success?).to be true @@ -101,132 +101,132 @@ end end - context 'with not found error' do - it 'returns an unsuccessful response' do - response_double = double('Response', success?: false, failed?: true, - error_message: 'Transaction reference not found.') + context "with not found error" do + it "returns an unsuccessful response" do + response_double = double("Response", success?: false, failed?: true, + error_message: "Transaction reference not found.") expect(connection).to receive(:get) .with("/transaction/verify/#{reference}") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = transactions.verify(reference: reference) expect(response.success?).to be false expect(response.failed?).to be true - expect(response.error_message).to eq('Transaction reference not found.') + expect(response.error_message).to eq("Transaction reference not found.") end end - context 'with validation errors' do - it 'raises MissingParamError when reference is missing' do + context "with validation errors" do + it "raises MissingParamError when reference is missing" do expect { transactions.verify(reference: nil) }.to raise_error( PaystackSdk::MissingParamError, - 'Missing required parameter: Reference' + "Missing required parameter: Reference" ) end end end - describe '#charge_authorization' do + describe "#charge_authorization" do let(:payload) do { - authorization_code: 'AUTH_72btv547', - email: 'customer@email.com', + authorization_code: "AUTH_72btv547", + email: "customer@email.com", amount: 10_000 } end - context 'with successful response' do - it 'charges the authorization' do - response_double = double('Response', success?: true, status: 'success', - api_message: 'Charge attempted', amount: 35_247, reference: '0m7frfnr47ezyxl', - authorization: double('Authorization', authorization_code: 'AUTH_uh8bcl3zbn'), - customer: double('Customer', email: 'customer@email.com')) + context "with successful response" do + it "charges the authorization" do + response_double = double("Response", success?: true, status: "success", + api_message: "Charge attempted", amount: 35_247, reference: "0m7frfnr47ezyxl", + authorization: double("Authorization", authorization_code: "AUTH_uh8bcl3zbn"), + customer: double("Customer", email: "customer@email.com")) expect(connection).to receive(:post) - .with('/transaction/charge_authorization', payload) + .with("/transaction/charge_authorization", payload) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = transactions.charge_authorization(payload) expect(response.success?).to be true - expect(response.status).to be 'success' - expect(response.api_message).to eq('Charge attempted') + expect(response.status).to be "success" + expect(response.api_message).to eq("Charge attempted") expect(response.amount).to eq(35_247) - expect(response.reference).to eq('0m7frfnr47ezyxl') - expect(response.authorization.authorization_code).to eq('AUTH_uh8bcl3zbn') - expect(response.customer.email).to eq('customer@email.com') + expect(response.reference).to eq("0m7frfnr47ezyxl") + expect(response.authorization.authorization_code).to eq("AUTH_uh8bcl3zbn") + expect(response.customer.email).to eq("customer@email.com") end end - context 'with validation errors' do - it 'raises MissingParamError when authorization_code is missing' do + context "with validation errors" do + it "raises MissingParamError when authorization_code is missing" do invalid_payload = payload.except(:authorization_code) expect { transactions.charge_authorization(invalid_payload) }.to raise_error( PaystackSdk::MissingParamError, - 'Missing required parameter: authorization_code' + "Missing required parameter: authorization_code" ) end - it 'raises MissingParamError when email is missing' do + it "raises MissingParamError when email is missing" do invalid_payload = payload.except(:email) expect { transactions.charge_authorization(invalid_payload) }.to raise_error( PaystackSdk::MissingParamError, - 'Missing required parameter: email' + "Missing required parameter: email" ) end - it 'raises MissingParamError when amount is missing' do + it "raises MissingParamError when amount is missing" do invalid_payload = payload.except(:amount) expect { transactions.charge_authorization(invalid_payload) }.to raise_error( PaystackSdk::MissingParamError, - 'Missing required parameter: amount' + "Missing required parameter: amount" ) end end end - describe '#partial_debit' do + describe "#partial_debit" do let(:payload) do { - authorization_code: 'AUTH_72btv547', - currency: 'NGN', + authorization_code: "AUTH_72btv547", + currency: "NGN", amount: 10_000, - email: 'customer@email.com' + email: "customer@email.com" } end - context 'with successful response' do - it 'performs a partial debit' do - response_double = double('Response', success?: true, status: 'success') + context "with successful response" do + it "performs a partial debit" do + response_double = double("Response", success?: true, status: "success") expect(connection).to receive(:post) - .with('/transaction/partial_debit', payload) + .with("/transaction/partial_debit", payload) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - .and_return(response_double) + .and_return(response_double) response = transactions.partial_debit(payload) expect(response.success?).to be true - expect(response.status).to eq('success') + expect(response.status).to eq("success") end end - context 'with validation errors' do - it 'raises MissingParamError when authorization_code is missing' do + context "with validation errors" do + it "raises MissingParamError when authorization_code is missing" do invalid_payload = payload.except(:authorization_code) expect { transactions.partial_debit(invalid_payload) }.to raise_error( PaystackSdk::MissingParamError, - 'Missing required parameter: authorization_code' + "Missing required parameter: authorization_code" ) end - it 'raises InvalidFormatError when currency format is invalid' do - invalid_payload = payload.merge(currency: 'INVALID') + it "raises InvalidFormatError when currency format is invalid" do + invalid_payload = payload.merge(currency: "INVALID") expect { transactions.partial_debit(invalid_payload) }.to raise_error( PaystackSdk::InvalidFormatError, - 'Invalid format for currency. Expected format: 3-letter ISO code (e.g., NGN, USD, GHS)' + "Invalid format for currency. Expected format: 3-letter ISO code (e.g., NGN, USD, GHS)" ) end end diff --git a/spec/resources/transfer_recipients_spec.rb b/spec/resources/transfer_recipients_spec.rb index 1b2a203..cad0396 100644 --- a/spec/resources/transfer_recipients_spec.rb +++ b/spec/resources/transfer_recipients_spec.rb @@ -1,74 +1,74 @@ RSpec.describe PaystackSdk::Resources::TransferRecipients do - let(:connection) { instance_double('PaystackSdk::Connection') } + let(:connection) { instance_double("PaystackSdk::Connection") } let(:recipients) { described_class.new(connection) } let(:params) do { - type: 'nuban', - name: 'Jane Doe', - account_number: '0001234567', - bank_code: '058', - currency: 'NGN' + type: "nuban", + name: "Jane Doe", + account_number: "0001234567", + bank_code: "058", + currency: "NGN" } end - describe '#create' do - it 'creates a transfer recipient and wraps response' do - response_double = double('Response', success?: true) + describe "#create" do + it "creates a transfer recipient and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:post) - .with('/transferrecipient', params) + .with("/transferrecipient", params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) recipients.create(params) end - it 'raises error for missing required params' do + it "raises error for missing required params" do expect do recipients.create({}) end.to raise_error(PaystackSdk::MissingParamError) end end - describe '#list' do - it 'lists transfer recipients and wraps response' do - response_double = double('Response', success?: true) + describe "#list" do + it "lists transfer recipients and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:get) - .with('/transferrecipient', {}) + .with("/transferrecipient", {}) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) recipients.list end end - describe '#fetch' do - it 'fetches a transfer recipient and wraps response' do - response_double = double('Response', success?: true) + describe "#fetch" do + it "fetches a transfer recipient and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:get) - .with('/transferrecipient/RCP_123') + .with("/transferrecipient/RCP_123") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - recipients.fetch(recipient_code: 'RCP_123') + recipients.fetch(recipient_code: "RCP_123") end end - describe '#update' do - it 'updates a transfer recipient and wraps response' do - response_double = double('Response', success?: true) + describe "#update" do + it "updates a transfer recipient and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:put) - .with('/transferrecipient/RCP_123', { name: 'New Name' }) + .with("/transferrecipient/RCP_123", {name: "New Name"}) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - recipients.update(recipient_code: 'RCP_123', params: { name: 'New Name' }) + recipients.update(recipient_code: "RCP_123", params: {name: "New Name"}) end end - describe '#delete' do - it 'deletes a transfer recipient and wraps response' do - response_double = double('Response', success?: true) + describe "#delete" do + it "deletes a transfer recipient and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:delete) - .with('/transferrecipient/RCP_123') + .with("/transferrecipient/RCP_123") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - recipients.delete(recipient_code: 'RCP_123') + recipients.delete(recipient_code: "RCP_123") end end end diff --git a/spec/resources/transfers_spec.rb b/spec/resources/transfers_spec.rb index 6e77cf2..1bbcfe3 100644 --- a/spec/resources/transfers_spec.rb +++ b/spec/resources/transfers_spec.rb @@ -1,75 +1,75 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Resources::Transfers do - let(:connection) { instance_double('PaystackSdk::Connection') } + let(:connection) { instance_double("PaystackSdk::Connection") } let(:transfers) { described_class.new(connection) } let(:params) do { - source: 'balance', + source: "balance", amount: 50_000, - recipient: 'RCP_1234567890', - reason: 'Test transfer' + recipient: "RCP_1234567890", + reason: "Test transfer" } end - describe '#create' do - it 'creates a transfer and wraps response' do - response_double = double('Response', success?: true) + describe "#create" do + it "creates a transfer and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:post) - .with('/transfer', params) + .with("/transfer", params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) transfers.create(params) end - it 'raises error for missing required params' do + it "raises error for missing required params" do expect do transfers.create({}) end.to raise_error(PaystackSdk::MissingParamError) end end - describe '#list' do - it 'lists transfers and wraps response' do - response_double = double('Response', success?: true) + describe "#list" do + it "lists transfers and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:get) - .with('/transfer', {}) + .with("/transfer", {}) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) transfers.list end end - describe '#fetch' do - it 'fetches a transfer and wraps response' do - response_double = double('Response', success?: true) + describe "#fetch" do + it "fetches a transfer and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:get) - .with('/transfer/12345') + .with("/transfer/12345") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - transfers.fetch(id: '12345') + transfers.fetch(id: "12345") end end - describe '#finalize' do - it 'finalizes a transfer and wraps response' do - response_double = double('Response', success?: true) + describe "#finalize" do + it "finalizes a transfer and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:post) - .with('/transfer/finalize_transfer', { transfer_code: 'TRF_abc', otp: '123456' }) + .with("/transfer/finalize_transfer", {transfer_code: "TRF_abc", otp: "123456"}) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - transfers.finalize(transfer_code: 'TRF_abc', otp: '123456') + transfers.finalize(transfer_code: "TRF_abc", otp: "123456") end end - describe '#verify' do - it 'verifies a transfer and wraps response' do - response_double = double('Response', success?: true) + describe "#verify" do + it "verifies a transfer and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:get) - .with('/transfer/verify/abc123') + .with("/transfer/verify/abc123") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - transfers.verify(reference: 'abc123') + transfers.verify(reference: "abc123") end end end diff --git a/spec/resources/verification_spec.rb b/spec/resources/verification_spec.rb index 119f8e4..2e728c6 100644 --- a/spec/resources/verification_spec.rb +++ b/spec/resources/verification_spec.rb @@ -1,71 +1,71 @@ # frozen_string_literal: true RSpec.describe PaystackSdk::Resources::Verification do - let(:connection) { instance_double('PaystackSdk::Connection') } + let(:connection) { instance_double("PaystackSdk::Connection") } let(:verification) { described_class.new(connection) } - describe '#resolve_account' do - it 'resolves a bank account and wraps response' do - response_double = double('Response', success?: true) + describe "#resolve_account" do + it "resolves a bank account and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:get) - .with('/bank/resolve', { account_number: '0001234567', bank_code: '058' }) + .with("/bank/resolve", {account_number: "0001234567", bank_code: "058"}) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - verification.resolve_account(account_number: '0001234567', bank_code: '058') + verification.resolve_account(account_number: "0001234567", bank_code: "058") end - it 'raises error for missing account_number' do + it "raises error for missing account_number" do expect do - verification.resolve_account(account_number: nil, bank_code: '058') + verification.resolve_account(account_number: nil, bank_code: "058") end.to raise_error(PaystackSdk::MissingParamError, /account_number/) end - it 'raises error for missing bank_code' do + it "raises error for missing bank_code" do expect do - verification.resolve_account(account_number: '0001234567', bank_code: nil) + verification.resolve_account(account_number: "0001234567", bank_code: nil) end.to raise_error(PaystackSdk::MissingParamError, /bank_code/) end end - describe '#resolve_card_bin' do - it 'resolves a card bin and wraps response' do - response_double = double('Response', success?: true) + describe "#resolve_card_bin" do + it "resolves a card bin and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:get) - .with('/decision/bin/539983') + .with("/decision/bin/539983") .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) - verification.resolve_card_bin('539983') + verification.resolve_card_bin("539983") end - it 'raises error for missing bin' do + it "raises error for missing bin" do expect do verification.resolve_card_bin(nil) end.to raise_error(PaystackSdk::MissingParamError, /bin/) end end - describe '#validate_account' do + describe "#validate_account" do let(:required_params) do { - account_number: '0001234567', - account_name: 'Jane Doe', - account_type: 'personal', - bank_code: '058', - country_code: 'NG', - document_type: 'identityNumber' + account_number: "0001234567", + account_name: "Jane Doe", + account_type: "personal", + bank_code: "058", + country_code: "NG", + document_type: "identityNumber" } end - it 'validates an account and wraps response' do - response_double = double('Response', success?: true) + it "validates an account and wraps response" do + response_double = double("Response", success?: true) expect(connection).to receive(:post) - .with('/bank/validate', required_params) + .with("/bank/validate", required_params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) verification.validate_account(required_params) end - it 'raises error for missing required fields' do + it "raises error for missing required fields" do %i[account_number account_name account_type bank_code country_code document_type].each do |field| params = required_params.dup params.delete(field) @@ -75,13 +75,13 @@ end end - it 'validates an account with all params and wraps response' do + it "validates an account with all params and wraps response" do all_params = required_params.merge( - document_number: '1234567890123' + document_number: "1234567890123" ) - response_double = double('Response', success?: true) + response_double = double("Response", success?: true) expect(connection).to receive(:post) - .with('/bank/validate', all_params) + .with("/bank/validate", all_params) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) verification.validate_account(all_params) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5c8fbf2..a2e3fd8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require 'debug' -require_relative '../lib/paystack_sdk' +require "debug" +require_relative "../lib/paystack_sdk" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = '.rspec_status' + config.example_status_persistence_file_path = ".rspec_status" # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! From 8055695b004741d125c35d8b7563cef5f78a17f7 Mon Sep 17 00:00:00 2001 From: Shepherd Yaw Morttey Date: Mon, 22 Sep 2025 21:37:21 +0000 Subject: [PATCH 3/3] normalize mobile money provider codes and improve payload handling --- lib/paystack_sdk/resources/charges.rb | 14 ++++++++++++-- lib/paystack_sdk/validations.rb | 6 +++--- spec/resources/charges_spec.rb | 7 +++++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/paystack_sdk/resources/charges.rb b/lib/paystack_sdk/resources/charges.rb index b1dea8e..1920533 100644 --- a/lib/paystack_sdk/resources/charges.rb +++ b/lib/paystack_sdk/resources/charges.rb @@ -31,7 +31,8 @@ class Charges < PaystackSdk::Resources::Base def mobile_money(payload) validate_mobile_money_payload!(payload) - response = @connection.post("/charge", payload) + normalized_payload = normalize_mobile_money_provider(payload) + response = @connection.post("/charge", normalized_payload) handle_response(response) end @@ -83,13 +84,22 @@ def validate_mobile_money_payload!(payload) end def validate_mobile_money_provider!(provider) + normalized = provider&.to_s&.downcase validate_allowed_values!( - value: provider&.downcase, + value: normalized, allowed_values: MOBILE_MONEY_PROVIDERS, name: "mobile_money provider", allow_nil: false ) end + + def normalize_mobile_money_provider(payload) + mm = payload[:mobile_money] || payload["mobile_money"] || {} + provider = mm[:provider] || mm["provider"] + normalized_provider = provider&.to_s&.downcase + # Avoid mutating the caller's payload + payload.merge(mobile_money: mm.merge(provider: normalized_provider)) + end end end end diff --git a/lib/paystack_sdk/validations.rb b/lib/paystack_sdk/validations.rb index d1f63e7..d6a3bbc 100644 --- a/lib/paystack_sdk/validations.rb +++ b/lib/paystack_sdk/validations.rb @@ -65,9 +65,9 @@ def validate_required_params!(payload:, required_params:, operation_name: "Opera # @param name [String] Name of the parameter for error messages # @raise [PaystackSdk::MissingParamError] If value is nil or empty def validate_presence!(value:, name: "Parameter") - return unless value.nil? || (value.respond_to?(:empty?) && value.empty?) - - raise PaystackSdk::MissingParamError.new(name) + if value.nil? || (value.respond_to?(:empty?) && value.empty?) + raise PaystackSdk::MissingParamError.new(name) + end end # Validates that a number is a positive integer. diff --git a/spec/resources/charges_spec.rb b/spec/resources/charges_spec.rb index 3a03a28..23b6959 100644 --- a/spec/resources/charges_spec.rb +++ b/spec/resources/charges_spec.rb @@ -31,11 +31,14 @@ expect(response.status).to eq("pay_offline") end - it "accepts provider codes in different cases" do + it "accepts provider codes in different cases and normalizes before POST" do payload[:mobile_money][:provider] = "MTN" + normalized_payload = payload.merge( + mobile_money: payload[:mobile_money].merge(provider: "mtn") + ) response_double = double("Response", success?: true) expect(connection).to receive(:post) - .with("/charge", payload) + .with("/charge", normalized_payload) .and_return(response_double) expect(PaystackSdk::Response).to receive(:new).with(response_double) .and_return(response_double)