Git Product home page Git Product logo

daraja-test's Introduction

Implementing M-pesa STK push and STK push query APIs in Rails API

Setup

Daraja

  • To get started head over to daraja and sign up for a developer account or login if you already have one.
  • On my apps tab, create a new sandbox app and name it whatever you want, then tick all the check boxes and click on create app.
  • You will be redirected to the app details page where you will find your consumer key and consumer secret.
  • Save these somewhere safe as you will need them later.
  • Navigate to the APIs tab and on M-pesa Express click on Simulate, on the input prompt select the app you just created.
  • This will autopopulate some fields for you, you can leave them as they are.
  • Scroll down and click on test credentials.
  • Save your initiator password and passkey somewhere safe as you will need them later.

Ngrok

  • Go to ngrok and sign up for a free account or login if you already have one.
  • To install on ubuntu sudo snap install ngrok or download the zip file from the website and extract it.
  • To connect your account to ngrok run ngrok authtoken <your authtoken> and replace with your authtoken.
  • We will get back to ngrok later, let's first setup our rails app.

Rails

  • Create a new rails app rails new <app-name> --api in my case rails new daraja-test --api .
  • Add the following gems to your gemfile and run bundle install.
gem 'rack-cors'
gem 'rest-client'
  • We need to create an M-pesa resource, all datatypes are strings.
  • Run rails g resource Mpesa phoneNumber amount checkoutRequestID merchantRequestID mpesaReceiptNumber.
  • We also need a model for the access token, run rails g model AccessToken token.
  • Run rails db:migrate

Configurations

  • Navigate to config/environments/development.rb and add the following code.
config.hosts << /[a-z0-9]+\.ngrok\.io/
  • This will allow us to access our rails app from ngrok.
  • Navigate to config/initializers/cors.rb and add the following code or uncomment the existing code.
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: [:get, :post, :options]
  end
end
  • Be sure to replace origins 'example.com' with origins '*'if you uncomment existing code instead of adding the above code.

Environment variables

  • Inside the config folder create a file called local_env.yml and add the following code.
MPESA_CONSUMER_KEY: '<your consumer key>'
MPESA_CONSUMER_SECRET: '<your consumer secret>'
MPESA_PASSKEY: '<your passkey>'
MPESA_SHORT_CODE: '174379'
MPESA_INITIATOR_NAME: 'testapi'
MPESA_INITIATOR_PASSWORD: '<your initiator password>'
CALLBACK_URL: '< your ngrok url>'
REGISTER_URL: "https://sandbox.safaricom.co.ke/mpesa/c2b/v1/registerurl"

** Note about the CALLBACK_URL **

  • To get you callback url first run your rails server rails s and copy the url from the terminal.

  • Then go to ngrok and run ngrok http <port number> and replace with the port number from your rails server.

  • This will generate a url that you can use as your callback url.

  • In my case above ngrok http http://127.0.0.1:3000 and the url generated was https://5d5b-105-161-115-83.in.ngrok.io

  • Note that the url generated by ngrok changes every time you run it, so you will need to update your local_env.yml file with the new url every time you run ngrok.

  • Navigate to the ngrok url, you should see the page below, click on visit site which should take you to your rails app.

  • If you get a Blocked Host error, check these stackoverflow solutions.

  • In my case I had to replace config.hosts << /[a-z0-9]+\.ngrok\.io/ with config.hosts.clear in config/environments/development.rb. This however is not recommended for production.

  • Remember to add your local_env.yml file to your .gitignore file.

  • We need rails to load our environment variables, to do this add the following code to config/application.rb.

config.before_configuration do
  env_file = File.join(Rails.root, 'config', 'local_env.yml')
  YAML.load(File.open(env_file)).each do |key, value|
    ENV[key.to_s] = value
  end if File.exists?(env_file)
end
  • Wheeww!! That was a lot of configurations, let's now implement the code.

Implementing the code

  • First we need to write private methods to generate and get an access token from the Authorization API.
  • Generate Access Token Request -> Gives you a time bound access token to call allowed APIs in the sandbox.
  • Get Access Token -> Used to check if generate_acces_token_request is successful or not then it reads the responses and extracts the access token from the response and saves it to the database.
  • Add the following code to app/controllers/mpesa_controller.rb.
  • First require the rest-client gem require 'rest-client' and add then following code.
private

    def generate_access_token_request
        @url = "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials"
        @consumer_key = ENV['MPESA_CONSUMER_KEY']
        @consumer_secret = ENV['MPESA_CONSUMER_SECRET']
        @userpass = Base64::strict_encode64("#{@consumer_key}:#{@consumer_secret}")
        headers = {
            Authorization: "Bearer #{@userpass}"
        }
        res = RestClient::Request.execute( url: @url, method: :get, headers: {
            Authorization: "Basic #{@userpass}"
        })
        res
    end

    def get_access_token
        res = generate_access_token_request()
        if res.code != 200
        r = generate_access_token_request()
        if res.code != 200
        raise MpesaError('Unable to generate access token')
        end
        end
        body = JSON.parse(res, { symbolize_names: true })
        token = body[:access_token]
        AccessToken.destroy_all()
        AccessToken.create!(token: token)
        token
    end
Stk Push Request
  • Under APIs -> M-pesa Express you can simulate a stk push request by selecting your app and changing Party A and Phone Number to your phone number.
  • Looking at the JSON the request body has the following parameters; { BusinessShortCode - The organization shortcode used to receive the transaction. Password - The password for encrypting the request.(Base64 encoded string,a combination of your BusinessShortCode, Passkey and Timestamp) Timestamp - The timestamp of the transaction in the format yyyymmddhhiiss TransactionType - The type of transaction (CustomerPayBillOnline or CustomerBuyGoodsOnline) Amount - The amount being transacted PartyA - The phone number sending the money. PartyB - The organization shortcode receiving the funds.Can be the same as the business shortcode. PhoneNumber - The mobile number to receive the STK push.Can be the same as Party A. CallBackURL - The url to where responses from M-Pesa will be sent to. Should be valid and secure. AccountReference - Value displayed to the customer in the STK Pin prompt message. TransactionDesc - A description of the transaction. }
  • You can read more on their documentation -> Lipa Na M-pesa Online API -> Request Parameter Definition.
  • Add the following code to app/controllers/mpesa_controller.rb.
 def stkpush
        phoneNumber = params[:phoneNumber]
        amount = params[:amount]
        url = "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest"
        timestamp = "#{Time.now.strftime "%Y%m%d%H%M%S"}"
        business_short_code = ENV["MPESA_SHORTCODE"]
        password = Base64.strict_encode64("#{business_short_code}#{ENV["MPESA_PASSKEY"]}#{timestamp}")
        payload = {
        'BusinessShortCode': business_short_code,
        'Password': password,
        'Timestamp': timestamp,
        'TransactionType': "CustomerPayBillOnline",
        'Amount': amount,
        'PartyA': phoneNumber,
        'PartyB': business_short_code,
        'PhoneNumber': phoneNumber,
        'CallBackURL': "#{ENV["CALLBACK_URL"]}/callback_url",
        'AccountReference': 'Codearn',
        'TransactionDesc': "Payment for Codearn premium"
        }.to_json

        headers = {
        Content_type: 'application/json',
        Authorization: "Bearer #{access_token}"
        }

        response = RestClient::Request.new({
        method: :post,
        url: url,
        payload: payload,
        headers: headers
        }).execute do |response, request|
        case response.code
        when 500
        [ :error, JSON.parse(response.to_str) ]
        when 400
        [ :error, JSON.parse(response.to_str) ]
        when 200
        [ :success, JSON.parse(response.to_str) ]
        else
        fail "Invalid response #{response.to_str} received."
        end
        end
        render json: response
    end
  • Navigate to routes.rb and add the following code.
post 'stkpush', to: 'mpesa#stkpush'
  • Open postman and make a post request to your ngrok url with the following parameters.
{
    "phoneNumber": "2547xxxxxxxx",
    "amount": "1"
}
  • The request sents an STK push to the phone number provided.
  • Your response should look like this.
{
    "MerchantRequestID": "xxxx-xxxx-xxxx-xxxx",
    "CheckoutRequestID": "ws_CO_XXXXXXXXXXXXXXXXXXXXXXXXX",
    "ResponseCode": "0",
    "ResponseDescription": "Success. Request accepted for processing",
    "CustomerMessage": "Success. Request accepted for processing"
}
  • Save the CheckoutRequestID for the next step.

Stk Query Request

  • We can use the mpesa query to check if the payment was successful or not.
  • Under APIs -> M-pesa Express you can simulate a query a stk push request by selecting your app and inputing the CheckoutRequestID you got from the previous step.
  • The request body has the following parameters; { BusinessShortCode - The organization shortcode used to receive the transaction. Password - The password for encrypting the request.(Base64 encoded string,a combination of your BusinessShortCode, Passkey and Timestamp) Timestamp - The timestamp of the transaction in the format yyyymmddhhiiss CheckoutRequestID - The CheckoutRequestID used to identify the transaction on M-Pesa. }
  • Add the following code to app/controllers/mpesa_controller.rb.
def stkquery
        url = "https://sandbox.safaricom.co.ke/mpesa/stkpushquery/v1/query"
        timestamp = "#{Time.now.strftime "%Y%m%d%H%M%S"}"
        business_short_code = ENV["MPESA_SHORTCODE"]
        password = Base64.strict_encode64("#{business_short_code}#{ENV["MPESA_PASSKEY"]}#{timestamp}")
        payload = {
        'BusinessShortCode': business_short_code,
        'Password': password,
        'Timestamp': timestamp,
        'CheckoutRequestID': params[:checkoutRequestID]
        }.to_json

        headers = {
        Content_type: 'application/json',
        Authorization: "Bearer #{ access_token }"
        }

        response = RestClient::Request.new({
        method: :post,
        url: url,
        payload: payload,
        headers: headers
        }).execute do |response, request|
        case response.code
        when 500
        [ :error, JSON.parse(response.to_str) ]
        when 400
        [ :error, JSON.parse(response.to_str) ]
        when 200
        [ :success, JSON.parse(response.to_str) ]
        else
        fail "Invalid response #{response.to_str} received."
        end
        end
        render json: response
    end
  • Navigate to routes.rb and add the following code.
post 'stkquery', to: 'mpesa#stkquery'
  • Open postman and make a post request to your ngrok url with the following parameters.
{
    "checkoutRequestID": "ws_CO_XXXXXXXXXXXXXXXXXXXXXXXXX"
}
  • Your response should look like this.
[
    "success",
    {
        "ResponseCode": "0",
        "ResponseDescription": "The service request has been accepted successsfully",
        "MerchantRequestID": "8491-75014543-2",
        "CheckoutRequestID": "ws_CO_12122022094855872768372439",
        "ResultCode": "1032",
        "ResultDesc": "Request cancelled by user"
    }
]
  • You can use the ResultCode to check if the payment was successful or not.
  • Your mpesas_controller.rb should look like this.
class MpesasController < ApplicationController
    
    require 'rest-client'

    # stkpush
     def stkpush
        phoneNumber = params[:phoneNumber]
        amount = params[:amount]
        url = "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest"
        timestamp = "#{Time.now.strftime "%Y%m%d%H%M%S"}"
        business_short_code = ENV["MPESA_SHORTCODE"]
        password = Base64.strict_encode64("#{business_short_code}#{ENV["MPESA_PASSKEY"]}#{timestamp}")
        payload = {
        'BusinessShortCode': business_short_code,
        'Password': password,
        'Timestamp': timestamp,
        'TransactionType': "CustomerPayBillOnline",
        'Amount': amount,
        'PartyA': phoneNumber,
        'PartyB': business_short_code,
        'PhoneNumber': phoneNumber,
        'CallBackURL': "#{ENV["CALLBACK_URL"]}/callback_url",
        'AccountReference': 'Codearn',
        'TransactionDesc': "Payment for Codearn premium"
        }.to_json

        headers = {
        Content_type: 'application/json',
        Authorization: "Bearer #{access_token}"
        }

        response = RestClient::Request.new({
        method: :post,
        url: url,
        payload: payload,
        headers: headers
        }).execute do |response, request|
        case response.code
        when 500
        [ :error, JSON.parse(response.to_str) ]
        when 400
        [ :error, JSON.parse(response.to_str) ]
        when 200
        [ :success, JSON.parse(response.to_str) ]
        else
        fail "Invalid response #{response.to_str} received."
        end
        end
        render json: response
    end

    # stkquery

    def stkquery
        url = "https://sandbox.safaricom.co.ke/mpesa/stkpushquery/v1/query"
        timestamp = "#{Time.now.strftime "%Y%m%d%H%M%S"}"
        business_short_code = ENV["MPESA_SHORTCODE"]
        password = Base64.strict_encode64("#{business_short_code}#{ENV["MPESA_PASSKEY"]}#{timestamp}")
        payload = {
        'BusinessShortCode': business_short_code,
        'Password': password,
        'Timestamp': timestamp,
        'CheckoutRequestID': params[:checkoutRequestID]
        }.to_json

        headers = {
        Content_type: 'application/json',
        Authorization: "Bearer #{ access_token }"
        }

        response = RestClient::Request.new({
        method: :post,
        url: url,
        payload: payload,
        headers: headers
        }).execute do |response, request|
        case response.code
        when 500
        [ :error, JSON.parse(response.to_str) ]
        when 400
        [ :error, JSON.parse(response.to_str) ]
        when 200
        [ :success, JSON.parse(response.to_str) ]
        else
        fail "Invalid response #{response.to_str} received."
        end
        end
        render json: response
    end

    private

    def generate_access_token_request
        @url = "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials"
        @consumer_key = ENV['MPESA_CONSUMER_KEY']
        @consumer_secret = ENV['MPESA_CONSUMER_SECRET']
        @userpass = Base64::strict_encode64("#{@consumer_key}:#{@consumer_secret}")
        headers = {
            Authorization: "Bearer #{@userpass}"
        }
        res = RestClient::Request.execute( url: @url, method: :get, headers: {
            Authorization: "Basic #{@userpass}"
        })
        res
    end

    def access_token
        res = generate_access_token_request()
        if res.code != 200
        r = generate_access_token_request()
        if res.code != 200
        raise MpesaError('Unable to generate access token')
        end
        end
        body = JSON.parse(res, { symbolize_names: true })
        token = body[:access_token]
        AccessToken.destroy_all()
        AccessToken.create!(token: token)
        token
    end


end
  • Your routes.rb should look like this.
Rails.application.routes.draw do
    post 'stkpush', to: 'mpesas#stkpush'
    post 'stkquery', to: 'mpesas#stkquery'
end
  • The full code for this tutorial can be found here.

That's it for this tutorial. I hope you found it helpful. If you have any questions, feel free to reach out to me on email: [email protected]. THANK YOU!

daraja-test's People

Contributors

annastacia-dev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

daraja-test's Issues

add more api services

Integrate more endpoints offered by daraja API

  • Dynamic QR
  • C2B
  • B2C
  • Transaction Status
  • Account Balance
  • Reversals
  • Tax Remittance
  • Business PayBill
  • Business Buy Goods
  • Bill Mananger
  • B2B Express Checkout

Add views (where applicable for these)

Current Integrations

  • Authorization
  • M-pesa Express

update readme

  • update readme with refactored code structure
  • segment docs

code refactoring

Refactor code - readability, structure (DRY code, have concerns)
set up rubocop for linting and fix the errors

add views

Develop a UI view to make stk push, should have an input for phone number and amount and button that will send the request.
Show alert for request status

Bug

When using postman to make a #stkpush, I have encountered an error (as shown below), so I got into debug mode;

Processing by MpesasController#stkpush as /
  Parameters: {"phoneNumber"=>"2547
******", "amount"=>"1", "mpesa"=>{"phoneNumber"=>"2547********", "amount"=>"1"}}
Consumer Key:
Consumer Secret:
Encoded Userpass: Og==
Request Headers: {:Authorization=>"Bearer Og=="}
Completed 500 Internal Server Error in 15224ms (ActiveRecord: 1.9ms | Allocations: 5450)**

It's quite clear consumer key and consumer secret ,for lack of a better term, 'hazionekani'. Why? You have the following code in your config/application.rb;  
config.before_configuration do
env_file = File.join(Rails.root, 'config', 'local_env.yml')
YAML.load(File.open(env_file)).each do |key, value|
ENV[key.to_s] = value
end if File.exist?(env_file)
end

because of this line env_file = File.join(Rails.root, 'config', 'local_env.yml' consumer key and consumer secret hazionekani, you should move the local_env.yml from root and have it inside the config folder.

going live

Get a shortcode from Mpesa
Set up web hooks
Write a going live tutorial & add it to the README.md

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.