lib/pesapal/merchant.rb in pesapal-1.5.4 vs lib/pesapal/merchant.rb in pesapal-1.5.5

- old
+ new

@@ -1,184 +1,439 @@ module Pesapal - + # Pesapal Merchant object responsible for posting and handling transaction + # queries. class Merchant - attr_accessor :config, :order_details + # Holds configuration details for the Pesapal object. + # + # 1. `:callback_url` - the page on your site that users will be redirected to, after they have made the payment on PesaPal + # 2. `:consumer_key` - your Pesapal consumer key sent to you via email or obtained from the dashboard + # 3. `:consumer_secret` - your Pesapal consumer secret sent to you via email or obtained from the dashboard + # + # It typically looks like this: + # + # ``` + # { :callback_url => 'http://0.0.0.0:3000/pesapal/callback', + # :consumer_key => '<YOUR_CONSUMER_KEY>', + # :consumer_secret => '<YOUR_CONSUMER_SECRET>' + # } + # ``` + # + # @return [Hash] the Pesapal config def config @config ||= {} end + # Holds the order details for the transaction. + # + # 1. `:amount` - the order amount + # 2. `:description` - a note about the order + # 3. `:type` - MERCHANT + # 4. `:reference` - the unique id generated for the transaction by your application before posting the order + # 5. `:first_name` - first name of the customer + # 6. `:last_name` - second name of the customer + # 7. `:email` - email of the customer + # 8. `:phonenumber` - phone number of the customer + # 9. `:currency` - ISO code for the currency + # + # It typically looks like this: + # + # ``` + # { :amount => 1000, + # :description => 'this is the transaction description', + # :type => 'MERCHANT', + # :reference => '808-707-606', + # :first_name => 'Swaleh', + # :last_name => 'Mdoe', + # :email => 'user@example.com', + # :phonenumber => '+254722222222', + # :currency => 'KES' + # } + # ``` + # + # @note Make sure **ALL** expected hash attributes are present, the method + # assumes they are and no checks are done to certify that this has been + # done nor are any fallbacks built in. Also the `:amount` should be a + # number, no commas, or else Pesapal will convert the comma to a period (.) + # which will result in the incorrect amount for the transaction. + # + # @return [Hash] the order details def order_details @order_details ||= {} end private - def api_domain - @api_domain - end + attr_reader :api_domain, :api_endpoints, :env - def api_endpoints - @api_endpoints - end + def params + @params ||= nil + end - def env - @env - end + def post_xml + @post_xml ||= nil + end - def params - @params ||= nil - end + def token_secret + @token_secret ||= nil + end - def post_xml - @post_xml ||= nil - end - - def token_secret - @token_secret ||= nil - end - public - # constructor - def initialize(env = false) - set_env env - if defined?(Rails) - set_configuration Rails.application.config.pesapal_credentials - else - set_configuration - end + # Creates a new instance of {Pesapal::Merchant}. + # + # Initialize Pesapal object and choose the environment, there are two + # environments; `:development` and `:production`. They determine if the code + # will interact with the testing or the live Pesapal API. Like so ... + # + # ```ruby + # # Sets environment intelligently to 'Rails.env' (if Rails) or :development (if non-Rails) + # pesapal = Pesapal::Merchant.new + # + # # Sets environment to :development + # pesapal = Pesapal::Merchant.new(:development) + # + # # Sets environment to :production + # pesapal = Pesapal::Merchant.new(:production) + # ``` + # + # A few things to note about the constructor as it behaves differently + # depending on the context within which it is called i.e. _Rails_ app vs + # _non-Rails_ app ... + # + # ### Case 1: Rails app + # + # The constructor attempts to set configuration details that should be + # available at runtime from `Rails.application.config.pesapal_credentials`. + # This contains values loaded at application start from a YAML file located + # at `config/pesapal.yml` which typically looks like this: + # + # ```yaml + # development: + # callback_url: 'http://0.0.0.0:3000/pesapal/callback' + # consumer_key: '<YOUR_DEV_CONSUMER_KEY>' + # consumer_secret: '<YOUR_DEV_CONSUMER_SECRET>' + # + # production: + # callback_url: 'http://1.2.3.4:3000/pesapal/callback' + # consumer_key: '<YOUR_PROD_CONSUMER_KEY>' + # consumer_secret: '<YOUR_PROD_CONSUMER_SECRET>' + # ``` + # + # The appropriate credentials are picked and set to {#config} instance + # attribute depending on set environment. The setting of environment is + # explained above. It's worth nothing that if for some reason the YAML file + # could not be read, then it fallbacks to setting {#config} instance + # attribute with default values. The exact definition of default values is + # shown below. + # + # ### Case 2: Non-Rails app + # + # Since (and if) no predefined configuration files are available, the + # constructor sets the {#config} instance attribute up with default values + # as shown below: + # + # ``` + # { :callback_url => 'http://0.0.0.0:3000/pesapal/callback', + # :consumer_key => '<YOUR_CONSUMER_KEY>', + # :consumer_secret => '<YOUR_CONSUMER_SECRET>' + # } + # ``` + # + # @note You can change the environment at runtime using {#set_env} + # + # @param env [Symbol] the environment we want to use i.e. `:development` or + # `:production`. Leaving it blank sets environment intelligently to + # `Rails.env` (if Rails) or `:development` (if non-Rails). + def initialize(env = false) + set_env env + if defined?(Rails) + set_configuration Rails.application.config.pesapal_credentials + else + set_configuration end + end - # generate pesapal order url (often iframed) - def generate_order_url + # Generate URL that's used to post a transaction to PesaPal. + # + # PesaPal will present the user with a page which contains the available + # payment options and will redirect to your site to the _callback url_ once + # the user has completed the payment process. A tracking id will be returned + # as a query parameter – this can be used subsequently to track the payment + # status on Pesapal for the transaction later on. + # + # Generating the URL is a 3-step process: + # + # 1. Initialize {Pesapal::Merchant}, making sure credentials are set. See {#initialize} for details. + # 2. Set the order details. See {#order_details} for details. + # 3. Call {#generate_order_url} on the object. + # + # Example: + # + # ```ruby + # # generate transaction url after step #1 & #2 + # order_url = pesapal.generate_order_url + # + # # order_url now contains a string with the order url. + # # http://demo.pesapal.com/API/PostPesapalDirectOrderV4?oauth_callback=http%3A%2F%2F1.2.3.4%3A3000%2Fpesapal%2Fcallback&oauth_consumer_key=A9MXocJiHK1P4w0M%2F%2FYzxgIVMX557Jt4&oauth_nonce=13804335543pDXs4q3djsy&oauth_signature=BMmLR0AVInfoBI9D4C38YDA9eSM%3D&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1380433554&oauth_version=1.0&pesapal_request_data=%26lt%3B%3Fxml%20version%3D%26quot%3B1.0%26quot%3B%20encoding%3D%26quot%3Butf-8%26quot%3B%3F%26gt%3B%26lt%3BPesapalDirectOrderInfo%20xmlns%3Axsi%3D%26quot%3Bhttp%3A%2F%2Fwww.w3.org%2F2001%2FXMLSchema-instance%26quot%3B%20xmlns%3Axsd%3D%26quot%3Bhttp%3A%2F%2Fwww.w3.org%2F2001%2FXMLSchema%26quot%3B%20Amount%3D%26quot%3B1000%26quot%3B%20Description%3D%26quot%3Bthis%20is%20the%20transaction%20description%26quot%3B%20Type%3D%26quot%3BMERCHANT%26quot%3B%20Reference%3D%26quot%3B808%26quot%3B%20FirstName%3D%26quot%3BSwaleh%26quot%3B%20LastName%3D%26quot%3BMdoe%26quot%3B%20Email%3D%26quot%3Bj%40kingori.co%26quot%3B%20PhoneNumber%3D%26quot%3B%2B254722222222%26quot%3B%20xmlns%3D%26quot%3Bhttp%3A%2F%2Fwww.pesapal.com%26quot%3B%20%2F%26gt%3B + # ``` + # + # @note You **MUST** set up your order details before you call this method on the object. + # + # @return [String] URL of the Pesapal post order form + def generate_order_url + # build xml with input data, the format is standard so no editing is + # required + @post_xml = Pesapal::Helper::Post.generate_post_xml @order_details - # build xml with input data, the format is standard so no editing is - # required - @post_xml = Pesapal::Post::generate_post_xml @order_details + # initialize setting of @params (oauth_signature left empty) + @params = Pesapal::Helper::Post.set_parameters(@config[:callback_url], @config[:consumer_key], @post_xml) - # initialize setting of @params (oauth_signature left empty) - @params = Pesapal::Post::set_parameters(@config[:callback_url], @config[:consumer_key], @post_xml) + # generate oauth signature and add signature to the request parameters + @params[:oauth_signature] = Pesapal::Oauth::generate_oauth_signature("GET", @api_endpoints[:postpesapaldirectorderv4], @params, @config[:consumer_secret], @token_secret) - # generate oauth signature and add signature to the request parameters - @params[:oauth_signature] = Pesapal::Oauth::generate_oauth_signature("GET", @api_endpoints[:postpesapaldirectorderv4], @params, @config[:consumer_secret], @token_secret) + # change params (with signature) to a query string + query_string = Pesapal::Oauth.generate_encoded_params_query_string @params - # change params (with signature) to a query string - query_string = Pesapal::Oauth::generate_encoded_params_query_string @params + "#{@api_endpoints[:postpesapaldirectorderv4]}?#{query_string}" + end - "#{@api_endpoints[:postpesapaldirectorderv4]}?#{query_string}" - end + # Same as {#query_payment_status}, but additional information is returned in + # a Hash. + # + # Call method on initialized {Pesapal::Merchant} object (see {#initialize} + # for details): + # + # ```ruby + # # pass in merchant reference and transaction id + # payment_details = pesapal.query_payment_details("<MERCHANT_REFERENCE>","<TRANSACTION_ID>") + # ``` + # + # Response should contain the following: + # + # 1. `:method` - the payment method used by the user to make the payment + # 2. `:status` - one of `PENDING | COMPLETED | FAILED | INVALID` + # 3. `:merchant_reference` - this is the same as the parameter you sent when making the query + # 4. `:transaction_tracking_id` - this is the same as the parameter you sent when making the query + # + # Example: + # + # ``` + # { + # :method => "<PAYMENT_METHOD>", + # :status => "<PAYMENT_STATUS>", + # :merchant_reference => "<MERCHANT_REFERENCE>", + # :transaction_tracking_id => "<TRANSACTION_ID>" + # } + # ``` + # + # @param merchant_reference [String] the unique id generated for the + # transaction by your application before posting the order + # + # @param transaction_tracking_id [String] the unique id assigned by Pesapal + # to the transaction after it's posted + # + # @return [Hash] transaction payment details + def query_payment_details(merchant_reference, transaction_tracking_id) + # initialize setting of @params (oauth_signature left empty) + @params = Pesapal::Helper::Details.set_parameters(@config[:consumer_key], merchant_reference, transaction_tracking_id) - # query the details of the transaction - def query_payment_details(merchant_reference, transaction_tracking_id) + # generate oauth signature and add signature to the request parameters + @params[:oauth_signature] = Pesapal::Oauth.generate_oauth_signature("GET", @api_endpoints[:querypaymentdetails], @params, @config[:consumer_secret], @token_secret) - # initialize setting of @params (oauth_signature left empty) - @params = Pesapal::Details::set_parameters(@config[:consumer_key], merchant_reference, transaction_tracking_id) + # change params (with signature) to a query string + query_string = Pesapal::Oauth.generate_encoded_params_query_string @params - # generate oauth signature and add signature to the request parameters - @params[:oauth_signature] = Pesapal::Oauth::generate_oauth_signature("GET", @api_endpoints[:querypaymentdetails], @params, @config[:consumer_secret], @token_secret) - - # change params (with signature) to a query string - query_string = Pesapal::Oauth::generate_encoded_params_query_string @params - - # get status response - uri = URI.parse "#{@api_endpoints[:querypaymentstatus]}?#{query_string}" - http = Net::HTTP.new(uri.host, uri.port) - if @env == 'production' - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - end - response = http.request(Net::HTTP::Get.new(uri.request_uri)) - response = CGI::parse response.body - response = response['pesapal_response_data'][0].split(',') - - details = { :method => response[1], - :status => response[2], - :merchant_reference => response[3], - :transaction_tracking_id => response[0] } + # get status response + uri = URI.parse "#{@api_endpoints[:querypaymentdetails]}?#{query_string}" + http = Net::HTTP.new(uri.host, uri.port) + if @env == 'production' + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE end + response = http.request(Net::HTTP::Get.new(uri.request_uri)) + response = CGI.parse response.body + response = response['pesapal_response_data'][0].split(',') - # query the status of the transaction - def query_payment_status(merchant_reference, transaction_tracking_id = nil) + { :method => response[1], + :status => response[2], + :merchant_reference => response[3], + :transaction_tracking_id => response[0] + } + end - # initialize setting of @params (oauth_signature left empty) - @params = Pesapal::Status::set_parameters(@config[:consumer_key], merchant_reference, transaction_tracking_id) + # Query the status of a transaction. + # + # When a transaction is posted to PesaPal, it may be in a `PENDING`, + # `COMPLETED` or `FAILED` state. If the transaction is `PENDING`, the + # payment may complete or fail at a later stage. + # + # ```ruby + # # option 1: using merchant reference only + # payment_status = pesapal.query_payment_status("<MERCHANT_REFERENCE>") + # + # # option 2: using merchant reference and transaction id (recommended, see note for reason why) + # payment_status = pesapal.query_payment_status("<MERCHANT_REFERENCE>","<TRANSACTION_ID>") + # ``` + # + # @note If you don't ensure that the merchant reference is unique for each + # order on your system, you may get INVALID as the response. Because of + # this, it is recommended that you provide both the merchant reference and + # transaction tracking id as parameters to guarantee uniqueness. + # + # @param merchant_reference [String] the unique id generated for the + # transaction by your application before posting the order + # + # @param transaction_tracking_id [String] the unique id assigned by Pesapal + # to the transaction after it's posted + # + # @return [String] the status of the transaction. Possible values include + # PENDING | COMPLETED | FAILED | INVALID + def query_payment_status(merchant_reference, transaction_tracking_id = nil) + # initialize setting of @params (oauth_signature left empty) + @params = Pesapal::Helper::Status.set_parameters(@config[:consumer_key], merchant_reference, transaction_tracking_id) - # generate oauth signature and add signature to the request parameters - @params[:oauth_signature] = Pesapal::Oauth::generate_oauth_signature("GET", @api_endpoints[:querypaymentstatus], @params, @config[:consumer_secret], @token_secret) + # generate oauth signature and add signature to the request parameters + @params[:oauth_signature] = Pesapal::Oauth.generate_oauth_signature("GET", @api_endpoints[:querypaymentstatus], @params, @config[:consumer_secret], @token_secret) - # change params (with signature) to a query string - query_string = Pesapal::Oauth::generate_encoded_params_query_string @params + # change params (with signature) to a query string + query_string = Pesapal::Oauth.generate_encoded_params_query_string @params - # get status response - uri = URI.parse "#{@api_endpoints[:querypaymentstatus]}?#{query_string}" - http = Net::HTTP.new(uri.host, uri.port) - if @env == 'production' - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - end - response = http.request(Net::HTTP::Get.new(uri.request_uri)) - response = CGI::parse response.body - response['pesapal_response_data'][0] + # get status response + uri = URI.parse "#{@api_endpoints[:querypaymentstatus]}?#{query_string}" + http = Net::HTTP.new(uri.host, uri.port) + if @env == 'production' + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE end + response = http.request(Net::HTTP::Get.new(uri.request_uri)) + response = CGI.parse response.body + response['pesapal_response_data'][0] + end - # set env when called - def set_env(env = false) - env = env.to_s.downcase - if env == 'production' - @env = 'production' - else - @env = 'development' - @env = Rails.env if defined?(Rails) - end - set_endpoints + # Set the environment in use. + # + # Useful especially if you want to change the environment at runtime from + # what was set during initialization in the constructor. It also makes sure + # that we use the appropriate endpoints when making calls to Pesapal. See + # below: + # + # ``` + # # endpoint values set if :development + # { + # :postpesapaldirectorderv4 => "http://demo.pesapal.com/API/PostPesapalDirectOrderV4", + # :querypaymentstatus => "http://demo.pesapal.com/API/QueryPaymentStatus", + # :querypaymentdetails => "http://demo.pesapal.com/API/QueryPaymentDetails" + # } + # + # # endpoint values set if :production + # { + # :postpesapaldirectorderv4 => "https://www.pesapal.com/API/PostPesapalDirectOrderV4", + # :querypaymentstatus => "https://www.pesapal.com/API/QueryPaymentStatus", + # :querypaymentdetails => "https://www.pesapal.com/API/QueryPaymentDetails" + # } + # ``` + # + # @note For a Rails app, you'd expect that calling this would also flip the + # credentials if there was a YAML file containing both environment + # credentials but that's not the case. It could be something that we can + # add later. + # + # @param env [Symbol] the environment we want to use i.e. :development or + # :production + # + # @return [Hash] contains Pesapal endpoints appropriate for the set + # environment + def set_env(env = false) + env = env.to_s.downcase + if env == 'production' + @env = 'production' + else + @env = 'development' + @env = Rails.env if defined?(Rails) end + set_endpoints + end - # listen to ipn response - def ipn_listener(notification_type, merchant_reference, transaction_tracking_id) + # Generates the appropriate IPN response depending on the status of the + # transaction. + # + # ```ruby + # # pass in the notification type, merchant reference and transaction id + # response_to_ipn = pesapal.ipn_listener("<NOTIFICATION_TYPE>", "<MERCHANT_REFERENCE>","<TRANSACTION_ID>") + # ``` + # + # The variable, `response_to_ipn`, now holds a response as the one shown + # below. Using the status you can customise any actions (e.g. database + # inserts and updates). + # + # ``` + # { + # :status => "<PAYMENT_STATUS>", + # :response => "<IPN_RESPONSE>" + # } + # ``` + # + # _Ps: The response you send to PesaPal must be the same as what you + # received from PesaPal if successful, which the method generates for you + # and should be in `:response`._ + # + # @note It's up to you to send the response back to Pesapal by providing the + # `:response` back to the IPN. The hard part is done. + # + # @param notification_type [String] the IPN notification type, should be set + # to CHANGE always + # + # @param merchant_reference [String] the unique id generated for the + # transaction by your application before posting the order + # + # @param transaction_tracking_id [String] the unique id assigned by Pesapal + # to the transaction after it's posted + # + # @return [Hash] contains the status and IPN response that should be sent + # back to Pesapal + def ipn_listener(notification_type, merchant_reference, transaction_tracking_id) + status = query_payment_status(merchant_reference, transaction_tracking_id) + output = { :status => status, :response => nil } - status = query_payment_status(merchant_reference, transaction_tracking_id) - output = { :status => status, :response => nil } - - case status - when 'COMPLETED' then output[:response] = "pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=#{transaction_tracking_id}&pesapal_merchant_reference=#{merchant_reference}" - when 'FAILED' then output[:response] = "pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=#{transaction_tracking_id}&pesapal_merchant_reference=#{merchant_reference}" - end - - output + case status + when 'COMPLETED' then output[:response] = "pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=#{transaction_tracking_id}&pesapal_merchant_reference=#{merchant_reference}" + when 'FAILED' then output[:response] = "pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=#{transaction_tracking_id}&pesapal_merchant_reference=#{merchant_reference}" end + output + end + private - # set endpoints - def set_endpoints - - if @env == 'production' - @api_domain = 'https://www.pesapal.com' - else - @api_domain = 'http://demo.pesapal.com' - end - - @api_endpoints = {} - @api_endpoints[:postpesapaldirectorderv4] = "#{@api_domain}/API/PostPesapalDirectOrderV4" - @api_endpoints[:querypaymentstatus] = "#{@api_domain}/API/QueryPaymentStatus" - @api_endpoints[:querypaymentdetails] = "#{@api_domain}/API/QueryPaymentDetails" - - return @api_endpoints + # Set API endpoints depending on the environment. + def set_endpoints + if @env == 'production' + @api_domain = 'https://www.pesapal.com' + else + @api_domain = 'http://demo.pesapal.com' end - # set credentialts through hash, uses default if nothing is input - def set_configuration(consumer_details = {}) + @api_endpoints = {} + @api_endpoints[:postpesapaldirectorderv4] = "#{@api_domain}/API/PostPesapalDirectOrderV4" + @api_endpoints[:querypaymentstatus] = "#{@api_domain}/API/QueryPaymentStatus" + @api_endpoints[:querypaymentdetails] = "#{@api_domain}/API/QueryPaymentDetails" - # set the configuration - @config = { :callback_url => 'http://0.0.0.0:3000/pesapal/callback', - :consumer_key => '<YOUR_CONSUMER_KEY>', - :consumer_secret => '<YOUR_CONSUMER_SECRET>' - } + @api_endpoints + end - valid_config_keys = @config.keys + # Set credentials through hash that passed in (does a little processing to + # remove unwanted data & uses default if nothing is input). + def set_configuration(consumer_details = {}) + # set the configuration + @config = { :callback_url => 'http://0.0.0.0:3000/pesapal/callback', + :consumer_key => '<YOUR_CONSUMER_KEY>', + :consumer_secret => '<YOUR_CONSUMER_SECRET>' + } - consumer_details.each { |k,v| @config[k.to_sym] = v if valid_config_keys.include? k.to_sym } - end + valid_config_keys = @config.keys + + consumer_details.each { |k, v| @config[k.to_sym] = v if valid_config_keys.include? k.to_sym } + end end end