Class: Pesapal::Merchant

Inherits:
Object
  • Object
show all
Defined in:
lib/pesapal/merchant.rb

Overview

Pesapal Merchant object responsible for posting and handling transaction queries.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(env = false) ⇒ Merchant

Note:

You can change the environment at runtime using #change_env

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 ...

# 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:

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>'
}

Parameters:

  • env (Symbol) (defaults to: false)

    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).



148
149
150
151
152
153
154
155
# File 'lib/pesapal/merchant.rb', line 148

def initialize(env = false)
  change_env env
  if defined?(Rails)
    configure Rails.application.config.pesapal_credentials
  else
    configure
  end
end

Instance Attribute Details

#configHash

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>'
}

Returns:

  • (Hash)

    the Pesapal config



23
24
25
# File 'lib/pesapal/merchant.rb', line 23

def config
  @config
end

#order_detailsHash

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.

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 => '[email protected]',
  :phonenumber => '+254722222222',
  :currency => 'KES'
}

Returns:

  • (Hash)

    the order details



61
62
63
# File 'lib/pesapal/merchant.rb', line 61

def order_details
  @order_details
end

Instance Method Details

#change_env(env = false) ⇒ Hash

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.

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"
}

Parameters:

  • env (Symbol) (defaults to: false)

    the environment we want to use i.e. :development or :production

Returns:

  • (Hash)

    contains Pesapal endpoints appropriate for the set environment



343
344
345
346
347
348
349
350
351
352
# File 'lib/pesapal/merchant.rb', line 343

def change_env(env = false)
  env = env.to_s.downcase
  if env == 'production'
    @env = 'production'
  else
    @env = 'development'
    @env = Rails.env if defined?(Rails)
  end
  assign_endpoints
end

#generate_order_urlString

Note:

You MUST set up your order details before you call this method on the object.

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:

# 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

Returns:

  • (String)

    URL of the Pesapal post order form



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/pesapal/merchant.rb', line 184

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

  # initialize setting of @params (oauth_signature left empty)
  @params = Pesapal::Helper::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)

  # change params (with signature) to a query string
  query_string = Pesapal::Oauth.generate_encoded_params_query_string @params

  "#{@api_endpoints[:postpesapaldirectorderv4]}?#{query_string}"
end

#ipn_listener(notification_type, merchant_reference, transaction_tracking_id) ⇒ Hash

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.

Generates the appropriate IPN response depending on the status of the transaction.

# 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.

Parameters:

  • notification_type (String)

    the IPN notification type, overridden and set to CHANGE internally. Left here for extensibility.

  • merchant_reference (String)

    the unique id generated for the transaction by your application before posting the order

  • transaction_tracking_id (String)

    the unique id assigned by Pesapal to the transaction after it's posted

Returns:

  • (Hash)

    contains the status and IPN response that should be sent back to Pesapal



391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/pesapal/merchant.rb', line 391

def ipn_listener(notification_type, merchant_reference, transaction_tracking_id)
  notification_type = 'CHANGE'
  status = query_payment_status(merchant_reference, transaction_tracking_id)
  output = { status: status, response: nil }

  case status
  when 'COMPLETED' then output[:response] = "pesapal_notification_type=#{notification_type}&pesapal_transaction_tracking_id=#{transaction_tracking_id}&pesapal_merchant_reference=#{merchant_reference}"
  when 'FAILED'    then output[:response] = "pesapal_notification_type=#{notification_type}&pesapal_transaction_tracking_id=#{transaction_tracking_id}&pesapal_merchant_reference=#{merchant_reference}"
  end

  output
end

#query_payment_details(merchant_reference, transaction_tracking_id) ⇒ Hash

Same as #query_payment_status, but additional information is returned in a Hash.

Call method on initialized Pesapal::Merchant object (see #initialize for details):

# 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>"
}

Parameters:

  • merchant_reference (String)

    the unique id generated for the transaction by your application before posting the order

  • transaction_tracking_id (String)

    the unique id assigned by Pesapal to the transaction after it's posted

Returns:

  • (Hash)

    transaction payment details



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/pesapal/merchant.rb', line 237

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)

  # 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[:querypaymentdetails]}?#{query_string}"
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  http.verify_mode = OpenSSL::SSL::VERIFY_PEER
  response = http.request(Net::HTTP::Get.new(uri.request_uri))
  response = CGI.parse response.body
  response = response['pesapal_response_data'][0].split(',')

  { method: response[1],
    status: response[2],
    merchant_reference: response[3],
    transaction_tracking_id: response[0]
  }
end

#query_payment_status(merchant_reference, transaction_tracking_id = nil) ⇒ String

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.

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.

# 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>")

Parameters:

  • merchant_reference (String)

    the unique id generated for the transaction by your application before posting the order

  • transaction_tracking_id (String) (defaults to: nil)

    the unique id assigned by Pesapal to the transaction after it's posted

Returns:

  • (String)

    the status of the transaction. Possible values include PENDING | COMPLETED | FAILED | INVALID



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/pesapal/merchant.rb', line 290

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)

  # 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)
  http.use_ssl = true
  http.verify_mode = OpenSSL::SSL::VERIFY_PEER
  response = http.request(Net::HTTP::Get.new(uri.request_uri))
  response = CGI.parse response.body
  response['pesapal_response_data'][0]
end