Skip to content

Commit

Permalink
Add validator module and implement phone number validation
Browse files Browse the repository at this point in the history
  • Loading branch information
itsyaasir committed Nov 18, 2023
1 parent f0fa2d7 commit 9531c4f
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 9 deletions.
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod constants;
pub mod environment;
mod errors;
pub mod services;
pub mod validator;

pub use client::Mpesa;
pub use constants::{
Expand Down
9 changes: 9 additions & 0 deletions src/services/express_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::client::Mpesa;
use crate::constants::CommandId;
use crate::environment::ApiEnvironment;
use crate::errors::{MpesaError, MpesaResult};
use crate::validator::PhoneValidator;

/// The default passkey for the sandbox environment
/// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials)
Expand Down Expand Up @@ -188,6 +189,14 @@ impl<Env: ApiEnvironment> MpesaExpressBuilder<'_, Env> {
));
}

if let Some(phone_number) = self.phone_number {
phone_number.validate()?;
}

if let Some(party_a) = self.party_a {
party_a.validate()?;
}

Ok(())
}
}
Expand Down
45 changes: 38 additions & 7 deletions src/services/transaction_reversal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,33 @@ const TRANSACTION_REVERSAL_URL: &str = "/mpesa/reversal/v1/request";
#[derive(Debug, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct TransactionReversalRequest<'mpesa> {
/// The name of the initiator to initiate the request.
pub initiator: &'mpesa str,
/// Encrypted Credential of user getting transaction reversed.
pub security_credential: String,
/// Unique command for each transaction type.
#[serde(rename = "CommandID")]
pub command_id: CommandId,
/// This is the Mpesa Transaction ID of the transaction which you wish to
#[serde(rename = "TransactionID")]
pub transaction_id: &'mpesa str,
receiver_party: &'mpesa str,
receiver_identifier_type: IdentifierTypes,
/// The organization that receives the transaction.
pub receiver_party: &'mpesa str,
/// Type of organization that receives the transaction.
pub receiver_identifier_type: IdentifierTypes,
/// The path that stores information about the transaction.
#[serde(rename = "ResultURL")]
result_url: Url,
pub result_url: Url,
/// The path that stores information about the time-out transaction.
#[serde(rename = "QueueTimeOutURL")]
queue_timeout_url: Url,
remarks: &'mpesa str,
occasion: Option<&'mpesa str>,
amount: f64,
pub queue_timeout_url: Url,
/// Comments that are sent along with the transaction.
pub remarks: &'mpesa str,
/// Comments that are sent along with the transaction.
pub occasion: Option<&'mpesa str>,
/// The amount transacted in the transaction is to be reversed, down to the
/// cent.
pub amount: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -107,6 +119,25 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversal<'mpesa, Env> {
TransactionReversalBuilder::default().client(client)
}

/// Creates a new `TransactionReversal` from a `TransactionReversalRequest`
pub fn from_request(
client: &'mpesa Mpesa<Env>,
request: TransactionReversalRequest<'mpesa>,
) -> TransactionReversal<'mpesa, Env> {
TransactionReversal {
client,
initiator: request.initiator,
transaction_id: request.transaction_id,
receiver_party: request.receiver_party,
receiver_identifier_type: request.receiver_identifier_type,
result_url: request.result_url,
timeout_url: request.queue_timeout_url,
remarks: request.remarks,
occasion: request.occasion,
amount: request.amount,
}
}

/// # Transaction Reversal API
///
/// Requests for transaction reversal
Expand Down
90 changes: 90 additions & 0 deletions src/validator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use crate::{MpesaError, MpesaResult};

pub trait PhoneValidator {
fn validate(&self) -> MpesaResult<()>;
}

impl PhoneValidator for &str {
fn validate(&self) -> MpesaResult<()> {
if self.starts_with("254")
&& self.len() == 12
&& self.chars().skip(3).all(|c| c.is_ascii_digit())
{
Ok(())
} else {
Err(MpesaError::Message(
"Invalid phone number, must be in the format 2547XXXXXXXX",
))
}
}
}

impl PhoneValidator for String {
fn validate(&self) -> MpesaResult<()> {
self.as_str().validate()
}
}

impl PhoneValidator for u64 {
fn validate(&self) -> MpesaResult<()> {
self.to_string().validate()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_validate_phone() {
assert!("254712345678".validate().is_ok());
assert!("254012345678".validate().is_ok());
assert!("254712345678900".validate().is_err());
assert!("25471234567".validate().is_err());
assert!("2547".validate().is_err());
assert!("2547a".validate().is_err());
assert!("254".validate().is_err());
assert!("254a".validate().is_err());
assert!("25".validate().is_err());
assert!("25a".validate().is_err());
assert!("2".validate().is_err());
assert!("2a".validate().is_err());
assert!("".validate().is_err());
assert!("a".validate().is_err());
}

#[test]
fn test_validate_phone_string() {
assert!("254712345678".to_string().validate().is_ok());
assert!("254012345678".to_string().validate().is_ok());
assert!("254712345678900".to_string().validate().is_err());
assert!("25471234567".to_string().validate().is_err());
assert!("2547".to_string().validate().is_err());
assert!("2547a".to_string().validate().is_err());
assert!("254".to_string().validate().is_err());
assert!("254a".to_string().validate().is_err());
assert!("25".to_string().validate().is_err());
assert!("25a".to_string().validate().is_err());
assert!("2".to_string().validate().is_err());
assert!("2a".to_string().validate().is_err());
assert!("".to_string().validate().is_err());
assert!("a".to_string().validate().is_err());
}

#[test]
fn test_validate_phone_u64() {
assert!(254712345678u64.validate().is_ok());
assert!(254012345678u64.validate().is_ok());
assert!(254712345678900u64.validate().is_err());
assert!(25471234567u64.validate().is_err());
assert!(2547u64.validate().is_err());
assert!(2547u64.validate().is_err());
assert!(254u64.validate().is_err());
assert!(254u64.validate().is_err());
assert!(25u64.validate().is_err());
assert!(25u64.validate().is_err());
assert!(2u64.validate().is_err());
assert!(2u64.validate().is_err());
assert!(0u64.validate().is_err());
}
}
4 changes: 2 additions & 2 deletions tests/mpesa-rust/stk_push_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ async fn stk_push_only_accepts_specific_tx_type() {
.express_request()
.business_short_code("174379")
.transaction_type(mpesa::CommandId::SalaryPayment)
.party_a("254708374149")
.party_a("254704837414")
.party_b("174379")
.account_ref("test")
.phone_number("254708374149")
.phone_number("254708437414")
.amount(500)
.pass_key("test")
.try_callback_url("https://test.example.com/api")
Expand Down
44 changes: 44 additions & 0 deletions tests/mpesa-rust/transaction_reversal_test.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use mpesa::services::{TransactionReversal, TransactionReversalRequest};
use mpesa::IdentifierTypes;
use serde_json::json;
use wiremock::matchers::{method, path};
Expand Down Expand Up @@ -45,3 +46,46 @@ async fn transaction_reversal_success() {
"Accept the service request successfully."
);
}

#[tokio::test]
async fn transaction_reversal_test_using_struct_initialization() {
let (client, server) = get_mpesa_client!();
let sample_response_body = json!({
"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"))
.respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body))
.expect(1)
.mount(&server)
.await;

let payload = TransactionReversalRequest {
initiator: "testapi496",
security_credential: "testapi496".to_string(),
command_id: mpesa::CommandId::TransactionReversal,
transaction_id: "OEI2AK4Q16",
receiver_party: "600111",
receiver_identifier_type: IdentifierTypes::ShortCode,
result_url: "https://testdomain.com/ok".parse().unwrap(),
queue_timeout_url: "https://testdomain.com/err".parse().unwrap(),
remarks: "wrong recipient",
occasion: None,
amount: 1.0,
};

let response = TransactionReversal::from_request(&client, payload)
.send()
.await
.unwrap();

assert_eq!(response.originator_conversation_id, "29464-48063588-1");
assert_eq!(response.conversation_id, "AG_20230206_201056794190723278ff");
assert_eq!(
response.response_description,
"Accept the service request successfully."
);
}

0 comments on commit 9531c4f

Please sign in to comment.