Skip to content

Commit

Permalink
chore: migrate api, reorganise imports and remove redundant tests
Browse files Browse the repository at this point in the history
  • Loading branch information
itsyaasir committed Nov 18, 2023
1 parent 7c53407 commit c85bea8
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 457 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
/.idea
Cargo.lock
.env
.DS_Store
.DS_Store

.vscode
49 changes: 25 additions & 24 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
20 changes: 10 additions & 10 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::cell::RefCell;
use std::time::Duration;

use cached::Cached;
use openssl::base64;
Expand All @@ -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};

Expand Down Expand Up @@ -45,15 +47,15 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa<Env> {
/// ```
///
/// # 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<S: Into<String>>(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()),
Expand All @@ -69,6 +71,7 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa<Env> {
let Some(p) = &*self.initiator_password.borrow() else {
return DEFAULT_INITIATOR_PASSWORD.to_owned();
};

p.expose_secret().into()
}

Expand Down Expand Up @@ -473,11 +476,8 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa<Env> {
///
/// 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**
Expand Down
2 changes: 1 addition & 1 deletion src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
41 changes: 33 additions & 8 deletions src/services/express_request.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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";

Expand All @@ -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<Utc>,
pub timestamp: DateTime<Local>,
/// This is the transaction type that is used to identify the transaction
/// when sending the request to M-PESA
pub transaction_type: CommandId,
Expand Down Expand Up @@ -57,7 +59,7 @@ pub struct MpesaExpressRequest<'mpesa> {
pub transaction_desc: Option<&'mpesa str>,
}

fn serialize_utc_to_string<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
fn serialize_utc_to_string<S>(date: &DateTime<Local>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
Expand All @@ -66,13 +68,16 @@ 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 {
///This is a global unique identifier of the processed checkout transaction
/// 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")]
Expand All @@ -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<Env>,
Expand All @@ -114,7 +119,7 @@ pub struct MpesaExpress<'mpesa, Env: ApiEnvironment> {

impl<'mpesa, Env: ApiEnvironment> From<MpesaExpress<'mpesa, Env>> 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!(
Expand All @@ -140,15 +145,35 @@ impl<'mpesa, Env: ApiEnvironment> From<MpesaExpress<'mpesa, Env>> for MpesaExpre
}
}

impl<Env: ApiEnvironment> 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<Env>) -> MpesaExpressBuilder<'mpesa, Env> {
MpesaExpressBuilder::default().client(client)
}

/// Creates a new `MpesaExpress` from a `MpesaExpressRequest`
pub fn from_request(
client: &'mpesa Mpesa<Env>,
request: MpesaExpressRequest<'mpesa>,
pass_key: Option<&'mpesa str>,
) -> MpesaExpress<'mpesa, Env> {
MpesaExpress {
client,
Expand All @@ -161,15 +186,15 @@ 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),
}
}

/// # Lipa na M-Pesa Online Payment / Mpesa Express/ Stk push
///
/// 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
Expand Down
7 changes: 6 additions & 1 deletion src/services/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Loading

0 comments on commit c85bea8

Please sign in to comment.