-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from mbashia/mpesa_integration_liveview
Add mpesa integration live pages to test stk
- Loading branch information
Showing
7 changed files
with
277 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters