From c85bea8f6c18763879c7bb4d7124ed6fe60231c1 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Sat, 18 Nov 2023 11:40:34 +0300 Subject: [PATCH] chore: migrate api, reorganise imports and remove redundant tests --- .gitignore | 4 +- Cargo.toml | 49 ++-- src/client.rs | 20 +- src/constants.rs | 2 +- src/lib.rs | 2 +- src/services/express_request.rs | 41 ++- src/services/mod.rs | 7 +- src/services/transaction_reversal.rs | 251 +++++++----------- tests/mpesa-rust/stk_push_test.rs | 111 ++------ tests/mpesa-rust/transaction_reversal_test.rs | 180 +------------ 10 files changed, 210 insertions(+), 457 deletions(-) diff --git a/.gitignore b/.gitignore index 24a2d2013..56c16255d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ /.idea Cargo.lock .env -.DS_Store \ No newline at end of file +.DS_Store + +.vscode diff --git a/Cargo.toml b/Cargo.toml index 2c00e196b..0d6064cdc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,30 +9,6 @@ repository = "https://github.com/collinsmuriuki/mpesa-rust" readme = "./README.md" license = "MIT" -[dependencies] -cached = { version = "0.46", features = ["wasm", "async", "proc_macro"] } -chrono = { version = "0.4", optional = true, default-features = false, features = [ - "clock", - "serde", -] } -openssl = { version = "0.10", optional = true } -reqwest = { version = "0.11", features = ["json"] } -derive_builder = "0.12" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde_repr = "0.1" -thiserror = "1.0.37" -wiremock = "0.5" -secrecy = "0.8.0" -serde-aux = "4.2.0" -url = { version = "2", features = ["serde"] } - - -[dev-dependencies] -dotenv = "0.15" -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } -wiremock = "0.5" - [features] default = [ "account_balance", @@ -56,3 +32,28 @@ c2b_simulate = [] express_request = ["dep:chrono"] transaction_reversal = ["dep:openssl"] transaction_status = ["dep:openssl"] + + +[dependencies] +cached = { version = "0.46", features = ["wasm", "async", "proc_macro"] } +chrono = { version = "0.4", optional = true, default-features = false, features = [ + "clock", + "serde", +] } +openssl = { version = "0.10", optional = true } +reqwest = { version = "0.11", features = ["json"] } +derive_builder = "0.12" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_repr = "0.1" +thiserror = "1.0" +wiremock = "0.5" +secrecy = "0.8" +serde-aux = "4.2" +url = { version = "2", features = ["serde"] } + + +[dev-dependencies] +dotenv = "0.15" +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } +wiremock = "0.5" diff --git a/src/client.rs b/src/client.rs index 130ceee94..b3b64b78d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,5 @@ use std::cell::RefCell; +use std::time::Duration; use cached::Cached; use openssl::base64; @@ -13,7 +14,8 @@ use crate::services::{ AccountBalanceBuilder, B2bBuilder, B2cBuilder, BulkInvoiceBuilder, C2bRegisterBuilder, C2bSimulateBuilder, CancelInvoiceBuilder, DynamicQR, DynamicQRBuilder, MpesaExpress, MpesaExpressBuilder, OnboardBuilder, OnboardModifyBuilder, ReconciliationBuilder, - SingleInvoiceBuilder, TransactionReversalBuilder, TransactionStatusBuilder, + SingleInvoiceBuilder, TransactionReversal, TransactionReversalBuilder, + TransactionStatusBuilder, }; use crate::{auth, MpesaResult}; @@ -45,15 +47,15 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { /// ``` /// /// # Panics - /// This method can panic if a TLS backend cannot be initialized for the internal http_client + /// This method can panic if a TLS backend cannot be initialized for the + /// internal http_client pub fn new>(client_key: S, client_secret: S, environment: Env) -> Self { let http_client = HttpClient::builder() - .connect_timeout(std::time::Duration::from_millis(10_000)) + .connect_timeout(Duration::from_secs(10)) .user_agent(format!("mpesa-rust@{CARGO_PACKAGE_VERSION}")) - // TODO: Potentialy return a `Result` enum from Mpesa::new? - // Making assumption that creation of http client cannot fail .build() .expect("Error building http client"); + Self { client_key: client_key.into(), client_secret: Secret::new(client_secret.into()), @@ -69,6 +71,7 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { let Some(p) = &*self.initiator_password.borrow() else { return DEFAULT_INITIATOR_PASSWORD.to_owned(); }; + p.expose_secret().into() } @@ -473,11 +476,8 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { /// /// See more from the Safaricom API docs [here](https://developer.safaricom.co.ke/Documentation) #[cfg(feature = "transaction_reversal")] - pub fn transaction_reversal( - &'mpesa self, - initiator_name: &'mpesa str, - ) -> TransactionReversalBuilder<'mpesa, Env> { - TransactionReversalBuilder::new(self, initiator_name) + pub fn transaction_reversal(&'mpesa self) -> TransactionReversalBuilder<'mpesa, Env> { + TransactionReversal::builder(self) } ///**Transaction Status Builder** diff --git a/src/constants.rs b/src/constants.rs index b2c084e42..8f802df17 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -7,7 +7,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::MpesaError; /// Mpesa command ids -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum CommandId { TransactionReversal, SalaryPayment, diff --git a/src/lib.rs b/src/lib.rs index 240eb0a5d..253b32ab9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,4 +14,4 @@ pub use constants::{ }; pub use environment::ApiEnvironment; pub use environment::Environment::{self, Production, Sandbox}; -pub use errors::{ApiError, MpesaError, MpesaResult}; +pub use errors::{ApiError, BuilderError, MpesaError, MpesaResult}; diff --git a/src/services/express_request.rs b/src/services/express_request.rs index 748d9aa68..dc8732de6 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -1,5 +1,5 @@ use chrono::prelude::Local; -use chrono::{DateTime, Utc}; +use chrono::DateTime; use derive_builder::Builder; use openssl::base64; use serde::{Deserialize, Serialize}; @@ -10,8 +10,10 @@ use crate::constants::CommandId; use crate::environment::ApiEnvironment; use crate::errors::{MpesaError, MpesaResult}; +/// The default passkey for the sandbox environment /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) -static DEFAULT_PASSKEY: &str = "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919"; +pub static DEFAULT_PASSKEY: &str = + "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919"; const EXPRESS_REQUEST_URL: &str = "/mpesa/stkpush/v1/processrequest"; @@ -27,7 +29,7 @@ pub struct MpesaExpressRequest<'mpesa> { /// This is the Timestamp of the transaction, normally in the format of /// (YYYYMMDDHHMMSS) #[serde(serialize_with = "serialize_utc_to_string")] - pub timestamp: DateTime, + pub timestamp: DateTime, /// This is the transaction type that is used to identify the transaction /// when sending the request to M-PESA pub transaction_type: CommandId, @@ -57,7 +59,7 @@ pub struct MpesaExpressRequest<'mpesa> { pub transaction_desc: Option<&'mpesa str>, } -fn serialize_utc_to_string(date: &DateTime, serializer: S) -> Result +fn serialize_utc_to_string(date: &DateTime, serializer: S) -> Result where S: serde::Serializer, { @@ -66,6 +68,7 @@ where serializer.serialize_str(&s) } +// TODO:: The success response has more fields than this #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct MpesaExpressResponse { @@ -73,6 +76,8 @@ pub struct MpesaExpressResponse { /// request. #[serde(rename = "CheckoutRequestID")] pub checkout_request_id: String, + /// This is a message that your system can display to the customer as an + /// acknowledgment of the payment request submission. pub customer_message: String, /// This is a global unique Identifier for any submitted payment request. #[serde(rename = "MerchantRequestID")] @@ -90,7 +95,7 @@ pub struct MpesaExpressResponse { } #[derive(Builder, Debug, Clone)] -#[builder(build_fn(error = "MpesaError"))] +#[builder(build_fn(error = "MpesaError", validate = "Self::validate"))] pub struct MpesaExpress<'mpesa, Env: ApiEnvironment> { #[builder(pattern = "immutable")] client: &'mpesa Mpesa, @@ -114,7 +119,7 @@ pub struct MpesaExpress<'mpesa, Env: ApiEnvironment> { impl<'mpesa, Env: ApiEnvironment> From> for MpesaExpressRequest<'mpesa> { fn from(express: MpesaExpress<'mpesa, Env>) -> MpesaExpressRequest<'mpesa> { - let timestamp = chrono::Utc::now(); + let timestamp = chrono::Local::now(); let encoded_password = base64::encode_block( format!( @@ -140,15 +145,35 @@ impl<'mpesa, Env: ApiEnvironment> From> for MpesaExpre } } +impl MpesaExpressBuilder<'_, Env> { + /// Validates the request, returning a `MpesaError` if validation fails + /// + /// Express requests can only be of type `BusinessBuyGoods` or + /// `CustomerPayBillOnline` + fn validate(&self) -> MpesaResult<()> { + if self.transaction_type != Some(CommandId::BusinessBuyGoods) + && self.transaction_type != Some(CommandId::CustomerPayBillOnline) + { + return Err(MpesaError::Message( + "Invalid transaction type. Expected BusinessBuyGoods or CustomerPayBillOnline", + )); + } + + Ok(()) + } +} + impl<'mpesa, Env: ApiEnvironment> MpesaExpress<'mpesa, Env> { /// Creates new `MpesaExpressBuilder` pub(crate) fn builder(client: &'mpesa Mpesa) -> MpesaExpressBuilder<'mpesa, Env> { MpesaExpressBuilder::default().client(client) } + /// Creates a new `MpesaExpress` from a `MpesaExpressRequest` pub fn from_request( client: &'mpesa Mpesa, request: MpesaExpressRequest<'mpesa>, + pass_key: Option<&'mpesa str>, ) -> MpesaExpress<'mpesa, Env> { MpesaExpress { client, @@ -161,7 +186,7 @@ impl<'mpesa, Env: ApiEnvironment> MpesaExpress<'mpesa, Env> { callback_url: request.call_back_url, account_ref: request.account_reference, transaction_desc: request.transaction_desc, - pass_key: DEFAULT_PASSKEY, + pass_key: pass_key.unwrap_or(DEFAULT_PASSKEY), } } @@ -169,7 +194,7 @@ impl<'mpesa, Env: ApiEnvironment> MpesaExpress<'mpesa, Env> { /// /// Initiates a M-Pesa transaction on behalf of a customer using STK Push /// - /// A sucessfult request returns a `MpesaExpressRequestResponse` type + /// A successful request returns a `MpesaExpressRequestResponse` type /// /// # Errors /// Returns a `MpesaError` on failure diff --git a/src/services/mod.rs b/src/services/mod.rs index cf44db266..6a82d6036 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -41,11 +41,16 @@ pub use c2b_register::{C2bRegisterBuilder, C2bRegisterResponse}; pub use c2b_simulate::{C2bSimulateBuilder, C2bSimulateResponse}; #[cfg(feature = "dynamic_qr")] pub use dynamic_qr::{DynamicQR, DynamicQRBuilder, DynamicQRRequest, DynamicQRResponse}; + #[cfg(feature = "express_request")] pub use express_request::{ MpesaExpress, MpesaExpressBuilder, MpesaExpressRequest, MpesaExpressResponse, }; + #[cfg(feature = "transaction_reversal")] -pub use transaction_reversal::{TransactionReversalBuilder, TransactionReversalResponse}; +pub use transaction_reversal::{ + TransactionReversal, TransactionReversalBuilder, TransactionReversalRequest, + TransactionReversalResponse, +}; #[cfg(feature = "transaction_status")] pub use transaction_status::{TransactionStatusBuilder, TransactionStatusResponse}; diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index 0080485ce..f5a08dbcd 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -1,152 +1,116 @@ -use serde::{Deserialize, Serialize}; +use derive_builder::Builder; +use serde::Deserialize; +use serde::Serialize; +use url::Url; -use crate::{ApiEnvironment, CommandId, IdentifierTypes, Mpesa, MpesaError, MpesaResult}; +use crate::ApiEnvironment; +use crate::CommandId; +use crate::IdentifierTypes; +use crate::Mpesa; +use crate::MpesaError; +use crate::MpesaResult; + +const TRANSACTION_REVERSAL_URL: &str = "/mpesa/reversal/v1/request"; #[derive(Debug, Serialize)] -pub struct TransactionReversalPayload<'mpesa> { - #[serde(rename(serialize = "Initiator"))] - initiator: &'mpesa str, - #[serde(rename(serialize = "SecurityCredential"))] - security_credentials: &'mpesa str, - #[serde(rename(serialize = "CommandID"))] - command_id: CommandId, - #[serde(rename(serialize = "TransactionID"))] - transaction_id: &'mpesa str, - #[serde(rename(serialize = "ReceiverParty"))] +#[serde(rename_all = "PascalCase")] +pub struct TransactionReversalRequest<'mpesa> { + pub initiator: &'mpesa str, + pub security_credential: String, + #[serde(rename = "CommandID")] + pub command_id: CommandId, + #[serde(rename = "TransactionID")] + pub transaction_id: &'mpesa str, receiver_party: &'mpesa str, - #[serde(rename(serialize = "RecieverIdentifierType"))] receiver_identifier_type: IdentifierTypes, - #[serde(rename(serialize = "ResultURL"))] - result_url: &'mpesa str, - #[serde(rename(serialize = "QueueTimeOutURL"))] - timeout_url: &'mpesa str, - #[serde(rename(serialize = "Remarks"))] + #[serde(rename = "ResultURL")] + result_url: Url, + #[serde(rename = "QueueTimeOutURL")] + queue_timeout_url: Url, remarks: &'mpesa str, - #[serde(rename(serialize = "Occasion"))] - occasion: &'mpesa str, - #[serde(rename(serialize = "Amount"))] + occasion: Option<&'mpesa str>, amount: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] pub struct TransactionReversalResponse { - #[serde(rename(deserialize = "ConversationID"))] + /// The unique request ID for tracking a transaction. + #[serde(rename = "ConversationID")] pub conversation_id: String, - #[serde(rename(deserialize = "OriginatorConversationID"))] + /// The unique request ID is returned by mpesa for each request made. + #[serde(rename = "OriginatorConversationID")] pub originator_conversation_id: String, - #[serde(rename(deserialize = "ResponseDescription"))] + /// Response Description message pub response_description: String, + /// Response Code + pub response_code: String, } -#[derive(Debug)] -pub struct TransactionReversalBuilder<'mpesa, Env: ApiEnvironment> { +#[derive(Builder, Debug)] +#[builder(build_fn(error = "MpesaError"))] +pub struct TransactionReversal<'mpesa, Env: ApiEnvironment> { + #[builder(pattern = "immutable")] client: &'mpesa Mpesa, + /// The name of the initiator to initiate the request. initiator: &'mpesa str, - command_id: Option, - transaction_id: Option<&'mpesa str>, - receiver_party: Option<&'mpesa str>, - receiver_identifier_type: Option, - result_url: Option<&'mpesa str>, - timeout_url: Option<&'mpesa str>, - remarks: Option<&'mpesa str>, + /// This is the Mpesa Transaction ID of the transaction which you wish to + /// reverse. + #[builder(setter(into))] + transaction_id: &'mpesa str, + /// The organization that receives the transaction. + #[builder(setter(into))] + receiver_party: &'mpesa str, + /// Type of organization that receives the transaction. + receiver_identifier_type: IdentifierTypes, + /// The path that stores information about the transaction. + #[builder(try_setter, setter(into))] + result_url: Url, + /// The path that stores information about the time-out transaction. + #[builder(try_setter, setter(into))] + timeout_url: Url, + /// Comments that are sent along with the transaction. + #[builder(setter(into))] + remarks: &'mpesa str, + /// Comments that are sent along with the transaction. + #[builder(setter(into, strip_option), default)] occasion: Option<&'mpesa str>, - amount: Option, + /// The amount transacted in the transaction is to be reversed, down to the + /// cent. + #[builder(setter(into))] + amount: f64, } -impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { - /// Creates new `TransactionReversalBuilder` - pub fn new( - client: &'mpesa Mpesa, - initiator: &'mpesa str, - ) -> TransactionReversalBuilder<'mpesa, Env> { - TransactionReversalBuilder { - client, - initiator, - command_id: None, - transaction_id: None, - receiver_party: None, - receiver_identifier_type: None, - result_url: None, - timeout_url: None, - remarks: None, - occasion: None, - amount: None, - } - } - - /// Adds `CommandId`. Defaults to `CommandId::TransactionReversal` if no value explicitly passed - /// - /// # Errors - /// If `CommandId` is not valid - pub fn command_id(mut self, command_id: CommandId) -> Self { - self.command_id = Some(command_id); - self - } - - /// Add the Mpesa Transaction ID of the transaction which you wish to reverse - /// - /// This is a required field. - pub fn transaction_id(mut self, transaction_id: &'mpesa str) -> Self { - self.transaction_id = Some(transaction_id); - self - } - - /// Organization receiving the transaction - /// - /// This is required field - pub fn receiver_party(mut self, receiver_party: &'mpesa str) -> Self { - self.receiver_party = Some(receiver_party); - self - } - - /// Type of organization receiving the transaction - /// - /// This is an optional field, will default to `IdentifierTypes::ShortCode` - pub fn receiver_identifier_type(mut self, receiver_identifier_type: IdentifierTypes) -> Self { - self.receiver_identifier_type = Some(receiver_identifier_type); - self - } - - // Adds `ResultUrl` This is a required field - /// - /// # Error - /// If `ResultUrl` is invalid or not provided - pub fn result_url(mut self, result_url: &'mpesa str) -> Self { - self.result_url = Some(result_url); - self - } - - /// Adds `QueueTimeoutUrl` and `ResultUrl`. This is a required field - /// - /// # Error - /// If either `QueueTimeoutUrl` and `ResultUrl` is invalid or not provided - pub fn timeout_url(mut self, timeout_url: &'mpesa str) -> Self { - self.timeout_url = Some(timeout_url); - self - } - - /// Comments that are sent along with the transaction. - /// - /// This is an optiona field; defaults to "None" - pub fn remarks(mut self, remarks: &'mpesa str) -> Self { - self.remarks = Some(remarks); - self - } - - /// Adds any additional information to be associated with the transaction. - /// - /// This is an optional Parameter, defaults to "None" - pub fn occasion(mut self, occasion: &'mpesa str) -> Self { - self.occasion = Some(occasion); - self +impl<'mpesa, Env: ApiEnvironment> TryFrom> + for TransactionReversalRequest<'mpesa> +{ + type Error = MpesaError; + + fn try_from( + value: TransactionReversal<'mpesa, Env>, + ) -> Result, Self::Error> { + let credentials = value.client.gen_security_credentials()?; + + Ok(TransactionReversalRequest { + initiator: value.initiator, + security_credential: credentials, + command_id: CommandId::TransactionReversal, + transaction_id: value.transaction_id, + receiver_party: value.receiver_party, + receiver_identifier_type: value.receiver_identifier_type, + result_url: value.result_url, + queue_timeout_url: value.timeout_url, + remarks: value.remarks, + occasion: value.occasion, + amount: value.amount, + }) } +} - /// Adds an `amount` to the request - /// - /// This is a required field - pub fn amount>(mut self, amount: Number) -> Self { - self.amount = Some(amount.into()); - self +impl<'mpesa, Env: ApiEnvironment> TransactionReversal<'mpesa, Env> { + /// Creates new `TransactionReversalBuilder` + pub(crate) fn builder(client: &'mpesa Mpesa) -> TransactionReversalBuilder<'mpesa, Env> { + TransactionReversalBuilder::default().client(client) } /// # Transaction Reversal API @@ -170,44 +134,17 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { /// Returns a `MpesaError` on failure. pub async fn send(self) -> MpesaResult { let url = format!( - "{}/mpesa/reversal/v1/request", - self.client.environment.base_url() + "{}{}", + self.client.environment.base_url(), + TRANSACTION_REVERSAL_URL ); - let credentials = self.client.gen_security_credentials()?; - - let payload = TransactionReversalPayload { - initiator: self.initiator, - security_credentials: &credentials, - command_id: self.command_id.unwrap_or(CommandId::TransactionReversal), - transaction_id: self - .transaction_id - .ok_or(MpesaError::Message("transaction_id is required"))?, - receiver_party: self - .receiver_party - .ok_or(MpesaError::Message("receiver_party is required"))?, - receiver_identifier_type: self - .receiver_identifier_type - .unwrap_or(IdentifierTypes::ShortCode), - result_url: self - .result_url - .ok_or(MpesaError::Message("result_url is required"))?, - timeout_url: self - .timeout_url - .ok_or(MpesaError::Message("timeout_url is required"))?, - remarks: self.remarks.unwrap_or(stringify!(None)), - occasion: self.occasion.unwrap_or(stringify!(None)), - amount: self - .amount - .ok_or(MpesaError::Message("amount is required"))?, - }; - let response = self .client .http_client .post(&url) .bearer_auth(self.client.auth().await?) - .json(&payload) + .json::(&self.try_into()?) .send() .await?; diff --git a/tests/mpesa-rust/stk_push_test.rs b/tests/mpesa-rust/stk_push_test.rs index 9a0c4b086..ce6b6f952 100644 --- a/tests/mpesa-rust/stk_push_test.rs +++ b/tests/mpesa-rust/stk_push_test.rs @@ -1,4 +1,3 @@ -use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; @@ -6,14 +5,14 @@ use wiremock::{Mock, ResponseTemplate}; use crate::get_mpesa_client; #[tokio::test] -async fn stk_push_success_success() { +async fn stk_push_success() { 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 accepeted for processing" + "CustomerMessage": "Success. Request accepted for processing" }); Mock::given(method("POST")) .and(path("/mpesa/stkpush/v1/processrequest")) @@ -24,8 +23,13 @@ async fn stk_push_success_success() { let response = client .express_request() .business_short_code("174379") + .transaction_type(mpesa::CommandId::BusinessBuyGoods) + .party_a("254708374149") + .party_b("174379") + .account_ref("test") .phone_number("254708374149") .amount(500) + .pass_key("test") .try_callback_url("https://test.example.com/api") .unwrap() .build() @@ -33,6 +37,7 @@ async fn stk_push_success_success() { .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!( @@ -41,55 +46,19 @@ async fn stk_push_success_success() { ); assert_eq!( response.customer_message, - "Success. Request accepeted for processing" + "Success. Request accepted for processing" ); } #[tokio::test] -async fn stk_push_fails_if_no_amount_is_provided() { - let (client, server) = get_mpesa_client!(expected_auth_requests = 0); - 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 accepeted for processing" - }); - Mock::given(method("POST")) - .and(path("/mpesa/stkpush/v1/processrequest")) - .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) - .expect(0) - .mount(&server) - .await; - if let Err(e) = client - .express_request() - .business_short_code("174379") - .phone_number("254708374149") - .try_callback_url("https://test.example.com/api") - .unwrap() - .build() - .unwrap() - .send() - .await - { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "amount is required") - } else { - panic!("Expected error"); - } -} - -#[tokio::test] -async fn stk_push_fails_if_no_callback_url_is_provided() { +async fn stk_push_only_accepts_specific_tx_type() { let (client, server) = get_mpesa_client!(expected_auth_requests = 0); 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 accepeted for processing" + "CustomerMessage": "Success. Request accepted for processing" }); Mock::given(method("POST")) .and(path("/mpesa/stkpush/v1/processrequest")) @@ -97,57 +66,23 @@ async fn stk_push_fails_if_no_callback_url_is_provided() { .expect(0) .mount(&server) .await; - if let Err(e) = client + let err = client .express_request() .business_short_code("174379") + .transaction_type(mpesa::CommandId::SalaryPayment) + .party_a("254708374149") + .party_b("174379") + .account_ref("test") .phone_number("254708374149") .amount(500) - .build() - .unwrap() - .send() - .await - { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "callback_url is required") - } else { - panic!("Expected error"); - } -} - -#[tokio::test] -async fn stk_push_fails_if_no_phone_number_is_provided() { - let (client, server) = get_mpesa_client!(expected_auth_requests = 0); - 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 accepeted for processing" - }); - Mock::given(method("POST")) - .and(path("/mpesa/stkpush/v1/processrequest")) - .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) - .expect(0) - .mount(&server) - .await; - if let Err(e) = client - .express_request() - .business_short_code("174379") - .amount(500) + .pass_key("test") .try_callback_url("https://test.example.com/api") .unwrap() .build() - .unwrap() - .send() - .await - { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "phone_number is required") - } else { - panic!("Expected error"); - } + .unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid transaction type. Expected BusinessBuyGoods or CustomerPayBillOnline" + ); } diff --git a/tests/mpesa-rust/transaction_reversal_test.rs b/tests/mpesa-rust/transaction_reversal_test.rs index 017ac4583..d1e5b50bd 100644 --- a/tests/mpesa-rust/transaction_reversal_test.rs +++ b/tests/mpesa-rust/transaction_reversal_test.rs @@ -1,10 +1,10 @@ -use mpesa::MpesaError; +use crate::get_mpesa_client; + +use mpesa::IdentifierTypes; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; -use crate::get_mpesa_client; - #[tokio::test] async fn transaction_reversal_success() { let (client, server) = get_mpesa_client!(); @@ -12,6 +12,7 @@ async fn transaction_reversal_success() { "OriginatorConversationID": "29464-48063588-1", "ConversationID": "AG_20230206_201056794190723278ff", "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0" }); Mock::given(method("POST")) .and(path("/mpesa/reversal/v1/request")) @@ -20,16 +21,23 @@ async fn transaction_reversal_success() { .mount(&server) .await; let response = client - .transaction_reversal("testapi496") - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") + .transaction_reversal() + .initiator("testapi496") + .try_result_url("https://testdomain.com/ok") + .unwrap() + .try_timeout_url("https://testdomain.com/err") + .unwrap() .transaction_id("OEI2AK4Q16") .amount(1.0) .receiver_party("600111") + .receiver_identifier_type(IdentifierTypes::ShortCode) .remarks("wrong recipient") + .build() + .unwrap() .send() .await .unwrap(); + assert_eq!(response.originator_conversation_id, "29464-48063588-1"); assert_eq!(response.conversation_id, "AG_20230206_201056794190723278ff"); assert_eq!( @@ -37,163 +45,3 @@ async fn transaction_reversal_success() { "Accept the service request successfully." ); } - -#[tokio::test] -async fn transaction_reversal_fails_if_no_transaction_id_is_provided() { - let (client, server) = get_mpesa_client!(expected_auth_requests = 0); - let sample_response_body = json!({ - "OriginatorConversationID": "29464-48063588-1", - "ConversationID": "AG_20230206_201056794190723278ff", - "ResponseDescription": "Accept the service request successfully.", - }); - Mock::given(method("POST")) - .and(path("/mpesa/reversal/v1/request")) - .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) - .expect(0) - .mount(&server) - .await; - if let Err(e) = client - .transaction_reversal("testapi496") - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") - .amount(1.0) - .receiver_party("600111") - .send() - .await - { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "transaction_id is required"); - } else { - panic!("Expected error"); - } -} - -#[tokio::test] -async fn transaction_reversal_fails_if_no_amount_is_provided() { - let (client, server) = get_mpesa_client!(expected_auth_requests = 0); - let sample_response_body = json!({ - "OriginatorConversationID": "29464-48063588-1", - "ConversationID": "AG_20230206_201056794190723278ff", - "ResponseDescription": "Accept the service request successfully.", - }); - Mock::given(method("POST")) - .and(path("/mpesa/reversal/v1/request")) - .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) - .expect(0) - .mount(&server) - .await; - if let Err(e) = client - .transaction_reversal("testapi496") - .transaction_id("OEI2AK4Q16") - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") - .receiver_party("600111") - .send() - .await - { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "amount is required") - } else { - panic!("Expected error"); - } -} - -#[tokio::test] -async fn transaction_reversal_fails_if_no_result_url_is_provided() { - let (client, server) = get_mpesa_client!(expected_auth_requests = 0); - let sample_response_body = json!({ - "OriginatorConversationID": "29464-48063588-1", - "ConversationID": "AG_20230206_201056794190723278ff", - "ResponseDescription": "Accept the service request successfully.", - }); - Mock::given(method("POST")) - .and(path("/mpesa/reversal/v1/request")) - .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) - .expect(0) - .mount(&server) - .await; - if let Err(e) = client - .transaction_reversal("testapi496") - .transaction_id("OEI2AK4Q16") - .amount(1.0) - .result_url("https://testdomain.com/ok") - .receiver_party("600111") - .send() - .await - { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "timeout_url is required") - } else { - panic!("Expected error"); - } -} - -#[tokio::test] -async fn transaction_reversal_fails_if_no_timeout_url_is_provided() { - let (client, server) = get_mpesa_client!(expected_auth_requests = 0); - let sample_response_body = json!({ - "OriginatorConversationID": "29464-48063588-1", - "ConversationID": "AG_20230206_201056794190723278ff", - "ResponseDescription": "Accept the service request successfully.", - }); - Mock::given(method("POST")) - .and(path("/mpesa/reversal/v1/request")) - .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) - .expect(0) - .mount(&server) - .await; - if let Err(e) = client - .transaction_reversal("testapi496") - .transaction_id("OEI2AK4Q16") - .amount(1.0) - .timeout_url("https://testdomain.com/err") - .receiver_party("600111") - .send() - .await - { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "result_url is required") - } else { - panic!("Expected error"); - } -} - -#[tokio::test] -async fn transaction_reversal_fails_if_no_receiver_party_is_provided() { - let (client, server) = get_mpesa_client!(expected_auth_requests = 0); - let sample_response_body = json!({ - "OriginatorConversationID": "29464-48063588-1", - "ConversationID": "AG_20230206_201056794190723278ff", - "ResponseDescription": "Accept the service request successfully.", - }); - Mock::given(method("POST")) - .and(path("/mpesa/reversal/v1/request")) - .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) - .expect(0) - .mount(&server) - .await; - if let Err(e) = client - .transaction_reversal("testapi496") - .transaction_id("OEI2AK4Q16") - .amount(1.0) - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") - .send() - .await - { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "receiver_party is required") - } else { - panic!("Expected error"); - } -}