From 88b8290401efd60613fcc85eb3b8ff72ca727f4a Mon Sep 17 00:00:00 2001 From: Paul Oguda Date: Mon, 10 Aug 2020 01:13:05 +0300 Subject: [PATCH 01/65] Update README.md Adds JengaWS features to ReadMe. --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e073c1d..35ac313 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ ##### [badges][badges] -# ExPesa +# ExPesa :dollar: :pound: :yen: :euro: -> Payment Library +> Payment Library For Most Public Payment API's in Kenya and hopefully Africa. Let us get this :moneybag: ## Table of contents @@ -22,7 +22,14 @@ - [ ] B2C - [ ] B2B - [ ] C2B -- [ ] Equity +- [ ] JengaWS(Equity) + - [ ] Send Money + - [ ] Receive Payments + - [ ] Buy Goods, Pay Bills, Get Airtime + - [ ] Credit + - [ ] Reg Tech: KYC, AML, & CDD API + - [ ] Account Services + - [ ] Paypal ## Installation From 5b287d9b9511e07b0a8d85dfbe10980303c9e81c Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Wed, 26 Aug 2020 21:24:49 +0300 Subject: [PATCH 02/65] added stk transaction validation, and test case --- lib/ex_pesa/Mpesa/stk.ex | 59 +++++++++++++++++++++++++++++++++ test/ex_pesa/Mpesa/stk_test.exs | 29 +++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/lib/ex_pesa/Mpesa/stk.ex b/lib/ex_pesa/Mpesa/stk.ex index 919853e..d9103d7 100644 --- a/lib/ex_pesa/Mpesa/stk.ex +++ b/lib/ex_pesa/Mpesa/stk.ex @@ -77,4 +77,63 @@ defmodule ExPesa.Mpesa.Stk do def request(_) do {:error, "Required Parameters missing, 'phone, 'amount', 'reference', 'description'"} end + + @doc """ + STK PUSH Transaction Validation + + ## Configuration + + Add below config to dev.exs / prod.exs files (at this stage after STK, the config keys should be there) + This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#lipa-na-m-pesa-online-query-request + + `config.exs` + ```elixir + config :ex_pesa, + mpesa: [ + consumer_key: "", + consumer_secret: "", + mpesa_short_code: "", + mpesa_passkey: "", + ] + ``` + + ## Parameters + + attrs: - a map containing: + - `checkout_request_id` - Checkout RequestID. + + ## Example + + iex> ExPesa.Mpesa.Stk.validate(%{checkout_request_id: "ws_CO_260820202102496165"}) + {:ok, + %{ + "CheckoutRequestID" => "ws_CO_260820202102496165", + "MerchantRequestID" => "11130-78831728-4", + "ResponseCode" => "0", + "ResponseDescription" => "The service request has been accepted successsfully", + "ResultCode" => "1032", + "ResultDesc" => "Request cancelled by user" + } + } + """ + @spec validate(map()) :: {:error, any()} | {:ok, any()} + def validate(%{checkout_request_id: checkout_request_id}) do + paybill = Application.get_env(:ex_pesa, :mpesa)[:mpesa_short_code] + passkey = Application.get_env(:ex_pesa, :mpesa)[:mpesa_passkey] + {:ok, timestamp} = Timex.now() |> Timex.format("%Y%m%d%H%M%S", :strftime) + password = Base.encode64(paybill <> passkey <> timestamp) + + payload = %{ + "BusinessShortCode" => paybill, + "Password" => password, + "Timestamp" => timestamp, + "CheckoutRequestID" => checkout_request_id + } + + make_request("/mpesa/stkpushquery/v1/query", payload) + end + + def validate(_) do + {:error, "Required Parameter missing, 'CheckoutRequestID'"} + end end diff --git a/test/ex_pesa/Mpesa/stk_test.exs b/test/ex_pesa/Mpesa/stk_test.exs index 64417b3..7962359 100644 --- a/test/ex_pesa/Mpesa/stk_test.exs +++ b/test/ex_pesa/Mpesa/stk_test.exs @@ -34,12 +34,26 @@ defmodule ExPesa.Mpesa.StkTest do "ResponseDescription" => "Success. Request accepted for processing" }) } + + %{url: "https://sandbox.safaricom.co.ke/mpesa/stkpushquery/v1/query", method: :post} -> + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "CheckoutRequestID" => "ws_CO_260820202102496165", + "MerchantRequestID" => "11130-78831728-4", + "ResponseCode" => "0", + "ResponseDescription" => "The service request has been accepted successsfully", + "ResultCode" => "1032", + "ResultDesc" => "Request cancelled by user" + }) + } end) :ok end - describe "Mpesa STK Push" do + describe "Mpesa STK Push/ Validate Transaction" do test "request/1 should Initiate STK with required parameters" do request_details = %{ amount: 10, @@ -59,4 +73,17 @@ defmodule ExPesa.Mpesa.StkTest do {:error, result} = Stk.request(%{}) "Required Parameters missing, 'phone, 'amount', 'reference', 'description'" = result end + + test "validate/1 should validate transaction successfully" do + {:ok, result} = Stk.validate(%{checkout_request_id: "ws_CO_260820202102496165"}) + + assert result["CheckoutRequestID"] == "ws_CO_260820202102496165" + assert result["ResponseCode"] == "0" + assert result["ResultDesc"] == "Request cancelled by user" + end + + test "validate/1 should error out without required parameter" do + {:error, result} = Stk.validate(%{}) + "Required Parameter missing, 'CheckoutRequestID'" = result + end end From fb50f09a04df54e491ccc85182ced5dfe71a5989 Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Mon, 31 Aug 2020 14:56:41 +0300 Subject: [PATCH 03/65] added OTP Server to keep token state --- lib/ex_pesa/Mpesa/mpesa_base.ex | 17 ++++++++++++++++- lib/ex_pesa/Mpesa/token_server.ex | 30 ++++++++++++++++++++++++++++++ lib/ex_pesa/application.ex | 20 ++++++++++++++++++++ mix.exs | 3 ++- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 lib/ex_pesa/Mpesa/token_server.ex create mode 100644 lib/ex_pesa/application.ex diff --git a/lib/ex_pesa/Mpesa/mpesa_base.ex b/lib/ex_pesa/Mpesa/mpesa_base.ex index 81f41a8..3a08891 100644 --- a/lib/ex_pesa/Mpesa/mpesa_base.ex +++ b/lib/ex_pesa/Mpesa/mpesa_base.ex @@ -2,6 +2,7 @@ defmodule ExPesa.Mpesa.MpesaBase do @moduledoc false import ExPesa.Util + alias ExPesa.Mpesa.TokenServer @live "https://api.safaricom.co.ke" @sandbox "https://sandbox.safaricom.co.ke" @@ -24,7 +25,21 @@ defmodule ExPesa.Mpesa.MpesaBase do end def token(client) do - Tesla.get(client, "/oauth/v1/generate?grant_type=client_credentials") |> get_token + {token, datetime} = TokenServer.get() + + if DateTime.compare(datetime, DateTime.utc_now()) !== :gt do + case Tesla.get(client, "/oauth/v1/generate?grant_type=client_credentials") |> get_token do + {:ok, token} -> + # added 3550 secs, 50 less normal 3600 in 1 hr + TokenServer.insert({token, DateTime.add(DateTime.utc_now(), 3550, :second)}) + {:ok, token} + + {:error, message} -> + {:error, message} + end + else + {:ok, token} + end end @doc false diff --git a/lib/ex_pesa/Mpesa/token_server.ex b/lib/ex_pesa/Mpesa/token_server.ex new file mode 100644 index 0000000..460f678 --- /dev/null +++ b/lib/ex_pesa/Mpesa/token_server.ex @@ -0,0 +1,30 @@ +defmodule ExPesa.Mpesa.TokenServer do + @moduledoc false + + use GenServer + @name __MODULE__ + + def start_link(opts \\ []) do + opts = Keyword.put_new(opts, :name, @name) + GenServer.start_link(__MODULE__, {'token', DateTime.utc_now()}, opts) + end + + def insert(pid \\ @name, token), do: GenServer.cast(pid, {:insert, token}) + + def get(pid \\ @name), do: GenServer.call(pid, :get) + + @impl true + def init(token_tuple) do + {:ok, token_tuple} + end + + @impl true + def handle_cast({:insert, token_tuple}, _state) do + {:noreply, token_tuple} + end + + @impl true + def handle_call(:get, _from, state) do + {:reply, state, state} + end +end diff --git a/lib/ex_pesa/application.ex b/lib/ex_pesa/application.ex new file mode 100644 index 0000000..463aa65 --- /dev/null +++ b/lib/ex_pesa/application.ex @@ -0,0 +1,20 @@ +defmodule ExPesa.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + def start(_type, _args) do + # List all child processes to be supervised + children = [ + # Starts a MpesaServer by calling: ExPesa.Mpesa.TokenServer.start_link(arg) + ExPesa.Mpesa.TokenServer + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: ExPesa.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/mix.exs b/mix.exs index 3f81c86..e58103f 100644 --- a/mix.exs +++ b/mix.exs @@ -38,7 +38,8 @@ defmodule ExPesa.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger] + extra_applications: [:logger], + mod: {ExPesa.Application, []} ] end From 3599f0c6adaea56b6216029ffd18ee37197b23c5 Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Tue, 1 Sep 2020 16:05:26 +0300 Subject: [PATCH 04/65] addded token server tests --- test/ex_pesa/Mpesa/token_server_test.exs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 test/ex_pesa/Mpesa/token_server_test.exs diff --git a/test/ex_pesa/Mpesa/token_server_test.exs b/test/ex_pesa/Mpesa/token_server_test.exs new file mode 100644 index 0000000..87c5709 --- /dev/null +++ b/test/ex_pesa/Mpesa/token_server_test.exs @@ -0,0 +1,19 @@ +defmodule ExPesa.Mpesa.TokenServerTest do + @moduledoc false + + use ExUnit.Case, async: true + alias ExPesa.Mpesa.TokenServer + + # setup do + # start_supervised!(TokenServer) + # end + + test "stores and retrieves token" do + org_token = 'tsdgu66t327uygfe' + TokenServer.insert({org_token, DateTime.add(DateTime.utc_now(), 3550, :second)}) + + assert {token, datetime} = TokenServer.get() + assert token === 'tsdgu66t327uygfe' + assert DateTime.compare(datetime, DateTime.utc_now()) !== :g + end +end From 2dd1429dddf1c782f745c1227a6548bdbfd2fb30 Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Tue, 1 Sep 2020 16:27:19 +0300 Subject: [PATCH 05/65] fixed string in tests --- test/ex_pesa/Mpesa/token_server_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ex_pesa/Mpesa/token_server_test.exs b/test/ex_pesa/Mpesa/token_server_test.exs index 87c5709..febfc1b 100644 --- a/test/ex_pesa/Mpesa/token_server_test.exs +++ b/test/ex_pesa/Mpesa/token_server_test.exs @@ -9,11 +9,11 @@ defmodule ExPesa.Mpesa.TokenServerTest do # end test "stores and retrieves token" do - org_token = 'tsdgu66t327uygfe' + org_token = "SGWcJPtNtYNPGm6uSYR9yPYrAI3Bm" TokenServer.insert({org_token, DateTime.add(DateTime.utc_now(), 3550, :second)}) assert {token, datetime} = TokenServer.get() - assert token === 'tsdgu66t327uygfe' + assert token === org_token assert DateTime.compare(datetime, DateTime.utc_now()) !== :g end end From f44e3079281ee921059ce0f3a29abba635fb9f74 Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Tue, 1 Sep 2020 16:35:41 +0300 Subject: [PATCH 06/65] ignored .vscode --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1348ef9..645eebb 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ erl_crash.dump # Ignore package tarball (built via "mix hex.build"). ex_pesa-*.tar -config/dev.exs \ No newline at end of file +config/dev.exs +/.vscode \ No newline at end of file From 8bbed26331a465762a84c7b2d244da7a9762e3c4 Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Wed, 2 Sep 2020 19:40:42 +0300 Subject: [PATCH 07/65] added tests for token regeneration --- test/ex_pesa/Mpesa/token_server_test.exs | 35 +++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/test/ex_pesa/Mpesa/token_server_test.exs b/test/ex_pesa/Mpesa/token_server_test.exs index febfc1b..d438227 100644 --- a/test/ex_pesa/Mpesa/token_server_test.exs +++ b/test/ex_pesa/Mpesa/token_server_test.exs @@ -1,19 +1,52 @@ defmodule ExPesa.Mpesa.TokenServerTest do @moduledoc false + import Tesla.Mock use ExUnit.Case, async: true alias ExPesa.Mpesa.TokenServer + alias ExPesa.Mpesa.MpesaBase # setup do # start_supervised!(TokenServer) # end + setup do + mock(fn + %{ + url: "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials", + method: :get + } -> + %Tesla.Env{ + status: 200, + body: %{ + "access_token" => "SGWcJPtNtYNPGm6uSYR9yPYrAI3Bm", + "expires_in" => "3599" + } + } + end) + + :ok + end + test "stores and retrieves token" do org_token = "SGWcJPtNtYNPGm6uSYR9yPYrAI3Bm" TokenServer.insert({org_token, DateTime.add(DateTime.utc_now(), 3550, :second)}) assert {token, datetime} = TokenServer.get() assert token === org_token - assert DateTime.compare(datetime, DateTime.utc_now()) !== :g + assert DateTime.compare(datetime, DateTime.utc_now()) === :gt + end + + test "new token generated when expired" do + org_token = "SGWcJPtNtYNPGm6uSYR9yPYrAI" + TokenServer.insert({org_token, DateTime.add(DateTime.utc_now(), -60, :second)}) + {_token, datetime} = TokenServer.get() + assert DateTime.compare(datetime, DateTime.utc_now()) !== :gt + + MpesaBase.token(MpesaBase.auth_client()) + + {token, datetime} = TokenServer.get() + assert token !== org_token + assert DateTime.compare(datetime, DateTime.utc_now()) === :gt end end From d211fe031ea9c56cf9975949ea55e085604379ad Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Thu, 3 Sep 2020 19:30:12 +0300 Subject: [PATCH 08/65] updated readme --- README.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 35ac313..4a96d2b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -##### [badges][badges] +[![Actions Status](https://github.com/beamkenya/ex_pesa/workflows/Elixir%20CI/badge.svg)](https://github.com/beamkenya/ex_pesa/actions)   ![Hex.pm](https://img.shields.io/hexpm/v/ex_pesa)   ![Hex.pm](https://img.shields.io/hexpm/dt/ex_pesa) # ExPesa :dollar: :pound: :yen: :euro: @@ -11,6 +11,7 @@ - [Configuration](#configuration) - [Documentation](#documentation) - [Contribution](#contribution) +- [Contributors](#contributors) - [Licence](#licence) ## Features @@ -18,10 +19,14 @@ [WIP] - [x] Mpesa - - [x] STK push + - [x] Mpesa Express (STK) + - [x] STK Transaction Validation - [ ] B2C - [ ] B2B - [ ] C2B + - [ ] Reversal + - [ ] Transaction Status + - [ ] Account Balance - [ ] JengaWS(Equity) - [ ] Send Money - [ ] Receive Payments @@ -29,8 +34,8 @@ - [ ] Credit - [ ] Reg Tech: KYC, AML, & CDD API - [ ] Account Services - - [ ] Paypal +- [ ] Card ## Installation @@ -69,6 +74,22 @@ config :ex_pesa, The docs can be found at [https://hexdocs.pm/ex_pesa](https://hexdocs.pm/ex_pesa). +### Quick Examples + +#### Mpesa Express (STK) + +```elixir + iex> ExPesa.Mpesa.Stk.request(%{amount: 10, phone: "254724540000", reference: "reference", description: "description"}) + {:ok, + %{ + "CheckoutRequestID" => "ws_CO_010320202011179845", + "CustomerMessage" => "Success. Request accepted for processing", + "MerchantRequestID" => "25558-10595705-4", + "ResponseCode" => "0", + "ResponseDescription" => "Success. Request accepted for processing" + }} +``` + ## Contribution If you'd like to contribute, start by searching through the [issues](https://github.com/beamkenya/ex_pesa/issues) and [pull requests](https://github.com/beamkenya/ex_pesa/pulls) to see whether someone else has raised a similar idea or question. @@ -76,6 +97,15 @@ If you don't see your idea listed, [Open an issue](https://github.com/beamkenya/ Check the [Contribution guide](contributing.md) on how to contribute. +## Contributors + +Auto-populated from: +[contributors-img](https://contributors-img.firebaseapp.com/image?repo=beamkenya/ex_pesa) + + + + + ## Licence ExPesa is released under [MIT License](https://github.com/appcues/exsentry/blob/master/LICENSE.txt) From e3dc64d01a16b16a5ef3112150234aafa9f97b1f Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Thu, 3 Sep 2020 19:52:17 +0300 Subject: [PATCH 09/65] prepared docs generation --- README.md | 4 +++- assets/logo.png | Bin 0 -> 6342 bytes lib/ex_pesa.ex | 13 +------------ mix.exs | 21 ++++++++++++--------- 4 files changed, 16 insertions(+), 22 deletions(-) create mode 100644 assets/logo.png diff --git a/README.md b/README.md index 4a96d2b..751abc2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -[![Actions Status](https://github.com/beamkenya/ex_pesa/workflows/Elixir%20CI/badge.svg)](https://github.com/beamkenya/ex_pesa/actions)   ![Hex.pm](https://img.shields.io/hexpm/v/ex_pesa)   ![Hex.pm](https://img.shields.io/hexpm/dt/ex_pesa) +

+ +[![Actions Status](https://github.com/beamkenya/ex_pesa/workflows/Elixir%20CI/badge.svg)](https://github.com/beamkenya/ex_pesa/actions) ![Hex.pm](https://img.shields.io/hexpm/v/ex_pesa) ![Hex.pm](https://img.shields.io/hexpm/dt/ex_pesa) # ExPesa :dollar: :pound: :yen: :euro: diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4803640aadf6b64a820f9432ebe52e3b6f2b7923 GIT binary patch literal 6342 zcmaKRWl$ST)GqGsZpEdz6)Tjy5WGkOMS@##r)Z&20xguF#hv0sibIQqLXbjmEd-|| zNC^&?@5lG!{rn7@OclarUft9Jkz_lN2u zNbG+=FZ`VB0^Gd4xJ=zVov=(@1GyxmxlEjVxTGYcB)BBxpGiwUlab>x(&qxGo8l7C zJRW4?1Zt|AhU6XP2m4shN9?NYu3+A(hf?;WVyP!!g-O%vn3ijfW_qMJTE13Hgq^@z z<<@+aF|SkePxuY&1(YmYYD1vrUQi^DQ1Z6^j3@*;yU5GX7@dzd^^UMRW*)Vw_LPV! z94kq)q!&H*1sf%VUODUev3`}m?#9EBV!0)`%T?Kl6RJmkIym`q3sPA6i&jl5RUk5% z$MBX7P;jZ3sC?p27rMDC_&I2(f8OorDVjE%5x{T z&#d0AB~EsVr=ruMaGu-=8g!E60yE!|+%MY;uEsT{6YabJ9&+=q@$qnUKYIXD46`Y8 zwn-l9%h>tyoO#(4sWH~F*kSr?02SfceK{Q&bfgN5;+^2Eee(x)nESOs1ZL;@1nO?V z@OW39IOo=}?uUKXqoN;mg|3$ATedW`{wwQLL~LHT!BroghenqeQGaKB^CkE^Jcho{ zqL|p3?j#~rVf|r)!6x(s>%<(oX{<&Wj(RpH+5kYLOTNEoJ3reoT>Eo`_H>ev)0WE^ zYJv$|_frXW##ja;!qGhScl`t;)udlWZVkg8zpq3M`e#<2^5mCcq@m#rSG~UkO^KL0vD9;Hd6ucba>D%uhjYjdT7O8ey-j59#5JF)~TW2(GtX z?13N0-AU`<>b=>#_}+Tf7Igt;Q(Ac;_oik)9!A4)z2qzm|LV*thFzNZz0u0BhquQ-3eLYQ-0Xjh-UwK>u@B2= z{g{6@flcA9s3Vm_Fjy9^Ck(%Lp*U^eyyGSWyYZz>SXDW9)MjT6dPiO0t>2;i{{?CJ zZ#FTffmJ#(Opa1hU1Z^J%->GZ@aO((9{5uwsJ8IW0i)W6ZfYIM=x^b@fBr^Ez~G-g z(|S-z7U$hx*)cnpkEXmeXzRF!>0wi7;Y69@@vXYv*!cDxtD&B- zrI27WP!z5~q(0sxtn<3xiefXujx)D6koKPBfp43YMUN(BP~1PsDpF5(6Ppg;2R~10 zcXK#<@5U41yY}`*uquE7EBIY*#WKUOIuU49n6KuA%<8OmNMu5&{5+KntwvxKV@8x< z4lf6D=gPS6qGX-5L1(?+#W1Mxp$EaHy6}b8+v0e%hVbf=9bP3!&h_4Qq=KQ~AvcTJ zz3V*;^b@pMeznH`Dk7YDjYmAwY2#F>V;e5dO|LGM={VmU7P<@S+FzYF>=u8QF%kH# zjMfbrJ3=icV_mF#`6o6?ayCPObHxkS%w(bGhOWvH#iU5ByD1*f?3n6F#=;R>k5Fmx z^dc-i^wJn0#)=%Q`1+yPtUAixeY`JX>bhM7E|1yth3P z02R@FElMJd->lH^irD+lJVHj~|5qVZu z`BElsDPtC|`VN`tVlOEpdV*j^6ogwjB7mp|K`UXzriec8Z6 zFiOXSM8|hZ6yDCTlu7kohi}C7@R|gHiVjH5=I54-LpV0gvPS#g9HveO^28Oh+gyz}SvN)LIXSFlz}{VXMDF-d`6Q2tF$s9BWa#)aZy$Qh5R z^$6q;opOdMNXs6W3-I;i?yD}ki>Jj0*isl`0fpRNgS}Z@Y04_1i8GmUhnqw@lP{J~ zn~>4`Ub$7G$Ixg@@=C0syjL}kHj8D{#}=&AXlH2iZN~7W8Ma0SEdlA?vpL)a7N?=3 zq$&$D$P7K5bz}LfguL*E+e=Ralv`oDhvMCr!k2@%>2VQOJ-yg~@w)5c0qcPC1?jg2 z4YAM)s(z#A^vL*Hr_(hwWh#pfu92z5&kAwt$?ibefx%e5krBqDhkxmDi3zP#Y99xtzbJGL_9L-b)mlK;pq;?ix6X1D7Qqxv2xx_s1{1 zo;#|&KDO^lX(R2rEYfZv{*MzuES($@9X&acUud3xrkpm~bkozC`k;HDA-J0wVyfOE z4vSK`rLi6kYt$p<#$UscC!L=~`s^#jPMqzi$rD*G&?+G;|G4mHGu%G-78FR za_Y!ZZ&+072V1j0F~&=gMY)_2=fs%aIh|*+&**C0>i;-^yT0H@8GTSr*-=o^HtXAv zJ*~di={uTP4A9k`I4dRW+CxFGxyo$Us@*?t#uOj=a2iJ5DB6UEvqJ1}UkXF7;O^~* zhq)a)YqNVc$e3-x_(a5R_DpMtT6W_gZmorxmM_G6tT{j!o@_0E-BsA&S=H_;tMZ$6 zdq3*!m*L(kR-V<^Cr)o;y}UXf+`X>Rpwh7Ov&`Z!88%e+AoyiE-Hofe=y+)FQNF_y ziuBWK35_&HdArhuwT`<6&h6QUg+3!?Mf;PQoc_*~rw(U~4FilAgY5NVCWW3*$d8ia zcJfToy8q;16Hn2r5q^%zYv8RRldWFCz^EXq#I0ZTCdwCkhl2!vcWt~^@ zWc8N}E>hT%smN~lC17Q%4=J>juC4GnHp~k9`uapNOK+@0Y9TE+v`_zZ=T40#v!!Ojr^IvMH>@It|Y*w^|v{zn<5XLKie%u*fo~*dCtI1~kzNP=Rjx6*uv#t-+ws#S$+-yL99s!@ z@%9;OmK>AGRCj$4QG7U^8QyS42)yNavQD5ngp-@E!p-;ERUQ)Ju2;}GMBlEASpODl zp&9NJ`uW)6K$XGpQ|$nPoP0G^x7(DSq+gWH!L-$5<8G2c=QBTk$#wMzYKZ7IK( zC8L)QdFc%i-^vm@0o%3R^{K_0iSq@aSy_=iv45F8B|gS)&$=#+xlMdAL6AOw$(vz@ z-@ZzKn~L}1gTf6*Kp`IyWxQiG_H0vR z6z4JT$(7TZYs;+w%yr)`XVx4Jw1_rz)H)e3IJ2Js)a4D@3|gXy%o_EX5Hcf<<7c{P zJ`H)9`kwM34CUDKv9Z^Mgik9C)Ehrs+R`{>UwX_n33xn32Dsgo__?$C1^*kP8xyX% zC)0uBxaLST4L{AF_&nh_@goIA6Vvc>sOkHeZ|BF6h(>sVg}@2c#%IcLBS|()a;Bry z3t>+is96+2r^&#s|K?fO@pXRt3PHIOzKl%fxPtDz$D@u)B2i|FPqbSgWEesUE$jS9 zP_@wpw$Du7;+K!*a#kq(9p3o;(hO3fy?iHvZ%oY8tN)*MH7Y{#)SGO zZC}xdmlu8e89RT(l|M2)#jIXUGQ=0@)$u(JhVSGL{o5=l0+DYd%DQ?<*H=Y*=h``% z^PRjFyq*wD;1QHf_W0h|NO^#+3K4e#47w6IOu`_OUNZT!%b}jG;(V4D&1iwp`HBd1oPnGiq^n!QAG_;gIT`@qf$E|JA9IRRbNahB=k@{}gTt|8mOP_>%Mn zI7&VRqqv6{V{yd+mnkA`Dcrs_yf+`9D6| z7FwtMqoI%&4xmaBZllTO{S6K6Aea3FSZciO`R)J_c4Gk<;RE>nk=lOQb|;VOy*4}Q zdBH8v6Z_^PHYD*+{KHEipjkxV-A?$!BVmo+bJ;R^+`H2o(>qEt^c3oz4rWm{AMG2& zfc<@zFRIrj?ei?+fcO9KNpR>PsB|V2#O$qbLHUI?4IW+%8QJ7Q3$CY7iM5-u9l5u8 zvtd58i1=NyroVQX@bBp98mroeQey*8R>xiGd@IDum>-$~Q-#Io1-v#5lH(77D;ra| zkY=~EcP4H;o@h$ z+c>RpwCA^Qn+^!$K74QyDrOMa<(bRhXjG!~Ie7jNivJXK55(QrSqVt{G?Ed5ok+Du zG>D%ZjLCBMxQ--OyFRvQjciC-`mU-0Qj~0F@D4AHMK)0UZfZ)Xpd{2nRZ$xkk-T%? z&h%{O>rA)Jaikq+GbRe-+*(o-vKtXdf_;tR#(&o|W!55K13B$dQv{8mGJz;i)WuBYvl_ zFT+AZop~#UI8*7X`N5atk|)t=T_t6N%_|rm+2Xj;R$x)zPm?mF@WfH8lu%llH@Q7U zw#V(EPGm=8#@sG}>y_qI`Z80R*IM;Jb>xj&@FvA4mES)$=CJ5RR2&NGuJnj23zD18 zl=5d9D8Joksqk7NR!$a_Z?};jN#aM;SQlGHQY#51o>=sgdy%~PwDGUV*LQ+U|D_Ri zK#!8<%N|-qnl$x^tfkE%Z zPAWM+6{jbAo53#@bgylY5#Mf z!huq!%~kSv+zeBExDLIwW zRyhC#q*_!NTq?{g>Eio7u8Nt0qoNL-W^s#?E@wqPjF_F{m~#K7@`!BBEU9RO7tuWd zVajmygBg7OH4=STKd5Y99NHA8D^Y-12#Y;`@mrp1w-R(T(9h%f#W9V)z6GXrUzfb} zsh9fDQs?*Qzt?b%ZfY zxjn<3FZXH}4cpBHHb!-2;0^ef-?DDtlQxH=s1{06+J<6j9~s%xRDw5Trg+1V%*FQk zgn&X85%1}`Yw7BH`wT0`3o621gqG{OwDSj}zE)epn}_coR@L_Gt_nY`)(MwAr=`&s z)CT0u>kxjmSM)+zFkriqzepsQCR|1~9xZPQB;tnJ54#}Xn78EKgNt!-? zc2(HkRtNKX%efVE?=5C<&1J$s(y||f^?H4SiD?S;U;n)x_L9)gjF5fbY~<7zMLK;F zo~{B$mtmaM`T(W#(b^uwuf$JD1SMC7;iRUpCeiD4U8w$Rx>f}$msE^=f|^e;&BMN{rpFGRw4#76pL1#>HE`1m&8dz-P`wRCpDXSZ?T~0CXVSHOaAk^cVC)Z zqofk0cwMj7hh|m^l&%GPmd2Cj*~<@)6t+1rC7sTSCuElA&!E=eea!;I4B z;U1qS9zK48n6N?Pz(OUb4yt8zR_7V^YoSH4>|l>!P^ANtup5GRsQ{*9DSBT+Rjn%( z!V7R*r-~#SmD{pOBr@tA1K2gE)q`QcNY&Wa@>d6u#`Ci-oBZsSC7LN^!wnzbhhE^P z<`mt0Qs_aGQCl7Gw!kvdg8mpm5tEv}Ah-0jJ#|UmMx@(P6eHkFSJ?Xk?4xRX>;rAB zEJe-(;(pJ1;kb}1n%r3!C@cQl=OmK|5KQ9X#lKLj>QYOnoO`v{m?mVP#yVu;V&r#p zIf&ch4UzgL0K9VML{*Rc_~Q4<>udY8gg&W==i=)f+*;0Toh=Y=H>I}F&+ISn3;Sja q$#y0A{=en)|IK6mpY`_JsMC%Ezmj_ExW^s?77$>dS+8Lm^M3#^L3APj literal 0 HcmV?d00001 diff --git a/lib/ex_pesa.ex b/lib/ex_pesa.ex index 03df549..f66eb4f 100644 --- a/lib/ex_pesa.ex +++ b/lib/ex_pesa.ex @@ -1,17 +1,6 @@ defmodule ExPesa do - @moduledoc """ - Documentation for `ExPesa`. - """ + @moduledoc false - @doc """ - Hello world. - - ## Examples - - iex> ExPesa.hello() - :world - - """ def hello do :world end diff --git a/mix.exs b/mix.exs index e58103f..ede7b96 100644 --- a/mix.exs +++ b/mix.exs @@ -7,31 +7,34 @@ defmodule ExPesa.MixProject do version: "0.1.0", elixir: "~> 1.9", start_permanent: Mix.env() == :prod, - description: "This is a Payments Library", + description: "Payment Library For Most Public Payment API's in Kenya and hopefully Africa.", package: package(), deps: deps(), - name: "ex_pesa", + name: "ExPesa", source_url: "https://github.com/beamkenya/ex_pesa.git", - homepage_url: "https://hexdocs.pm/ex_pesa/ExPesa.html", docs: [ # The main page in the docs - main: "ex_pesa", - # logo: - # "https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/M-PESA_LOGO-01.svg/1200px-M-PESA_LOGO-01.svg.png", - extras: ["README.md"] + main: "readme", + canonical: "http://hexdocs.pm/at_ex", + source_url: "https://github.com/beamkenya/ex_pesa.git", + logo: "assets/logo.png", + assets: "assets", + extras: ["README.md", "contributing.md"] ] ] end defp package do [ - maintainers: ["Beam Kenya"], + name: "ex_pesa", + maintainers: ["Paul Oguda, Magak Emmanuel, Frank Midigo, Tracey Onim"], licenses: ["MIT"], links: %{ "GitHub" => "https://github.com/beamkenya/ex_pesa.git", "Documentation" => "https://hexdocs.pm/ex_pesa/ExPesa.html", "README" => "https://hexdocs.pm/ex_pesa/readme.html" - } + }, + homepage_url: "https://github.com/elixirkenya/africastalking-elixir" ] end From e5fe365341a3794b65ba00d920af07e57f661c98 Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Thu, 3 Sep 2020 19:57:03 +0300 Subject: [PATCH 10/65] reduced image size --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 751abc2..e8e0d7a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

+

[![Actions Status](https://github.com/beamkenya/ex_pesa/workflows/Elixir%20CI/badge.svg)](https://github.com/beamkenya/ex_pesa/actions) ![Hex.pm](https://img.shields.io/hexpm/v/ex_pesa) ![Hex.pm](https://img.shields.io/hexpm/dt/ex_pesa) From 1a5c723cc6fe0c4786602ded1e6a8cf2bca5c4a3 Mon Sep 17 00:00:00 2001 From: Paul Oguda Date: Sun, 6 Sep 2020 12:40:21 +0300 Subject: [PATCH 11/65] Update README.md A few changes on the README. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e8e0d7a..37542c7 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,12 @@ end ## Configuration -Create a copy of `config/dev.exs` or `config/prod.exs` form `config/dev.sample.exs` +Create a copy of `config/dev.exs` or `config/prod.exs` from `config/dev.sample.exs` ### Mpesa (Daraja) -Add below config to dec.exs / prod.exs files -This asumes you have a clear understanding of how Daraja API works. +Add below config to dev.exs / prod.exs files +This asumes you have a clear understanding of how [Daraja API works](https://developer.safaricom.co.ke/get-started). ```elixir config :ex_pesa, From cce92297b57e9608c0bed5b43a5e948939c7fbe0 Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Sun, 6 Sep 2020 18:16:56 +0300 Subject: [PATCH 12/65] few fixes --- assets/logo.jpg | Bin 0 -> 2721 bytes mix.exs | 1 - mix.lock | 1 + 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 assets/logo.jpg diff --git a/assets/logo.jpg b/assets/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..972d28fa2959efcd5cdaabe7da53b15684bbea0d GIT binary patch literal 2721 zcmbVNc|6oxAO6jZSuDk1B21VeyUdWtlI=2-ZL((_Bto{zzOUChA#1LE30V?Rgsf?@ zh9O~eMQ_|&wvx;{s(U~0-|utIALpFU=RDu%JfH9PeCf0FC4j>~S6>$ZfdBx+7y$hc z5Y)cm<>Tk-@9*j&L<$n}@NspM@pEVN_gmWKGjx4IB{#mJ^6dp(o zqyt9XEQ2a;l+;{lwIzTQc%%RM#Pkx(^+6}1vbrUbB|0XVnm>~PqV?4oIU@9e zFGDs*x>*bcOFKuKrDPkW&@9mFo0iQre_D!0?s=zaFk-?P>i8Jp!H^#kj$|AG0tGR_ zz(1scIT(92{ao1C1JTX{$$sGhl?FLh>~hXU zbdAuK=*TJ=8}_a7%PX(X!K>~5Rrl>8`o|PRnyL6frnGPKwtIfV`-H86I4#yy(rRyO zktC17i;sWBH`!KCKQf=%yn^=N&+;6x9|iuSduuVQVEo(F&cBEGMV)En#MN5@tc`QG z>&3H6&t7{~(p!Dc;mr$T@O?~RRXT{$^7PAZ#r5LB!Ake6WX#vV;+GF?I(LFEJp3MN z=Pyv8>co=Kbb=1#)RNz3Iwf^H8*foJu=r?}S48!z#E03ltt1h$Z|)b~P39zy(HhmC zJnb|yU-LCc`W9+X)K%Rj!FG%NCE9=-jyf+F7gMtC-aGZ%U|KUa#GsH4#F0-gjt9jz zx7MT-z0VPr^-X1VbzA` zD&tVXVFuatRv8z9oLkcwSnY7bi5s{L5=gUC-lI2kFF!Inr;Lp`t#XFhQ2(n!NV42( zYVB@jeuB&&xsKBHr8xHrSt+o_->E{@+b&*)>{}br*JSSv6hVwSt0tZ`1?!nQi%ngvYG2TFZM_Q+Z#QCRZsXWLkd{fFQ_|)(9~{wX&wB<3oa=! zdY`ZSxk3|TUYQW2Lb|Vh=$R*R5ocTP(XNK31MG|nQQ`o=U;qLG!~%DQ61fAUgR*-@)Redb!VPXqj9l`H@STe=Jrc!LefhOl% zbMh#*c&)@pMkCWSTAk1y;IkCA3!gI5IHuoc*eTG2(<7=x<8&K~q+;~)ReQ-oy9u!$ z%(LSiV@lvWdsUUsORrOShST+InbZBS?;`3E5f&4u&Dk~&J*4?!XtXvT2P^n|tD-Gt z%AI&SxU~$QIGdH#WITJdnsTzlgT_1SkCv%T1< zO9>cU6E|;^@TK{fww8n%w5~K3B;t}=r*%b*|8bw0bSCXe2Zfy+{~4`rp;X|m(lF4& z{0RCk+z?+#WdGhdm#yJ$=EJhS`gL_mM9*wVud4g@ z0Iuk_vXo8sWHjuA$ySWy!)(6I$nu@uNPrK3Cqeh8=hbQO#gP;B7nL~KmJ?Mt!3}32mFiIDU35QmFbO^Kr<6z%zy3WEyFx4|38KZb-4b*XeKR1CKO@ED zv1@xPF|_Vt^JNJv%R=C2F45QG!GxYqx;!wJ#V2%CDQ0|-Iz}xz{a|-h+GR1-7WT$L z)d<6hwVx|gY&6zxVCOWA6Q^id)9O)$qukW9iH?JYZ$5!%+t95LK6D;8ZULU zKHh$nn(Xsjp>Ew`f^e0Pf#%@7a*z&jG4{~MCsk8Rm;2BCXqho+PfP_d(+fB`=!>3E!QBLB>3xpl7z`Tw2`|K0~wuB5yavTJ-2ZR z*Epe#kLq;3`rpV4vNKXVe0wElsXEwFdGiLkFT>*9H@q=?<#F9R*n(JI5$g>*4f53TQ`%#9O>dGTibhJ^-O}2W zU?=4E`97)2OncqGoW?(370F_z2$#ky;LEFh%wqI0hwcjzv14_AEEgM=x2FdjtC9@2 z)9d%OoVgX2b^Q9tv-Mr!Rtv_H3airscYRv|i2idG86()CkS4J^;;#4ld`jGe3fVmZ z-<}pX*|U`6gc!2oTlsC41v1=N)2!egPpf}tAkyPLJA6Ih{to*DVN<4*ObsMPkFyzcWNFSz2uF8Xdmi9&Kq9w)OEAY z7oE~bfmBPad>*|YlIZ;VYi?LZf62Th3+sLjTST3TXB@&&Uh>-)B^uc?=a#apSzcbw SdChJ}G~pBF6tmzXI`zK;O>wON literal 0 HcmV?d00001 diff --git a/mix.exs b/mix.exs index ede7b96..917bbd2 100644 --- a/mix.exs +++ b/mix.exs @@ -31,7 +31,6 @@ defmodule ExPesa.MixProject do licenses: ["MIT"], links: %{ "GitHub" => "https://github.com/beamkenya/ex_pesa.git", - "Documentation" => "https://hexdocs.pm/ex_pesa/ExPesa.html", "README" => "https://hexdocs.pm/ex_pesa/readme.html" }, homepage_url: "https://github.com/elixirkenya/africastalking-elixir" diff --git a/mix.lock b/mix.lock index 6706ef4..66bf2d0 100644 --- a/mix.lock +++ b/mix.lock @@ -4,6 +4,7 @@ "cowlib": {:hex, :cowlib, "2.6.0", "8aa629f81a0fc189f261dc98a42243fa842625feea3c7ec56c48f4ccdb55490f", [:rebar3], [], "hexpm", "45a1a08e05e4c66f2af665295955e337d52c2d33b1f1cf24d353cadeddf34992"}, "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"}, + "ex_doc_makeup": {:hex, :ex_doc_makeup, "0.1.2", "88d9a9c4be29a8486c3aa410927706c2da0c4e22fcc135e49dceb15f17510de0", [:mix], [{:ex_doc, ">= 0.18.1", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:makeup_elixir, ">= 0.3.1", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "7cc49a1634112799252ce540e85eb321734d67a75c76db4fd0cba646fef37574"}, "gettext": {:hex, :gettext, "0.18.1", "89e8499b051c7671fa60782faf24409b5d2306aa71feb43d79648a8bc63d0522", [:mix], [], "hexpm", "e70750c10a5f88cb8dc026fc28fa101529835026dec4a06dba3b614f2a99c7a9"}, "gun": {:hex, :gun, "1.3.2", "542064cbb9f613650b8a8100b3a927505f364fbe198b7a5a112868ff43f3e477", [:rebar3], [{:cowlib, "~> 2.6.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "ba323f0a5fd8abac379a3e1fe6d8ce570c4a12c7fd1c68f4994b53447918e462"}, "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"}, From e4da758df02bbe2df8c9608962c12a84b09d1a59 Mon Sep 17 00:00:00 2001 From: Tee22 Date: Sat, 19 Sep 2020 15:44:01 +0300 Subject: [PATCH 13/65] mpesa b2c --- lib/ex_pesa/Mpesa/b2c.ex | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 lib/ex_pesa/Mpesa/b2c.ex diff --git a/lib/ex_pesa/Mpesa/b2c.ex b/lib/ex_pesa/Mpesa/b2c.ex new file mode 100644 index 0000000..3dd1e58 --- /dev/null +++ b/lib/ex_pesa/Mpesa/b2c.ex @@ -0,0 +1,46 @@ +defmodule ExPesa.Mpesa.B2c do + @moduledoc """ + Business to Customer (B2C) API enables the Business or organization to pay its customers who are the end-users of its products or services. + Currently, the B2C API allows the org to perform around 3 types of transactions: Salary Payments, Business Payments or Promotion payments. + Salary payments are used by organizations paying their employees via M-Pesa, Business Payments are normal business transactions to customers + e.g. bank transfers to mobile, Promotion payments are payments made by organization carrying out promotional services e.g. + betting companies paying out winnings to clients + """ + + import ExPesa.Mpesa.MpesaBase + + @doc """ + Initiates a B2C mpesa request. + + + """ + + def initiate_request(%{ + username: username, + command_id: command_id, + amount: amount, + phone_number: phone_number, + remarks: remarks, + occassion: occassion + }) do + short_code = Application.get_env(:ex_pesa, :mpesa)[:mpesa_short_code] + passkey = Application.get_env(:ex_pesa, :mpesa)[:mpesa_passkey] + {:ok, timestamp} = Timex.now() |> Timex.format("%Y%m%d%H%M%S", :strftime) + credential = Base.encode64(short_code <> passkey <> timestamp) + + payload = %{ + "InitiatorName" => username, + "SecurityCredential" => credential, + "CommandID" => command_id, + "Amount" => amount, + "PartyA" => short_code, + "PartyB" => phone_number, + "Remarks" => remarks, + "QueueTimeOutURL" => Application.get_env(:ex_pesa, :mpesa)[:mpesa_callback_url], + "ResultURL" => Application.get_env(:ex_pesa, :mpesa)[:mpesa_callback_url], + "Occassion" => occassion + } + + make_request("/mpesa/b2c/v1/paymentrequest", payload) + end +end From 73be8754aefc07a10cea74301eec29bb50003554 Mon Sep 17 00:00:00 2001 From: Tee22 Date: Sun, 20 Sep 2020 22:45:16 +0300 Subject: [PATCH 14/65] added b2c_keys to dev.exs --- config/dev.sample.exs | 47 +++++++++++++++++++++++++++++++++++++++- lib/ex_pesa/Mpesa/b2c.ex | 25 +++++++++++---------- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/config/dev.sample.exs b/config/dev.sample.exs index 31d9a5d..584f41b 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -8,5 +8,50 @@ config :ex_pesa, consumer_secret: "vRzZiD5RllMLIdLD", mpesa_short_code: "174379", mpesa_passkey: "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919", - mpesa_callback_url: "http://91eb0af5.ngrok.io/api/payment/callback" + mpesa_callback_url: "http://91eb0af5.ngrok.io/api/payment/callback", + b2c_initiator_name: "initiator", + b2c_password: "", + b2c_security_credential: + "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==", + b2c_partyA: "601426", + b2c_result_url: "http://91eb0af5.ngrok.io/api/payment/callback", + b2c_timeout_url: "http://91eb0af5.ngrok.io/api/payment/callback", + b2c_public_key: "-----BEGIN CERTIFICATE----- +MIIGkzCCBXugAwIBAgIKXfBp5gAAAD+hNjANBgkqhkiG9w0BAQsFADBbMRMwEQYK +CZImiZPyLGQBGRYDbmV0MRkwFwYKCZImiZPyLGQBGRYJc2FmYXJpY29tMSkwJwYD +VQQDEyBTYWZhcmljb20gSW50ZXJuYWwgSXNzdWluZyBDQSAwMjAeFw0xNzA0MjUx +NjA3MjRaFw0xODAzMjExMzIwMTNaMIGNMQswCQYDVQQGEwJLRTEQMA4GA1UECBMH +TmFpcm9iaTEQMA4GA1UEBxMHTmFpcm9iaTEaMBgGA1UEChMRU2FmYXJpY29tIExp +bWl0ZWQxEzARBgNVBAsTClRlY2hub2xvZ3kxKTAnBgNVBAMTIGFwaWdlZS5hcGlj +YWxsZXIuc2FmYXJpY29tLmNvLmtlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAoknIb5Tm1hxOVdFsOejAs6veAai32Zv442BLuOGkFKUeCUM2s0K8XEsU +t6BP25rQGNlTCTEqfdtRrym6bt5k0fTDscf0yMCoYzaxTh1mejg8rPO6bD8MJB0c +FWRUeLEyWjMeEPsYVSJFv7T58IdAn7/RhkrpBl1dT7SmIZfNVkIlD35+Cxgab+u7 ++c7dHh6mWguEEoE3NbV7Xjl60zbD/Buvmu6i9EYz+27jNVPI6pRXHvp+ajIzTSsi +eD8Ztz1eoC9mphErasAGpMbR1sba9bM6hjw4tyTWnJDz7RdQQmnsW1NfFdYdK0qD +RKUX7SG6rQkBqVhndFve4SDFRq6wvQIDAQABo4IDJDCCAyAwHQYDVR0OBBYEFG2w +ycrgEBPFzPUZVjh8KoJ3EpuyMB8GA1UdIwQYMBaAFOsy1E9+YJo6mCBjug1evuh5 +TtUkMIIBOwYDVR0fBIIBMjCCAS4wggEqoIIBJqCCASKGgdZsZGFwOi8vL0NOPVNh +ZmFyaWNvbSUyMEludGVybmFsJTIwSXNzdWluZyUyMENBJTIwMDIsQ049U1ZEVDNJ +U1NDQTAxLENOPUNEUCxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2 +aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPXNhZmFyaWNvbSxEQz1uZXQ/Y2VydGlm +aWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNzPWNSTERpc3RyaWJ1 +dGlvblBvaW50hkdodHRwOi8vY3JsLnNhZmFyaWNvbS5jby5rZS9TYWZhcmljb20l +MjBJbnRlcm5hbCUyMElzc3VpbmclMjBDQSUyMDAyLmNybDCCAQkGCCsGAQUFBwEB +BIH8MIH5MIHJBggrBgEFBQcwAoaBvGxkYXA6Ly8vQ049U2FmYXJpY29tJTIwSW50 +ZXJuYWwlMjBJc3N1aW5nJTIwQ0ElMjAwMixDTj1BSUEsQ049UHVibGljJTIwS2V5 +JTIwU2VydmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1zYWZh +cmljb20sREM9bmV0P2NBQ2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0 +aWZpY2F0aW9uQXV0aG9yaXR5MCsGCCsGAQUFBzABhh9odHRwOi8vY3JsLnNhZmFy +aWNvbS5jby5rZS9vY3NwMAsGA1UdDwQEAwIFoDA9BgkrBgEEAYI3FQcEMDAuBiYr +BgEEAYI3FQiHz4xWhMLEA4XphTaE3tENhqCICGeGwcdsg7m5awIBZAIBDDAdBgNV +HSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwJwYJKwYBBAGCNxUKBBowGDAKBggr +BgEFBQcDAjAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAC/hWx7KTwSYr +x2SOyyHNLTRmCnCJmqxA/Q+IzpW1mGtw4Sb/8jdsoWrDiYLxoKGkgkvmQmB2J3zU +ngzJIM2EeU921vbjLqX9sLWStZbNC2Udk5HEecdpe1AN/ltIoE09ntglUNINyCmf +zChs2maF0Rd/y5hGnMM9bX9ub0sqrkzL3ihfmv4vkXNxYR8k246ZZ8tjQEVsKehE +dqAmj8WYkYdWIHQlkKFP9ba0RJv7aBKb8/KP+qZ5hJip0I5Ey6JJ3wlEWRWUYUKh +gYoPHrJ92ToadnFCCpOlLKWc0xVxANofy6fqreOVboPO0qTAYpoXakmgeRNLUiar +0ah6M/q/KA== +-----END CERTIFICATE-----" ] diff --git a/lib/ex_pesa/Mpesa/b2c.ex b/lib/ex_pesa/Mpesa/b2c.ex index 3dd1e58..8366dc5 100644 --- a/lib/ex_pesa/Mpesa/b2c.ex +++ b/lib/ex_pesa/Mpesa/b2c.ex @@ -15,32 +15,33 @@ defmodule ExPesa.Mpesa.B2c do """ - def initiate_request(%{ - username: username, + def request(%{ command_id: command_id, amount: amount, phone_number: phone_number, remarks: remarks, occassion: occassion }) do - short_code = Application.get_env(:ex_pesa, :mpesa)[:mpesa_short_code] - passkey = Application.get_env(:ex_pesa, :mpesa)[:mpesa_passkey] - {:ok, timestamp} = Timex.now() |> Timex.format("%Y%m%d%H%M%S", :strftime) - credential = Base.encode64(short_code <> passkey <> timestamp) - payload = %{ - "InitiatorName" => username, - "SecurityCredential" => credential, + "InitiatorName" => Application.get_env(:ex_pesa, :mpesa)[:b2c_initiator_name], + "SecurityCredential" => _security_credential(), "CommandID" => command_id, "Amount" => amount, - "PartyA" => short_code, + "PartyA" => Application.get_env(:ex_pesa, :mpesa)[:b2c_partyA], "PartyB" => phone_number, "Remarks" => remarks, - "QueueTimeOutURL" => Application.get_env(:ex_pesa, :mpesa)[:mpesa_callback_url], - "ResultURL" => Application.get_env(:ex_pesa, :mpesa)[:mpesa_callback_url], + "QueueTimeOutURL" => Application.get_env(:ex_pesa, :mpesa)[:b2c_timeout_url], + "ResultURL" => Application.get_env(:ex_pesa, :mpesa)[:b2c_result_url], "Occassion" => occassion } make_request("/mpesa/b2c/v1/paymentrequest", payload) end + + defp _security_credential do + case Application.get_env(:ex_pesa, :mpesa)[:b2c_security_credential] do + "" -> "generate" + credential -> credential + end + end end From ca985a6fe943b375790aed5eb5887ad4a8696ba2 Mon Sep 17 00:00:00 2001 From: kamalogudah Date: Sat, 26 Sep 2020 01:00:11 +0300 Subject: [PATCH 15/65] Implements C2B functionality --- config/dev.sample.exs | 1 + lib/ex_pesa/Mpesa/c2b.ex | 107 ++++++++++++++++++++++++++++++++ test/ex_pesa/Mpesa/c2b_test.exs | 86 +++++++++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 lib/ex_pesa/Mpesa/c2b.ex create mode 100644 test/ex_pesa/Mpesa/c2b_test.exs diff --git a/config/dev.sample.exs b/config/dev.sample.exs index 31d9a5d..eb89a7e 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -7,6 +7,7 @@ config :ex_pesa, consumer_key: "72yw1nun6g1QQPPgOsAObCGSfuimGO7b", consumer_secret: "vRzZiD5RllMLIdLD", mpesa_short_code: "174379", + c2b_short_code: "600247", # had to pass this for registerurl to work for C2B mpesa_passkey: "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919", mpesa_callback_url: "http://91eb0af5.ngrok.io/api/payment/callback" ] diff --git a/lib/ex_pesa/Mpesa/c2b.ex b/lib/ex_pesa/Mpesa/c2b.ex new file mode 100644 index 0000000..c3e48a5 --- /dev/null +++ b/lib/ex_pesa/Mpesa/c2b.ex @@ -0,0 +1,107 @@ +defmodule ExPesa.Mpesa.C2B do + @moduledoc """ + C2B M-Pesa API enables Paybill and Buy Goods merchants to integrate to M-Pesa and receive real time payments notifications. + """ + + import ExPesa.Mpesa.MpesaBase + + @doc """ + There are two URLs required for RegisterURL API: Validation URL and Confirmation URL. + For the two URLs, below are some pointers. This will also apply to the Callback URLs used on other APIs: + - Use publicly available (Internet-accessible) IP addresses or domain names. + - Do not use the words MPesa, M-Pesa, Safaricom or any of their variants in either upper or lower cases in your URLs, the system filters these URLs out and blocks them. Of course any Localhost URL will be refused. + - Do not use public URL testers e.g. mockbin or requestbin especially on production, they are also blocked by the API. + + ## Parameters + + attrs: - a map containing: + - `ShortCode` - This is your paybill number/till number, which you expect to receive payments notifications about. + - `ResponseType` - [Cancelled/Completed] This is the default action value that determines what MPesa will do in the scenario that + your endpoint is unreachable or is unable to respond on time. Only two values are allowed: Completed or Cancelled. + Completed means MPesa will automatically complete your transaction, whereas Cancelled means + MPesa will automatically cancel the transaction, in the event MPesa is unable to reach your Validation URL. + - `ConfirmationURL` - [confirmation URL]. + - `ValidationURL` - [validation URL]. + + ## Example + + iex> ExPesa.Mpesa.C2B.registerurl(%{ConfirmationURL: "https://58cb49b30213.ngrok.io/confirmation", ValidationURL: "https://58cb49b30213.ngrok.io/validation", ResponseType: "Completed"}) + {:ok, + %{ + "ConversationID" => "", + "OriginatorCoversationID" => "", + "ResponseDescription" => "success" + } + } + """ + + def registerurl(%{ + ConfirmationURL: confirmation_url, + ValidationURL: validation_url, + ResponseType: response_type + }) do + paybill = Application.get_env(:ex_pesa, :mpesa)[:c2b_short_code] + + payload = %{ + "ShortCode" => paybill, + "ResponseType" => response_type, + "ConfirmationURL" => confirmation_url, + "ValidationURL" => validation_url + } + + make_request("/mpesa/c2b/v1/registerurl", payload) + end + + def registerurl(%{}) do + {:error, "Required Parameter missing, 'ConfirmationURL', 'ValidationURL','ResponseType'"} + end + + @doc """ + This API is used to make payment requests from Client to Business (C2B). + ## Parameters + + attrs: - a map containing: + - `CommandID` - This is a unique identifier of the transaction type: There are two types of these Identifiers: + CustomerPayBillOnline: This is used for Pay Bills shortcodes. + CustomerBuyGoodsOnline: This is used for Buy Goods shortcodes. + - `Amount` - This is the amount being transacted. The parameter expected is a numeric value. + - `Msisdn` - This is the phone number initiating the C2B transaction. + - `BillRefNumber` - This is used on CustomerPayBillOnline option only. + This is where a customer is expected to enter a unique bill identifier, e.g an Account Number. + - `ShortCode` - This is the Short Code receiving the amount being transacted. + You can use the sandbox provided test credentials down below to simulates a payment made from the client phone's STK/SIM Toolkit menu, and enables you to receive the payment requests in real time. + ## Example + + iex> ExPesa.Mpesa.C2B.simulate(%{command_id: "CustomerPayBillOnline", phone_number: "254728833100", amount: 10, bill_reference: "Some Reference" }) + {:ok, + %{ + "ConversationID" => "AG_20200921_00006e93a78f009f7025", + "OriginatorCoversationID" => "9769-145819182-2", + "ResponseDescription" => "Accept the service request successfully." + } + } + """ + + def simulate(%{ + command_id: command_id, + phone_number: phone_number, + amount: amount, + bill_reference: bill_reference + }) do + paybill = Application.get_env(:ex_pesa, :mpesa)[:mpesa_short_code] + + payload = %{ + "ShortCode" => paybill, + "CommandID" => command_id, + "Amount" => amount, + "Msisdn" => phone_number, + "BillRefNumber" => bill_reference + } + + make_request("/mpesa/c2b/v1/simulate", payload) + end + + def simulate(%{}) do + {:error, "Required Parameter missing, 'CommandID','Amount','Msisdn', 'BillRefNumber'"} + end +end diff --git a/test/ex_pesa/Mpesa/c2b_test.exs b/test/ex_pesa/Mpesa/c2b_test.exs new file mode 100644 index 0000000..4bda302 --- /dev/null +++ b/test/ex_pesa/Mpesa/c2b_test.exs @@ -0,0 +1,86 @@ +defmodule ExPesa.Mpesa.C2BTest do + @moduledoc false + + use ExUnit.Case, async: true + + import Tesla.Mock + doctest ExPesa.Mpesa.C2B + + alias ExPesa.Mpesa.C2B + + setup do + mock(fn + + %{ + url: "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials", + method: :get + } -> + %Tesla.Env{ + status: 200, + body: %{ + "access_token" => "SGWcJPtNtYNPGm6uSYR9yPYrAI3Bm", + "expires_in" => "3599" + } + } + %{ + url: "https://sandbox.safaricom.co.ke/mpesa/c2b/v1/registerurl", + method: :post + } -> + %Tesla.Env{ + status: 200, + body: %{ + "ConversationID" => "", + "OriginatorCoversationID" => "", + "ResponseDescription" => "success" + } + } + + + %{url: "https://sandbox.safaricom.co.ke/mpesa/c2b/v1/simulate", method: :post} -> + %Tesla.Env{ + status: 200, + body: + %{ + "ConversationID" => "AG_20200921_00006e93a78f009f7025", + "OriginatorCoversationID" => "9769-145819182-2", + "ResponseDescription" => "Accept the service request successfully." + } + } + end) + + :ok + end + + describe "Mpesa C2B" do + test "registerurl/1 should register a URL" do + register_details = %{ + ConfirmationURL: "https://58cb49b30213.ngrok.io/confirmation", + ValidationURL: "https://58cb49b30213.ngrok.io/validation", + ResponseType: "Completed" + } + + {:ok, result} = C2B.registerurl(register_details) + + assert result["ResponseDescription"] == "success" + end + + + test "request/1 should error out without required parameter" do + {:error, result} = C2B.registerurl(%{}) + "Required Parameter missing, 'ConfirmationURL', 'ValidationURL','ResponseType'" = result + end + + test "simulate/1 should send money successfully" do + {:ok, result} = C2B.simulate(%{command_id: "CustomerPayBillOnline", phone_number: "254728833100", amount: 10, bill_reference: "Some Reference" }) + IO.inspect(result) + + assert result["OriginatorCoversationID"] == "9769-145819182-2" + assert result["ResponseDescription"] == "Accept the service request successfully." + end + + test "validate/1 should error out without required parameter" do + {:error, result} = C2B.simulate(%{}) + "Required Parameter missing, 'CommandID','Amount','Msisdn', 'BillRefNumber'" = result + end + end +end From 0bdb1aa0cef31bd862f0d31df8fd857f0e6e28b4 Mon Sep 17 00:00:00 2001 From: kamalogudah Date: Sat, 26 Sep 2020 01:21:08 +0300 Subject: [PATCH 16/65] Mix format --- config/dev.sample.exs | 3 +- test/ex_pesa/Mpesa/c2b_test.exs | 52 ++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/config/dev.sample.exs b/config/dev.sample.exs index eb89a7e..3f93758 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -7,7 +7,8 @@ config :ex_pesa, consumer_key: "72yw1nun6g1QQPPgOsAObCGSfuimGO7b", consumer_secret: "vRzZiD5RllMLIdLD", mpesa_short_code: "174379", - c2b_short_code: "600247", # had to pass this for registerurl to work for C2B + # had to pass this for registerurl to work for C2B + c2b_short_code: "600247", mpesa_passkey: "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919", mpesa_callback_url: "http://91eb0af5.ngrok.io/api/payment/callback" ] diff --git a/test/ex_pesa/Mpesa/c2b_test.exs b/test/ex_pesa/Mpesa/c2b_test.exs index 4bda302..63692e8 100644 --- a/test/ex_pesa/Mpesa/c2b_test.exs +++ b/test/ex_pesa/Mpesa/c2b_test.exs @@ -10,8 +10,7 @@ defmodule ExPesa.Mpesa.C2BTest do setup do mock(fn - - %{ + %{ url: "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials", method: :get } -> @@ -22,6 +21,7 @@ defmodule ExPesa.Mpesa.C2BTest do "expires_in" => "3599" } } + %{ url: "https://sandbox.safaricom.co.ke/mpesa/c2b/v1/registerurl", method: :post @@ -35,16 +35,14 @@ defmodule ExPesa.Mpesa.C2BTest do } } - - %{url: "https://sandbox.safaricom.co.ke/mpesa/c2b/v1/simulate", method: :post} -> + %{url: "https://sandbox.safaricom.co.ke/mpesa/c2b/v1/simulate", method: :post} -> %Tesla.Env{ status: 200, - body: - %{ - "ConversationID" => "AG_20200921_00006e93a78f009f7025", - "OriginatorCoversationID" => "9769-145819182-2", - "ResponseDescription" => "Accept the service request successfully." - } + body: %{ + "ConversationID" => "AG_20200921_00006e93a78f009f7025", + "OriginatorCoversationID" => "9769-145819182-2", + "ResponseDescription" => "Accept the service request successfully." + } } end) @@ -64,23 +62,29 @@ defmodule ExPesa.Mpesa.C2BTest do assert result["ResponseDescription"] == "success" end + test "request/1 should error out without required parameter" do + {:error, result} = C2B.registerurl(%{}) + "Required Parameter missing, 'ConfirmationURL', 'ValidationURL','ResponseType'" = result + end - test "request/1 should error out without required parameter" do - {:error, result} = C2B.registerurl(%{}) - "Required Parameter missing, 'ConfirmationURL', 'ValidationURL','ResponseType'" = result - end + test "simulate/1 should send money successfully" do + {:ok, result} = + C2B.simulate(%{ + command_id: "CustomerPayBillOnline", + phone_number: "254728833100", + amount: 10, + bill_reference: "Some Reference" + }) - test "simulate/1 should send money successfully" do - {:ok, result} = C2B.simulate(%{command_id: "CustomerPayBillOnline", phone_number: "254728833100", amount: 10, bill_reference: "Some Reference" }) - IO.inspect(result) + IO.inspect(result) - assert result["OriginatorCoversationID"] == "9769-145819182-2" - assert result["ResponseDescription"] == "Accept the service request successfully." - end + assert result["OriginatorCoversationID"] == "9769-145819182-2" + assert result["ResponseDescription"] == "Accept the service request successfully." + end - test "validate/1 should error out without required parameter" do - {:error, result} = C2B.simulate(%{}) - "Required Parameter missing, 'CommandID','Amount','Msisdn', 'BillRefNumber'" = result - end + test "validate/1 should error out without required parameter" do + {:error, result} = C2B.simulate(%{}) + "Required Parameter missing, 'CommandID','Amount','Msisdn', 'BillRefNumber'" = result + end end end From 6611a9cbac054bfd34e64d805f5c03b23d862953 Mon Sep 17 00:00:00 2001 From: kamalogudah Date: Sat, 26 Sep 2020 11:11:34 +0300 Subject: [PATCH 17/65] Use C2B shortcode for simulate --- lib/ex_pesa/Mpesa/c2b.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ex_pesa/Mpesa/c2b.ex b/lib/ex_pesa/Mpesa/c2b.ex index c3e48a5..9ea014d 100644 --- a/lib/ex_pesa/Mpesa/c2b.ex +++ b/lib/ex_pesa/Mpesa/c2b.ex @@ -88,7 +88,7 @@ defmodule ExPesa.Mpesa.C2B do amount: amount, bill_reference: bill_reference }) do - paybill = Application.get_env(:ex_pesa, :mpesa)[:mpesa_short_code] + paybill = Application.get_env(:ex_pesa, :mpesa)[:c2b_short_code] payload = %{ "ShortCode" => paybill, From 4f4fb5ede2c3a30615de581232f091f60e923711 Mon Sep 17 00:00:00 2001 From: Magak Emmanuel Date: Sat, 26 Sep 2020 14:43:40 +0300 Subject: [PATCH 18/65] Update greetings.yml --- .github/workflows/greetings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index 74f59f9..c00a391 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -1,6 +1,6 @@ name: Greetings -on: [pull_request, issues] +on: [pull_request_target, issues] jobs: greeting: From 7068b5fe531e69335f5431ac81c48bd6e61cbfac Mon Sep 17 00:00:00 2001 From: lenileiro Date: Wed, 23 Sep 2020 18:52:49 +0300 Subject: [PATCH 19/65] bump up hackney version --- mix.exs | 2 +- mix.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mix.exs b/mix.exs index 917bbd2..6a6b130 100644 --- a/mix.exs +++ b/mix.exs @@ -49,7 +49,7 @@ defmodule ExPesa.MixProject do defp deps do [ {:tesla, "~> 1.3.0"}, - {:hackney, "~> 1.15.2"}, + {:hackney, "~> 1.16.0"}, {:jason, ">= 1.0.0"}, {:timex, "~> 3.6.2"}, {:ex_doc, "~> 0.21", only: :dev, runtime: false} diff --git a/mix.lock b/mix.lock index 66bf2d0..5c81eb1 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, + "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "cowlib": {:hex, :cowlib, "2.6.0", "8aa629f81a0fc189f261dc98a42243fa842625feea3c7ec56c48f4ccdb55490f", [:rebar3], [], "hexpm", "45a1a08e05e4c66f2af665295955e337d52c2d33b1f1cf24d353cadeddf34992"}, "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, @@ -7,9 +7,9 @@ "ex_doc_makeup": {:hex, :ex_doc_makeup, "0.1.2", "88d9a9c4be29a8486c3aa410927706c2da0c4e22fcc135e49dceb15f17510de0", [:mix], [{:ex_doc, ">= 0.18.1", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:makeup_elixir, ">= 0.3.1", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "7cc49a1634112799252ce540e85eb321734d67a75c76db4fd0cba646fef37574"}, "gettext": {:hex, :gettext, "0.18.1", "89e8499b051c7671fa60782faf24409b5d2306aa71feb43d79648a8bc63d0522", [:mix], [], "hexpm", "e70750c10a5f88cb8dc026fc28fa101529835026dec4a06dba3b614f2a99c7a9"}, "gun": {:hex, :gun, "1.3.2", "542064cbb9f613650b8a8100b3a927505f364fbe198b7a5a112868ff43f3e477", [:rebar3], [{:cowlib, "~> 2.6.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "ba323f0a5fd8abac379a3e1fe6d8ce570c4a12c7fd1c68f4994b53447918e462"}, - "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"}, + "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"}, - "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, + "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, @@ -19,9 +19,9 @@ "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "tesla": {:hex, :tesla, "1.3.3", "26ae98627af5c406584aa6755ab5fc96315d70d69a24dd7f8369cfcb75094a45", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2648f1c276102f9250299e0b7b57f3071c67827349d9173f34c281756a1b124c"}, "timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"}, "tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, } From cc0327c45b2131042abaa8ecb2f4e8328b8fc704 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sun, 27 Sep 2020 13:20:35 +0300 Subject: [PATCH 20/65] b2b implementation --- config/dev.sample.exs | 13 +++++- lib/ex_pesa/Mpesa/b2b.ex | 87 ++++++++++++++++++++++++++++++++++++++++ lib/ex_pesa/Mpesa/stk.ex | 4 +- lib/ex_pesa/util.ex | 81 +++++++++++++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 lib/ex_pesa/Mpesa/b2b.ex diff --git a/config/dev.sample.exs b/config/dev.sample.exs index 3f93758..7b371b3 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -10,5 +10,16 @@ config :ex_pesa, # had to pass this for registerurl to work for C2B c2b_short_code: "600247", mpesa_passkey: "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919", - mpesa_callback_url: "http://91eb0af5.ngrok.io/api/payment/callback" + mpesa_callback_url: "http://91eb0af5.ngrok.io/api/payment/callback", + cert: + "-----BEGIN CERTIFICATE-----\nMIIGKzCCBROgAwIBAgIQDL7NH8cxSdUpl0ihH0A1wTANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwODI3MDAwMDAwWhcN\nMTkwNDA0MTIwMDAwWjBuMQswCQYDVQQGEwJLRTEQMA4GA1UEBxMHTmFpcm9iaTEW\nMBQGA1UEChMNU2FmYXJpY29tIFBMQzETMBEGA1UECxMKRGlnaXRhbCBJVDEgMB4G\nA1UEAxMXc2FuZGJveC5zYWZhcmljb20uY28ua2UwggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQC78yeC/wLoZY6TJeqc4g/9eAKIpeCwEsjX09pD8ZxAGXqT\nOi7ssdIGJBPmJZNeEVyf8ocFhisCuLngJ9Z5e/AvH52PhrEFmVu2D03zSf4C+rhZ\nndEKP6G79pUAb/bemOliU9zM8xYYkpCRzPWUzk6zSDarg0ZDLw5FrtZj/VJ9YEDL\nWGgAfwExEgSN3wjyUlJ2UwI3wqQXLka0VNFWoZxUH5j436gbSWRIL6NJUmrq8V8S\naTEPz3eJHj3NOToDu245c7VKdF/KExyZjRjD2p5I+Aip80TXzKlZj6DjMb3DlfXF\nHsnu0+1uJE701mvKX7BiscxKr8tCRphL63as4dqvAgMBAAGjggLkMIIC4DAfBgNV\nHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUzZmY7ZORLw9w\nqRbAQN5m9lJ28qMwIgYDVR0RBBswGYIXc2FuZGJveC5zYWZhcmljb20uY28ua2Uw\nDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBr\nBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1z\naGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Et\nc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcC\nARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYB\nBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w\nRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy\ndFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwCQYDVR0TBAIwADCCAQUGCisGAQQB1nkC\nBAIEgfYEgfMA8QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB\nZXs1FvEAAAQDAEcwRQIgBzVMkm7SNprjJ1GBqiXIc9rNzY+y7gt6s/O02oMkyFoC\nIQDBuThGlpmUKpeZoHhK6HGwB4jDMIecmKaOcMS18R2jxwB3AId1v+dZfPiMQ5lf\nvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZXs1F8IAAAQDAEgwRgIhAIRq2XFiC+RS\nuDCYq8ICJg0QafSV+e9BLpJnElEdaSjiAiEAyiiW4vxwv4cWcAXE6FAipctyUBs6\nbE5QyaCnmNpoDiQwDQYJKoZIhvcNAQELBQADggEBAB0YoWve9Sxhb0PBS3Hc46Rf\na7H1jhHuwE+UyscSQsdJdk8uPAgDuKRZMvJPGEaCkNHm36NfcaXXFjPOl7LI1d1a\n9zqSP0xeZBI6cF0x96WuQGrI9/WR2tfxjmaUSp8a/aJ6n+tZA28eJZNPrIaMm+6j\ngh7AkKnqcf+g8F/MvCCVdNAiVMdz6UpCscf6BRPHNZ5ifvChGh7aUKjrVLLuF4Ls\nHE05qm6HNyV5eTa6wvcbc4ewguN1UDZvPWetSyfBk10Wbpor4znQ4TJ3Y9uCvsJH\n41ldblDvZZ2z4kB2UYQ7iBkPlJSxSOaFgW/GGDXq49sz/995xzhVITHxh2SdLkI=\n-----END CERTIFICATE-----\n", + b2b: [ + short_code: "600247", + initiator_name: "John Doe", + password: "Safaricom133", + timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", + security_credential: + "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + ] ] diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex new file mode 100644 index 0000000..0ac15ee --- /dev/null +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -0,0 +1,87 @@ +defmodule ExPesa.Mpesa.B2B do + @moduledoc """ + This API enables Business to Business (B2B) transactions between a business and another business. + Use of this API requires a valid and verified B2B M-Pesa short code for the business initiating the transaction and the both businesses involved in the transaction. + """ + import ExPesa.Mpesa.MpesaBase + import ExPesa.Util + + @doc """ + This API enables Business to Business (B2B) transactions between a business and another business. Use of this API requires a valid and verified B2B M-Pesa short code for the business initiating the transaction and the both businesses involved in the transaction. + + ## Configuration + + Add below config to dev.exs / prod.exs files (at this stage after STK, the config keys should be there) + This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#lipa-na-m-pesa-online-query-request + + `config.exs` + ```elixir + config :ex_pesa, + mpesa: [ + b2b: [ + short_code: "", + initiator_name: "", + password: "", + timeout_url: "", + result_url: "", + security_credential: "" + ] + ] + ``` + + ## Parameters + + attrs: - a map containing: + - `command_id` - Unique command for each transaction type, possible values are: BusinessPayBill, MerchantToMerchantTransfer, MerchantTransferFromMerchantToWorking, MerchantServicesMMFAccountTransfer, AgencyFloatAdvance + - `amount` - The amount being transacted. + - `receiver_party` - Organization’s short code receiving the funds being transacted + - `remarks` - Comments that are sent along with the transaction. + - `account_reference` - Account Reference mandatory for “BusinessPaybill” CommandID. + + ## Example + + iex> ExPesa.Mpesa.B2B.request(%{command_id: "BusinessPayBill", amount: 10500, receiver_party: 600000, remarks: "B2B Request", account_reference: "BILL PAYMENT"}) + {:ok, + %{ + "ConversationID" => "AG_20200927_00007d4c98884c889b25", + "OriginatorConversationID" => "27274-37744848-4", + "ResponseCode" => "0", + "ResponseDescription" => "Accept the service request successfully." + }} + """ + + def request(%{ + command_id: command_id, + amount: amount, + receiver_party: receiver_party, + remarks: remarks, + account_reference: account_reference + }) do + security_credential = + case Application.get_env(:ex_pesa, :mpesa)[:b2b][:security_credential] do + nil -> + password = Application.get_env(:ex_pesa, :mpesa)[:b2b][:password] + get_security_credential(%{Password: password}) + + credential -> + credential + end + + payload = %{ + "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:initiator_name], + "SecurityCredential" => security_credential, + "CommandID" => command_id, + "Amount" => amount, + "PartyA" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:short_code], + "SenderIdentifierType" => 4, + "PartyB" => receiver_party, + "RecieverIdentifierType" => 4, + "Remarks" => remarks, + "AccountReference" => account_reference, + "QueueTimeOutURL" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:timeout_url], + "ResultURL" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:result_url] + } + + make_request("/mpesa/b2b/v1/paymentrequest", payload) + end +end diff --git a/lib/ex_pesa/Mpesa/stk.ex b/lib/ex_pesa/Mpesa/stk.ex index d9103d7..45e3120 100644 --- a/lib/ex_pesa/Mpesa/stk.ex +++ b/lib/ex_pesa/Mpesa/stk.ex @@ -10,7 +10,7 @@ defmodule ExPesa.Mpesa.Stk do ## Configuration - Add below config to dev.exs / prod.exs files + Add below config to dev.exs / prod.exs files This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#lipa-na-m-pesa-online-payment `config.exs` @@ -111,7 +111,7 @@ defmodule ExPesa.Mpesa.Stk do "MerchantRequestID" => "11130-78831728-4", "ResponseCode" => "0", "ResponseDescription" => "The service request has been accepted successsfully", - "ResultCode" => "1032", + "ResultCode" => "1032", "ResultDesc" => "Request cancelled by user" } } diff --git a/lib/ex_pesa/util.ex b/lib/ex_pesa/util.ex index 000cfb5..260bcc0 100644 --- a/lib/ex_pesa/util.ex +++ b/lib/ex_pesa/util.ex @@ -1,4 +1,21 @@ +defmodule ExPesa.Util.Records do + @moduledoc """ + Extracts all records information from an Erlang file. + Returns a keyword list of {record_name, fields} tuples where record_name is the name of an extracted record and fields is a list of {field, value} tuples representing the fields for that record. + """ + + require Record + import Record, only: [defrecord: 2, extract: 2] + + @public_key "public_key/include/public_key.hrl" + + defrecord :Certificate, extract(:Certificate, from_lib: @public_key) + defrecord :TBSCertificate, extract(:TBSCertificate, from_lib: @public_key) + defrecord :SubjectPublicKeyInfo, extract(:SubjectPublicKeyInfo, from_lib: @public_key) +end + defmodule ExPesa.Util do + require ExPesa.Util.Records @moduledoc false @doc false @@ -10,4 +27,68 @@ defmodule ExPesa.Util do true -> sandbox_url end end + + @doc """ + Security Credentials + + M-Pesa Core authenticates a transaction by decrypting the security credentials. Security credentials are generated by encrypting the base64 encoded initiator password with M-Pesa’s public key, a X509 certificate. + + The algorithm for generating security credentials is as follows: + Write the unencrypted password into a byte array. + + Encrypt the array with the M-Pesa public key certificate. Use the RSA algorithm, and use PKCS #1.5 padding (not OAEP), and add the result to the encrypted stream. + + Convert the resulting encrypted byte array into a string using base64 encoding. The resulting base64 encoded string is the security credential. + + ## Parameters + + attrs:- a map containing: + - `CertFile` - This is the M-Pesa public key certificate used to encrypt your plain password. + There are 2 types of certificates. + - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer + - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer + + - `Password` - This is a plain unencrypted password. + Environment + - production - set password from the organization portal. + - sandbox - use anything as your sandbox password. + + ## Example + iex> certfile = "-----BEGIN CERTIFICATE-----\nMIIGKzCCBROgAwIBAgIQDL7NH8cxSdUpl0ihH0A1wTANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwODI3MDAwMDAwWhcN\nMTkwNDA0MTIwMDAwWjBuMQswCQYDVQQGEwJLRTEQMA4GA1UEBxMHTmFpcm9iaTEW\nMBQGA1UEChMNU2FmYXJpY29tIFBMQzETMBEGA1UECxMKRGlnaXRhbCBJVDEgMB4G\nA1UEAxMXc2FuZGJveC5zYWZhcmljb20uY28ua2UwggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQC78yeC/wLoZY6TJeqc4g/9eAKIpeCwEsjX09pD8ZxAGXqT\nOi7ssdIGJBPmJZNeEVyf8ocFhisCuLngJ9Z5e/AvH52PhrEFmVu2D03zSf4C+rhZ\nndEKP6G79pUAb/bemOliU9zM8xYYkpCRzPWUzk6zSDarg0ZDLw5FrtZj/VJ9YEDL\nWGgAfwExEgSN3wjyUlJ2UwI3wqQXLka0VNFWoZxUH5j436gbSWRIL6NJUmrq8V8S\naTEPz3eJHj3NOToDu245c7VKdF/KExyZjRjD2p5I+Aip80TXzKlZj6DjMb3DlfXF\nHsnu0+1uJE701mvKX7BiscxKr8tCRphL63as4dqvAgMBAAGjggLkMIIC4DAfBgNV\nHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUzZmY7ZORLw9w\nqRbAQN5m9lJ28qMwIgYDVR0RBBswGYIXc2FuZGJveC5zYWZhcmljb20uY28ua2Uw\nDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBr\nBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1z\naGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Et\nc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcC\nARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYB\nBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w\nRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy\ndFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwCQYDVR0TBAIwADCCAQUGCisGAQQB1nkC\nBAIEgfYEgfMA8QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB\nZXs1FvEAAAQDAEcwRQIgBzVMkm7SNprjJ1GBqiXIc9rNzY+y7gt6s/O02oMkyFoC\nIQDBuThGlpmUKpeZoHhK6HGwB4jDMIecmKaOcMS18R2jxwB3AId1v+dZfPiMQ5lf\nvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZXs1F8IAAAQDAEgwRgIhAIRq2XFiC+RS\nuDCYq8ICJg0QafSV+e9BLpJnElEdaSjiAiEAyiiW4vxwv4cWcAXE6FAipctyUBs6\nbE5QyaCnmNpoDiQwDQYJKoZIhvcNAQELBQADggEBAB0YoWve9Sxhb0PBS3Hc46Rf\na7H1jhHuwE+UyscSQsdJdk8uPAgDuKRZMvJPGEaCkNHm36NfcaXXFjPOl7LI1d1a\n9zqSP0xeZBI6cF0x96WuQGrI9/WR2tfxjmaUSp8a/aJ6n+tZA28eJZNPrIaMm+6j\ngh7AkKnqcf+g8F/MvCCVdNAiVMdz6UpCscf6BRPHNZ5ifvChGh7aUKjrVLLuF4Ls\nHE05qm6HNyV5eTa6wvcbc4ewguN1UDZvPWetSyfBk10Wbpor4znQ4TJ3Y9uCvsJH\n41ldblDvZZ2z4kB2UYQ7iBkPlJSxSOaFgW/GGDXq49sz/995xzhVITHxh2SdLkI=\n-----END CERTIFICATE-----\n" + iex> password = "Safaricom133" + iex> ExPesa.Util.generate_security_credential(%{CertFile: certfile, Password: password}) + """ + + def generate_security_credential(%{CertFile: certfile, Password: password}) do + cert_text = certfile |> String.trim() + + # 1) Decode the certificate. + [pem_entry] = :public_key.pem_decode(cert_text) + cert_decoded = :public_key.pem_entry_decode(pem_entry) + + # 2) Extract public key. + plk = + cert_decoded + |> ExPesa.Util.Records."Certificate"(:tbsCertificate) + |> ExPesa.Util.Records."TBSCertificate"(:subjectPublicKeyInfo) + |> ExPesa.Util.Records."SubjectPublicKeyInfo"(:subjectPublicKey) + + public_key = :public_key.der_decode(:RSAPublicKey, plk) + + # 3) Encrypt the plain password text + ciphertext = + :public_key.encrypt_public(password, public_key, [{:rsa_pad, :rsa_pkcs1_padding}]) + + # 4) Base64 encode and return the result + :base64.encode(ciphertext) + end + + def generate_security_credential(%{}) do + nil + end + + def get_security_credential(%{Password: password}) do + cert = Application.get_env(:ex_pesa, :mpesa)[:cert] + ExPesa.Util.generate_security_credential(%{CertFile: cert, Password: password}) + end end From dff796c3384c9ee9c93e0ce9375fe2de5b8ad796 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sun, 27 Sep 2020 21:17:49 +0300 Subject: [PATCH 21/65] modified b2b docs --- lib/ex_pesa/Mpesa/b2b.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index 0ac15ee..48ea19e 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -11,8 +11,8 @@ defmodule ExPesa.Mpesa.B2B do ## Configuration - Add below config to dev.exs / prod.exs files (at this stage after STK, the config keys should be there) - This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#lipa-na-m-pesa-online-query-request + Add below config to dev.exs / prod.exs files + This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#b2b-api `config.exs` ```elixir From bfb802e2519f5bf2d38a82ddfee2a3807877106e Mon Sep 17 00:00:00 2001 From: lenileiro Date: Mon, 28 Sep 2020 01:48:11 +0300 Subject: [PATCH 22/65] better error handling --- lib/ex_pesa/Mpesa/b2b.ex | 90 ++++++++++++++++++++++++++-------------- lib/ex_pesa/util.ex | 27 ++++++------ 2 files changed, 72 insertions(+), 45 deletions(-) diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index 48ea19e..e20c5fa 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -8,38 +8,67 @@ defmodule ExPesa.Mpesa.B2B do @doc """ This API enables Business to Business (B2B) transactions between a business and another business. Use of this API requires a valid and verified B2B M-Pesa short code for the business initiating the transaction and the both businesses involved in the transaction. - ## Configuration - Add below config to dev.exs / prod.exs files This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#b2b-api - - `config.exs` - ```elixir - config :ex_pesa, - mpesa: [ - b2b: [ - short_code: "", - initiator_name: "", - password: "", - timeout_url: "", - result_url: "", - security_credential: "" - ] - ] - ``` - + #### B2B - Configuration Parameters + - `initiator` - This is the credential/username used to authenticate the transaction request. + Environment + - production - + - create a user with api access method (access channel) + - Enter user name + - assign business manager role and B2B ORG API initiator role. + Use the username from your notifitation channel (SMS) + - sandbox - use your own custom username + - `timeout_url' - The path that stores information of time out transactions.it should be properly validated to make sure that it contains the port, URI and domain name or publicly available IP. + - `result_url` - The path that receives results from M-Pesa it should be properly validated to make sure that it contains the port, URI and domain name or publicly available IP. + - `security credential` - To generate security_credential, head over to https://developer.safaricom.co.ke/test_credentials, then Initiator Security Password for your environment + - Test - use the above test security credential + - Production - use the actual production security credential + `config.exs` + ```elixir + config :ex_pesa, + mpesa: [ + b2b: [ + short_code: "", + initiator_name: "", + timeout_url: "", + result_url: "", + security_credential: "" + ] + ] + ``` + Alternatively, generate security credential using certificate + `cert` - This is the M-Pesa public key certificate used to encrypt your plain password. + There are 2 types of certificates. + - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer + - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer + `password` - This is a plain unencrypted password. + Environment + - production - set password from the organization portal. + - sandbox - use your own custom password + `config.exs` + ```elixir + config :ex_pesa, + mpesa: [ + cert: "" + b2b: [ + short_code: "", + initiator_name: "", + password: "", + timeout_url: "", + result_url: "" + ] + ] + ``` ## Parameters - attrs: - a map containing: - `command_id` - Unique command for each transaction type, possible values are: BusinessPayBill, MerchantToMerchantTransfer, MerchantTransferFromMerchantToWorking, MerchantServicesMMFAccountTransfer, AgencyFloatAdvance - `amount` - The amount being transacted. - `receiver_party` - Organization’s short code receiving the funds being transacted - `remarks` - Comments that are sent along with the transaction. - `account_reference` - Account Reference mandatory for “BusinessPaybill” CommandID. - ## Example - iex> ExPesa.Mpesa.B2B.request(%{command_id: "BusinessPayBill", amount: 10500, receiver_party: 600000, remarks: "B2B Request", account_reference: "BILL PAYMENT"}) {:ok, %{ @@ -50,23 +79,20 @@ defmodule ExPesa.Mpesa.B2B do }} """ - def request(%{ + def request(params) do + case get_security_credential_for(:b2b) do + nil -> {:error, "cannot generate security_credential due to missing configuration fields"} + security_credential -> b2b_request(security_credential, params) + end + end + + def b2b_request(security_credential, %{ command_id: command_id, amount: amount, receiver_party: receiver_party, remarks: remarks, account_reference: account_reference }) do - security_credential = - case Application.get_env(:ex_pesa, :mpesa)[:b2b][:security_credential] do - nil -> - password = Application.get_env(:ex_pesa, :mpesa)[:b2b][:password] - get_security_credential(%{Password: password}) - - credential -> - credential - end - payload = %{ "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:initiator_name], "SecurityCredential" => security_credential, diff --git a/lib/ex_pesa/util.ex b/lib/ex_pesa/util.ex index 260bcc0..615ef19 100644 --- a/lib/ex_pesa/util.ex +++ b/lib/ex_pesa/util.ex @@ -30,37 +30,31 @@ defmodule ExPesa.Util do @doc """ Security Credentials - M-Pesa Core authenticates a transaction by decrypting the security credentials. Security credentials are generated by encrypting the base64 encoded initiator password with M-Pesa’s public key, a X509 certificate. - The algorithm for generating security credentials is as follows: Write the unencrypted password into a byte array. - Encrypt the array with the M-Pesa public key certificate. Use the RSA algorithm, and use PKCS #1.5 padding (not OAEP), and add the result to the encrypted stream. - Convert the resulting encrypted byte array into a string using base64 encoding. The resulting base64 encoded string is the security credential. - ## Parameters - attrs:- a map containing: - `CertFile` - This is the M-Pesa public key certificate used to encrypt your plain password. There are 2 types of certificates. - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer - - `Password` - This is a plain unencrypted password. Environment - production - set password from the organization portal. - sandbox - use anything as your sandbox password. - ## Example iex> certfile = "-----BEGIN CERTIFICATE-----\nMIIGKzCCBROgAwIBAgIQDL7NH8cxSdUpl0ihH0A1wTANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwODI3MDAwMDAwWhcN\nMTkwNDA0MTIwMDAwWjBuMQswCQYDVQQGEwJLRTEQMA4GA1UEBxMHTmFpcm9iaTEW\nMBQGA1UEChMNU2FmYXJpY29tIFBMQzETMBEGA1UECxMKRGlnaXRhbCBJVDEgMB4G\nA1UEAxMXc2FuZGJveC5zYWZhcmljb20uY28ua2UwggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQC78yeC/wLoZY6TJeqc4g/9eAKIpeCwEsjX09pD8ZxAGXqT\nOi7ssdIGJBPmJZNeEVyf8ocFhisCuLngJ9Z5e/AvH52PhrEFmVu2D03zSf4C+rhZ\nndEKP6G79pUAb/bemOliU9zM8xYYkpCRzPWUzk6zSDarg0ZDLw5FrtZj/VJ9YEDL\nWGgAfwExEgSN3wjyUlJ2UwI3wqQXLka0VNFWoZxUH5j436gbSWRIL6NJUmrq8V8S\naTEPz3eJHj3NOToDu245c7VKdF/KExyZjRjD2p5I+Aip80TXzKlZj6DjMb3DlfXF\nHsnu0+1uJE701mvKX7BiscxKr8tCRphL63as4dqvAgMBAAGjggLkMIIC4DAfBgNV\nHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUzZmY7ZORLw9w\nqRbAQN5m9lJ28qMwIgYDVR0RBBswGYIXc2FuZGJveC5zYWZhcmljb20uY28ua2Uw\nDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBr\nBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1z\naGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Et\nc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcC\nARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYB\nBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w\nRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy\ndFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwCQYDVR0TBAIwADCCAQUGCisGAQQB1nkC\nBAIEgfYEgfMA8QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB\nZXs1FvEAAAQDAEcwRQIgBzVMkm7SNprjJ1GBqiXIc9rNzY+y7gt6s/O02oMkyFoC\nIQDBuThGlpmUKpeZoHhK6HGwB4jDMIecmKaOcMS18R2jxwB3AId1v+dZfPiMQ5lf\nvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZXs1F8IAAAQDAEgwRgIhAIRq2XFiC+RS\nuDCYq8ICJg0QafSV+e9BLpJnElEdaSjiAiEAyiiW4vxwv4cWcAXE6FAipctyUBs6\nbE5QyaCnmNpoDiQwDQYJKoZIhvcNAQELBQADggEBAB0YoWve9Sxhb0PBS3Hc46Rf\na7H1jhHuwE+UyscSQsdJdk8uPAgDuKRZMvJPGEaCkNHm36NfcaXXFjPOl7LI1d1a\n9zqSP0xeZBI6cF0x96WuQGrI9/WR2tfxjmaUSp8a/aJ6n+tZA28eJZNPrIaMm+6j\ngh7AkKnqcf+g8F/MvCCVdNAiVMdz6UpCscf6BRPHNZ5ifvChGh7aUKjrVLLuF4Ls\nHE05qm6HNyV5eTa6wvcbc4ewguN1UDZvPWetSyfBk10Wbpor4znQ4TJ3Y9uCvsJH\n41ldblDvZZ2z4kB2UYQ7iBkPlJSxSOaFgW/GGDXq49sz/995xzhVITHxh2SdLkI=\n-----END CERTIFICATE-----\n" iex> password = "Safaricom133" iex> ExPesa.Util.generate_security_credential(%{CertFile: certfile, Password: password}) """ - def generate_security_credential(%{CertFile: certfile, Password: password}) do - cert_text = certfile |> String.trim() + def generate_security_credential(%{CertFile: certfile, Password: password}) + when certfile != nil and password != nil do + cert_text = + certfile |> String.trim() |> String.split(~r{\n *}, trim: true) |> Enum.join("\n") # 1) Decode the certificate. [pem_entry] = :public_key.pem_decode(cert_text) @@ -87,8 +81,15 @@ defmodule ExPesa.Util do nil end - def get_security_credential(%{Password: password}) do - cert = Application.get_env(:ex_pesa, :mpesa)[:cert] - ExPesa.Util.generate_security_credential(%{CertFile: cert, Password: password}) + def get_security_credential_for(key) do + case Application.get_env(:ex_pesa, :mpesa)[key][:security_credential] do + nil -> + cert = Application.get_env(:ex_pesa, :mpesa)[:cert] + password = Application.get_env(:ex_pesa, :mpesa)[key][:password] + generate_security_credential(%{CertFile: cert, Password: password}) + + credential -> + credential + end end end From f32ffb83031b2338faa03f70967fde6486cc153d Mon Sep 17 00:00:00 2001 From: lenileiro Date: Mon, 28 Sep 2020 15:20:56 +0300 Subject: [PATCH 23/65] added more Security Credentials documentation --- lib/ex_pesa/util.ex | 115 ++++++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 37 deletions(-) diff --git a/lib/ex_pesa/util.ex b/lib/ex_pesa/util.ex index 615ef19..95b41e2 100644 --- a/lib/ex_pesa/util.ex +++ b/lib/ex_pesa/util.ex @@ -1,21 +1,4 @@ -defmodule ExPesa.Util.Records do - @moduledoc """ - Extracts all records information from an Erlang file. - Returns a keyword list of {record_name, fields} tuples where record_name is the name of an extracted record and fields is a list of {field, value} tuples representing the fields for that record. - """ - - require Record - import Record, only: [defrecord: 2, extract: 2] - - @public_key "public_key/include/public_key.hrl" - - defrecord :Certificate, extract(:Certificate, from_lib: @public_key) - defrecord :TBSCertificate, extract(:TBSCertificate, from_lib: @public_key) - defrecord :SubjectPublicKeyInfo, extract(:SubjectPublicKeyInfo, from_lib: @public_key) -end - defmodule ExPesa.Util do - require ExPesa.Util.Records @moduledoc false @doc false @@ -29,22 +12,83 @@ defmodule ExPesa.Util do end @doc """ - Security Credentials + Security Credentials read more https://developer.safaricom.co.ke/docs#security-credentials M-Pesa Core authenticates a transaction by decrypting the security credentials. Security credentials are generated by encrypting the base64 encoded initiator password with M-Pesa’s public key, a X509 certificate. - The algorithm for generating security credentials is as follows: - Write the unencrypted password into a byte array. - Encrypt the array with the M-Pesa public key certificate. Use the RSA algorithm, and use PKCS #1.5 padding (not OAEP), and add the result to the encrypted stream. - Convert the resulting encrypted byte array into a string using base64 encoding. The resulting base64 encoded string is the security credential. - ## Parameters - attrs:- a map containing: - - `CertFile` - This is the M-Pesa public key certificate used to encrypt your plain password. - There are 2 types of certificates. - - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer - - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer - - `Password` - This is a plain unencrypted password. - Environment - - production - set password from the organization portal. - - sandbox - use anything as your sandbox password. + + The algorithm for generating security credentials is as follows: + Write the unencrypted password into a byte array. + Encrypt the array with the M-Pesa public key certificate. Use the RSA algorithm, and use PKCS #1.5 padding (not OAEP), and add the result to the encrypted stream. + Convert the resulting encrypted byte array into a string using base64 encoding. The resulting base64 encoded string is the security credential. + + Impementation Examples + PHP + + + Node Js + module.exports = (certPath, shortCodeSecurityCredential) => { + const bufferToEncrypt = Buffer.from(shortCodeSecurityCredential) + const data = fs.readFileSync(path.resolve(certPath)) + const privateKey = String(data) + const encrypted = crypto.publicEncrypt({ + key: privateKey, + padding: crypto.constants.RSA_PKCS1_PADDING + }, bufferToEncrypt) + const securityCredential = encrypted.toString('base64') + return securityCredential + } + + JAVA + + // Function to encrypt the initiator credentials + public static String encryptInitiatorPassword(String securityCertificate, String password) { + String encryptedPassword = "YOUR_INITIATOR_PASSWORD"; + try { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + byte[] input = password.getBytes(); + + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC"); + FileInputStream fin = new FileInputStream(new File(securityCertificate)); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate certificate = (X509Certificate) cf.generateCertificate(fin); + PublicKey pk = certificate.getPublicKey(); + cipher.init(Cipher.ENCRYPT_MODE, pk); + + byte[] cipherText = cipher.doFinal(input); + + // Convert the resulting encrypted byte array into a string using base64 encoding + encryptedPassword = Base64.encode(cipherText); + } + } + + Python + + from M2Crypto import RSA, X509 + from base64 import b64encode + + INITIATOR_PASS = "YOUR_PASSWORD" + CERTIFICATE_FILE = "PATH_TO_CERTIFICATE_FILE" + + def encryptInitiatorPassword(): + cert_file = open(CERTIFICATE_FILE, 'r') + cert_data = cert_file.read() #read certificate file + cert_file.close() + + cert = X509.load_cert_string(cert_data) + #pub_key = X509.load_cert_string(cert_data) + pub_key = cert.get_pubkey() + rsa_key = pub_key.get_rsa() + cipher = rsa_key.public_encrypt(INITIATOR_PASS, RSA.pkcs1_padding) + return b64encode(cipher) + + print encryptInitiatorPassword() + ## Example iex> certfile = "-----BEGIN CERTIFICATE-----\nMIIGKzCCBROgAwIBAgIQDL7NH8cxSdUpl0ihH0A1wTANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwODI3MDAwMDAwWhcN\nMTkwNDA0MTIwMDAwWjBuMQswCQYDVQQGEwJLRTEQMA4GA1UEBxMHTmFpcm9iaTEW\nMBQGA1UEChMNU2FmYXJpY29tIFBMQzETMBEGA1UECxMKRGlnaXRhbCBJVDEgMB4G\nA1UEAxMXc2FuZGJveC5zYWZhcmljb20uY28ua2UwggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQC78yeC/wLoZY6TJeqc4g/9eAKIpeCwEsjX09pD8ZxAGXqT\nOi7ssdIGJBPmJZNeEVyf8ocFhisCuLngJ9Z5e/AvH52PhrEFmVu2D03zSf4C+rhZ\nndEKP6G79pUAb/bemOliU9zM8xYYkpCRzPWUzk6zSDarg0ZDLw5FrtZj/VJ9YEDL\nWGgAfwExEgSN3wjyUlJ2UwI3wqQXLka0VNFWoZxUH5j436gbSWRIL6NJUmrq8V8S\naTEPz3eJHj3NOToDu245c7VKdF/KExyZjRjD2p5I+Aip80TXzKlZj6DjMb3DlfXF\nHsnu0+1uJE701mvKX7BiscxKr8tCRphL63as4dqvAgMBAAGjggLkMIIC4DAfBgNV\nHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUzZmY7ZORLw9w\nqRbAQN5m9lJ28qMwIgYDVR0RBBswGYIXc2FuZGJveC5zYWZhcmljb20uY28ua2Uw\nDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBr\nBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1z\naGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Et\nc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcC\nARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYB\nBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w\nRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy\ndFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwCQYDVR0TBAIwADCCAQUGCisGAQQB1nkC\nBAIEgfYEgfMA8QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB\nZXs1FvEAAAQDAEcwRQIgBzVMkm7SNprjJ1GBqiXIc9rNzY+y7gt6s/O02oMkyFoC\nIQDBuThGlpmUKpeZoHhK6HGwB4jDMIecmKaOcMS18R2jxwB3AId1v+dZfPiMQ5lf\nvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZXs1F8IAAAQDAEgwRgIhAIRq2XFiC+RS\nuDCYq8ICJg0QafSV+e9BLpJnElEdaSjiAiEAyiiW4vxwv4cWcAXE6FAipctyUBs6\nbE5QyaCnmNpoDiQwDQYJKoZIhvcNAQELBQADggEBAB0YoWve9Sxhb0PBS3Hc46Rf\na7H1jhHuwE+UyscSQsdJdk8uPAgDuKRZMvJPGEaCkNHm36NfcaXXFjPOl7LI1d1a\n9zqSP0xeZBI6cF0x96WuQGrI9/WR2tfxjmaUSp8a/aJ6n+tZA28eJZNPrIaMm+6j\ngh7AkKnqcf+g8F/MvCCVdNAiVMdz6UpCscf6BRPHNZ5ifvChGh7aUKjrVLLuF4Ls\nHE05qm6HNyV5eTa6wvcbc4ewguN1UDZvPWetSyfBk10Wbpor4znQ4TJ3Y9uCvsJH\n41ldblDvZZ2z4kB2UYQ7iBkPlJSxSOaFgW/GGDXq49sz/995xzhVITHxh2SdLkI=\n-----END CERTIFICATE-----\n" iex> password = "Safaricom133" @@ -61,11 +105,8 @@ defmodule ExPesa.Util do cert_decoded = :public_key.pem_entry_decode(pem_entry) # 2) Extract public key. - plk = - cert_decoded - |> ExPesa.Util.Records."Certificate"(:tbsCertificate) - |> ExPesa.Util.Records."TBSCertificate"(:subjectPublicKeyInfo) - |> ExPesa.Util.Records."SubjectPublicKeyInfo"(:subjectPublicKey) + list = Tuple.to_list(elem(cert_decoded, 1)) + plk = List.keyfind(list, :SubjectPublicKeyInfo, 0) |> elem(2) public_key = :public_key.der_decode(:RSAPublicKey, plk) From fadf861f36ff1204e00470d98263f807c00f2b13 Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Mon, 28 Sep 2020 21:02:53 +0300 Subject: [PATCH 24/65] fixed keys added sandbox key to toggle dev/prod envs added daraja links, perter njeru link --- README.md | 5 +++++ config/dev.sample.exs | 3 ++- config/test.exs | 2 +- lib/ex_pesa/util.ex | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 37542c7..a2cf172 100644 --- a/README.md +++ b/README.md @@ -55,12 +55,17 @@ end ## Configuration Create a copy of `config/dev.exs` or `config/prod.exs` from `config/dev.sample.exs` +Use the `sandbox` key to `true` when you are using sandbox credentials, chnage to `false` when going to `:prod` ### Mpesa (Daraja) +Mpesa Daraja API link: https://developer.safaricom.co.ke + Add below config to dev.exs / prod.exs files This asumes you have a clear understanding of how [Daraja API works](https://developer.safaricom.co.ke/get-started). +You can also refer to this Safaricom Daraja API Tutorial: https://peternjeru.co.ke/safdaraja/ui/ by Peter Njeru + ```elixir config :ex_pesa, mpesa: [ diff --git a/config/dev.sample.exs b/config/dev.sample.exs index 31d9a5d..187a253 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -2,7 +2,8 @@ use Mix.Config config :tesla, adapter: Tesla.Adapter.Hackney config :ex_pesa, - force_live_url: "NO", + # When changed to "false" one will use the live endpoint url + sandbox: true, mpesa: [ consumer_key: "72yw1nun6g1QQPPgOsAObCGSfuimGO7b", consumer_secret: "vRzZiD5RllMLIdLD", diff --git a/config/test.exs b/config/test.exs index 699d37b..7481b9c 100644 --- a/config/test.exs +++ b/config/test.exs @@ -2,7 +2,7 @@ use Mix.Config config :tesla, adapter: Tesla.Mock config :ex_pesa, - force_live_url: "NO", + sandbox: true, mpesa: [ consumer_key: "72yw1nun6g1QQPPgOsAObCGSfuimGO7b", consumer_secret: "vRzZiD5RllMLIdLD", diff --git a/lib/ex_pesa/util.ex b/lib/ex_pesa/util.ex index 000cfb5..ead0a04 100644 --- a/lib/ex_pesa/util.ex +++ b/lib/ex_pesa/util.ex @@ -5,8 +5,8 @@ defmodule ExPesa.Util do @spec get_url(String.t(), String.t()) :: String.t() def get_url(live_url, sandbox_url) do cond do - Mix.env() == :prod -> live_url - Application.get_env(:ex_pesa, :force_live_url) == "YES" -> live_url + Application.get_env(:ex_pesa, :sandbox) === false -> live_url + Application.get_env(:ex_pesa, :sandbox) === true -> sandbox_url true -> sandbox_url end end From 3fc558cbb0e76225136d66feb900df7684cc0b6d Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sun, 27 Sep 2020 13:20:35 +0300 Subject: [PATCH 25/65] b2b implementation --- config/dev.sample.exs | 13 +++++- lib/ex_pesa/Mpesa/b2b.ex | 87 ++++++++++++++++++++++++++++++++++++++++ lib/ex_pesa/Mpesa/stk.ex | 4 +- lib/ex_pesa/util.ex | 81 +++++++++++++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 lib/ex_pesa/Mpesa/b2b.ex diff --git a/config/dev.sample.exs b/config/dev.sample.exs index 7814714..aa5a174 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -11,5 +11,16 @@ config :ex_pesa, # had to pass this for registerurl to work for C2B c2b_short_code: "600247", mpesa_passkey: "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919", - mpesa_callback_url: "http://91eb0af5.ngrok.io/api/payment/callback" + mpesa_callback_url: "http://91eb0af5.ngrok.io/api/payment/callback", + cert: + "-----BEGIN CERTIFICATE-----\nMIIGKzCCBROgAwIBAgIQDL7NH8cxSdUpl0ihH0A1wTANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwODI3MDAwMDAwWhcN\nMTkwNDA0MTIwMDAwWjBuMQswCQYDVQQGEwJLRTEQMA4GA1UEBxMHTmFpcm9iaTEW\nMBQGA1UEChMNU2FmYXJpY29tIFBMQzETMBEGA1UECxMKRGlnaXRhbCBJVDEgMB4G\nA1UEAxMXc2FuZGJveC5zYWZhcmljb20uY28ua2UwggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQC78yeC/wLoZY6TJeqc4g/9eAKIpeCwEsjX09pD8ZxAGXqT\nOi7ssdIGJBPmJZNeEVyf8ocFhisCuLngJ9Z5e/AvH52PhrEFmVu2D03zSf4C+rhZ\nndEKP6G79pUAb/bemOliU9zM8xYYkpCRzPWUzk6zSDarg0ZDLw5FrtZj/VJ9YEDL\nWGgAfwExEgSN3wjyUlJ2UwI3wqQXLka0VNFWoZxUH5j436gbSWRIL6NJUmrq8V8S\naTEPz3eJHj3NOToDu245c7VKdF/KExyZjRjD2p5I+Aip80TXzKlZj6DjMb3DlfXF\nHsnu0+1uJE701mvKX7BiscxKr8tCRphL63as4dqvAgMBAAGjggLkMIIC4DAfBgNV\nHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUzZmY7ZORLw9w\nqRbAQN5m9lJ28qMwIgYDVR0RBBswGYIXc2FuZGJveC5zYWZhcmljb20uY28ua2Uw\nDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBr\nBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1z\naGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Et\nc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcC\nARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYB\nBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w\nRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy\ndFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwCQYDVR0TBAIwADCCAQUGCisGAQQB1nkC\nBAIEgfYEgfMA8QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB\nZXs1FvEAAAQDAEcwRQIgBzVMkm7SNprjJ1GBqiXIc9rNzY+y7gt6s/O02oMkyFoC\nIQDBuThGlpmUKpeZoHhK6HGwB4jDMIecmKaOcMS18R2jxwB3AId1v+dZfPiMQ5lf\nvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZXs1F8IAAAQDAEgwRgIhAIRq2XFiC+RS\nuDCYq8ICJg0QafSV+e9BLpJnElEdaSjiAiEAyiiW4vxwv4cWcAXE6FAipctyUBs6\nbE5QyaCnmNpoDiQwDQYJKoZIhvcNAQELBQADggEBAB0YoWve9Sxhb0PBS3Hc46Rf\na7H1jhHuwE+UyscSQsdJdk8uPAgDuKRZMvJPGEaCkNHm36NfcaXXFjPOl7LI1d1a\n9zqSP0xeZBI6cF0x96WuQGrI9/WR2tfxjmaUSp8a/aJ6n+tZA28eJZNPrIaMm+6j\ngh7AkKnqcf+g8F/MvCCVdNAiVMdz6UpCscf6BRPHNZ5ifvChGh7aUKjrVLLuF4Ls\nHE05qm6HNyV5eTa6wvcbc4ewguN1UDZvPWetSyfBk10Wbpor4znQ4TJ3Y9uCvsJH\n41ldblDvZZ2z4kB2UYQ7iBkPlJSxSOaFgW/GGDXq49sz/995xzhVITHxh2SdLkI=\n-----END CERTIFICATE-----\n", + b2b: [ + short_code: "600247", + initiator_name: "John Doe", + password: "Safaricom133", + timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", + security_credential: + "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + ] ] diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex new file mode 100644 index 0000000..0ac15ee --- /dev/null +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -0,0 +1,87 @@ +defmodule ExPesa.Mpesa.B2B do + @moduledoc """ + This API enables Business to Business (B2B) transactions between a business and another business. + Use of this API requires a valid and verified B2B M-Pesa short code for the business initiating the transaction and the both businesses involved in the transaction. + """ + import ExPesa.Mpesa.MpesaBase + import ExPesa.Util + + @doc """ + This API enables Business to Business (B2B) transactions between a business and another business. Use of this API requires a valid and verified B2B M-Pesa short code for the business initiating the transaction and the both businesses involved in the transaction. + + ## Configuration + + Add below config to dev.exs / prod.exs files (at this stage after STK, the config keys should be there) + This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#lipa-na-m-pesa-online-query-request + + `config.exs` + ```elixir + config :ex_pesa, + mpesa: [ + b2b: [ + short_code: "", + initiator_name: "", + password: "", + timeout_url: "", + result_url: "", + security_credential: "" + ] + ] + ``` + + ## Parameters + + attrs: - a map containing: + - `command_id` - Unique command for each transaction type, possible values are: BusinessPayBill, MerchantToMerchantTransfer, MerchantTransferFromMerchantToWorking, MerchantServicesMMFAccountTransfer, AgencyFloatAdvance + - `amount` - The amount being transacted. + - `receiver_party` - Organization’s short code receiving the funds being transacted + - `remarks` - Comments that are sent along with the transaction. + - `account_reference` - Account Reference mandatory for “BusinessPaybill” CommandID. + + ## Example + + iex> ExPesa.Mpesa.B2B.request(%{command_id: "BusinessPayBill", amount: 10500, receiver_party: 600000, remarks: "B2B Request", account_reference: "BILL PAYMENT"}) + {:ok, + %{ + "ConversationID" => "AG_20200927_00007d4c98884c889b25", + "OriginatorConversationID" => "27274-37744848-4", + "ResponseCode" => "0", + "ResponseDescription" => "Accept the service request successfully." + }} + """ + + def request(%{ + command_id: command_id, + amount: amount, + receiver_party: receiver_party, + remarks: remarks, + account_reference: account_reference + }) do + security_credential = + case Application.get_env(:ex_pesa, :mpesa)[:b2b][:security_credential] do + nil -> + password = Application.get_env(:ex_pesa, :mpesa)[:b2b][:password] + get_security_credential(%{Password: password}) + + credential -> + credential + end + + payload = %{ + "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:initiator_name], + "SecurityCredential" => security_credential, + "CommandID" => command_id, + "Amount" => amount, + "PartyA" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:short_code], + "SenderIdentifierType" => 4, + "PartyB" => receiver_party, + "RecieverIdentifierType" => 4, + "Remarks" => remarks, + "AccountReference" => account_reference, + "QueueTimeOutURL" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:timeout_url], + "ResultURL" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:result_url] + } + + make_request("/mpesa/b2b/v1/paymentrequest", payload) + end +end diff --git a/lib/ex_pesa/Mpesa/stk.ex b/lib/ex_pesa/Mpesa/stk.ex index d9103d7..45e3120 100644 --- a/lib/ex_pesa/Mpesa/stk.ex +++ b/lib/ex_pesa/Mpesa/stk.ex @@ -10,7 +10,7 @@ defmodule ExPesa.Mpesa.Stk do ## Configuration - Add below config to dev.exs / prod.exs files + Add below config to dev.exs / prod.exs files This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#lipa-na-m-pesa-online-payment `config.exs` @@ -111,7 +111,7 @@ defmodule ExPesa.Mpesa.Stk do "MerchantRequestID" => "11130-78831728-4", "ResponseCode" => "0", "ResponseDescription" => "The service request has been accepted successsfully", - "ResultCode" => "1032", + "ResultCode" => "1032", "ResultDesc" => "Request cancelled by user" } } diff --git a/lib/ex_pesa/util.ex b/lib/ex_pesa/util.ex index ead0a04..39951f1 100644 --- a/lib/ex_pesa/util.ex +++ b/lib/ex_pesa/util.ex @@ -1,4 +1,21 @@ +defmodule ExPesa.Util.Records do + @moduledoc """ + Extracts all records information from an Erlang file. + Returns a keyword list of {record_name, fields} tuples where record_name is the name of an extracted record and fields is a list of {field, value} tuples representing the fields for that record. + """ + + require Record + import Record, only: [defrecord: 2, extract: 2] + + @public_key "public_key/include/public_key.hrl" + + defrecord :Certificate, extract(:Certificate, from_lib: @public_key) + defrecord :TBSCertificate, extract(:TBSCertificate, from_lib: @public_key) + defrecord :SubjectPublicKeyInfo, extract(:SubjectPublicKeyInfo, from_lib: @public_key) +end + defmodule ExPesa.Util do + require ExPesa.Util.Records @moduledoc false @doc false @@ -10,4 +27,68 @@ defmodule ExPesa.Util do true -> sandbox_url end end + + @doc """ + Security Credentials + + M-Pesa Core authenticates a transaction by decrypting the security credentials. Security credentials are generated by encrypting the base64 encoded initiator password with M-Pesa’s public key, a X509 certificate. + + The algorithm for generating security credentials is as follows: + Write the unencrypted password into a byte array. + + Encrypt the array with the M-Pesa public key certificate. Use the RSA algorithm, and use PKCS #1.5 padding (not OAEP), and add the result to the encrypted stream. + + Convert the resulting encrypted byte array into a string using base64 encoding. The resulting base64 encoded string is the security credential. + + ## Parameters + + attrs:- a map containing: + - `CertFile` - This is the M-Pesa public key certificate used to encrypt your plain password. + There are 2 types of certificates. + - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer + - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer + + - `Password` - This is a plain unencrypted password. + Environment + - production - set password from the organization portal. + - sandbox - use anything as your sandbox password. + + ## Example + iex> certfile = "-----BEGIN CERTIFICATE-----\nMIIGKzCCBROgAwIBAgIQDL7NH8cxSdUpl0ihH0A1wTANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwODI3MDAwMDAwWhcN\nMTkwNDA0MTIwMDAwWjBuMQswCQYDVQQGEwJLRTEQMA4GA1UEBxMHTmFpcm9iaTEW\nMBQGA1UEChMNU2FmYXJpY29tIFBMQzETMBEGA1UECxMKRGlnaXRhbCBJVDEgMB4G\nA1UEAxMXc2FuZGJveC5zYWZhcmljb20uY28ua2UwggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQC78yeC/wLoZY6TJeqc4g/9eAKIpeCwEsjX09pD8ZxAGXqT\nOi7ssdIGJBPmJZNeEVyf8ocFhisCuLngJ9Z5e/AvH52PhrEFmVu2D03zSf4C+rhZ\nndEKP6G79pUAb/bemOliU9zM8xYYkpCRzPWUzk6zSDarg0ZDLw5FrtZj/VJ9YEDL\nWGgAfwExEgSN3wjyUlJ2UwI3wqQXLka0VNFWoZxUH5j436gbSWRIL6NJUmrq8V8S\naTEPz3eJHj3NOToDu245c7VKdF/KExyZjRjD2p5I+Aip80TXzKlZj6DjMb3DlfXF\nHsnu0+1uJE701mvKX7BiscxKr8tCRphL63as4dqvAgMBAAGjggLkMIIC4DAfBgNV\nHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUzZmY7ZORLw9w\nqRbAQN5m9lJ28qMwIgYDVR0RBBswGYIXc2FuZGJveC5zYWZhcmljb20uY28ua2Uw\nDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBr\nBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1z\naGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Et\nc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcC\nARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYB\nBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w\nRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy\ndFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwCQYDVR0TBAIwADCCAQUGCisGAQQB1nkC\nBAIEgfYEgfMA8QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB\nZXs1FvEAAAQDAEcwRQIgBzVMkm7SNprjJ1GBqiXIc9rNzY+y7gt6s/O02oMkyFoC\nIQDBuThGlpmUKpeZoHhK6HGwB4jDMIecmKaOcMS18R2jxwB3AId1v+dZfPiMQ5lf\nvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZXs1F8IAAAQDAEgwRgIhAIRq2XFiC+RS\nuDCYq8ICJg0QafSV+e9BLpJnElEdaSjiAiEAyiiW4vxwv4cWcAXE6FAipctyUBs6\nbE5QyaCnmNpoDiQwDQYJKoZIhvcNAQELBQADggEBAB0YoWve9Sxhb0PBS3Hc46Rf\na7H1jhHuwE+UyscSQsdJdk8uPAgDuKRZMvJPGEaCkNHm36NfcaXXFjPOl7LI1d1a\n9zqSP0xeZBI6cF0x96WuQGrI9/WR2tfxjmaUSp8a/aJ6n+tZA28eJZNPrIaMm+6j\ngh7AkKnqcf+g8F/MvCCVdNAiVMdz6UpCscf6BRPHNZ5ifvChGh7aUKjrVLLuF4Ls\nHE05qm6HNyV5eTa6wvcbc4ewguN1UDZvPWetSyfBk10Wbpor4znQ4TJ3Y9uCvsJH\n41ldblDvZZ2z4kB2UYQ7iBkPlJSxSOaFgW/GGDXq49sz/995xzhVITHxh2SdLkI=\n-----END CERTIFICATE-----\n" + iex> password = "Safaricom133" + iex> ExPesa.Util.generate_security_credential(%{CertFile: certfile, Password: password}) + """ + + def generate_security_credential(%{CertFile: certfile, Password: password}) do + cert_text = certfile |> String.trim() + + # 1) Decode the certificate. + [pem_entry] = :public_key.pem_decode(cert_text) + cert_decoded = :public_key.pem_entry_decode(pem_entry) + + # 2) Extract public key. + plk = + cert_decoded + |> ExPesa.Util.Records."Certificate"(:tbsCertificate) + |> ExPesa.Util.Records."TBSCertificate"(:subjectPublicKeyInfo) + |> ExPesa.Util.Records."SubjectPublicKeyInfo"(:subjectPublicKey) + + public_key = :public_key.der_decode(:RSAPublicKey, plk) + + # 3) Encrypt the plain password text + ciphertext = + :public_key.encrypt_public(password, public_key, [{:rsa_pad, :rsa_pkcs1_padding}]) + + # 4) Base64 encode and return the result + :base64.encode(ciphertext) + end + + def generate_security_credential(%{}) do + nil + end + + def get_security_credential(%{Password: password}) do + cert = Application.get_env(:ex_pesa, :mpesa)[:cert] + ExPesa.Util.generate_security_credential(%{CertFile: cert, Password: password}) + end end From 01794217c4acbcf15b039410335d0d3f963689e3 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sun, 27 Sep 2020 21:17:49 +0300 Subject: [PATCH 26/65] modified b2b docs --- lib/ex_pesa/Mpesa/b2b.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index 0ac15ee..48ea19e 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -11,8 +11,8 @@ defmodule ExPesa.Mpesa.B2B do ## Configuration - Add below config to dev.exs / prod.exs files (at this stage after STK, the config keys should be there) - This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#lipa-na-m-pesa-online-query-request + Add below config to dev.exs / prod.exs files + This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#b2b-api `config.exs` ```elixir From 65e3c44febcef539660f5ad26ca63982f51777c9 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Mon, 28 Sep 2020 01:48:11 +0300 Subject: [PATCH 27/65] better error handling --- lib/ex_pesa/Mpesa/b2b.ex | 90 ++++++++++++++++++++++++++-------------- lib/ex_pesa/util.ex | 27 ++++++------ 2 files changed, 72 insertions(+), 45 deletions(-) diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index 48ea19e..e20c5fa 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -8,38 +8,67 @@ defmodule ExPesa.Mpesa.B2B do @doc """ This API enables Business to Business (B2B) transactions between a business and another business. Use of this API requires a valid and verified B2B M-Pesa short code for the business initiating the transaction and the both businesses involved in the transaction. - ## Configuration - Add below config to dev.exs / prod.exs files This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#b2b-api - - `config.exs` - ```elixir - config :ex_pesa, - mpesa: [ - b2b: [ - short_code: "", - initiator_name: "", - password: "", - timeout_url: "", - result_url: "", - security_credential: "" - ] - ] - ``` - + #### B2B - Configuration Parameters + - `initiator` - This is the credential/username used to authenticate the transaction request. + Environment + - production - + - create a user with api access method (access channel) + - Enter user name + - assign business manager role and B2B ORG API initiator role. + Use the username from your notifitation channel (SMS) + - sandbox - use your own custom username + - `timeout_url' - The path that stores information of time out transactions.it should be properly validated to make sure that it contains the port, URI and domain name or publicly available IP. + - `result_url` - The path that receives results from M-Pesa it should be properly validated to make sure that it contains the port, URI and domain name or publicly available IP. + - `security credential` - To generate security_credential, head over to https://developer.safaricom.co.ke/test_credentials, then Initiator Security Password for your environment + - Test - use the above test security credential + - Production - use the actual production security credential + `config.exs` + ```elixir + config :ex_pesa, + mpesa: [ + b2b: [ + short_code: "", + initiator_name: "", + timeout_url: "", + result_url: "", + security_credential: "" + ] + ] + ``` + Alternatively, generate security credential using certificate + `cert` - This is the M-Pesa public key certificate used to encrypt your plain password. + There are 2 types of certificates. + - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer + - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer + `password` - This is a plain unencrypted password. + Environment + - production - set password from the organization portal. + - sandbox - use your own custom password + `config.exs` + ```elixir + config :ex_pesa, + mpesa: [ + cert: "" + b2b: [ + short_code: "", + initiator_name: "", + password: "", + timeout_url: "", + result_url: "" + ] + ] + ``` ## Parameters - attrs: - a map containing: - `command_id` - Unique command for each transaction type, possible values are: BusinessPayBill, MerchantToMerchantTransfer, MerchantTransferFromMerchantToWorking, MerchantServicesMMFAccountTransfer, AgencyFloatAdvance - `amount` - The amount being transacted. - `receiver_party` - Organization’s short code receiving the funds being transacted - `remarks` - Comments that are sent along with the transaction. - `account_reference` - Account Reference mandatory for “BusinessPaybill” CommandID. - ## Example - iex> ExPesa.Mpesa.B2B.request(%{command_id: "BusinessPayBill", amount: 10500, receiver_party: 600000, remarks: "B2B Request", account_reference: "BILL PAYMENT"}) {:ok, %{ @@ -50,23 +79,20 @@ defmodule ExPesa.Mpesa.B2B do }} """ - def request(%{ + def request(params) do + case get_security_credential_for(:b2b) do + nil -> {:error, "cannot generate security_credential due to missing configuration fields"} + security_credential -> b2b_request(security_credential, params) + end + end + + def b2b_request(security_credential, %{ command_id: command_id, amount: amount, receiver_party: receiver_party, remarks: remarks, account_reference: account_reference }) do - security_credential = - case Application.get_env(:ex_pesa, :mpesa)[:b2b][:security_credential] do - nil -> - password = Application.get_env(:ex_pesa, :mpesa)[:b2b][:password] - get_security_credential(%{Password: password}) - - credential -> - credential - end - payload = %{ "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:initiator_name], "SecurityCredential" => security_credential, diff --git a/lib/ex_pesa/util.ex b/lib/ex_pesa/util.ex index 39951f1..9a05913 100644 --- a/lib/ex_pesa/util.ex +++ b/lib/ex_pesa/util.ex @@ -30,37 +30,31 @@ defmodule ExPesa.Util do @doc """ Security Credentials - M-Pesa Core authenticates a transaction by decrypting the security credentials. Security credentials are generated by encrypting the base64 encoded initiator password with M-Pesa’s public key, a X509 certificate. - The algorithm for generating security credentials is as follows: Write the unencrypted password into a byte array. - Encrypt the array with the M-Pesa public key certificate. Use the RSA algorithm, and use PKCS #1.5 padding (not OAEP), and add the result to the encrypted stream. - Convert the resulting encrypted byte array into a string using base64 encoding. The resulting base64 encoded string is the security credential. - ## Parameters - attrs:- a map containing: - `CertFile` - This is the M-Pesa public key certificate used to encrypt your plain password. There are 2 types of certificates. - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer - - `Password` - This is a plain unencrypted password. Environment - production - set password from the organization portal. - sandbox - use anything as your sandbox password. - ## Example iex> certfile = "-----BEGIN CERTIFICATE-----\nMIIGKzCCBROgAwIBAgIQDL7NH8cxSdUpl0ihH0A1wTANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwODI3MDAwMDAwWhcN\nMTkwNDA0MTIwMDAwWjBuMQswCQYDVQQGEwJLRTEQMA4GA1UEBxMHTmFpcm9iaTEW\nMBQGA1UEChMNU2FmYXJpY29tIFBMQzETMBEGA1UECxMKRGlnaXRhbCBJVDEgMB4G\nA1UEAxMXc2FuZGJveC5zYWZhcmljb20uY28ua2UwggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQC78yeC/wLoZY6TJeqc4g/9eAKIpeCwEsjX09pD8ZxAGXqT\nOi7ssdIGJBPmJZNeEVyf8ocFhisCuLngJ9Z5e/AvH52PhrEFmVu2D03zSf4C+rhZ\nndEKP6G79pUAb/bemOliU9zM8xYYkpCRzPWUzk6zSDarg0ZDLw5FrtZj/VJ9YEDL\nWGgAfwExEgSN3wjyUlJ2UwI3wqQXLka0VNFWoZxUH5j436gbSWRIL6NJUmrq8V8S\naTEPz3eJHj3NOToDu245c7VKdF/KExyZjRjD2p5I+Aip80TXzKlZj6DjMb3DlfXF\nHsnu0+1uJE701mvKX7BiscxKr8tCRphL63as4dqvAgMBAAGjggLkMIIC4DAfBgNV\nHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUzZmY7ZORLw9w\nqRbAQN5m9lJ28qMwIgYDVR0RBBswGYIXc2FuZGJveC5zYWZhcmljb20uY28ua2Uw\nDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBr\nBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1z\naGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Et\nc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcC\nARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYB\nBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w\nRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy\ndFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwCQYDVR0TBAIwADCCAQUGCisGAQQB1nkC\nBAIEgfYEgfMA8QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB\nZXs1FvEAAAQDAEcwRQIgBzVMkm7SNprjJ1GBqiXIc9rNzY+y7gt6s/O02oMkyFoC\nIQDBuThGlpmUKpeZoHhK6HGwB4jDMIecmKaOcMS18R2jxwB3AId1v+dZfPiMQ5lf\nvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZXs1F8IAAAQDAEgwRgIhAIRq2XFiC+RS\nuDCYq8ICJg0QafSV+e9BLpJnElEdaSjiAiEAyiiW4vxwv4cWcAXE6FAipctyUBs6\nbE5QyaCnmNpoDiQwDQYJKoZIhvcNAQELBQADggEBAB0YoWve9Sxhb0PBS3Hc46Rf\na7H1jhHuwE+UyscSQsdJdk8uPAgDuKRZMvJPGEaCkNHm36NfcaXXFjPOl7LI1d1a\n9zqSP0xeZBI6cF0x96WuQGrI9/WR2tfxjmaUSp8a/aJ6n+tZA28eJZNPrIaMm+6j\ngh7AkKnqcf+g8F/MvCCVdNAiVMdz6UpCscf6BRPHNZ5ifvChGh7aUKjrVLLuF4Ls\nHE05qm6HNyV5eTa6wvcbc4ewguN1UDZvPWetSyfBk10Wbpor4znQ4TJ3Y9uCvsJH\n41ldblDvZZ2z4kB2UYQ7iBkPlJSxSOaFgW/GGDXq49sz/995xzhVITHxh2SdLkI=\n-----END CERTIFICATE-----\n" iex> password = "Safaricom133" iex> ExPesa.Util.generate_security_credential(%{CertFile: certfile, Password: password}) """ - def generate_security_credential(%{CertFile: certfile, Password: password}) do - cert_text = certfile |> String.trim() + def generate_security_credential(%{CertFile: certfile, Password: password}) + when certfile != nil and password != nil do + cert_text = + certfile |> String.trim() |> String.split(~r{\n *}, trim: true) |> Enum.join("\n") # 1) Decode the certificate. [pem_entry] = :public_key.pem_decode(cert_text) @@ -87,8 +81,15 @@ defmodule ExPesa.Util do nil end - def get_security_credential(%{Password: password}) do - cert = Application.get_env(:ex_pesa, :mpesa)[:cert] - ExPesa.Util.generate_security_credential(%{CertFile: cert, Password: password}) + def get_security_credential_for(key) do + case Application.get_env(:ex_pesa, :mpesa)[key][:security_credential] do + nil -> + cert = Application.get_env(:ex_pesa, :mpesa)[:cert] + password = Application.get_env(:ex_pesa, :mpesa)[key][:password] + generate_security_credential(%{CertFile: cert, Password: password}) + + credential -> + credential + end end end From 901a5cad7ecd6275cc19516d4d68db5c1d4238b8 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Mon, 28 Sep 2020 15:20:56 +0300 Subject: [PATCH 28/65] added more Security Credentials documentation --- lib/ex_pesa/util.ex | 115 ++++++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 37 deletions(-) diff --git a/lib/ex_pesa/util.ex b/lib/ex_pesa/util.ex index 9a05913..667ca21 100644 --- a/lib/ex_pesa/util.ex +++ b/lib/ex_pesa/util.ex @@ -1,21 +1,4 @@ -defmodule ExPesa.Util.Records do - @moduledoc """ - Extracts all records information from an Erlang file. - Returns a keyword list of {record_name, fields} tuples where record_name is the name of an extracted record and fields is a list of {field, value} tuples representing the fields for that record. - """ - - require Record - import Record, only: [defrecord: 2, extract: 2] - - @public_key "public_key/include/public_key.hrl" - - defrecord :Certificate, extract(:Certificate, from_lib: @public_key) - defrecord :TBSCertificate, extract(:TBSCertificate, from_lib: @public_key) - defrecord :SubjectPublicKeyInfo, extract(:SubjectPublicKeyInfo, from_lib: @public_key) -end - defmodule ExPesa.Util do - require ExPesa.Util.Records @moduledoc false @doc false @@ -29,22 +12,83 @@ defmodule ExPesa.Util do end @doc """ - Security Credentials + Security Credentials read more https://developer.safaricom.co.ke/docs#security-credentials M-Pesa Core authenticates a transaction by decrypting the security credentials. Security credentials are generated by encrypting the base64 encoded initiator password with M-Pesa’s public key, a X509 certificate. - The algorithm for generating security credentials is as follows: - Write the unencrypted password into a byte array. - Encrypt the array with the M-Pesa public key certificate. Use the RSA algorithm, and use PKCS #1.5 padding (not OAEP), and add the result to the encrypted stream. - Convert the resulting encrypted byte array into a string using base64 encoding. The resulting base64 encoded string is the security credential. - ## Parameters - attrs:- a map containing: - - `CertFile` - This is the M-Pesa public key certificate used to encrypt your plain password. - There are 2 types of certificates. - - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer - - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer - - `Password` - This is a plain unencrypted password. - Environment - - production - set password from the organization portal. - - sandbox - use anything as your sandbox password. + + The algorithm for generating security credentials is as follows: + Write the unencrypted password into a byte array. + Encrypt the array with the M-Pesa public key certificate. Use the RSA algorithm, and use PKCS #1.5 padding (not OAEP), and add the result to the encrypted stream. + Convert the resulting encrypted byte array into a string using base64 encoding. The resulting base64 encoded string is the security credential. + + Impementation Examples + PHP + + + Node Js + module.exports = (certPath, shortCodeSecurityCredential) => { + const bufferToEncrypt = Buffer.from(shortCodeSecurityCredential) + const data = fs.readFileSync(path.resolve(certPath)) + const privateKey = String(data) + const encrypted = crypto.publicEncrypt({ + key: privateKey, + padding: crypto.constants.RSA_PKCS1_PADDING + }, bufferToEncrypt) + const securityCredential = encrypted.toString('base64') + return securityCredential + } + + JAVA + + // Function to encrypt the initiator credentials + public static String encryptInitiatorPassword(String securityCertificate, String password) { + String encryptedPassword = "YOUR_INITIATOR_PASSWORD"; + try { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + byte[] input = password.getBytes(); + + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC"); + FileInputStream fin = new FileInputStream(new File(securityCertificate)); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate certificate = (X509Certificate) cf.generateCertificate(fin); + PublicKey pk = certificate.getPublicKey(); + cipher.init(Cipher.ENCRYPT_MODE, pk); + + byte[] cipherText = cipher.doFinal(input); + + // Convert the resulting encrypted byte array into a string using base64 encoding + encryptedPassword = Base64.encode(cipherText); + } + } + + Python + + from M2Crypto import RSA, X509 + from base64 import b64encode + + INITIATOR_PASS = "YOUR_PASSWORD" + CERTIFICATE_FILE = "PATH_TO_CERTIFICATE_FILE" + + def encryptInitiatorPassword(): + cert_file = open(CERTIFICATE_FILE, 'r') + cert_data = cert_file.read() #read certificate file + cert_file.close() + + cert = X509.load_cert_string(cert_data) + #pub_key = X509.load_cert_string(cert_data) + pub_key = cert.get_pubkey() + rsa_key = pub_key.get_rsa() + cipher = rsa_key.public_encrypt(INITIATOR_PASS, RSA.pkcs1_padding) + return b64encode(cipher) + + print encryptInitiatorPassword() + ## Example iex> certfile = "-----BEGIN CERTIFICATE-----\nMIIGKzCCBROgAwIBAgIQDL7NH8cxSdUpl0ihH0A1wTANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwODI3MDAwMDAwWhcN\nMTkwNDA0MTIwMDAwWjBuMQswCQYDVQQGEwJLRTEQMA4GA1UEBxMHTmFpcm9iaTEW\nMBQGA1UEChMNU2FmYXJpY29tIFBMQzETMBEGA1UECxMKRGlnaXRhbCBJVDEgMB4G\nA1UEAxMXc2FuZGJveC5zYWZhcmljb20uY28ua2UwggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQC78yeC/wLoZY6TJeqc4g/9eAKIpeCwEsjX09pD8ZxAGXqT\nOi7ssdIGJBPmJZNeEVyf8ocFhisCuLngJ9Z5e/AvH52PhrEFmVu2D03zSf4C+rhZ\nndEKP6G79pUAb/bemOliU9zM8xYYkpCRzPWUzk6zSDarg0ZDLw5FrtZj/VJ9YEDL\nWGgAfwExEgSN3wjyUlJ2UwI3wqQXLka0VNFWoZxUH5j436gbSWRIL6NJUmrq8V8S\naTEPz3eJHj3NOToDu245c7VKdF/KExyZjRjD2p5I+Aip80TXzKlZj6DjMb3DlfXF\nHsnu0+1uJE701mvKX7BiscxKr8tCRphL63as4dqvAgMBAAGjggLkMIIC4DAfBgNV\nHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUzZmY7ZORLw9w\nqRbAQN5m9lJ28qMwIgYDVR0RBBswGYIXc2FuZGJveC5zYWZhcmljb20uY28ua2Uw\nDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBr\nBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1z\naGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Et\nc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcC\nARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYB\nBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w\nRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy\ndFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwCQYDVR0TBAIwADCCAQUGCisGAQQB1nkC\nBAIEgfYEgfMA8QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB\nZXs1FvEAAAQDAEcwRQIgBzVMkm7SNprjJ1GBqiXIc9rNzY+y7gt6s/O02oMkyFoC\nIQDBuThGlpmUKpeZoHhK6HGwB4jDMIecmKaOcMS18R2jxwB3AId1v+dZfPiMQ5lf\nvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZXs1F8IAAAQDAEgwRgIhAIRq2XFiC+RS\nuDCYq8ICJg0QafSV+e9BLpJnElEdaSjiAiEAyiiW4vxwv4cWcAXE6FAipctyUBs6\nbE5QyaCnmNpoDiQwDQYJKoZIhvcNAQELBQADggEBAB0YoWve9Sxhb0PBS3Hc46Rf\na7H1jhHuwE+UyscSQsdJdk8uPAgDuKRZMvJPGEaCkNHm36NfcaXXFjPOl7LI1d1a\n9zqSP0xeZBI6cF0x96WuQGrI9/WR2tfxjmaUSp8a/aJ6n+tZA28eJZNPrIaMm+6j\ngh7AkKnqcf+g8F/MvCCVdNAiVMdz6UpCscf6BRPHNZ5ifvChGh7aUKjrVLLuF4Ls\nHE05qm6HNyV5eTa6wvcbc4ewguN1UDZvPWetSyfBk10Wbpor4znQ4TJ3Y9uCvsJH\n41ldblDvZZ2z4kB2UYQ7iBkPlJSxSOaFgW/GGDXq49sz/995xzhVITHxh2SdLkI=\n-----END CERTIFICATE-----\n" iex> password = "Safaricom133" @@ -61,11 +105,8 @@ defmodule ExPesa.Util do cert_decoded = :public_key.pem_entry_decode(pem_entry) # 2) Extract public key. - plk = - cert_decoded - |> ExPesa.Util.Records."Certificate"(:tbsCertificate) - |> ExPesa.Util.Records."TBSCertificate"(:subjectPublicKeyInfo) - |> ExPesa.Util.Records."SubjectPublicKeyInfo"(:subjectPublicKey) + list = Tuple.to_list(elem(cert_decoded, 1)) + plk = List.keyfind(list, :SubjectPublicKeyInfo, 0) |> elem(2) public_key = :public_key.der_decode(:RSAPublicKey, plk) From dc45a4244f0b1dc6806bdb822d4254d6a280f36e Mon Sep 17 00:00:00 2001 From: kamalogudah Date: Sat, 3 Oct 2020 14:40:16 +0300 Subject: [PATCH 29/65] Handle B2B Test --- config/test.exs | 13 ++++++- lib/ex_pesa/Mpesa/b2b.ex | 19 ++++++---- test/ex_pesa/Mpesa/b2b_test.exs | 65 +++++++++++++++++++++++++++++++++ test/ex_pesa/Mpesa/c2b_test.exs | 2 - 4 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 test/ex_pesa/Mpesa/b2b_test.exs diff --git a/config/test.exs b/config/test.exs index 7481b9c..439cf38 100644 --- a/config/test.exs +++ b/config/test.exs @@ -8,5 +8,16 @@ config :ex_pesa, consumer_secret: "vRzZiD5RllMLIdLD", mpesa_short_code: "174379", mpesa_passkey: "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919", - mpesa_callback_url: "http://91eb0af5.ngrok.io/api/payment/callback" + mpesa_callback_url: "http://91eb0af5.ngrok.io/api/payment/callback", + cert: + "-----BEGIN CERTIFICATE-----\nMIIGKzCCBROgAwIBAgIQDL7NH8cxSdUpl0ihH0A1wTANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwODI3MDAwMDAwWhcN\nMTkwNDA0MTIwMDAwWjBuMQswCQYDVQQGEwJLRTEQMA4GA1UEBxMHTmFpcm9iaTEW\nMBQGA1UEChMNU2FmYXJpY29tIFBMQzETMBEGA1UECxMKRGlnaXRhbCBJVDEgMB4G\nA1UEAxMXc2FuZGJveC5zYWZhcmljb20uY28ua2UwggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQC78yeC/wLoZY6TJeqc4g/9eAKIpeCwEsjX09pD8ZxAGXqT\nOi7ssdIGJBPmJZNeEVyf8ocFhisCuLngJ9Z5e/AvH52PhrEFmVu2D03zSf4C+rhZ\nndEKP6G79pUAb/bemOliU9zM8xYYkpCRzPWUzk6zSDarg0ZDLw5FrtZj/VJ9YEDL\nWGgAfwExEgSN3wjyUlJ2UwI3wqQXLka0VNFWoZxUH5j436gbSWRIL6NJUmrq8V8S\naTEPz3eJHj3NOToDu245c7VKdF/KExyZjRjD2p5I+Aip80TXzKlZj6DjMb3DlfXF\nHsnu0+1uJE701mvKX7BiscxKr8tCRphL63as4dqvAgMBAAGjggLkMIIC4DAfBgNV\nHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUzZmY7ZORLw9w\nqRbAQN5m9lJ28qMwIgYDVR0RBBswGYIXc2FuZGJveC5zYWZhcmljb20uY28ua2Uw\nDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBr\nBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1z\naGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Et\nc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcC\nARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYB\nBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w\nRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy\ndFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwCQYDVR0TBAIwADCCAQUGCisGAQQB1nkC\nBAIEgfYEgfMA8QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB\nZXs1FvEAAAQDAEcwRQIgBzVMkm7SNprjJ1GBqiXIc9rNzY+y7gt6s/O02oMkyFoC\nIQDBuThGlpmUKpeZoHhK6HGwB4jDMIecmKaOcMS18R2jxwB3AId1v+dZfPiMQ5lf\nvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZXs1F8IAAAQDAEgwRgIhAIRq2XFiC+RS\nuDCYq8ICJg0QafSV+e9BLpJnElEdaSjiAiEAyiiW4vxwv4cWcAXE6FAipctyUBs6\nbE5QyaCnmNpoDiQwDQYJKoZIhvcNAQELBQADggEBAB0YoWve9Sxhb0PBS3Hc46Rf\na7H1jhHuwE+UyscSQsdJdk8uPAgDuKRZMvJPGEaCkNHm36NfcaXXFjPOl7LI1d1a\n9zqSP0xeZBI6cF0x96WuQGrI9/WR2tfxjmaUSp8a/aJ6n+tZA28eJZNPrIaMm+6j\ngh7AkKnqcf+g8F/MvCCVdNAiVMdz6UpCscf6BRPHNZ5ifvChGh7aUKjrVLLuF4Ls\nHE05qm6HNyV5eTa6wvcbc4ewguN1UDZvPWetSyfBk10Wbpor4znQ4TJ3Y9uCvsJH\n41ldblDvZZ2z4kB2UYQ7iBkPlJSxSOaFgW/GGDXq49sz/995xzhVITHxh2SdLkI=\n-----END CERTIFICATE-----\n", + b2b: [ + short_code: "600247", + initiator_name: "John Doe", + password: "Safaricom133", + timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", + security_credential: + "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + ] ] diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index e20c5fa..4cab107 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -86,13 +86,18 @@ defmodule ExPesa.Mpesa.B2B do end end - def b2b_request(security_credential, %{ - command_id: command_id, - amount: amount, - receiver_party: receiver_party, - remarks: remarks, - account_reference: account_reference - }) do + def request() do + {:error, + "Required Parameter missing, 'CommandID','Amount','PartyB', 'Remarks','AccountReference'"} + end + + defp b2b_request(security_credential, %{ + command_id: command_id, + amount: amount, + receiver_party: receiver_party, + remarks: remarks, + account_reference: account_reference + }) do payload = %{ "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:initiator_name], "SecurityCredential" => security_credential, diff --git a/test/ex_pesa/Mpesa/b2b_test.exs b/test/ex_pesa/Mpesa/b2b_test.exs new file mode 100644 index 0000000..95354f0 --- /dev/null +++ b/test/ex_pesa/Mpesa/b2b_test.exs @@ -0,0 +1,65 @@ +defmodule ExPesa.Mpesa.B2BTest do + @moduledoc false + + use ExUnit.Case, async: true + + import Tesla.Mock + doctest ExPesa.Mpesa.B2B + + alias ExPesa.Mpesa.B2B + + setup do + mock(fn + %{ + url: "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials", + method: :get + } -> + %Tesla.Env{ + status: 200, + body: %{ + "access_token" => "SGWcJPtNtYNPGm6uSYR9yPYrAI3Bm", + "expires_in" => "3599" + } + } + + %{ + url: "https://sandbox.safaricom.co.ke/mpesa/b2b/v1/paymentrequest", + method: :post + } -> + %Tesla.Env{ + status: 200, + body: %{ + "ConversationID" => "AG_20200927_00007d4c98884c889b25", + "OriginatorConversationID" => "27274-37744848-4", + "ResponseCode" => "0", + "ResponseDescription" => "Accept the service request successfully." + } + } + end) + + :ok + end + + describe "Mpesa B2B" do + test "request/1 should register a URL" do + payment_details = %{ + command_id: "BusinessPayBill", + amount: 10500, + receiver_party: 600_000, + remarks: "B2B Request", + account_reference: "BILL PAYMENT" + } + + {:ok, result} = B2B.request(payment_details) + + assert result["ResponseCode"] == "0" + end + + test "request/1 should error out without required parameter" do + {:error, result} = B2B.request() + + "Required Parameter missing, 'CommandID','Amount','PartyB', 'Remarks','AccountReference'" = + result + end + end +end diff --git a/test/ex_pesa/Mpesa/c2b_test.exs b/test/ex_pesa/Mpesa/c2b_test.exs index 63692e8..518eeb7 100644 --- a/test/ex_pesa/Mpesa/c2b_test.exs +++ b/test/ex_pesa/Mpesa/c2b_test.exs @@ -76,8 +76,6 @@ defmodule ExPesa.Mpesa.C2BTest do bill_reference: "Some Reference" }) - IO.inspect(result) - assert result["OriginatorCoversationID"] == "9769-145819182-2" assert result["ResponseDescription"] == "Accept the service request successfully." end From 7a2c2ea597add4153a1f00350e4258cdc446b49c Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Sat, 3 Oct 2020 15:17:59 +0300 Subject: [PATCH 30/65] rebased --- lib/ex_pesa/Mpesa/b2b.ex | 25 ++++++++++++++----------- lib/ex_pesa/util.ex | 10 ++++++---- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index 4cab107..2aeb13d 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -8,9 +8,11 @@ defmodule ExPesa.Mpesa.B2B do @doc """ This API enables Business to Business (B2B) transactions between a business and another business. Use of this API requires a valid and verified B2B M-Pesa short code for the business initiating the transaction and the both businesses involved in the transaction. + ## Configuration Add below config to dev.exs / prod.exs files This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#b2b-api + #### B2B - Configuration Parameters - `initiator` - This is the credential/username used to authenticate the transaction request. Environment @@ -25,6 +27,7 @@ defmodule ExPesa.Mpesa.B2B do - `security credential` - To generate security_credential, head over to https://developer.safaricom.co.ke/test_credentials, then Initiator Security Password for your environment - Test - use the above test security credential - Production - use the actual production security credential + `config.exs` ```elixir config :ex_pesa, @@ -61,6 +64,8 @@ defmodule ExPesa.Mpesa.B2B do ] ] ``` + + ## Parameters attrs: - a map containing: - `command_id` - Unique command for each transaction type, possible values are: BusinessPayBill, MerchantToMerchantTransfer, MerchantTransferFromMerchantToWorking, MerchantServicesMMFAccountTransfer, AgencyFloatAdvance @@ -68,6 +73,8 @@ defmodule ExPesa.Mpesa.B2B do - `receiver_party` - Organization’s short code receiving the funds being transacted - `remarks` - Comments that are sent along with the transaction. - `account_reference` - Account Reference mandatory for “BusinessPaybill” CommandID. + + ## Example iex> ExPesa.Mpesa.B2B.request(%{command_id: "BusinessPayBill", amount: 10500, receiver_party: 600000, remarks: "B2B Request", account_reference: "BILL PAYMENT"}) {:ok, @@ -86,18 +93,14 @@ defmodule ExPesa.Mpesa.B2B do end end - def request() do - {:error, - "Required Parameter missing, 'CommandID','Amount','PartyB', 'Remarks','AccountReference'"} - end - + @doc false defp b2b_request(security_credential, %{ - command_id: command_id, - amount: amount, - receiver_party: receiver_party, - remarks: remarks, - account_reference: account_reference - }) do + command_id: command_id, + amount: amount, + receiver_party: receiver_party, + remarks: remarks, + account_reference: account_reference + }) do payload = %{ "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:initiator_name], "SecurityCredential" => security_credential, diff --git a/lib/ex_pesa/util.ex b/lib/ex_pesa/util.ex index 667ca21..8995990 100644 --- a/lib/ex_pesa/util.ex +++ b/lib/ex_pesa/util.ex @@ -123,14 +123,16 @@ defmodule ExPesa.Util do end def get_security_credential_for(key) do - case Application.get_env(:ex_pesa, :mpesa)[key][:security_credential] do - nil -> + securityCredential = Application.get_env(:ex_pesa, :mpesa)[key][:security_credential] + + cond do + securityCredential === "" || securityCredential === nil -> cert = Application.get_env(:ex_pesa, :mpesa)[:cert] password = Application.get_env(:ex_pesa, :mpesa)[key][:password] generate_security_credential(%{CertFile: cert, Password: password}) - credential -> - credential + securityCredential -> + securityCredential end end end From 2a0e533f4c75045ee7160dddb1291a79a55377f8 Mon Sep 17 00:00:00 2001 From: Magak Emmanuel Date: Sat, 3 Oct 2020 15:24:34 +0300 Subject: [PATCH 31/65] Revert " fixed handling of both nil and "" security_credentials keys" --- lib/ex_pesa/Mpesa/b2b.ex | 25 +++++++++++-------------- lib/ex_pesa/util.ex | 10 ++++------ 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index 2aeb13d..4cab107 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -8,11 +8,9 @@ defmodule ExPesa.Mpesa.B2B do @doc """ This API enables Business to Business (B2B) transactions between a business and another business. Use of this API requires a valid and verified B2B M-Pesa short code for the business initiating the transaction and the both businesses involved in the transaction. - ## Configuration Add below config to dev.exs / prod.exs files This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#b2b-api - #### B2B - Configuration Parameters - `initiator` - This is the credential/username used to authenticate the transaction request. Environment @@ -27,7 +25,6 @@ defmodule ExPesa.Mpesa.B2B do - `security credential` - To generate security_credential, head over to https://developer.safaricom.co.ke/test_credentials, then Initiator Security Password for your environment - Test - use the above test security credential - Production - use the actual production security credential - `config.exs` ```elixir config :ex_pesa, @@ -64,8 +61,6 @@ defmodule ExPesa.Mpesa.B2B do ] ] ``` - - ## Parameters attrs: - a map containing: - `command_id` - Unique command for each transaction type, possible values are: BusinessPayBill, MerchantToMerchantTransfer, MerchantTransferFromMerchantToWorking, MerchantServicesMMFAccountTransfer, AgencyFloatAdvance @@ -73,8 +68,6 @@ defmodule ExPesa.Mpesa.B2B do - `receiver_party` - Organization’s short code receiving the funds being transacted - `remarks` - Comments that are sent along with the transaction. - `account_reference` - Account Reference mandatory for “BusinessPaybill” CommandID. - - ## Example iex> ExPesa.Mpesa.B2B.request(%{command_id: "BusinessPayBill", amount: 10500, receiver_party: 600000, remarks: "B2B Request", account_reference: "BILL PAYMENT"}) {:ok, @@ -93,14 +86,18 @@ defmodule ExPesa.Mpesa.B2B do end end - @doc false + def request() do + {:error, + "Required Parameter missing, 'CommandID','Amount','PartyB', 'Remarks','AccountReference'"} + end + defp b2b_request(security_credential, %{ - command_id: command_id, - amount: amount, - receiver_party: receiver_party, - remarks: remarks, - account_reference: account_reference - }) do + command_id: command_id, + amount: amount, + receiver_party: receiver_party, + remarks: remarks, + account_reference: account_reference + }) do payload = %{ "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:initiator_name], "SecurityCredential" => security_credential, diff --git a/lib/ex_pesa/util.ex b/lib/ex_pesa/util.ex index 8995990..667ca21 100644 --- a/lib/ex_pesa/util.ex +++ b/lib/ex_pesa/util.ex @@ -123,16 +123,14 @@ defmodule ExPesa.Util do end def get_security_credential_for(key) do - securityCredential = Application.get_env(:ex_pesa, :mpesa)[key][:security_credential] - - cond do - securityCredential === "" || securityCredential === nil -> + case Application.get_env(:ex_pesa, :mpesa)[key][:security_credential] do + nil -> cert = Application.get_env(:ex_pesa, :mpesa)[:cert] password = Application.get_env(:ex_pesa, :mpesa)[key][:password] generate_security_credential(%{CertFile: cert, Password: password}) - securityCredential -> - securityCredential + credential -> + credential end end end From 8bc2ef78d1db4810def028a5e5c332a12967e51a Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Sat, 3 Oct 2020 15:37:15 +0300 Subject: [PATCH 32/65] fixed keys for not existing securityCredential --- lib/ex_pesa/Mpesa/b2b.ex | 7 +++++++ lib/ex_pesa/util.ex | 10 ++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index 4cab107..a259f76 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -8,9 +8,11 @@ defmodule ExPesa.Mpesa.B2B do @doc """ This API enables Business to Business (B2B) transactions between a business and another business. Use of this API requires a valid and verified B2B M-Pesa short code for the business initiating the transaction and the both businesses involved in the transaction. + ## Configuration Add below config to dev.exs / prod.exs files This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#b2b-api + #### B2B - Configuration Parameters - `initiator` - This is the credential/username used to authenticate the transaction request. Environment @@ -25,6 +27,7 @@ defmodule ExPesa.Mpesa.B2B do - `security credential` - To generate security_credential, head over to https://developer.safaricom.co.ke/test_credentials, then Initiator Security Password for your environment - Test - use the above test security credential - Production - use the actual production security credential + `config.exs` ```elixir config :ex_pesa, @@ -38,6 +41,7 @@ defmodule ExPesa.Mpesa.B2B do ] ] ``` + Alternatively, generate security credential using certificate `cert` - This is the M-Pesa public key certificate used to encrypt your plain password. There are 2 types of certificates. @@ -47,6 +51,7 @@ defmodule ExPesa.Mpesa.B2B do Environment - production - set password from the organization portal. - sandbox - use your own custom password + `config.exs` ```elixir config :ex_pesa, @@ -61,6 +66,7 @@ defmodule ExPesa.Mpesa.B2B do ] ] ``` + ## Parameters attrs: - a map containing: - `command_id` - Unique command for each transaction type, possible values are: BusinessPayBill, MerchantToMerchantTransfer, MerchantTransferFromMerchantToWorking, MerchantServicesMMFAccountTransfer, AgencyFloatAdvance @@ -68,6 +74,7 @@ defmodule ExPesa.Mpesa.B2B do - `receiver_party` - Organization’s short code receiving the funds being transacted - `remarks` - Comments that are sent along with the transaction. - `account_reference` - Account Reference mandatory for “BusinessPaybill” CommandID. + ## Example iex> ExPesa.Mpesa.B2B.request(%{command_id: "BusinessPayBill", amount: 10500, receiver_party: 600000, remarks: "B2B Request", account_reference: "BILL PAYMENT"}) {:ok, diff --git a/lib/ex_pesa/util.ex b/lib/ex_pesa/util.ex index 667ca21..8995990 100644 --- a/lib/ex_pesa/util.ex +++ b/lib/ex_pesa/util.ex @@ -123,14 +123,16 @@ defmodule ExPesa.Util do end def get_security_credential_for(key) do - case Application.get_env(:ex_pesa, :mpesa)[key][:security_credential] do - nil -> + securityCredential = Application.get_env(:ex_pesa, :mpesa)[key][:security_credential] + + cond do + securityCredential === "" || securityCredential === nil -> cert = Application.get_env(:ex_pesa, :mpesa)[:cert] password = Application.get_env(:ex_pesa, :mpesa)[key][:password] generate_security_credential(%{CertFile: cert, Password: password}) - credential -> - credential + securityCredential -> + securityCredential end end end From 35a1b86ce62797916a69f33bdc9e82ccdb8c2465 Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Sat, 3 Oct 2020 15:56:23 +0300 Subject: [PATCH 33/65] small fix on test name --- test/ex_pesa/Mpesa/b2b_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ex_pesa/Mpesa/b2b_test.exs b/test/ex_pesa/Mpesa/b2b_test.exs index 95354f0..b6b0882 100644 --- a/test/ex_pesa/Mpesa/b2b_test.exs +++ b/test/ex_pesa/Mpesa/b2b_test.exs @@ -41,7 +41,7 @@ defmodule ExPesa.Mpesa.B2BTest do end describe "Mpesa B2B" do - test "request/1 should register a URL" do + test "request/1 should Initiate a B2B request" do payment_details = %{ command_id: "BusinessPayBill", amount: 10500, From fede172183860da95a244ffb54bc44c343022613 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sat, 3 Oct 2020 16:25:34 +0300 Subject: [PATCH 34/65] change params to snake case --- lib/ex_pesa/Mpesa/b2b.ex | 2 +- test/ex_pesa/Mpesa/b2b_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index a259f76..cf26359 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -95,7 +95,7 @@ defmodule ExPesa.Mpesa.B2B do def request() do {:error, - "Required Parameter missing, 'CommandID','Amount','PartyB', 'Remarks','AccountReference'"} + "Required Parameter missing, 'command_id','amount','receiver_party', 'remarks','account_reference'"} end defp b2b_request(security_credential, %{ diff --git a/test/ex_pesa/Mpesa/b2b_test.exs b/test/ex_pesa/Mpesa/b2b_test.exs index b6b0882..ae2ecb2 100644 --- a/test/ex_pesa/Mpesa/b2b_test.exs +++ b/test/ex_pesa/Mpesa/b2b_test.exs @@ -58,7 +58,7 @@ defmodule ExPesa.Mpesa.B2BTest do test "request/1 should error out without required parameter" do {:error, result} = B2B.request() - "Required Parameter missing, 'CommandID','Amount','PartyB', 'Remarks','AccountReference'" = + "Required Parameter missing, 'command_id','amount','receiver_party', 'remarks','account_reference'" = result end end From cb712238ca2a216a2e20383a542a57dbe075b5c3 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sat, 3 Oct 2020 16:39:50 +0300 Subject: [PATCH 35/65] utils clean up --- lib/ex_pesa/util.ex | 69 --------------------------------------------- 1 file changed, 69 deletions(-) diff --git a/lib/ex_pesa/util.ex b/lib/ex_pesa/util.ex index 8995990..00ee4f3 100644 --- a/lib/ex_pesa/util.ex +++ b/lib/ex_pesa/util.ex @@ -20,75 +20,6 @@ defmodule ExPesa.Util do Encrypt the array with the M-Pesa public key certificate. Use the RSA algorithm, and use PKCS #1.5 padding (not OAEP), and add the result to the encrypted stream. Convert the resulting encrypted byte array into a string using base64 encoding. The resulting base64 encoded string is the security credential. - Impementation Examples - PHP - - - Node Js - module.exports = (certPath, shortCodeSecurityCredential) => { - const bufferToEncrypt = Buffer.from(shortCodeSecurityCredential) - const data = fs.readFileSync(path.resolve(certPath)) - const privateKey = String(data) - const encrypted = crypto.publicEncrypt({ - key: privateKey, - padding: crypto.constants.RSA_PKCS1_PADDING - }, bufferToEncrypt) - const securityCredential = encrypted.toString('base64') - return securityCredential - } - - JAVA - - // Function to encrypt the initiator credentials - public static String encryptInitiatorPassword(String securityCertificate, String password) { - String encryptedPassword = "YOUR_INITIATOR_PASSWORD"; - try { - Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); - byte[] input = password.getBytes(); - - Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC"); - FileInputStream fin = new FileInputStream(new File(securityCertificate)); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - X509Certificate certificate = (X509Certificate) cf.generateCertificate(fin); - PublicKey pk = certificate.getPublicKey(); - cipher.init(Cipher.ENCRYPT_MODE, pk); - - byte[] cipherText = cipher.doFinal(input); - - // Convert the resulting encrypted byte array into a string using base64 encoding - encryptedPassword = Base64.encode(cipherText); - } - } - - Python - - from M2Crypto import RSA, X509 - from base64 import b64encode - - INITIATOR_PASS = "YOUR_PASSWORD" - CERTIFICATE_FILE = "PATH_TO_CERTIFICATE_FILE" - - def encryptInitiatorPassword(): - cert_file = open(CERTIFICATE_FILE, 'r') - cert_data = cert_file.read() #read certificate file - cert_file.close() - - cert = X509.load_cert_string(cert_data) - #pub_key = X509.load_cert_string(cert_data) - pub_key = cert.get_pubkey() - rsa_key = pub_key.get_rsa() - cipher = rsa_key.public_encrypt(INITIATOR_PASS, RSA.pkcs1_padding) - return b64encode(cipher) - - print encryptInitiatorPassword() - ## Example iex> certfile = "-----BEGIN CERTIFICATE-----\nMIIGKzCCBROgAwIBAgIQDL7NH8cxSdUpl0ihH0A1wTANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwODI3MDAwMDAwWhcN\nMTkwNDA0MTIwMDAwWjBuMQswCQYDVQQGEwJLRTEQMA4GA1UEBxMHTmFpcm9iaTEW\nMBQGA1UEChMNU2FmYXJpY29tIFBMQzETMBEGA1UECxMKRGlnaXRhbCBJVDEgMB4G\nA1UEAxMXc2FuZGJveC5zYWZhcmljb20uY28ua2UwggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQC78yeC/wLoZY6TJeqc4g/9eAKIpeCwEsjX09pD8ZxAGXqT\nOi7ssdIGJBPmJZNeEVyf8ocFhisCuLngJ9Z5e/AvH52PhrEFmVu2D03zSf4C+rhZ\nndEKP6G79pUAb/bemOliU9zM8xYYkpCRzPWUzk6zSDarg0ZDLw5FrtZj/VJ9YEDL\nWGgAfwExEgSN3wjyUlJ2UwI3wqQXLka0VNFWoZxUH5j436gbSWRIL6NJUmrq8V8S\naTEPz3eJHj3NOToDu245c7VKdF/KExyZjRjD2p5I+Aip80TXzKlZj6DjMb3DlfXF\nHsnu0+1uJE701mvKX7BiscxKr8tCRphL63as4dqvAgMBAAGjggLkMIIC4DAfBgNV\nHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUzZmY7ZORLw9w\nqRbAQN5m9lJ28qMwIgYDVR0RBBswGYIXc2FuZGJveC5zYWZhcmljb20uY28ua2Uw\nDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBr\nBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1z\naGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Et\nc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcC\nARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYB\nBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w\nRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy\ndFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwCQYDVR0TBAIwADCCAQUGCisGAQQB1nkC\nBAIEgfYEgfMA8QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB\nZXs1FvEAAAQDAEcwRQIgBzVMkm7SNprjJ1GBqiXIc9rNzY+y7gt6s/O02oMkyFoC\nIQDBuThGlpmUKpeZoHhK6HGwB4jDMIecmKaOcMS18R2jxwB3AId1v+dZfPiMQ5lf\nvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZXs1F8IAAAQDAEgwRgIhAIRq2XFiC+RS\nuDCYq8ICJg0QafSV+e9BLpJnElEdaSjiAiEAyiiW4vxwv4cWcAXE6FAipctyUBs6\nbE5QyaCnmNpoDiQwDQYJKoZIhvcNAQELBQADggEBAB0YoWve9Sxhb0PBS3Hc46Rf\na7H1jhHuwE+UyscSQsdJdk8uPAgDuKRZMvJPGEaCkNHm36NfcaXXFjPOl7LI1d1a\n9zqSP0xeZBI6cF0x96WuQGrI9/WR2tfxjmaUSp8a/aJ6n+tZA28eJZNPrIaMm+6j\ngh7AkKnqcf+g8F/MvCCVdNAiVMdz6UpCscf6BRPHNZ5ifvChGh7aUKjrVLLuF4Ls\nHE05qm6HNyV5eTa6wvcbc4ewguN1UDZvPWetSyfBk10Wbpor4znQ4TJ3Y9uCvsJH\n41ldblDvZZ2z4kB2UYQ7iBkPlJSxSOaFgW/GGDXq49sz/995xzhVITHxh2SdLkI=\n-----END CERTIFICATE-----\n" iex> password = "Safaricom133" From cc6a12c8cc7fa5f36d53624f9bae979b7465b92c Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Sat, 3 Oct 2020 16:54:36 +0300 Subject: [PATCH 36/65] fixed mix docs generation --- lib/ex_pesa/Mpesa/b2b.ex | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index cf26359..10834e9 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -7,7 +7,7 @@ defmodule ExPesa.Mpesa.B2B do import ExPesa.Util @doc """ - This API enables Business to Business (B2B) transactions between a business and another business. Use of this API requires a valid and verified B2B M-Pesa short code for the business initiating the transaction and the both businesses involved in the transaction. + Initiates the Mpesa B2B . ## Configuration Add below config to dev.exs / prod.exs files @@ -16,17 +16,16 @@ defmodule ExPesa.Mpesa.B2B do #### B2B - Configuration Parameters - `initiator` - This is the credential/username used to authenticate the transaction request. Environment - - production - - - create a user with api access method (access channel) - - Enter user name - - assign business manager role and B2B ORG API initiator role. - Use the username from your notifitation channel (SMS) - - sandbox - use your own custom username - - `timeout_url' - The path that stores information of time out transactions.it should be properly validated to make sure that it contains the port, URI and domain name or publicly available IP. + - production + - create a user with api access method (access channel) + - Enter user name + - assign business manager role and B2B ORG API initiator role. + Use the username from your notifitation channel (SMS) + - sandbox - use your own custom username. + + - `timeout_url` - The path that stores information of time out transactions.it should be properly validated to make sure that it contains the port, URI and domain name or publicly available IP. - `result_url` - The path that receives results from M-Pesa it should be properly validated to make sure that it contains the port, URI and domain name or publicly available IP. - - `security credential` - To generate security_credential, head over to https://developer.safaricom.co.ke/test_credentials, then Initiator Security Password for your environment - - Test - use the above test security credential - - Production - use the actual production security credential + - `security credential` - To generate security_credential, head over to https://developer.safaricom.co.ke/test_credentials, then Initiator Security Password for your environment. `config.exs` ```elixir @@ -45,8 +44,8 @@ defmodule ExPesa.Mpesa.B2B do Alternatively, generate security credential using certificate `cert` - This is the M-Pesa public key certificate used to encrypt your plain password. There are 2 types of certificates. - - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer - - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer + - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer . + - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer . `password` - This is a plain unencrypted password. Environment - production - set password from the organization portal. @@ -68,12 +67,13 @@ defmodule ExPesa.Mpesa.B2B do ``` ## Parameters + attrs: - a map containing: - - `command_id` - Unique command for each transaction type, possible values are: BusinessPayBill, MerchantToMerchantTransfer, MerchantTransferFromMerchantToWorking, MerchantServicesMMFAccountTransfer, AgencyFloatAdvance + - `command_id` - Unique command for each transaction type, possible values are: BusinessPayBill, MerchantToMerchantTransfer, MerchantTransferFromMerchantToWorking, MerchantServicesMMFAccountTransfer, AgencyFloatAdvance. - `amount` - The amount being transacted. - - `receiver_party` - Organization’s short code receiving the funds being transacted + - `receiver_party` - Organization’s short code receiving the funds being transacted. - `remarks` - Comments that are sent along with the transaction. - - `account_reference` - Account Reference mandatory for “BusinessPaybill” CommandID. + - `account_reference` - Account Reference mandatory for "BusinessPaybill" CommandID. ## Example iex> ExPesa.Mpesa.B2B.request(%{command_id: "BusinessPayBill", amount: 10500, receiver_party: 600000, remarks: "B2B Request", account_reference: "BILL PAYMENT"}) @@ -93,6 +93,7 @@ defmodule ExPesa.Mpesa.B2B do end end + @doc false def request() do {:error, "Required Parameter missing, 'command_id','amount','receiver_party', 'remarks','account_reference'"} From db3e0b1507ecf577cd20bd4d35520f39df8a65d2 Mon Sep 17 00:00:00 2001 From: manuelgeek Date: Sat, 3 Oct 2020 17:08:44 +0300 Subject: [PATCH 37/65] C2B, B2B added --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a2cf172..3e38d0a 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ - [x] Mpesa Express (STK) - [x] STK Transaction Validation - [ ] B2C - - [ ] B2B - - [ ] C2B + - [x] B2B + - [x] C2B - [ ] Reversal - [ ] Transaction Status - [ ] Account Balance From c18a0eb9f5153e2177b3b580565b7366f15d8b7f Mon Sep 17 00:00:00 2001 From: Tee22 Date: Sat, 19 Sep 2020 15:44:01 +0300 Subject: [PATCH 38/65] mpesa b2c --- lib/ex_pesa/Mpesa/b2c.ex | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 lib/ex_pesa/Mpesa/b2c.ex diff --git a/lib/ex_pesa/Mpesa/b2c.ex b/lib/ex_pesa/Mpesa/b2c.ex new file mode 100644 index 0000000..3dd1e58 --- /dev/null +++ b/lib/ex_pesa/Mpesa/b2c.ex @@ -0,0 +1,46 @@ +defmodule ExPesa.Mpesa.B2c do + @moduledoc """ + Business to Customer (B2C) API enables the Business or organization to pay its customers who are the end-users of its products or services. + Currently, the B2C API allows the org to perform around 3 types of transactions: Salary Payments, Business Payments or Promotion payments. + Salary payments are used by organizations paying their employees via M-Pesa, Business Payments are normal business transactions to customers + e.g. bank transfers to mobile, Promotion payments are payments made by organization carrying out promotional services e.g. + betting companies paying out winnings to clients + """ + + import ExPesa.Mpesa.MpesaBase + + @doc """ + Initiates a B2C mpesa request. + + + """ + + def initiate_request(%{ + username: username, + command_id: command_id, + amount: amount, + phone_number: phone_number, + remarks: remarks, + occassion: occassion + }) do + short_code = Application.get_env(:ex_pesa, :mpesa)[:mpesa_short_code] + passkey = Application.get_env(:ex_pesa, :mpesa)[:mpesa_passkey] + {:ok, timestamp} = Timex.now() |> Timex.format("%Y%m%d%H%M%S", :strftime) + credential = Base.encode64(short_code <> passkey <> timestamp) + + payload = %{ + "InitiatorName" => username, + "SecurityCredential" => credential, + "CommandID" => command_id, + "Amount" => amount, + "PartyA" => short_code, + "PartyB" => phone_number, + "Remarks" => remarks, + "QueueTimeOutURL" => Application.get_env(:ex_pesa, :mpesa)[:mpesa_callback_url], + "ResultURL" => Application.get_env(:ex_pesa, :mpesa)[:mpesa_callback_url], + "Occassion" => occassion + } + + make_request("/mpesa/b2c/v1/paymentrequest", payload) + end +end From 18c34a4f02e601fcca26608e8203162dc61328d3 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sat, 10 Oct 2020 17:54:23 +0300 Subject: [PATCH 39/65] finishes b2c module --- config/dev.sample.exs | 9 +++ config/test.exs | 9 +++ lib/ex_pesa/Mpesa/b2c.ex | 99 +++++++++++++++++++++++++-------- test/ex_pesa/Mpesa/b2c_test.exs | 63 +++++++++++++++++++++ 4 files changed, 158 insertions(+), 22 deletions(-) create mode 100644 test/ex_pesa/Mpesa/b2c_test.exs diff --git a/config/dev.sample.exs b/config/dev.sample.exs index aa5a174..d4f1ead 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -22,5 +22,14 @@ config :ex_pesa, result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + ], + b2c: [ + short_code: "600247", + initiator_name: "John Doe", + password: "Safaricom133", + timeout_url: "https://58cb49b30213.ngrok.io/b2c/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/b2c/result_url", + security_credential: + "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" ] ] diff --git a/config/test.exs b/config/test.exs index 439cf38..dc0c8f9 100644 --- a/config/test.exs +++ b/config/test.exs @@ -19,5 +19,14 @@ config :ex_pesa, result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + ], + b2c: [ + short_code: "600247", + initiator_name: "John Doe", + password: "Safaricom133", + timeout_url: "https://58cb49b30213.ngrok.io/b2c/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/b2c/result_url", + security_credential: + "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" ] ] diff --git a/lib/ex_pesa/Mpesa/b2c.ex b/lib/ex_pesa/Mpesa/b2c.ex index 3dd1e58..10b73cf 100644 --- a/lib/ex_pesa/Mpesa/b2c.ex +++ b/lib/ex_pesa/Mpesa/b2c.ex @@ -1,46 +1,101 @@ -defmodule ExPesa.Mpesa.B2c do +defmodule ExPesa.Mpesa.B2C do @moduledoc """ - Business to Customer (B2C) API enables the Business or organization to pay its customers who are the end-users of its products or services. - Currently, the B2C API allows the org to perform around 3 types of transactions: Salary Payments, Business Payments or Promotion payments. - Salary payments are used by organizations paying their employees via M-Pesa, Business Payments are normal business transactions to customers + Business to Customer (B2C) API enables the Business or organization to pay its customers who are the end-users of its products or services. + Currently, the B2C API allows the org to perform around 3 types of transactions: Salary Payments, Business Payments or Promotion payments. + Salary payments are used by organizations paying their employees via M-Pesa, Business Payments are normal business transactions to customers e.g. bank transfers to mobile, Promotion payments are payments made by organization carrying out promotional services e.g. betting companies paying out winnings to clients """ import ExPesa.Mpesa.MpesaBase + import ExPesa.Util @doc """ - Initiates a B2C mpesa request. + Initiates the Mpesa B2C . + ## Configuration + Add below config to dev.exs / prod.exs files + This asumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#account-balance-api + `config.exs` + ```elixir + config :ex_pesa, + mpesa: [ + cert: "", + b2c: [ + initiator_name: "Safaricom1", + password: "Safaricom133", + timeout_url: "", + result_url: "", + security_credential: "" + ] + ] + ``` + To generate security_credential, head over to https://developer.safaricom.co.ke/test_credentials, then Initiator Security Password for your environment. + Alternatively, generate security credential using certificate + `cert` - This is the M-Pesa public key certificate used to encrypt your plain password. + There are 2 types of certificates. + - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer . + - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer . + `password` - This is a plain unencrypted password. + Environment + - production - set password from the organization portal. + - sandbox - use your own custom password + ## Parameters + + attrs: - a map containing: + - `command_id` - Unique command for each transaction type, possible values are: BusinessPayBill, MerchantToMerchantTransfer, MerchantTransferFromMerchantToWorking, MerchantServicesMMFAccountTransfer, AgencyFloatAdvance. + - `amount` - The amount being transacted. + - `phone_number` - Phone number receiving the transaction. + - `remarks` - Comments that are sent along with the transaction. + - `occassion` - Optional. + + ## Example + iex> ExPesa.Mpesa.B2C.request(%{command_id: "BusinessPayment", amount: 10500, phone_number: "254722000000", remarks: "B2C Request"}) + {:ok, + %{ + "ConversationID" => "AG_20201010_00006bd489ffcaf79e91", + "OriginatorConversationID" => "27293-71728391-3", + "ResponseCode" => "0", + "ResponseDescription" => "Accept the service request successfully." + }} """ - def initiate_request(%{ - username: username, - command_id: command_id, - amount: amount, - phone_number: phone_number, - remarks: remarks, - occassion: occassion - }) do - short_code = Application.get_env(:ex_pesa, :mpesa)[:mpesa_short_code] - passkey = Application.get_env(:ex_pesa, :mpesa)[:mpesa_passkey] - {:ok, timestamp} = Timex.now() |> Timex.format("%Y%m%d%H%M%S", :strftime) - credential = Base.encode64(short_code <> passkey <> timestamp) + def request(params) do + case get_security_credential_for(:b2c) do + nil -> {:error, "cannot generate security_credential due to missing configuration fields"} + security_credential -> b2c_request(security_credential, params) + end + end + + defp b2c_request( + security_credential, + %{ + command_id: command_id, + amount: amount, + phone_number: phone_number, + remarks: remarks + } = params + ) do + occassion = Map.get(params, :occassion, nil) payload = %{ - "InitiatorName" => username, - "SecurityCredential" => credential, + "InitiatorName" => Application.get_env(:ex_pesa, :mpesa)[:b2c][:initiator_name], + "SecurityCredential" => security_credential, "CommandID" => command_id, "Amount" => amount, - "PartyA" => short_code, + "PartyA" => Application.get_env(:ex_pesa, :mpesa)[:b2c][:short_code], "PartyB" => phone_number, "Remarks" => remarks, - "QueueTimeOutURL" => Application.get_env(:ex_pesa, :mpesa)[:mpesa_callback_url], - "ResultURL" => Application.get_env(:ex_pesa, :mpesa)[:mpesa_callback_url], + "QueueTimeOutURL" => Application.get_env(:ex_pesa, :mpesa)[:b2c][:timeout_url], + "ResultURL" => Application.get_env(:ex_pesa, :mpesa)[:b2c][:result_url], "Occassion" => occassion } make_request("/mpesa/b2c/v1/paymentrequest", payload) end + + defp b2c_request(_security_credential, %{}) do + {:error, "Required Parameter missing, 'command_id','amount','phone_number', 'remarks'"} + end end diff --git a/test/ex_pesa/Mpesa/b2c_test.exs b/test/ex_pesa/Mpesa/b2c_test.exs new file mode 100644 index 0000000..d7a0c22 --- /dev/null +++ b/test/ex_pesa/Mpesa/b2c_test.exs @@ -0,0 +1,63 @@ +defmodule ExPesa.Mpesa.B2CTest do + @moduledoc false + + use ExUnit.Case, async: true + + import Tesla.Mock + doctest ExPesa.Mpesa.B2C + + alias ExPesa.Mpesa.B2C + + setup do + mock(fn + %{ + url: "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials", + method: :get + } -> + %Tesla.Env{ + status: 200, + body: %{ + "access_token" => "SGWcJPtNtYNPGm6uSYR9yPYrAI3Bm", + "expires_in" => "3599" + } + } + + %{ + url: "https://sandbox.safaricom.co.ke/mpesa/b2c/v1/paymentrequest", + method: :post + } -> + %Tesla.Env{ + status: 200, + body: %{ + "ConversationID" => "AG_20201010_00006bd489ffcaf79e91", + "OriginatorConversationID" => "27293-71728391-3", + "ResponseCode" => "0", + "ResponseDescription" => "Accept the service request successfully." + } + } + end) + + :ok + end + + describe "Mpesa B2C" do + test "request/1 should Initiate a B2C request" do + payment_details = %{ + command_id: "BusinessPayBill", + amount: 10500, + phone_number: "254722000000", + remarks: "B2C Request" + } + + {:ok, result} = B2C.request(payment_details) + + assert result["ResponseCode"] == "0" + end + + test "request/1 should error out without required parameter" do + {:error, result} = B2C.request(%{}) + + "Required Parameter missing, 'command_id','amount','phone_number', 'remarks'" = result + end + end +end From e6450591c081f4dfd7328849809aa0890cc8c5a1 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sat, 10 Oct 2020 19:01:21 +0300 Subject: [PATCH 40/65] catch missing params --- lib/ex_pesa/Mpesa/b2c.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ex_pesa/Mpesa/b2c.ex b/lib/ex_pesa/Mpesa/b2c.ex index 10b73cf..bf189d8 100644 --- a/lib/ex_pesa/Mpesa/b2c.ex +++ b/lib/ex_pesa/Mpesa/b2c.ex @@ -95,7 +95,7 @@ defmodule ExPesa.Mpesa.B2C do make_request("/mpesa/b2c/v1/paymentrequest", payload) end - defp b2c_request(_security_credential, %{}) do + defp b2c_request(_security_credential, _) do {:error, "Required Parameter missing, 'command_id','amount','phone_number', 'remarks'"} end end From 3c4f80aa2cbe9bdc61d8c07ca43085d33827b2e9 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sat, 10 Oct 2020 19:11:39 +0300 Subject: [PATCH 41/65] catch missing params --- lib/ex_pesa/Mpesa/b2b.ex | 19 +++++++++---------- test/ex_pesa/Mpesa/b2b_test.exs | 4 ++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index 10834e9..423b68c 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -16,7 +16,7 @@ defmodule ExPesa.Mpesa.B2B do #### B2B - Configuration Parameters - `initiator` - This is the credential/username used to authenticate the transaction request. Environment - - production + - production - create a user with api access method (access channel) - Enter user name - assign business manager role and B2B ORG API initiator role. @@ -93,19 +93,13 @@ defmodule ExPesa.Mpesa.B2B do end end - @doc false - def request() do - {:error, - "Required Parameter missing, 'command_id','amount','receiver_party', 'remarks','account_reference'"} - end - defp b2b_request(security_credential, %{ command_id: command_id, amount: amount, receiver_party: receiver_party, - remarks: remarks, - account_reference: account_reference - }) do + remarks: remarks + } = params) do + account_reference = Map.get(params, :account_reference, nil) payload = %{ "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:initiator_name], "SecurityCredential" => security_credential, @@ -123,4 +117,9 @@ defmodule ExPesa.Mpesa.B2B do make_request("/mpesa/b2b/v1/paymentrequest", payload) end + + defp b2b_request(_security_credential, _) do + {:error, + "Required Parameter missing, 'command_id','amount','receiver_party', 'remarks'"} + end end diff --git a/test/ex_pesa/Mpesa/b2b_test.exs b/test/ex_pesa/Mpesa/b2b_test.exs index ae2ecb2..2dfac27 100644 --- a/test/ex_pesa/Mpesa/b2b_test.exs +++ b/test/ex_pesa/Mpesa/b2b_test.exs @@ -56,9 +56,9 @@ defmodule ExPesa.Mpesa.B2BTest do end test "request/1 should error out without required parameter" do - {:error, result} = B2B.request() + {:error, result} = B2B.request(%{}) - "Required Parameter missing, 'command_id','amount','receiver_party', 'remarks','account_reference'" = + "Required Parameter missing, 'command_id','amount','receiver_party', 'remarks'" = result end end From 50ba0a99ca7bf7d97fc0305df7b9e083926aca85 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sat, 10 Oct 2020 19:18:46 +0300 Subject: [PATCH 42/65] mix format --- lib/ex_pesa/Mpesa/b2b.ex | 19 +++++++++++-------- test/ex_pesa/Mpesa/b2b_test.exs | 3 +-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index 423b68c..025b344 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -93,13 +93,17 @@ defmodule ExPesa.Mpesa.B2B do end end - defp b2b_request(security_credential, %{ - command_id: command_id, - amount: amount, - receiver_party: receiver_party, - remarks: remarks - } = params) do + defp b2b_request( + security_credential, + %{ + command_id: command_id, + amount: amount, + receiver_party: receiver_party, + remarks: remarks + } = params + ) do account_reference = Map.get(params, :account_reference, nil) + payload = %{ "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:initiator_name], "SecurityCredential" => security_credential, @@ -119,7 +123,6 @@ defmodule ExPesa.Mpesa.B2B do end defp b2b_request(_security_credential, _) do - {:error, - "Required Parameter missing, 'command_id','amount','receiver_party', 'remarks'"} + {:error, "Required Parameter missing, 'command_id','amount','receiver_party', 'remarks'"} end end diff --git a/test/ex_pesa/Mpesa/b2b_test.exs b/test/ex_pesa/Mpesa/b2b_test.exs index 2dfac27..039bfdc 100644 --- a/test/ex_pesa/Mpesa/b2b_test.exs +++ b/test/ex_pesa/Mpesa/b2b_test.exs @@ -58,8 +58,7 @@ defmodule ExPesa.Mpesa.B2BTest do test "request/1 should error out without required parameter" do {:error, result} = B2B.request(%{}) - "Required Parameter missing, 'command_id','amount','receiver_party', 'remarks'" = - result + "Required Parameter missing, 'command_id','amount','receiver_party', 'remarks'" = result end end end From 95602a29e312fc0fcb462de0a2e5123ad0d19122 Mon Sep 17 00:00:00 2001 From: Emmanuel Magak Date: Sat, 10 Oct 2020 19:51:02 +0300 Subject: [PATCH 43/65] increased test coverage --- config/test.exs | 4 +- test/ex_pesa/Mpesa/mpesa_base_test.exs | 53 ++++++++++++++++++++++++ test/ex_pesa/Mpesa/token_server_test.exs | 8 ++-- test/ex_pesa/utils_test.exs | 12 ++++++ 4 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 test/ex_pesa/Mpesa/mpesa_base_test.exs create mode 100644 test/ex_pesa/utils_test.exs diff --git a/config/test.exs b/config/test.exs index 439cf38..90152df 100644 --- a/config/test.exs +++ b/config/test.exs @@ -17,7 +17,7 @@ config :ex_pesa, password: "Safaricom133", timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", - security_credential: - "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + # Leace blank to increase util test coverage + security_credential: "" ] ] diff --git a/test/ex_pesa/Mpesa/mpesa_base_test.exs b/test/ex_pesa/Mpesa/mpesa_base_test.exs new file mode 100644 index 0000000..628231f --- /dev/null +++ b/test/ex_pesa/Mpesa/mpesa_base_test.exs @@ -0,0 +1,53 @@ +defmodule ExPesa.Mpesa.MpesaBaseTest do + @moduledoc false + + use ExUnit.Case, async: true + + alias ExPesa.Mpesa.MpesaBase + + describe "Process Results" do + test "response with 201 status with map body" do + resp = + {:ok, + %{ + status: 201, + body: %{ + responseCode: 0, + success: true + } + }} + + MpesaBase.process_result(resp) + end + + test "response with 201 status with json body" do + resp = + {:ok, + %{ + status: 201, + body: """ + { + "responseCode": 0, + "success": true + } + """ + }} + + MpesaBase.process_result(resp) + end + + test "response with result OK" do + resp = + {:ok, + %{ + status: 400, + body: %{ + responseCode: 0, + success: true + } + }} + + MpesaBase.process_result(resp) + end + end +end diff --git a/test/ex_pesa/Mpesa/token_server_test.exs b/test/ex_pesa/Mpesa/token_server_test.exs index d438227..7000388 100644 --- a/test/ex_pesa/Mpesa/token_server_test.exs +++ b/test/ex_pesa/Mpesa/token_server_test.exs @@ -6,10 +6,6 @@ defmodule ExPesa.Mpesa.TokenServerTest do alias ExPesa.Mpesa.TokenServer alias ExPesa.Mpesa.MpesaBase - # setup do - # start_supervised!(TokenServer) - # end - setup do mock(fn %{ @@ -28,6 +24,10 @@ defmodule ExPesa.Mpesa.TokenServerTest do :ok end + test "starts OTP server" do + TokenServer.start_link() + end + test "stores and retrieves token" do org_token = "SGWcJPtNtYNPGm6uSYR9yPYrAI3Bm" TokenServer.insert({org_token, DateTime.add(DateTime.utc_now(), 3550, :second)}) diff --git a/test/ex_pesa/utils_test.exs b/test/ex_pesa/utils_test.exs new file mode 100644 index 0000000..947ea85 --- /dev/null +++ b/test/ex_pesa/utils_test.exs @@ -0,0 +1,12 @@ +defmodule ExPesa.UtilsTest do + @moduledoc false + + use ExUnit.Case, async: true + + alias ExPesa.Util + + describe "generates secutityCredential" do + result = Util.get_security_credential_for(:b2b) + assert is_bitstring(result) + end +end From f99118175406cccea8850a0c09183a8de561f214 Mon Sep 17 00:00:00 2001 From: Emmanuel Magak Date: Sat, 10 Oct 2020 20:48:42 +0300 Subject: [PATCH 44/65] added coverals test coverage --- .github/workflows/elixir.yml | 13 +++++++++++-- mix.exs | 12 ++++++++++-- mix.lock | 1 + 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 2fe5e37..4041e41 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -14,6 +14,13 @@ jobs: check-lint: runs-on: ubuntu-latest + strategy: + matrix: + otp: [21.3.8.10, 22.2] + elixir: [1.8.2, 1.9.4] + env: + MIX_ENV: test + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v2 @@ -22,12 +29,14 @@ jobs: - name: Setup elixir uses: actions/setup-elixir@v1 with: - elixir-version: '1.9.4' - otp-version: '22.2' + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} - name: Install Dependencies run: mix deps.get - name: Run Tests run: mix test + - name: Coverals Coverage + run: mix coveralls.github - name: Check formatting run: mix format --check-formatted - name: Checking compile warnings diff --git a/mix.exs b/mix.exs index 6a6b130..3c4b064 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule ExPesa.MixProject do [ app: :ex_pesa, version: "0.1.0", - elixir: "~> 1.9", + elixir: "~> 1.8", start_permanent: Mix.env() == :prod, description: "Payment Library For Most Public Payment API's in Kenya and hopefully Africa.", package: package(), @@ -20,6 +20,13 @@ defmodule ExPesa.MixProject do logo: "assets/logo.png", assets: "assets", extras: ["README.md", "contributing.md"] + ], + test_coverage: [tool: ExCoveralls], + preferred_cli_env: [ + coveralls: :test, + "coveralls.detail": :test, + "coveralls.post": :test, + "coveralls.html": :test ] ] end @@ -52,7 +59,8 @@ defmodule ExPesa.MixProject do {:hackney, "~> 1.16.0"}, {:jason, ">= 1.0.0"}, {:timex, "~> 3.6.2"}, - {:ex_doc, "~> 0.21", only: :dev, runtime: false} + {:ex_doc, "~> 0.21", only: :dev, runtime: false}, + {:excoveralls, "~> 0.10", only: :test} ] end end diff --git a/mix.lock b/mix.lock index 5c81eb1..42fedec 100644 --- a/mix.lock +++ b/mix.lock @@ -5,6 +5,7 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"}, "ex_doc_makeup": {:hex, :ex_doc_makeup, "0.1.2", "88d9a9c4be29a8486c3aa410927706c2da0c4e22fcc135e49dceb15f17510de0", [:mix], [{:ex_doc, ">= 0.18.1", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:makeup_elixir, ">= 0.3.1", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "7cc49a1634112799252ce540e85eb321734d67a75c76db4fd0cba646fef37574"}, + "excoveralls": {:hex, :excoveralls, "0.13.2", "5ca05099750c086f144fcf75842c363fc15d7d9c6faa7ad323d010294ced685e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1e7ed75c158808a5a8f019d3ad63a5efe482994f2f8336c0a8c77d2f0ab152ce"}, "gettext": {:hex, :gettext, "0.18.1", "89e8499b051c7671fa60782faf24409b5d2306aa71feb43d79648a8bc63d0522", [:mix], [], "hexpm", "e70750c10a5f88cb8dc026fc28fa101529835026dec4a06dba3b614f2a99c7a9"}, "gun": {:hex, :gun, "1.3.2", "542064cbb9f613650b8a8100b3a927505f364fbe198b7a5a112868ff43f3e477", [:rebar3], [{:cowlib, "~> 2.6.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "ba323f0a5fd8abac379a3e1fe6d8ce570c4a12c7fd1c68f4994b53447918e462"}, "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, From 9588f327a226d0f69df6325647736e633fa5eee4 Mon Sep 17 00:00:00 2001 From: Emmanuel Magak Date: Sat, 10 Oct 2020 20:48:42 +0300 Subject: [PATCH 45/65] added coverals test coverage --- .github/workflows/elixir.yml | 13 +++++++++++-- README.md | 2 +- mix.exs | 12 ++++++++++-- mix.lock | 1 + 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 2fe5e37..4041e41 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -14,6 +14,13 @@ jobs: check-lint: runs-on: ubuntu-latest + strategy: + matrix: + otp: [21.3.8.10, 22.2] + elixir: [1.8.2, 1.9.4] + env: + MIX_ENV: test + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v2 @@ -22,12 +29,14 @@ jobs: - name: Setup elixir uses: actions/setup-elixir@v1 with: - elixir-version: '1.9.4' - otp-version: '22.2' + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} - name: Install Dependencies run: mix deps.get - name: Run Tests run: mix test + - name: Coverals Coverage + run: mix coveralls.github - name: Check formatting run: mix format --check-formatted - name: Checking compile warnings diff --git a/README.md b/README.md index 3e38d0a..e198dd5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

-[![Actions Status](https://github.com/beamkenya/ex_pesa/workflows/Elixir%20CI/badge.svg)](https://github.com/beamkenya/ex_pesa/actions) ![Hex.pm](https://img.shields.io/hexpm/v/ex_pesa) ![Hex.pm](https://img.shields.io/hexpm/dt/ex_pesa) +[![Actions Status](https://github.com/beamkenya/ex_pesa/workflows/Elixir%20CI/badge.svg)](https://github.com/beamkenya/ex_pesa/actions) ![Hex.pm](https://img.shields.io/hexpm/v/ex_pesa) ![Hex.pm](https://img.shields.io/hexpm/dt/ex_pesa) [![Coverage Status](https://coveralls.io/repos/github/beamkenya/ex_pesa/badge.svg?branch=develop)](https://coveralls.io/github/beamkenya/ex_pesa?branch=develop) # ExPesa :dollar: :pound: :yen: :euro: diff --git a/mix.exs b/mix.exs index 6a6b130..3c4b064 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule ExPesa.MixProject do [ app: :ex_pesa, version: "0.1.0", - elixir: "~> 1.9", + elixir: "~> 1.8", start_permanent: Mix.env() == :prod, description: "Payment Library For Most Public Payment API's in Kenya and hopefully Africa.", package: package(), @@ -20,6 +20,13 @@ defmodule ExPesa.MixProject do logo: "assets/logo.png", assets: "assets", extras: ["README.md", "contributing.md"] + ], + test_coverage: [tool: ExCoveralls], + preferred_cli_env: [ + coveralls: :test, + "coveralls.detail": :test, + "coveralls.post": :test, + "coveralls.html": :test ] ] end @@ -52,7 +59,8 @@ defmodule ExPesa.MixProject do {:hackney, "~> 1.16.0"}, {:jason, ">= 1.0.0"}, {:timex, "~> 3.6.2"}, - {:ex_doc, "~> 0.21", only: :dev, runtime: false} + {:ex_doc, "~> 0.21", only: :dev, runtime: false}, + {:excoveralls, "~> 0.10", only: :test} ] end end diff --git a/mix.lock b/mix.lock index 5c81eb1..42fedec 100644 --- a/mix.lock +++ b/mix.lock @@ -5,6 +5,7 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"}, "ex_doc_makeup": {:hex, :ex_doc_makeup, "0.1.2", "88d9a9c4be29a8486c3aa410927706c2da0c4e22fcc135e49dceb15f17510de0", [:mix], [{:ex_doc, ">= 0.18.1", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:makeup_elixir, ">= 0.3.1", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "7cc49a1634112799252ce540e85eb321734d67a75c76db4fd0cba646fef37574"}, + "excoveralls": {:hex, :excoveralls, "0.13.2", "5ca05099750c086f144fcf75842c363fc15d7d9c6faa7ad323d010294ced685e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1e7ed75c158808a5a8f019d3ad63a5efe482994f2f8336c0a8c77d2f0ab152ce"}, "gettext": {:hex, :gettext, "0.18.1", "89e8499b051c7671fa60782faf24409b5d2306aa71feb43d79648a8bc63d0522", [:mix], [], "hexpm", "e70750c10a5f88cb8dc026fc28fa101529835026dec4a06dba3b614f2a99c7a9"}, "gun": {:hex, :gun, "1.3.2", "542064cbb9f613650b8a8100b3a927505f364fbe198b7a5a112868ff43f3e477", [:rebar3], [{:cowlib, "~> 2.6.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "ba323f0a5fd8abac379a3e1fe6d8ce570c4a12c7fd1c68f4994b53447918e462"}, "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, From b64b332b45e180c2359848fbba2e1fb1aae4a32b Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sun, 11 Oct 2020 11:41:47 +0300 Subject: [PATCH 46/65] handle merge confict --- config/test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/test.exs b/config/test.exs index dc0c8f9..e56b5e6 100644 --- a/config/test.exs +++ b/config/test.exs @@ -17,8 +17,8 @@ config :ex_pesa, password: "Safaricom133", timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", - security_credential: - "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + # Leace blank to increase util test coverage + security_credential: "" ], b2c: [ short_code: "600247", From 7c0c3bb5980300b5398e987baa6532b5e5c00a23 Mon Sep 17 00:00:00 2001 From: kamalogudah Date: Sun, 4 Oct 2020 00:31:10 +0300 Subject: [PATCH 47/65] Started on Transaction Status --- lib/ex_pesa/Mpesa/transaction_status.ex | 69 +++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 lib/ex_pesa/Mpesa/transaction_status.ex diff --git a/lib/ex_pesa/Mpesa/transaction_status.ex b/lib/ex_pesa/Mpesa/transaction_status.ex new file mode 100644 index 0000000..c0670d2 --- /dev/null +++ b/lib/ex_pesa/Mpesa/transaction_status.ex @@ -0,0 +1,69 @@ +defmodule ExPesa.Mpesa.TransactionStatus do + @moduledoc """ + Use this api to check the transaction status. + """ + + import ExPesa.Mpesa.MpesaBase + + @doc """ + Initiates the Mpesa Lipa Online STK Push . + + ExPesa.Mpesa.TransactionStatus.query(%{command_id: "Command ID - TransactionStatusQuery", + identifier_type: identifier_type, + remarks: "Some Remarks", + initiator: "kamalogudah", + security_credential: security_credential, + queue_timeout_url: queue_timeout_url, + result_url: result_url, + transaction_id: transaction_id, + occasion: "Some Occasion"}) + +"TransactionID":"Transaction ID e.g LC7918MI73" , +"PartyA":"Phone number that initiated the transaction" , +"IdentifierType":"1" , +"ResultURL":"https://ip_address:port/result_url" , +"QueueTimeOutURL":"https://ip_address:port/timeout_url" , +"Remarks":"Remarks" , +"Occasion": "Optional parameter" + + """ + @spec request(map()) :: {:error, any()} | {:ok, any()} + def query(%{ + command_id: command_id, + remarks: remarks, + initiator: initiator, + security_credential: security_credential, + queue_timeout_url: queue_timeout_url, + result_url: result_url, + transaction_id: transaction_id, + occasion: occasion + + }) do + paybill = Application.get_env(:ex_pesa, :mpesa)[:mpesa_short_code] + passkey = Application.get_env(:ex_pesa, :mpesa)[:mpesa_passkey] + {:ok, timestamp} = Timex.now() |> Timex.format("%Y%m%d%H%M%S", :strftime) + password = Base.encode64(paybill <> passkey <> timestamp) + security_credential = Base.encode64(password) + + payload = %{ + "CommandID" => command_id, + "ShortCode" => paybill, + "IdentifierType" => "1", + "Remarks" => "CustomerPayBillOnline", + "SecurityCredential" => security_credential, + "Initiator" => amount, + "SecurityCredential" => phone, + "QueueTimeOutURL" => paybill, + "ResultURL" => phone, + "TransactionID" => Application.get_env(:ex_pesa, :mpesa)[:mpesa_callback_url], + "Occasion" => occasion + } + + make_request("/mpesa/transactionstatus/v1/query", payload) + end + + def query(_) do + {:error, "Required Parameters missing, 'CommandID, 'IdentifierType', 'Remarks', 'Initiator'"} + end + +end From bca2b698e5d3d14027372d5a10a1390cbca8c060 Mon Sep 17 00:00:00 2001 From: kamalogudah Date: Sun, 11 Oct 2020 11:44:45 +0300 Subject: [PATCH 48/65] Transaction Status --- config/dev.sample.exs | 8 ++ lib/ex_pesa/Mpesa/transaction_status.ex | 117 +++++++++++------- .../ex_pesa/Mpesa/transaction_status_test.exs | 65 ++++++++++ 3 files changed, 143 insertions(+), 47 deletions(-) create mode 100644 test/ex_pesa/Mpesa/transaction_status_test.exs diff --git a/config/dev.sample.exs b/config/dev.sample.exs index aa5a174..8ab2381 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -22,5 +22,13 @@ config :ex_pesa, result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + ], + transaction_status: [ + initiator_name: "John Doe", + password: "Safaricom133", + timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", + security_credential: + "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" ] ] diff --git a/lib/ex_pesa/Mpesa/transaction_status.ex b/lib/ex_pesa/Mpesa/transaction_status.ex index c0670d2..b177c92 100644 --- a/lib/ex_pesa/Mpesa/transaction_status.ex +++ b/lib/ex_pesa/Mpesa/transaction_status.ex @@ -4,66 +4,89 @@ defmodule ExPesa.Mpesa.TransactionStatus do """ import ExPesa.Mpesa.MpesaBase + import ExPesa.Util @doc """ - Initiates the Mpesa Lipa Online STK Push . + Transaction Status Query + ## Requirement Params + - `CommandID`[String] - Takes only 'TransactionStatusQuery' command id + - `PartyA` [Numeric] - Organization/MSISDN receiving the transaction, can be + -Shortcode (6 digits) + -MSISDN (12 Digits) + - `IdentifierType` [Numeric] - Type of organization receiving the transaction can be the folowing: + 1 – MSISDN + 2 – Till Number + 4 – Organization short code + - `Remarks`[String] - Comments that are sent along with the transaction, can be a sequence of characters up to 100 + - `Initiator` [Alpha-Numeric] - The name of Initiator to initiating the request. This is the credential/username + used to authenticate the transaction request + - `SecurityCredential` [String] - Encrypted Credential of user getting transaction amount String + Encrypted password for the initiator to authenticate the transaction request + - `QueueTimeOutURL` [URL] - The path that stores information of time out transaction. Takes the form of + https://ip or domain:port/path + - `ResultURL`[URL] - The path that stores information of transaction. Example https://ip or domain:port/path + - `TransactionID` [Alpha-Numeric] - Unique identifier to identify a transaction on M-Pesa Alpha-Numeric LKXXXX1234 + - `Occasion` [ String] - Optional Parameter String sequence of characters up to 100 - ExPesa.Mpesa.TransactionStatus.query(%{command_id: "Command ID - TransactionStatusQuery", - identifier_type: identifier_type, - remarks: "Some Remarks", - initiator: "kamalogudah", - security_credential: security_credential, - queue_timeout_url: queue_timeout_url, - result_url: result_url, - transaction_id: transaction_id, - occasion: "Some Occasion"}) -"TransactionID":"Transaction ID e.g LC7918MI73" , -"PartyA":"Phone number that initiated the transaction" , -"IdentifierType":"1" , -"ResultURL":"https://ip_address:port/result_url" , -"QueueTimeOutURL":"https://ip_address:port/timeout_url" , -"Remarks":"Remarks" , -"Occasion": "Optional parameter" + ## Parameters + The following are the parameters required for this method, the rest are fetched from config + files. + - `occasion`: + - `party_a`: + - `identifier_type`` + - `remarks`` + - `transaction_id` + Their details have been covered above in the documentation. + + ## Example + + iex> ExPesa.Mpesa.TransactionStatus.request(%{occasion: "Some Occasion",party_a: "600247",identifier_type: "4",remarks: "CustomerPayBillOnline",transaction_id: "SOME7803"}) + {:ok, + %{ + "ConversationID" => "AG_20201010_000056be35a7b266b43e", + "OriginatorConversationID" => "27288-72545279-2", + "ResponseCode" => "0", + "ResponseDescription" => "Accept the service request successfully." + } + } """ - @spec request(map()) :: {:error, any()} | {:ok, any()} - def query(%{ - command_id: command_id, - remarks: remarks, - initiator: initiator, - security_credential: security_credential, - queue_timeout_url: queue_timeout_url, - result_url: result_url, - transaction_id: transaction_id, - occasion: occasion + def request(params) do + case get_security_credential_for(:b2b) do + nil -> {:error, "cannot generate security_credential due to missing configuration fields"} + security_credential -> query(security_credential, params) + end + end - }) do - paybill = Application.get_env(:ex_pesa, :mpesa)[:mpesa_short_code] - passkey = Application.get_env(:ex_pesa, :mpesa)[:mpesa_passkey] - {:ok, timestamp} = Timex.now() |> Timex.format("%Y%m%d%H%M%S", :strftime) - password = Base.encode64(paybill <> passkey <> timestamp) - security_credential = Base.encode64(password) + @doc false + def request() do + {:error, + "Some Required Parameter missing, check whether you have 'occasion', 'party_a', 'identifier_type', 'remarks', and 'transaction_id'"} + end + @spec request(map()) :: {:error, any()} | {:ok, any()} + defp query(security_credential, %{ + occasion: occasion, + transaction_id: transaction_id, + party_a: party_a, + identifier_type: identifier_type, + remarks: remarks + }) do payload = %{ - "CommandID" => command_id, - "ShortCode" => paybill, - "IdentifierType" => "1", - "Remarks" => "CustomerPayBillOnline", + "CommandID" => "TransactionStatusQuery", + "PartyA" => party_a, + "IdentifierType" => identifier_type, + "Remarks" => remarks, "SecurityCredential" => security_credential, - "Initiator" => amount, - "SecurityCredential" => phone, - "QueueTimeOutURL" => paybill, - "ResultURL" => phone, - "TransactionID" => Application.get_env(:ex_pesa, :mpesa)[:mpesa_callback_url], + "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:transaction_status][:initiator_name], + "QueueTimeOutURL" => + Application.get_env(:ex_pesa, :mpesa)[:transaction_status][:timeout_url], + "ResultURL" => Application.get_env(:ex_pesa, :mpesa)[:transaction_status][:result_url], + "TransactionID" => transaction_id, "Occasion" => occasion } make_request("/mpesa/transactionstatus/v1/query", payload) end - - def query(_) do - {:error, "Required Parameters missing, 'CommandID, 'IdentifierType', 'Remarks', 'Initiator'"} - end - end diff --git a/test/ex_pesa/Mpesa/transaction_status_test.exs b/test/ex_pesa/Mpesa/transaction_status_test.exs new file mode 100644 index 0000000..104da25 --- /dev/null +++ b/test/ex_pesa/Mpesa/transaction_status_test.exs @@ -0,0 +1,65 @@ +defmodule ExPesa.Mpesa.TransactionStatusTest do + @moduledoc false + + use ExUnit.Case, async: true + + import Tesla.Mock + doctest ExPesa.Mpesa.TransactionStatus + + alias ExPesa.Mpesa.TransactionStatus + + setup do + mock(fn + %{ + url: "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials", + method: :get + } -> + %Tesla.Env{ + status: 200, + body: %{ + "access_token" => "SGWcJPtNtYNPGm6uSYR9yPYrAI3Bm", + "expires_in" => "3599" + } + } + + %{ + url: "https://sandbox.safaricom.co.ke/mpesa/transactionstatus/v1/query", + method: :post + } -> + %Tesla.Env{ + status: 200, + body: %{ + "ConversationID" => "AG_20201010_000056be35a7b266b43e", + "OriginatorConversationID" => "27288-72545279-2", + "ResponseCode" => "0", + "ResponseDescription" => "Accept the service request successfully." + } + } + end) + + :ok + end + + describe "Mpesa Transaction Status" do + test "request/1 should Initiate a B2B request" do + query_details = %{ + occasion: "Some Occasion", + party_a: "600247", + identifier_type: "4", + remarks: "CustomerPayBillOnline", + transaction_id: "SOME7803" + } + + {:ok, result} = TransactionStatus.request(query_details) + + assert result["ResponseDescription"] == "Accept the service request successfully." + end + + test "request/1 should error out without required parameter" do + {:error, result} = TransactionStatus.request() + + "Some Required Parameter missing, check whether you have 'occasion', 'party_a', 'identifier_type', 'remarks', and 'transaction_id'" = + result + end + end +end From 40646a1fa0f8cb03c64b041890fcc7a91de4b86b Mon Sep 17 00:00:00 2001 From: Emmanuel Magak Date: Sun, 11 Oct 2020 14:00:27 +0300 Subject: [PATCH 49/65] created new coverals .yml --- .github/workflows/coveralls.yml | 33 +++++++++++++++++++++++++++++++++ .github/workflows/elixir.yml | 7 +------ 2 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/coveralls.yml diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml new file mode 100644 index 0000000..5849190 --- /dev/null +++ b/.github/workflows/coveralls.yml @@ -0,0 +1,33 @@ +name: Elixir Coveralls + +on: + push: + branches: + - develop + - master + pull_request: + branches: + - master + - develop + +jobs: + coveralls-coverage: + + runs-on: ubuntu-latest + env: + MIX_ENV: test + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v2 + - name: Create dev file + run: mv config/dev.sample.exs config/dev.exs + - name: Setup elixir + uses: actions/setup-elixir@v1 + with: + elixir-version: '1.9.4' + otp-version: '22.2' + - name: Install Dependencies + run: mix deps.get + - name: Coverals Coverage + run: mix coveralls.github diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 4041e41..cfbc358 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -11,16 +11,13 @@ on: - develop jobs: - check-lint: + check-lint-and-test: runs-on: ubuntu-latest strategy: matrix: otp: [21.3.8.10, 22.2] elixir: [1.8.2, 1.9.4] - env: - MIX_ENV: test - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v2 @@ -35,8 +32,6 @@ jobs: run: mix deps.get - name: Run Tests run: mix test - - name: Coverals Coverage - run: mix coveralls.github - name: Check formatting run: mix format --check-formatted - name: Checking compile warnings From c8671742606505f068003653745d97c4afdca9bf Mon Sep 17 00:00:00 2001 From: Emmanuel Magak Date: Sat, 10 Oct 2020 14:13:27 +0300 Subject: [PATCH 50/65] added acount balance --- lib/ex_pesa/Mpesa/b2b.ex | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/ex_pesa/Mpesa/b2b.ex b/lib/ex_pesa/Mpesa/b2b.ex index 3b76683..025b344 100644 --- a/lib/ex_pesa/Mpesa/b2b.ex +++ b/lib/ex_pesa/Mpesa/b2b.ex @@ -93,7 +93,6 @@ defmodule ExPesa.Mpesa.B2B do end end -<<<<<<< HEAD defp b2b_request( security_credential, %{ @@ -104,12 +103,6 @@ defmodule ExPesa.Mpesa.B2B do } = params ) do account_reference = Map.get(params, :account_reference, nil) -======= - def request() do - {:error, - "Required Parameter missing, 'command_id','amount','receiver_party', 'remarks','account_reference'"} - end ->>>>>>> b1b9015... added acount balance payload = %{ "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:b2b][:initiator_name], From e2477e7adcb13e5b17770d820aad06c79b4435c5 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sat, 10 Oct 2020 16:09:14 +0300 Subject: [PATCH 51/65] modified account_balance fn --- config/dev.sample.exs | 1 + lib/ex_pesa/Mpesa/account_balance.ex | 37 +++++---------------- test/ex_pesa/Mpesa/account_balance_test.exs | 16 +-------- 3 files changed, 11 insertions(+), 43 deletions(-) diff --git a/config/dev.sample.exs b/config/dev.sample.exs index f88a013..c6c444a 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -24,6 +24,7 @@ config :ex_pesa, "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" ], balance: [ + short_code: "602843", initiator_name: "Safaricom1", password: "Safaricom133", timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", diff --git a/lib/ex_pesa/Mpesa/account_balance.ex b/lib/ex_pesa/Mpesa/account_balance.ex index a90d87a..7f043ed 100644 --- a/lib/ex_pesa/Mpesa/account_balance.ex +++ b/lib/ex_pesa/Mpesa/account_balance.ex @@ -19,6 +19,7 @@ defmodule ExPesa.Mpesa.AccountBalance do mpesa: [ cert: "", balance: [ + short_code: "", initiator_name: "Safaricom1", password: "Safaricom133", timeout_url: "", @@ -38,17 +39,9 @@ defmodule ExPesa.Mpesa.AccountBalance do - production - set password from the organization portal. - sandbox - use your own custom password - ## Parameters - - attrs: - a map containing: - - `command_id` - A unique command passed to the M-Pesa system.. - - `short_code` - The shortcode of the organisation receiving the transaction. - - `remarks` - Comments that are sent along with the transaction. - - `account_type` - Organisation receiving the funds. - ## Example - iex> ExPesa.Mpesa.AccountBalance.request(%{command_id: "AccountBalance", short_code: "602843", remarks: "remarks", account_type: "Customer"}) + iex> ExPesa.Mpesa.AccountBalance.request() {:ok, %{ "ConversationID" => "AG_20201010_00007d6021022d396df6", @@ -57,34 +50,22 @@ defmodule ExPesa.Mpesa.AccountBalance do "ResponseDescription" => "Accept the service request successfully." }} """ - @spec request(map()) :: {:error, any()} | {:ok, any()} - def request(params) do + + def request() do case get_security_credential_for(:balance) do nil -> {:error, "cannot generate security_credential due to missing configuration fields"} - security_credential -> account_balance(security_credential, params) + security_credential -> account_balance(security_credential) end end - @doc false - def request() do - {:error, - "Required Parameter missing, 'command_id','short_code', 'remarks','account_reference'"} - end - - def account_balance(security_credential, %{ - command_id: command_id, - short_code: short_code, - remarks: remarks, - account_type: account_type - }) do + def account_balance(security_credential) do payload = %{ "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:balance][:initiator_name], "SecurityCredential" => security_credential, - "CommandID" => command_id, - "PartyA" => short_code, + "CommandID" => "AccountBalance", + "PartyA" => Application.get_env(:ex_pesa, :mpesa)[:balance][:short_code], "IdentifierType" => 4, - "Remarks" => remarks, - "AccountType" => account_type, + "Remarks" => "Checking account balance", "QueueTimeOutURL" => Application.get_env(:ex_pesa, :mpesa)[:balance][:timeout_url], "ResultURL" => Application.get_env(:ex_pesa, :mpesa)[:balance][:result_url] } diff --git a/test/ex_pesa/Mpesa/account_balance_test.exs b/test/ex_pesa/Mpesa/account_balance_test.exs index e9c7331..8f2707d 100644 --- a/test/ex_pesa/Mpesa/account_balance_test.exs +++ b/test/ex_pesa/Mpesa/account_balance_test.exs @@ -42,23 +42,9 @@ defmodule ExPesa.Mpesa.AccountBalanceTest do describe "Mpesa AccountBalance" do test "request/1 should Initiate a AccountBalance request" do - payment_details = %{ - command_id: "AccountBalance", - short_code: "602843", - remarks: "remarks", - account_type: "Customer" - } - - {:ok, result} = AccountBalance.request(payment_details) + {:ok, result} = AccountBalance.request() assert result["ResponseCode"] == "0" end - - test "request/1 should error out without required parameter" do - {:error, result} = AccountBalance.request() - - "Required Parameter missing, 'command_id','short_code', 'remarks','account_reference'" = - result - end end end From 936485e085adb32a58746bccd7c7d6a0eea4543b Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sat, 10 Oct 2020 16:18:38 +0300 Subject: [PATCH 52/65] make account balance private --- config/test.exs | 9 +++++++++ lib/ex_pesa/Mpesa/account_balance.ex | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/config/test.exs b/config/test.exs index 90152df..f23bf03 100644 --- a/config/test.exs +++ b/config/test.exs @@ -19,5 +19,14 @@ config :ex_pesa, result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", # Leace blank to increase util test coverage security_credential: "" + ], + balance: [ + short_code: "600247", + initiator_name: "Safaricom1", + password: "Safaricom133", + timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", + security_credential: + "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" ] ] diff --git a/lib/ex_pesa/Mpesa/account_balance.ex b/lib/ex_pesa/Mpesa/account_balance.ex index 7f043ed..3fa7fe9 100644 --- a/lib/ex_pesa/Mpesa/account_balance.ex +++ b/lib/ex_pesa/Mpesa/account_balance.ex @@ -58,7 +58,7 @@ defmodule ExPesa.Mpesa.AccountBalance do end end - def account_balance(security_credential) do + defp account_balance(security_credential) do payload = %{ "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:balance][:initiator_name], "SecurityCredential" => security_credential, From 943ad569a51e2df49080bce68ffff77d4c1529c0 Mon Sep 17 00:00:00 2001 From: kamalogudah Date: Sun, 4 Oct 2020 00:31:10 +0300 Subject: [PATCH 53/65] Started on Transaction Status --- config/dev.sample.exs | 12 +- config/test.exs | 7 ++ lib/ex_pesa/Mpesa/transaction_status.ex | 114 ++++++++++++++++++ .../ex_pesa/Mpesa/transaction_status_test.exs | 65 ++++++++++ 4 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 lib/ex_pesa/Mpesa/transaction_status.ex create mode 100644 test/ex_pesa/Mpesa/transaction_status_test.exs diff --git a/config/dev.sample.exs b/config/dev.sample.exs index c6c444a..faded70 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -27,8 +27,16 @@ config :ex_pesa, short_code: "602843", initiator_name: "Safaricom1", password: "Safaricom133", - timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", - result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", + timeout_url: "https://58cb49b30213.ngrok.io/transaction/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/transaction/result_url", + security_credential: + "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + ], + transaction_status: [ + initiator_name: "John Doe", + password: "Safaricom133", + timeout_url: "https://58cb49b30213.ngrok.io/transaction/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/transaction/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" ] diff --git a/config/test.exs b/config/test.exs index f23bf03..f7ef371 100644 --- a/config/test.exs +++ b/config/test.exs @@ -28,5 +28,12 @@ config :ex_pesa, result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + ], + transaction_status: [ + initiator_name: "John Doe", + password: "Safaricom133", + timeout_url: "https://58cb49b30213.ngrok.io/transaction/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/transaction/result_url", + security_credential: "" ] ] diff --git a/lib/ex_pesa/Mpesa/transaction_status.ex b/lib/ex_pesa/Mpesa/transaction_status.ex new file mode 100644 index 0000000..5d6757a --- /dev/null +++ b/lib/ex_pesa/Mpesa/transaction_status.ex @@ -0,0 +1,114 @@ +defmodule ExPesa.Mpesa.TransactionStatus do + @moduledoc """ + Use this api to check the transaction status. + """ + + import ExPesa.Mpesa.MpesaBase + import ExPesa.Util + + @doc """ + Transaction Status Query + ## Requirement Params + - `CommandID`[String] - Takes only 'TransactionStatusQuery' command id + - `timeout_url` [URL] - The path that stores information of time out transaction. Takes the form of + https://ip or domain:port/path + - `result_url`[URL] - The path that stores information of transaction. Example https://ip or domain:port/path + - `Initiator` [Alpha-Numeric] - The name of Initiator to initiating the request. This is the credential/username + used to authenticate the transaction request + + - `security credential` - To generate security_credential, head over to https://developer.safaricom.co.ke/test_credentials, then Initiator Security Password for your environment. + + `config.exs` + ```elixir + config :ex_pesa, + mpesa: [ + cert: "", + transaction_status: [ + initiator_name: "", + password: "", + timeout_url: "", + result_url: "", + security_credential: "" + ] + ] + ``` + + Alternatively, generate security credential using certificate + `cert` - This is the M-Pesa public key certificate used to encrypt your plain password. + There are 2 types of certificates. + - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer . + - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer . + `password` - This is a plain unencrypted password. + Environment + - production - set password from the organization portal. + - sandbox - use your own custom password + + + ## Parameters + The following are the parameters required for this method, the rest are fetched from config + files. + + - `transaction_id` [Alpha-Numeric] - Unique identifier to identify a transaction on M-Pesa Alpha-Numeric LKXXXX1234 + - `receiver_party` [Numeric] - Organization/MSISDN receiving the transaction, can be + -Shortcode (6 digits) + -MSISDN (12 Digits) + - `identifier_type` [Numeric] - Type of organization receiving the transaction can be the folowing: + 1 – MSISDN + 2 – Till Number + 4 – Organization short code + - `remarks`[String] - Comments that are sent along with the transaction, can be a sequence of characters up to 100 + - `occasion` [ String] - Optional Parameter String sequence of characters up to 100 + + ## Example + + iex> ExPesa.Mpesa.TransactionStatus.request(%{transaction_id: "SOME7803", receiver_party: "600247", identifier_type: 4, remarks: "TransactionReversal", occasion: "TransactionReversal"}) + {:ok, + %{ + "ConversationID" => "AG_20201010_000056be35a7b266b43e", + "OriginatorConversationID" => "27288-72545279-2", + "ResponseCode" => "0", + "ResponseDescription" => "Accept the service request successfully." + } + } + + """ + def request(params) do + case get_security_credential_for(:transaction_status) do + nil -> {:error, "cannot generate security_credential due to missing configuration fields"} + security_credential -> query(security_credential, params) + end + end + + defp query( + security_credential, + %{ + transaction_id: transaction_id, + receiver_party: receiver_party, + identifier_type: identifier_type, + remarks: remarks + } = params + ) do + occasion = Map.get(params, :occasion, nil) + + payload = %{ + "CommandID" => "TransactionStatusQuery", + "PartyA" => receiver_party, + "IdentifierType" => identifier_type, + "Remarks" => remarks, + "SecurityCredential" => security_credential, + "Initiator" => Application.get_env(:ex_pesa, :mpesa)[:transaction_status][:initiator_name], + "QueueTimeOutURL" => + Application.get_env(:ex_pesa, :mpesa)[:transaction_status][:timeout_url], + "ResultURL" => Application.get_env(:ex_pesa, :mpesa)[:transaction_status][:result_url], + "TransactionID" => transaction_id, + "Occasion" => occasion + } + + make_request("/mpesa/transactionstatus/v1/query", payload) + end + + defp query(_security_credential, _) do + {:error, + "Some Required Parameter missing, check whether you have 'transaction_id', 'receiver_party', 'identifier_type', 'remarks'"} + end +end diff --git a/test/ex_pesa/Mpesa/transaction_status_test.exs b/test/ex_pesa/Mpesa/transaction_status_test.exs new file mode 100644 index 0000000..055e7a0 --- /dev/null +++ b/test/ex_pesa/Mpesa/transaction_status_test.exs @@ -0,0 +1,65 @@ +defmodule ExPesa.Mpesa.TransactionStatusTest do + @moduledoc false + + use ExUnit.Case, async: true + + import Tesla.Mock + doctest ExPesa.Mpesa.TransactionStatus + + alias ExPesa.Mpesa.TransactionStatus + + setup do + mock(fn + %{ + url: "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials", + method: :get + } -> + %Tesla.Env{ + status: 200, + body: %{ + "access_token" => "SGWcJPtNtYNPGm6uSYR9yPYrAI3Bm", + "expires_in" => "3599" + } + } + + %{ + url: "https://sandbox.safaricom.co.ke/mpesa/transactionstatus/v1/query", + method: :post + } -> + %Tesla.Env{ + status: 200, + body: %{ + "ConversationID" => "AG_20201010_000056be35a7b266b43e", + "OriginatorConversationID" => "27288-72545279-2", + "ResponseCode" => "0", + "ResponseDescription" => "Accept the service request successfully." + } + } + end) + + :ok + end + + describe "Mpesa Transaction Status" do + test "request/1 should Initiate a TransactionStatus request" do + query_details = %{ + transaction_id: "SOME7803", + receiver_party: "600247", + identifier_type: 4, + remarks: "CustomerPayBillOnline", + occasion: "TransactionReversal" + } + + {:ok, result} = TransactionStatus.request(query_details) + + assert result["ResponseDescription"] == "Accept the service request successfully." + end + + test "request/1 should error out without required parameter" do + {:error, result} = TransactionStatus.request({}) + + "Some Required Parameter missing, check whether you have 'transaction_id', 'receiver_party', 'identifier_type', 'remarks'" = + result + end + end +end From 6968ded5b9ef7cb49453afe7910c1967bacc1b05 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sun, 11 Oct 2020 15:00:37 +0300 Subject: [PATCH 54/65] formatting --- config/dev.sample.exs | 5 ++--- config/test.exs | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/config/dev.sample.exs b/config/dev.sample.exs index 298acbb..ff88734 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -28,7 +28,7 @@ config :ex_pesa, initiator_name: "John Doe", password: "Safaricom133", timeout_url: "https://58cb49b30213.ngrok.io/b2c/timeout_url", - result_url: "https://58cb49b30213.ngrok.io/b2c/result_url", + result_url: "https://58cb49b30213.ngrok.io/b2c/result_url" ], balance: [ short_code: "602843", @@ -38,6 +38,5 @@ config :ex_pesa, result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" - ], + ] ] - \ No newline at end of file diff --git a/config/test.exs b/config/test.exs index 3e07f33..5d618e2 100644 --- a/config/test.exs +++ b/config/test.exs @@ -25,7 +25,7 @@ config :ex_pesa, initiator_name: "John Doe", password: "Safaricom133", timeout_url: "https://58cb49b30213.ngrok.io/b2c/timeout_url", - result_url: "https://58cb49b30213.ngrok.io/b2c/result_url", + result_url: "https://58cb49b30213.ngrok.io/b2c/result_url" ], balance: [ short_code: "600247", @@ -35,5 +35,5 @@ config :ex_pesa, result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" - ], + ] ] From 0a47c067fdc264fa7ba48dab95c8cc5fca5f1200 Mon Sep 17 00:00:00 2001 From: kamalogudah Date: Sun, 11 Oct 2020 15:02:40 +0300 Subject: [PATCH 55/65] Update readme --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 16c97f1..aaec05b 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,6 @@ ## Features -[WIP] - - [x] Mpesa - [x] Mpesa Express (STK) - [x] STK Transaction Validation @@ -27,7 +25,7 @@ - [x] B2B - [x] C2B - [ ] Reversal - - [ ] Transaction Status + - [x] Transaction Status - [x] Account Balance - [ ] JengaWS(Equity) - [ ] Send Money From b92b55eb870e6a3f1fff3047fee27dc0a81bcf37 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sun, 11 Oct 2020 15:06:51 +0300 Subject: [PATCH 56/65] updated readme --- README.md | 2 +- config/dev.sample.exs | 7 +++++++ config/test.exs | 8 ++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 16c97f1..45cdd57 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ - [x] Mpesa - [x] Mpesa Express (STK) - [x] STK Transaction Validation - - [ ] B2C + - [x] B2C - [x] B2B - [x] C2B - [ ] Reversal diff --git a/config/dev.sample.exs b/config/dev.sample.exs index ff88734..6f97116 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -38,5 +38,12 @@ config :ex_pesa, result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + ], + transaction_status: [ + initiator_name: "John Doe", + password: "Safaricom133", + timeout_url: "https://58cb49b30213.ngrok.io/transaction/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/transaction/result_url", + security_credential: "" ] ] diff --git a/config/test.exs b/config/test.exs index 5d618e2..5f46643 100644 --- a/config/test.exs +++ b/config/test.exs @@ -35,5 +35,13 @@ config :ex_pesa, result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + ], + transaction_status: [ + initiator_name: "John Doe", + password: "Safaricom133", + timeout_url: "https://58cb49b30213.ngrok.io/transaction/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/transaction/result_url", + security_credential: + "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" ] ] From 764df78bf7b1ae6e5118d4c43f9e504eb663ab37 Mon Sep 17 00:00:00 2001 From: okothkongo Date: Sun, 11 Oct 2020 15:12:18 +0300 Subject: [PATCH 57/65] the reversal --- lib/ex_pesa/Mpesa/reversal.ex | 60 +++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 lib/ex_pesa/Mpesa/reversal.ex diff --git a/lib/ex_pesa/Mpesa/reversal.ex b/lib/ex_pesa/Mpesa/reversal.ex new file mode 100644 index 0000000..e305ac4 --- /dev/null +++ b/lib/ex_pesa/Mpesa/reversal.ex @@ -0,0 +1,60 @@ +defmodule ExPesa.Mpesa.Reversal do + @moduledoc """ + Reversal API enables reversal of transactions done + You will be able to reverse a transaction where you are the credit party. This means it will be done via the Web portal, and may require manual authorization from the Service Provider side. But if you are allowed to reverse a transaction via API, it may also need to be authorized. + + An initiator requires the Org Reversals Initiator role to be able to perform reversals via API + """ + import ExPesa.Mpesa.MpesaBase + alias ExPesa.Util + + def reverse(params, option \\ :standalone) + + def reverse(%{transaction_id: _trans_id, amount: _amount} = params, :standalone) do + config = Application.get_env(:ex_pesa, :mpesa)[:reversal] + credential = Util.get_security_credential_for(:reversal) + + %{ + security_credential: credential, + initiator: config[:initiator_name], + receiver_party: config[:shortcode] + } + |> Map.merge(params) + |> reversal_payload() + |> request_reversal() + end + + def reverse(%{transaction_id: _trans_id, amount: _amount} = params, api) do + config = Application.get_env(:ex_pesa, :mpesa)[api] + credential = Util.get_security_credential_for(api) + + %{ + security_credential: credential, + initiator: config[:initiator_name], + receiver_party: config[:shortcode] + } + |> Map.merge(params) + |> reversal_payload() + |> request_reversal() + end + + defp reversal_payload(params) do + %{ + "Initiator" => params.initiator, + "SecurityCredential" => params.security_credential, + "CommandID" => params.command_id, + "TransactionID" => params.transaction_id, + "Amount" => params.amount, + "ReceiverParty" => params.receiver_party, + "RecieverIdentifierType" => params.receiver_identifier, + "ResultURL" => params.result_url, + "QueueTimeOutURL" => params.queue_time_out_url, + "Remarks" => params.remark, + "Occasion" => params.work + } + end + + defp request_reversal(payload) do + make_request("/mpesa/reversal/v1/request", payload) + end +end From 78752c74ae3901e5e04379c8110786ee91026164 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sun, 11 Oct 2020 15:28:30 +0300 Subject: [PATCH 58/65] revert changes --- lib/ex_pesa/Mpesa/transaction_status.ex | 61 ------------------- .../ex_pesa/Mpesa/transaction_status_test.exs | 16 ----- 2 files changed, 77 deletions(-) diff --git a/lib/ex_pesa/Mpesa/transaction_status.ex b/lib/ex_pesa/Mpesa/transaction_status.ex index 2316453..8c122de 100644 --- a/lib/ex_pesa/Mpesa/transaction_status.ex +++ b/lib/ex_pesa/Mpesa/transaction_status.ex @@ -10,25 +10,6 @@ defmodule ExPesa.Mpesa.TransactionStatus do Transaction Status Query ## Requirement Params - `CommandID`[String] - Takes only 'TransactionStatusQuery' command id -<<<<<<< HEAD - - `PartyA` [Numeric] - Organization/MSISDN receiving the transaction, can be - -Shortcode (6 digits) - -MSISDN (12 Digits) - - `IdentifierType` [Numeric] - Type of organization receiving the transaction can be the folowing: - 1 – MSISDN - 2 – Till Number - 4 – Organization short code - - `Remarks`[String] - Comments that are sent along with the transaction, can be a sequence of characters up to 100 - - `Initiator` [Alpha-Numeric] - The name of Initiator to initiating the request. This is the credential/username - used to authenticate the transaction request - - `SecurityCredential` [String] - Encrypted Credential of user getting transaction amount String - Encrypted password for the initiator to authenticate the transaction request - - `QueueTimeOutURL` [URL] - The path that stores information of time out transaction. Takes the form of - https://ip or domain:port/path - - `ResultURL`[URL] - The path that stores information of transaction. Example https://ip or domain:port/path - - `TransactionID` [Alpha-Numeric] - Unique identifier to identify a transaction on M-Pesa Alpha-Numeric LKXXXX1234 - - `Occasion` [ String] - Optional Parameter String sequence of characters up to 100 -======= - `timeout_url` [URL] - The path that stores information of time out transaction. Takes the form of https://ip or domain:port/path - `result_url`[URL] - The path that stores information of transaction. Example https://ip or domain:port/path @@ -61,24 +42,10 @@ defmodule ExPesa.Mpesa.TransactionStatus do Environment - production - set password from the organization portal. - sandbox - use your own custom password ->>>>>>> 0a47c067fdc264fa7ba48dab95c8cc5fca5f1200 - ## Parameters The following are the parameters required for this method, the rest are fetched from config files. -<<<<<<< HEAD - - `occasion`: - - `party_a`: - - `identifier_type`` - - `remarks`` - - `transaction_id` - Their details have been covered above in the documentation. - - ## Example - - iex> ExPesa.Mpesa.TransactionStatus.request(%{occasion: "Some Occasion",party_a: "600247",identifier_type: "4",remarks: "CustomerPayBillOnline",transaction_id: "SOME7803"}) -======= - `transaction_id` [Alpha-Numeric] - Unique identifier to identify a transaction on M-Pesa Alpha-Numeric LKXXXX1234 - `receiver_party` [Numeric] - Organization/MSISDN receiving the transaction, can be @@ -94,7 +61,6 @@ defmodule ExPesa.Mpesa.TransactionStatus do ## Example iex> ExPesa.Mpesa.TransactionStatus.request(%{transaction_id: "SOME7803", receiver_party: "600247", identifier_type: 4, remarks: "TransactionReversal", occasion: "TransactionReversal"}) ->>>>>>> 0a47c067fdc264fa7ba48dab95c8cc5fca5f1200 {:ok, %{ "ConversationID" => "AG_20201010_000056be35a7b266b43e", @@ -106,35 +72,12 @@ defmodule ExPesa.Mpesa.TransactionStatus do """ def request(params) do -<<<<<<< HEAD - case get_security_credential_for(:b2b) do -======= case get_security_credential_for(:transaction_status) do ->>>>>>> 0a47c067fdc264fa7ba48dab95c8cc5fca5f1200 nil -> {:error, "cannot generate security_credential due to missing configuration fields"} security_credential -> query(security_credential, params) end end -<<<<<<< HEAD - @doc false - def request() do - {:error, - "Some Required Parameter missing, check whether you have 'occasion', 'party_a', 'identifier_type', 'remarks', and 'transaction_id'"} - end - - @spec request(map()) :: {:error, any()} | {:ok, any()} - defp query(security_credential, %{ - occasion: occasion, - transaction_id: transaction_id, - party_a: party_a, - identifier_type: identifier_type, - remarks: remarks - }) do - payload = %{ - "CommandID" => "TransactionStatusQuery", - "PartyA" => party_a, -======= defp query( security_credential, %{ @@ -149,7 +92,6 @@ defmodule ExPesa.Mpesa.TransactionStatus do payload = %{ "CommandID" => "TransactionStatusQuery", "PartyA" => receiver_party, ->>>>>>> 0a47c067fdc264fa7ba48dab95c8cc5fca5f1200 "IdentifierType" => identifier_type, "Remarks" => remarks, "SecurityCredential" => security_credential, @@ -163,12 +105,9 @@ defmodule ExPesa.Mpesa.TransactionStatus do make_request("/mpesa/transactionstatus/v1/query", payload) end -<<<<<<< HEAD -======= defp query(_security_credential, _) do {:error, "Some Required Parameter missing, check whether you have 'transaction_id', 'receiver_party', 'identifier_type', 'remarks'"} end ->>>>>>> 0a47c067fdc264fa7ba48dab95c8cc5fca5f1200 end diff --git a/test/ex_pesa/Mpesa/transaction_status_test.exs b/test/ex_pesa/Mpesa/transaction_status_test.exs index d0bbce6..055e7a0 100644 --- a/test/ex_pesa/Mpesa/transaction_status_test.exs +++ b/test/ex_pesa/Mpesa/transaction_status_test.exs @@ -41,15 +41,6 @@ defmodule ExPesa.Mpesa.TransactionStatusTest do end describe "Mpesa Transaction Status" do -<<<<<<< HEAD - test "request/1 should Initiate a B2B request" do - query_details = %{ - occasion: "Some Occasion", - party_a: "600247", - identifier_type: "4", - remarks: "CustomerPayBillOnline", - transaction_id: "SOME7803" -======= test "request/1 should Initiate a TransactionStatus request" do query_details = %{ transaction_id: "SOME7803", @@ -57,7 +48,6 @@ defmodule ExPesa.Mpesa.TransactionStatusTest do identifier_type: 4, remarks: "CustomerPayBillOnline", occasion: "TransactionReversal" ->>>>>>> 0a47c067fdc264fa7ba48dab95c8cc5fca5f1200 } {:ok, result} = TransactionStatus.request(query_details) @@ -66,15 +56,9 @@ defmodule ExPesa.Mpesa.TransactionStatusTest do end test "request/1 should error out without required parameter" do -<<<<<<< HEAD - {:error, result} = TransactionStatus.request() - - "Some Required Parameter missing, check whether you have 'occasion', 'party_a', 'identifier_type', 'remarks', and 'transaction_id'" = -======= {:error, result} = TransactionStatus.request({}) "Some Required Parameter missing, check whether you have 'transaction_id', 'receiver_party', 'identifier_type', 'remarks'" = ->>>>>>> 0a47c067fdc264fa7ba48dab95c8cc5fca5f1200 result end end From df0a4fbfc8681e62d83faff0005775655a0b87ce Mon Sep 17 00:00:00 2001 From: okothkongo Date: Sun, 11 Oct 2020 15:12:18 +0300 Subject: [PATCH 59/65] the reversal --- config/dev.sample.exs | 9 ++++++ lib/ex_pesa/Mpesa/reversal.ex | 60 +++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 lib/ex_pesa/Mpesa/reversal.ex diff --git a/config/dev.sample.exs b/config/dev.sample.exs index aa5a174..de76a05 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -22,5 +22,14 @@ config :ex_pesa, result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + ], + reversal: [ + initiator: "Jane Doe", + password: "superStrong@1", + password: "Safaricom133", + timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", + security_credential: + "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" ] ] diff --git a/lib/ex_pesa/Mpesa/reversal.ex b/lib/ex_pesa/Mpesa/reversal.ex new file mode 100644 index 0000000..e305ac4 --- /dev/null +++ b/lib/ex_pesa/Mpesa/reversal.ex @@ -0,0 +1,60 @@ +defmodule ExPesa.Mpesa.Reversal do + @moduledoc """ + Reversal API enables reversal of transactions done + You will be able to reverse a transaction where you are the credit party. This means it will be done via the Web portal, and may require manual authorization from the Service Provider side. But if you are allowed to reverse a transaction via API, it may also need to be authorized. + + An initiator requires the Org Reversals Initiator role to be able to perform reversals via API + """ + import ExPesa.Mpesa.MpesaBase + alias ExPesa.Util + + def reverse(params, option \\ :standalone) + + def reverse(%{transaction_id: _trans_id, amount: _amount} = params, :standalone) do + config = Application.get_env(:ex_pesa, :mpesa)[:reversal] + credential = Util.get_security_credential_for(:reversal) + + %{ + security_credential: credential, + initiator: config[:initiator_name], + receiver_party: config[:shortcode] + } + |> Map.merge(params) + |> reversal_payload() + |> request_reversal() + end + + def reverse(%{transaction_id: _trans_id, amount: _amount} = params, api) do + config = Application.get_env(:ex_pesa, :mpesa)[api] + credential = Util.get_security_credential_for(api) + + %{ + security_credential: credential, + initiator: config[:initiator_name], + receiver_party: config[:shortcode] + } + |> Map.merge(params) + |> reversal_payload() + |> request_reversal() + end + + defp reversal_payload(params) do + %{ + "Initiator" => params.initiator, + "SecurityCredential" => params.security_credential, + "CommandID" => params.command_id, + "TransactionID" => params.transaction_id, + "Amount" => params.amount, + "ReceiverParty" => params.receiver_party, + "RecieverIdentifierType" => params.receiver_identifier, + "ResultURL" => params.result_url, + "QueueTimeOutURL" => params.queue_time_out_url, + "Remarks" => params.remark, + "Occasion" => params.work + } + end + + defp request_reversal(payload) do + make_request("/mpesa/reversal/v1/request", payload) + end +end From c547114ecd7af8cf9d6c332cffbf3d128c7119d6 Mon Sep 17 00:00:00 2001 From: Midigo Frank Date: Sun, 11 Oct 2020 18:39:40 +0300 Subject: [PATCH 60/65] add doc for the reverse/2 function --- config/test.exs | 8 ++++ lib/ex_pesa/Mpesa/reversal.ex | 38 ++++++++++++++--- test/ex_pesa/Mpesa/reversal_test.exs | 64 ++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 test/ex_pesa/Mpesa/reversal_test.exs diff --git a/config/test.exs b/config/test.exs index 439cf38..78b2b08 100644 --- a/config/test.exs +++ b/config/test.exs @@ -19,5 +19,13 @@ config :ex_pesa, result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" + ], + reversal: [ + initiator: "603081", + password: "safaricom.i", + timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", + security_credential: + "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" ] ] diff --git a/lib/ex_pesa/Mpesa/reversal.ex b/lib/ex_pesa/Mpesa/reversal.ex index e305ac4..94394d0 100644 --- a/lib/ex_pesa/Mpesa/reversal.ex +++ b/lib/ex_pesa/Mpesa/reversal.ex @@ -8,6 +8,23 @@ defmodule ExPesa.Mpesa.Reversal do import ExPesa.Mpesa.MpesaBase alias ExPesa.Util + @doc """ + Makes a request to the mpesa reversal endpoint with the given params. + + ## Params + The function requires two keys to be present for a successful request, `:transation_id` and `:amount` + The params can be any of the accepted params by the api endpoint with the keys converted to snake case. For example + `QueueTimeOutURL` is expected to be in the format `queue_time_out_url`. + + Additionally, the keys `:security_credential, :initiator, :receiver_party, :result_url, :queue_time_out_url` are + loaded from the respective config and used if they're not provided as part of the params in the arguments. + + ## Options + Because reversal can be done for B2B, B2C or C2B, this function allows for an option to load the configs from. + It defaults to `:standalone` which means the config to use will be the `:reversal` under the `:mpesa` config. + In order to reuse the configs for the other apis, use the parent key under the mpesa config. + For example, in order to use the `b2b` configs, pass in `:b2b` as the option + """ def reverse(params, option \\ :standalone) def reverse(%{transaction_id: _trans_id, amount: _amount} = params, :standalone) do @@ -17,7 +34,9 @@ defmodule ExPesa.Mpesa.Reversal do %{ security_credential: credential, initiator: config[:initiator_name], - receiver_party: config[:shortcode] + receiver_party: config[:shortcode], + result_url: config[:result_url], + queue_time_out_url: config[:result_url] } |> Map.merge(params) |> reversal_payload() @@ -27,30 +46,37 @@ defmodule ExPesa.Mpesa.Reversal do def reverse(%{transaction_id: _trans_id, amount: _amount} = params, api) do config = Application.get_env(:ex_pesa, :mpesa)[api] credential = Util.get_security_credential_for(api) + reversal_config = Application.get_env(:ex_pesa, :mpesa)[:reversal] %{ security_credential: credential, initiator: config[:initiator_name], - receiver_party: config[:shortcode] + receiver_party: config[:shortcode], + result_url: reversal_config[:result_url], + queue_time_out_url: reversal_config[:timeout_url] } |> Map.merge(params) |> reversal_payload() |> request_reversal() end + def reverse(_, _) do + {:error, "either transaction_id or amount is missing from the given params"} + end + defp reversal_payload(params) do %{ "Initiator" => params.initiator, "SecurityCredential" => params.security_credential, - "CommandID" => params.command_id, + "CommandID" => "TransactionReversal", "TransactionID" => params.transaction_id, "Amount" => params.amount, "ReceiverParty" => params.receiver_party, - "RecieverIdentifierType" => params.receiver_identifier, + "RecieverIdentifierType" => "4", "ResultURL" => params.result_url, "QueueTimeOutURL" => params.queue_time_out_url, - "Remarks" => params.remark, - "Occasion" => params.work + "Remarks" => params[:remarks] || "Payment Reversal", + "Occasion" => params[:occasion] || "Payment Reversal" } end diff --git a/test/ex_pesa/Mpesa/reversal_test.exs b/test/ex_pesa/Mpesa/reversal_test.exs new file mode 100644 index 0000000..639ac27 --- /dev/null +++ b/test/ex_pesa/Mpesa/reversal_test.exs @@ -0,0 +1,64 @@ +defmodule ExPesa.Mpesa.ReversalTest do + @moduledoc false + + use ExUnit.Case, async: true + + import Tesla.Mock + doctest ExPesa.Mpesa.Reversal + + alias ExPesa.Mpesa.Reversal + + @base_url "https://sandbox.safaricom.co.ke" + + setup do + mock(fn + %{ + url: "#{@base_url}/oauth/v1/generate?grant_type=client_credentials", + method: :get + } -> + %Tesla.Env{ + status: 200, + body: %{ + "access_token" => "SGWcJPtNtYNPGm6uSYR9yPYrAI3Bm", + "expires_in" => "3599" + } + } + + %{url: "#{@base_url}/mpesa/reversal/v1/request", method: :post} -> + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "Result" => %{ + "ResultType" => 0, + "ResultCode" => 0, + "ResultDesc" => "The service request has been accepted successfully.", + "OriginatorConversationID" => "10819-695089-1", + "ConversationID" => "AG_20170727_00004efadacd98a01d15", + "TransactionID" => "LGR019G3J2" + } + }) + } + end) + + :ok + end + + describe "Mpesa Reversal" do + test "reverse/2 makes a successful request" do + {:ok, result} = Reversal.reverse(%{amount: 30, transaction_id: "LGR013H3J2"}) + + assert result["Result"]["ResultCode"] == 0 + end + + test "reverse/2 returns an error if amount is missing" do + {:error, result} = Reversal.reverse(%{transaction_id: "LGR013H3J2"}) + assert result =~ "either transaction_id or amount is missing from the given params" + end + + test "reverse/2 returns an error if transaction_id is missing" do + {:error, result} = Reversal.reverse(%{amount: 30}) + assert result =~ "either transaction_id or amount is missing from the given params" + end + end +end From cebcff0a557dc984c8073e44ccb3c652ca3c5140 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sun, 11 Oct 2020 20:47:24 +0300 Subject: [PATCH 61/65] modified reversal module --- config/dev.sample.exs | 8 ++-- config/test.exs | 9 ++-- lib/ex_pesa/Mpesa/b2c.ex | 8 ++-- lib/ex_pesa/Mpesa/reversal.ex | 65 +++++++++++++++++++--------- test/ex_pesa/Mpesa/reversal_test.exs | 14 +++--- 5 files changed, 62 insertions(+), 42 deletions(-) diff --git a/config/dev.sample.exs b/config/dev.sample.exs index 950762f..cd613cd 100644 --- a/config/dev.sample.exs +++ b/config/dev.sample.exs @@ -47,11 +47,11 @@ config :ex_pesa, security_credential: "" ], reversal: [ - initiator: "Jane Doe", + short_code: "600247", + initiator_name: "Jane Doe", password: "superStrong@1", - password: "Safaricom133", - timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", - result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", + timeout_url: "https://58cb49b30213.ngrok.io/reversal/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/reversal/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" ] diff --git a/config/test.exs b/config/test.exs index 50bd589..946ba13 100644 --- a/config/test.exs +++ b/config/test.exs @@ -45,10 +45,11 @@ config :ex_pesa, "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" ], reversal: [ - initiator: "603081", - password: "safaricom.i", - timeout_url: "https://58cb49b30213.ngrok.io/b2b/timeout_url", - result_url: "https://58cb49b30213.ngrok.io/b2b/result_url", + short_code: "600247", + initiator_name: "Jane Doe", + password: "superStrong@1", + timeout_url: "https://58cb49b30213.ngrok.io/reversal/timeout_url", + result_url: "https://58cb49b30213.ngrok.io/reversal/result_url", security_credential: "kxlZ1Twlfr6xQru0GId03LbegvuTPelnz3qITkvUJxaCTQt1HaD2hN801Pgbi38x6dEq/hsanBBtfj6JbePayipE/srOyMQ61ieiO+5uHb/JX/NLi1Jy6Alvi0hKbCbq9cVwC/bZBhli7AUAtpfKVgIyXq2InNyfzXpzR8FhrbXiaMhTPJ8WleozPm5CnXe2bFlFP7K0yhCRlT+UOPl7xh0LqU23rMTj3TN/ms600c3j/m2FxQZdmY5/rdORrJeTQV1vw6kXr1QrGaSDSdRMUiaGbg5uPL8LSNwC5bn3M92oPY2cWmkyH9rOzbCN+o5+23TvweaKZlrKuv7etKXMFg==" ] diff --git a/lib/ex_pesa/Mpesa/b2c.ex b/lib/ex_pesa/Mpesa/b2c.ex index 63c3ad8..2be41bf 100644 --- a/lib/ex_pesa/Mpesa/b2c.ex +++ b/lib/ex_pesa/Mpesa/b2c.ex @@ -1,8 +1,8 @@ defmodule ExPesa.Mpesa.B2c do @moduledoc """ - Business to Customer (B2C) API enables the Business or organization to pay its customers who are the end-users of its products or services. - Currently, the B2C API allows the org to perform around 3 types of transactions: SalaryPayments, BusinessPayments or Promotion payments. - Salary payments are used by organizations paying their employees via M-Pesa, Business Payments are normal business transactions to customers + Business to Customer (B2C) API enables the Business or organization to pay its customers who are the end-users of its products or services. + Currently, the B2C API allows the org to perform around 3 types of transactions: SalaryPayments, BusinessPayments or Promotion payments. + Salary payments are used by organizations paying their employees via M-Pesa, Business Payments are normal business transactions to customers e.g. bank transfers to mobile, Promotion payments are payments made by organization carrying out promotional services e.g. betting companies paying out winnings to clients """ @@ -15,7 +15,7 @@ defmodule ExPesa.Mpesa.B2c do ## Configuration Add below config to dev.exs / prod.exs files - This assumes you have a clear understanding of how Daraja API works. See docs here https://developer.safaricom.co.ke/docs#account-balance-api + `config.exs` ```elixir config :ex_pesa, diff --git a/lib/ex_pesa/Mpesa/reversal.ex b/lib/ex_pesa/Mpesa/reversal.ex index 94394d0..9b59dc2 100644 --- a/lib/ex_pesa/Mpesa/reversal.ex +++ b/lib/ex_pesa/Mpesa/reversal.ex @@ -21,27 +21,50 @@ defmodule ExPesa.Mpesa.Reversal do ## Options Because reversal can be done for B2B, B2C or C2B, this function allows for an option to load the configs from. - It defaults to `:standalone` which means the config to use will be the `:reversal` under the `:mpesa` config. - In order to reuse the configs for the other apis, use the parent key under the mpesa config. + It defaults to `:reversal` which means it will use config under the `:mpesa` config. + In order to reuse the configs for the other apis, use the parent key under the mpesa config. For example, in order to use the `b2b` configs, pass in `:b2b` as the option - """ - def reverse(params, option \\ :standalone) + ## Configuration + Add below config to dev.exs / prod.exs files - def reverse(%{transaction_id: _trans_id, amount: _amount} = params, :standalone) do - config = Application.get_env(:ex_pesa, :mpesa)[:reversal] - credential = Util.get_security_credential_for(:reversal) + `config.exs` + ```elixir + config :ex_pesa, + mpesa: [ + cert: "", + reversal: [ + short_code: "", + initiator_name: "", + password: "", + timeout_url: "", + result_url: "", + security_credential: "" + ] + ] + ``` - %{ - security_credential: credential, - initiator: config[:initiator_name], - receiver_party: config[:shortcode], - result_url: config[:result_url], - queue_time_out_url: config[:result_url] - } - |> Map.merge(params) - |> reversal_payload() - |> request_reversal() - end + To generate security_credential, head over to https://developer.safaricom.co.ke/test_credentials, then Initiator Security Password for your environment. + Alternatively, generate security credential using certificate + `cert` - This is the M-Pesa public key certificate used to encrypt your plain password. + There are 2 types of certificates. + - sandox - https://developer.safaricom.co.ke/sites/default/files/cert/cert_sandbox/cert.cer . + - production - https://developer.safaricom.co.ke/sites/default/files/cert/cert_prod/cert.cer . + `password` - This is a plain unencrypted password. + Environment + - production - set password from the organization portal. + - sandbox - use your own custom password + + ## Example + iex> ExPesa.Mpesa.Reversal.reverse(%{amount: 30, transaction_id: "LGR013H3J2"}, :reversal) + {:ok, + %{ + "ConversationID" => "AG_20201011_00006511c0024c170286", + "OriginatorConversationID" => "8094-41340768-1", + "ResponseCode" => "0", + "ResponseDescription" => "Accept the service request successfully." + }} + """ + def reverse(params, option \\ :reversal) def reverse(%{transaction_id: _trans_id, amount: _amount} = params, api) do config = Application.get_env(:ex_pesa, :mpesa)[api] @@ -51,7 +74,7 @@ defmodule ExPesa.Mpesa.Reversal do %{ security_credential: credential, initiator: config[:initiator_name], - receiver_party: config[:shortcode], + receiver_party: config[:short_code], result_url: reversal_config[:result_url], queue_time_out_url: reversal_config[:timeout_url] } @@ -75,8 +98,8 @@ defmodule ExPesa.Mpesa.Reversal do "RecieverIdentifierType" => "4", "ResultURL" => params.result_url, "QueueTimeOutURL" => params.queue_time_out_url, - "Remarks" => params[:remarks] || "Payment Reversal", - "Occasion" => params[:occasion] || "Payment Reversal" + "Remarks" => Map.get(params, :remarks, "Payment Reversal"), + "Occasion" => Map.get(params, :occassion, "Payment Reversal") } end diff --git a/test/ex_pesa/Mpesa/reversal_test.exs b/test/ex_pesa/Mpesa/reversal_test.exs index 639ac27..4d3e588 100644 --- a/test/ex_pesa/Mpesa/reversal_test.exs +++ b/test/ex_pesa/Mpesa/reversal_test.exs @@ -29,14 +29,10 @@ defmodule ExPesa.Mpesa.ReversalTest do status: 200, body: Jason.encode!(%{ - "Result" => %{ - "ResultType" => 0, - "ResultCode" => 0, - "ResultDesc" => "The service request has been accepted successfully.", - "OriginatorConversationID" => "10819-695089-1", - "ConversationID" => "AG_20170727_00004efadacd98a01d15", - "TransactionID" => "LGR019G3J2" - } + "ConversationID" => "AG_20201011_00006511c0024c170286", + "OriginatorConversationID" => "8094-41340768-1", + "ResponseCode" => "0", + "ResponseDescription" => "Accept the service request successfully." }) } end) @@ -48,7 +44,7 @@ defmodule ExPesa.Mpesa.ReversalTest do test "reverse/2 makes a successful request" do {:ok, result} = Reversal.reverse(%{amount: 30, transaction_id: "LGR013H3J2"}) - assert result["Result"]["ResultCode"] == 0 + assert result["ResponseCode"] == "0" end test "reverse/2 returns an error if amount is missing" do From 282886153d18ada2b043893187ec2605da98af75 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sun, 11 Oct 2020 20:50:26 +0300 Subject: [PATCH 62/65] formatting --- test/ex_pesa/Mpesa/reversal_test.exs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/ex_pesa/Mpesa/reversal_test.exs b/test/ex_pesa/Mpesa/reversal_test.exs index 4d3e588..42f5861 100644 --- a/test/ex_pesa/Mpesa/reversal_test.exs +++ b/test/ex_pesa/Mpesa/reversal_test.exs @@ -29,10 +29,10 @@ defmodule ExPesa.Mpesa.ReversalTest do status: 200, body: Jason.encode!(%{ - "ConversationID" => "AG_20201011_00006511c0024c170286", - "OriginatorConversationID" => "8094-41340768-1", - "ResponseCode" => "0", - "ResponseDescription" => "Accept the service request successfully." + "ConversationID" => "AG_20201011_00006511c0024c170286", + "OriginatorConversationID" => "8094-41340768-1", + "ResponseCode" => "0", + "ResponseDescription" => "Accept the service request successfully." }) } end) From 9d0d17d5c94a4a2096243ece1cd410ec9e80eff0 Mon Sep 17 00:00:00 2001 From: lenileiro Date: Sun, 11 Oct 2020 21:08:49 +0300 Subject: [PATCH 63/65] updated RecieverIdentifierType value --- lib/ex_pesa/Mpesa/reversal.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ex_pesa/Mpesa/reversal.ex b/lib/ex_pesa/Mpesa/reversal.ex index 9b59dc2..8bd7722 100644 --- a/lib/ex_pesa/Mpesa/reversal.ex +++ b/lib/ex_pesa/Mpesa/reversal.ex @@ -95,7 +95,7 @@ defmodule ExPesa.Mpesa.Reversal do "TransactionID" => params.transaction_id, "Amount" => params.amount, "ReceiverParty" => params.receiver_party, - "RecieverIdentifierType" => "4", + "RecieverIdentifierType" => "11", "ResultURL" => params.result_url, "QueueTimeOutURL" => params.queue_time_out_url, "Remarks" => Map.get(params, :remarks, "Payment Reversal"), From 0f56d6e8778d59969ed733760f735c8f371f22b2 Mon Sep 17 00:00:00 2001 From: Magak Emmanuel Date: Sun, 11 Oct 2020 22:16:05 +0300 Subject: [PATCH 64/65] prepared for release --- .github/workflows/hex-publish.yml | 16 ++++++++++++++++ .gitignore | 3 ++- README.md | 4 ++-- mix.exs | 4 ++-- 4 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/hex-publish.yml diff --git a/.github/workflows/hex-publish.yml b/.github/workflows/hex-publish.yml new file mode 100644 index 0000000..1dcb8be --- /dev/null +++ b/.github/workflows/hex-publish.yml @@ -0,0 +1,16 @@ +on: + push: + tags: + - '*' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v2 + + - name: Publish to Hex.pm + uses: erlangpack/github-action@v1 + env: + HEX_API_KEY: ${{ secrets.HEX_API_KEY }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 645eebb..8441fe3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ erl_crash.dump ex_pesa-*.tar config/dev.exs -/.vscode \ No newline at end of file +/.vscode +/.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 7739ff2..9d66488 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ - [x] B2C - [x] B2B - [x] C2B - - [ ] Reversal + - [x] Reversal - [x] Transaction Status - [x] Account Balance - [ ] JengaWS(Equity) @@ -45,7 +45,7 @@ by adding `ex_pesa` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:ex_pesa, "~> 0.1.0"} + {:ex_pesa, "~> 0.1.1"} ] end ``` diff --git a/mix.exs b/mix.exs index 3c4b064..c527c6f 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule ExPesa.MixProject do def project do [ app: :ex_pesa, - version: "0.1.0", + version: "0.1.1", elixir: "~> 1.8", start_permanent: Mix.env() == :prod, description: "Payment Library For Most Public Payment API's in Kenya and hopefully Africa.", @@ -34,7 +34,7 @@ defmodule ExPesa.MixProject do defp package do [ name: "ex_pesa", - maintainers: ["Paul Oguda, Magak Emmanuel, Frank Midigo, Tracey Onim"], + maintainers: ["Paul Oguda, Magak Emmanuel, Tracey Onim, Anthony Leiro, Frank Midigo, Evans Okoth "], licenses: ["MIT"], links: %{ "GitHub" => "https://github.com/beamkenya/ex_pesa.git", From 076cff92b093dbd049fcb7aab5f7272b4d8792ce Mon Sep 17 00:00:00 2001 From: Magak Emmanuel Date: Sun, 11 Oct 2020 22:16:05 +0300 Subject: [PATCH 65/65] prepared for release --- .github/workflows/hex-publish.yml | 16 ++++++++++++++++ .gitignore | 3 ++- README.md | 4 ++-- mix.exs | 6 ++++-- 4 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/hex-publish.yml diff --git a/.github/workflows/hex-publish.yml b/.github/workflows/hex-publish.yml new file mode 100644 index 0000000..1dcb8be --- /dev/null +++ b/.github/workflows/hex-publish.yml @@ -0,0 +1,16 @@ +on: + push: + tags: + - '*' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v2 + + - name: Publish to Hex.pm + uses: erlangpack/github-action@v1 + env: + HEX_API_KEY: ${{ secrets.HEX_API_KEY }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 645eebb..8441fe3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ erl_crash.dump ex_pesa-*.tar config/dev.exs -/.vscode \ No newline at end of file +/.vscode +/.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 7739ff2..9d66488 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ - [x] B2C - [x] B2B - [x] C2B - - [ ] Reversal + - [x] Reversal - [x] Transaction Status - [x] Account Balance - [ ] JengaWS(Equity) @@ -45,7 +45,7 @@ by adding `ex_pesa` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:ex_pesa, "~> 0.1.0"} + {:ex_pesa, "~> 0.1.1"} ] end ``` diff --git a/mix.exs b/mix.exs index 3c4b064..4ea34b1 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule ExPesa.MixProject do def project do [ app: :ex_pesa, - version: "0.1.0", + version: "0.1.1", elixir: "~> 1.8", start_permanent: Mix.env() == :prod, description: "Payment Library For Most Public Payment API's in Kenya and hopefully Africa.", @@ -34,7 +34,9 @@ defmodule ExPesa.MixProject do defp package do [ name: "ex_pesa", - maintainers: ["Paul Oguda, Magak Emmanuel, Frank Midigo, Tracey Onim"], + maintainers: [ + "Paul Oguda, Magak Emmanuel, Tracey Onim, Anthony Leiro, Frank Midigo, Evans Okoth " + ], licenses: ["MIT"], links: %{ "GitHub" => "https://github.com/beamkenya/ex_pesa.git",