Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/annastacia-dev/daraja-test

Implementing m-pesa stk push and query
https://github.com/annastacia-dev/daraja-test

mpesa mpesa-api mpesa-payments mpesa-sdk rails rails-api ruby

Last synced: 2 months ago
JSON representation

Implementing m-pesa stk push and query

Awesome Lists containing this project

README

        

## Implementing M-pesa STK push and STK push query APIs in Rails API
### Setup
#### Daraja
- To get started head over to [daraja](https://developer.safaricom.co.ke/) 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](https://ngrok.com/) 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 ``` 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 --api ``` in my case ``` rails new daraja-test --api ```.
- Add the following gems to your gemfile and run ``` bundle install ```.

```ruby
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.
```ruby
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.
```ruby
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.
```ruby
MPESA_CONSUMER_KEY: ''
MPESA_CONSUMER_SECRET: ''
MPESA_PASSKEY: ''
MPESA_SHORT_CODE: '174379'
MPESA_INITIATOR_NAME: 'testapi'
MPESA_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 ``` 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](https://stackoverflow.com/questions/53878453/upgraded-rails-to-6-getting-blocked-host-error) 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.
```ruby
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.
```ruby
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](https://developer.safaricom.co.ke/APIs/MpesaExpressSimulate) 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](https://developer.safaricom.co.ke/Documentation) -> Lipa Na M-pesa Online API -> Request Parameter Definition.
- Add the following code to app/controllers/mpesa_controller.rb.
```ruby
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.
```ruby
post 'stkpush', to: 'mpesa#stkpush'
```
- Open postman and make a post request to your ngrok url with the following parameters.
```json
{
"phoneNumber": "2547xxxxxxxx",
"amount": "1"
}
```
- The request sents an STK push to the phone number provided.
- Your response should look like this.
```json
{
"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](https://developer.safaricom.co.ke/APIs/MpesaExpressSimulate) 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.
```ruby
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.
```ruby
post 'stkquery', to: 'mpesa#stkquery'
```
- Open postman and make a post request to your ngrok url with the following parameters.
```json
{
"checkoutRequestID": "ws_CO_XXXXXXXXXXXXXXXXXXXXXXXXX"
}
```
- Your response should look like this.
```json
[
"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.
```ruby
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.
```ruby
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](https://github.com/Annastacia-dev/daraja-test).

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!