Skip to content

Commit

Permalink
Merge pull request #3 from mbashia/mpesa_integration_liveview
Browse files Browse the repository at this point in the history
Add mpesa integration live pages to test stk
  • Loading branch information
mbashia authored Dec 3, 2024
2 parents d95e155 + a27ba94 commit f2b247f
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 6 deletions.
17 changes: 17 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,20 @@
@import 'tailwindcss/utilities';

/* This file is for your main application CSS */

@import url('https://fonts.googleapis.com/css2?family=Kanit:ital,wght@1,100&family=Libre+Baskerville:wght@700&family=M+PLUS+Rounded+1c&family=Montserrat:ital@1&family=Open+Sans:ital@1&family=Poppins:wght@300;500;700&family=Questrial&family=Roboto&display=swap');

.poppins-light {
font-family: 'Poppins', sans-serif;
font-weight: 300;
}

.poppins-regular {
font-family: 'Poppins', sans-serif;
font-weight: 500;
}

.poppins-bold {
font-family: 'Poppins', sans-serif;
font-weight: 700;
}
12 changes: 6 additions & 6 deletions lib/mpesa/mpesa_stk.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ defmodule Mpesa.MpesaStk do
end

@doc false
defp generate_body(_phone_number, _amount) do
defp generate_body(phone_number, amount) do
timestamp = get_timestamp()
password = generate_stk_password()
transaction_reference = generate_transaction_reference()
Expand All @@ -43,11 +43,11 @@ defmodule Mpesa.MpesaStk do
Password: password,
Timestamp: timestamp,
TransactionType: "CustomerPayBillOnline",
Amount: "1",
PartyA: "254791531926",
Amount: amount,
PartyA: phone_number,
PartyB: @short_code,
PhoneNumber: "254791531926",
CallBackURL: "https://aa2d-102-214-157-80.ngrok-free.app/api/callback",
PhoneNumber: phone_number,
CallBackURL: "https://9e55-41-212-115-150.ngrok-free.app/api/callback",
AccountReference: transaction_reference,
TransactionDesc: "Test"
}
Expand All @@ -71,7 +71,7 @@ defmodule Mpesa.MpesaStk do

## generate timestamp
@doc false
def get_timestamp do
defp get_timestamp do
Timex.local()
|> Timex.format!("{YYYY}{0M}{0D}{h24}{m}{s}")
end
Expand Down
24 changes: 24 additions & 0 deletions lib/mpesa/stk_form.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Mpesa.StkForm do
@moduledoc """
A module for handling and validating STK push form data in the Mpesa integration.
It validates the phone number format and ensures that the amount is greater than zero.
"""

import Ecto.Changeset

@types %{
Phone_number: :string,
amount: :integer
}

def changeset(data, params) do
{data, @types}
|> cast(params, Map.keys(@types))
|> validate_required([:Phone_number, :amount])
|> validate_format(:Phone_number, ~r/^2547\d{8}$/,
message: "Phone number must be in the format 2547XXXXXXXX"
)
|> validate_number(:amount, greater_than: 0, message: "Amount must be greater than zero")
end
end
110 changes: 110 additions & 0 deletions lib/mpesa/transaction_status_checker.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
defmodule Mpesa.TransactionStatusChecker do
@moduledoc """
A module responsible for checking the status of a transaction with the Mpesa API.
"""

alias Mpesa.MpesaAuth

require Logger

@pass_key System.get_env("MPESA_PASS_KEY")
@short_code "174379"

def check_status(checkout_request_id) do
{:ok, mpesa_token} = MpesaAuth.generate_token()
headers = build_headers(mpesa_token)
body = generate_body(checkout_request_id)

response =
Finch.build(:post, url(), headers, body)
|> Finch.request(Mpesa.Finch)

case response do
{:ok, %Finch.Response{status: 200, body: resp_body}} ->
Logger.info("Suceessfully checked transaction status")
process_result_code(resp_body |> Jason.decode!() |> Map.get("ResultCode"))

{:ok, %Finch.Response{status: 400, body: resp_body}} ->
resp_body |> Jason.decode!()

{:ok, %Finch.Response{status: 500, body: resp_body}} ->
error_message = resp_body |> Jason.decode!() |> Map.get("errorMessage")
{:ok, error_message}

{:ok, %Finch.Response{status: _status, body: resp_body}} ->
resp_body |> Jason.decode!()

{:error, _reason} ->
{:error, "Failed to check transaction status"}
end
end

@doc false
defp url do
"https://sandbox.safaricom.co.ke/mpesa/stkpushquery/v1/query"
end

def build_headers(token) do
[
{"Authorization", "Bearer #{token}"},
{"Content-Type", "application/json"}
]
end

@doc false
defp generate_body(checkout_request_id) do
timestamp = get_timestamp()
password = generate_stk_password()

%{
BusinessShortCode: @short_code,
Password: password,
Timestamp: timestamp,
CheckoutRequestID: checkout_request_id
}
|> Jason.encode!()
end

@doc false
defp generate_stk_password do
timestamp = get_timestamp()
Base.encode64("#{@short_code}#{@pass_key}#{timestamp}")
end

@doc false
defp get_timestamp do
Timex.local()
|> Timex.format!("{YYYY}{0M}{0D}{h24}{m}{s}")
end

@doc false
defp process_result_code("0"), do: {:ok, "Payment made successfully"}

defp process_result_code("1"), do: {:error, "Balance is insufficient"}

defp process_result_code("26"), do: {:error, "System busy, Try again in a short while"}

defp process_result_code("2001"), do: {:error, "Wrong Pin entered"}

defp process_result_code("1001"), do: {:error, "Unable to lock subscriber"}

defp process_result_code("1025"),
do:
{:error,
"An error occurred while processing the request please try again after 2-3 minutes"}

defp process_result_code("1019"), do: {:error, "Transaction expired. No MO has been received"}

defp process_result_code("9999"),
do:
{:error,
"An error occurred while processing the request please try again after 2-3 minutes"}

defp process_result_code("1032"), do: {:error, "Request cancelled by user"}

defp process_result_code("1037"), do: {:error, "No response from the user"}

defp process_result_code("SFC_IC0003"), do: {:error, "Payment timeout"}

defp process_result_code(unknown), do: unknown
end
100 changes: 100 additions & 0 deletions lib/mpesa_web/live/stk_live/index.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
defmodule MpesaWeb.Stklive.Index do
@moduledoc """
The `MpesaWeb.Stklive.Index` LiveView is used to test the STK push functionality for M-Pesa integration.
This LiveView handles the rendering of the form and processes the events related to
M-Pesa STK push interactions, including validation and submission of parameters.
"""
use MpesaWeb, :live_view

alias Mpesa.MpesaStk
alias Mpesa.StkForm
alias Mpesa.TransactionStatusChecker

require Logger

def mount(_params, _session, socket) do
initial_changeset = StkForm.changeset(%{}, %{})

{:ok,
socket
|> assign(:form, to_form(initial_changeset, as: "stk_form"))}
end

def handle_event(
"validate",
%{"stk_form" => %{"Phone_number" => phone_number, "amount" => amount}},
socket
) do
# Use a plain map as data
data = %{}

# Build the changeset
changeset = StkForm.changeset(data, %{:Phone_number => phone_number, :amount => amount})

{:noreply, assign(socket, form: to_form(changeset, action: :validate, as: "stk_form"))}
end

def handle_event(
"pay",
%{"stk_form" => %{"Phone_number" => phone_number, "amount" => amount}},
socket
) do
payment_initiation = MpesaStk.initiate_payment(phone_number, amount)

case payment_initiation do
{:ok, response_body} ->
checkout_request_id = response_body["CheckoutRequestID"]

case recursive_check_status(checkout_request_id, 200) do
{:ok, response} ->
{:noreply,
socket
|> put_flash(:info, response)
|> push_navigate(to: "/stk-test")}

{:error, message} ->
{:noreply,
socket
|> put_flash(:error, message)
|> push_navigate(to: "/stk-test")}
end

{:error, _} ->
{:noreply,
socket
|> put_flash(:error, "Failed to initiate payment")
|> push_navigate(to: "/stk-test")}
end
end

defp recursive_check_status(checkout_request_id, retries_remaining)
when retries_remaining > 0 do
case TransactionStatusChecker.check_status(checkout_request_id) do
{:ok, "The transaction is being processed"} ->
# Pause for 2 seconds before retrying
Process.sleep(2000)
Logger.info("Retrying transaction status check: #{retries_remaining} attempts remaining")
recursive_check_status(checkout_request_id, retries_remaining - 1)

{:ok, response} ->
{:ok, response}

{:error, reason} ->
{:error, reason}

_ ->
{:error, "Transaction failed"}
end
end

defp recursive_check_status(_checkout_request_id, 0),
do: {:error, "Failed to get a valid status after multiple attempts"}
end

## test- if phone number is valid
## test- if amount is greater than 0
## test- if phone number is invalid
## validate phone number
19 changes: 19 additions & 0 deletions lib/mpesa_web/live/stk_live/index.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<div class="text-xl poppins-regular">
Hello use this page to test Mpesa Stk(Sim tool kit) push
<div class="mt-10">
<.simple_form for={@form} id="event-form" phx-change="validate" phx-submit="pay">
<.input
field={@form[:Phone_number]}
type="text"
label="Phone Number"
placeholder="eg. 254712345678"
/>

<.input field={@form[:amount]} type="number" label="Amount" />

<:actions>
<.button phx-disable-with="paying...">Pay</.button>
</:actions>
</.simple_form>
</div>
</div>
1 change: 1 addition & 0 deletions lib/mpesa_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule MpesaWeb.Router do
pipe_through :browser

get "/", PageController, :home
live "/stk-test", Stklive.Index
end

# Other scopes may use custom stacks.
Expand Down

0 comments on commit f2b247f

Please sign in to comment.