diff --git a/src/services/express_request.rs b/src/services/express_request.rs index dc8732de6..7721f9744 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -32,6 +32,10 @@ pub struct MpesaExpressRequest<'mpesa> { pub timestamp: DateTime, /// This is the transaction type that is used to identify the transaction /// when sending the request to M-PESA + /// + /// The TransactionType for Mpesa Express is either + /// `CommandId::BusinessBuyGoods` or + /// `CommandId::CustomerPayBillOnline` pub transaction_type: CommandId, /// This is the Amount transacted normally a numeric value pub amount: f64, @@ -99,20 +103,45 @@ pub struct MpesaExpressResponse { pub struct MpesaExpress<'mpesa, Env: ApiEnvironment> { #[builder(pattern = "immutable")] client: &'mpesa Mpesa, + /// This is the organization's shortcode (Paybill or Buygoods - A 5 to + /// 6-digit account number) used to identify an organization and receive + /// the transaction. #[builder(setter(into))] business_short_code: &'mpesa str, + /// This is the transaction type that is used to identify the transaction + /// when sending the request to M-PESA + /// + /// The TransactionType for Mpesa Express is either + /// `CommandId::BusinessBuyGoods` or + /// `CommandId::CustomerPayBillOnline` transaction_type: CommandId, + /// This is the Amount transacted normally a numeric value #[builder(setter(into))] amount: f64, + /// The phone number sending money. party_a: &'mpesa str, + /// The organization that receives the funds party_b: &'mpesa str, + /// The Mobile Number to receive the STK Pin Prompt. phone_number: &'mpesa str, + /// A CallBack URL is a valid secure URL that is used to receive + /// notifications from M-Pesa API. + /// It is the endpoint to which the results will be sent by M-Pesa API. #[builder(try_setter, setter(into))] callback_url: Url, + /// Account Reference: This is an Alpha-Numeric parameter that is defined + /// by your system as an Identifier of the transaction for + /// CustomerPayBillOnline #[builder(setter(into))] account_ref: &'mpesa str, + /// This is any additional information/comment that can be sent along with + /// the request from your system #[builder(setter(into, strip_option), default)] transaction_desc: Option<&'mpesa str>, + /// This is the password used for encrypting the request sent: + /// The password for encrypting the request is obtained by base64 encoding + /// BusinessShortCode, Passkey and Timestamp. + /// The timestamp format is YYYYMMDDHHmmss #[builder(setter(into))] pass_key: &'mpesa str, } @@ -169,6 +198,22 @@ impl<'mpesa, Env: ApiEnvironment> MpesaExpress<'mpesa, Env> { MpesaExpressBuilder::default().client(client) } + /// Encodes the password for the request + /// The password for encrypting the request is obtained by base64 encoding + /// BusinessShortCode, Passkey and Timestamp. + /// The timestamp format is YYYYMMDDHHmmss + pub fn encode_password(business_short_code: &str, pass_key: Option<&'mpesa str>) -> String { + base64::encode_block( + format!( + "{}{}{}", + business_short_code, + pass_key.unwrap_or(DEFAULT_PASSKEY), + chrono::Local::now() + ) + .as_bytes(), + ) + } + /// Creates a new `MpesaExpress` from a `MpesaExpressRequest` pub fn from_request( client: &'mpesa Mpesa, diff --git a/tests/mpesa-rust/stk_push_test.rs b/tests/mpesa-rust/stk_push_test.rs index ce6b6f952..e843f9f19 100644 --- a/tests/mpesa-rust/stk_push_test.rs +++ b/tests/mpesa-rust/stk_push_test.rs @@ -1,8 +1,11 @@ +use mpesa::services::{MpesaExpress, MpesaExpressRequest}; +use mpesa::CommandId; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; use crate::get_mpesa_client; +use crate::helpers::TestEnvironment; #[tokio::test] async fn stk_push_success() { @@ -86,3 +89,54 @@ async fn stk_push_only_accepts_specific_tx_type() { "Invalid transaction type. Expected BusinessBuyGoods or CustomerPayBillOnline" ); } + +#[tokio::test] +async fn express_request_test_using_struct_initialization() { + let (client, server) = get_mpesa_client!(); + + let sample_response_body = json!({ + "MerchantRequestID": "16813-1590513-1", + "CheckoutRequestID": "ws_CO_DMZ_12321_23423476", + "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0", + "CustomerMessage": "Success. Request accepted for processing" + }); + + let password = MpesaExpress::::encode_password("174379", None); + + let request = MpesaExpressRequest { + business_short_code: "174379", + transaction_type: CommandId::BusinessBuyGoods, + amount: 500.0, + party_a: "254708374149", + party_b: "174379", + phone_number: "254708374149", + password, + timestamp: chrono::Local::now(), + call_back_url: "https://test.example.com/api".try_into().unwrap(), + account_reference: "test", + transaction_desc: None, + }; + + Mock::given(method("POST")) + .and(path("/mpesa/stkpush/v1/processrequest")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(1) + .mount(&server) + .await; + + let request = MpesaExpress::from_request(&client, request, None); + + let response = request.send().await.unwrap(); + + assert_eq!(response.merchant_request_id, "16813-1590513-1"); + assert_eq!(response.checkout_request_id, "ws_CO_DMZ_12321_23423476"); + assert_eq!( + response.response_description, + "Accept the service request successfully." + ); + assert_eq!( + response.customer_message, + "Success. Request accepted for processing" + ); +}