From e1009d18496e2c8c9a75d25d01f657bc4847f4b2 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 14:39:55 +0300 Subject: [PATCH 001/140] Unify mpesa_* crates --- mpesa_core/.env.example => .env.example | 0 Cargo.toml | 39 +++++++++++++++- mpesa_core/Cargo.toml | 41 ----------------- mpesa_core/src/mpesa_security.rs | 13 ------ mpesa_derive/Cargo.toml | 17 ------- mpesa_derive/src/lib.rs | 44 ------------------- {mpesa_core/src => src}/client.rs | 12 +++-- {mpesa_core/src => src}/constants.rs | 0 {mpesa_core/src => src}/environment.rs | 0 {mpesa_core/src => src}/errors.rs | 0 {mpesa_core/src => src}/lib.rs | 0 src/mpesa_security.rs | 37 ++++++++++++++++ .../src => src}/services/account_balance.rs | 0 {mpesa_core/src => src}/services/b2b.rs | 0 {mpesa_core/src => src}/services/b2c.rs | 0 .../src => src}/services/c2b_register.rs | 0 .../src => src}/services/c2b_simulate.rs | 0 .../src => src}/services/express_request.rs | 0 {mpesa_core/src => src}/services/mod.rs | 0 .../tests => tests}/account_balance_test.rs | 0 {mpesa_core/tests => tests}/b2b_test.rs | 0 {mpesa_core/tests => tests}/b2c_test.rs | 0 .../tests => tests}/c2b_register_test.rs | 0 .../tests => tests}/c2b_simulate_test.rs | 0 {mpesa_core/tests => tests}/stk_push_test.rs | 0 25 files changed, 82 insertions(+), 121 deletions(-) rename mpesa_core/.env.example => .env.example (100%) delete mode 100644 mpesa_core/Cargo.toml delete mode 100644 mpesa_core/src/mpesa_security.rs delete mode 100644 mpesa_derive/Cargo.toml delete mode 100644 mpesa_derive/src/lib.rs rename {mpesa_core/src => src}/client.rs (98%) rename {mpesa_core/src => src}/constants.rs (100%) rename {mpesa_core/src => src}/environment.rs (100%) rename {mpesa_core/src => src}/errors.rs (100%) rename {mpesa_core/src => src}/lib.rs (100%) create mode 100644 src/mpesa_security.rs rename {mpesa_core/src => src}/services/account_balance.rs (100%) rename {mpesa_core/src => src}/services/b2b.rs (100%) rename {mpesa_core/src => src}/services/b2c.rs (100%) rename {mpesa_core/src => src}/services/c2b_register.rs (100%) rename {mpesa_core/src => src}/services/c2b_simulate.rs (100%) rename {mpesa_core/src => src}/services/express_request.rs (100%) rename {mpesa_core/src => src}/services/mod.rs (100%) rename {mpesa_core/tests => tests}/account_balance_test.rs (100%) rename {mpesa_core/tests => tests}/b2b_test.rs (100%) rename {mpesa_core/tests => tests}/b2c_test.rs (100%) rename {mpesa_core/tests => tests}/c2b_register_test.rs (100%) rename {mpesa_core/tests => tests}/c2b_simulate_test.rs (100%) rename {mpesa_core/tests => tests}/stk_push_test.rs (100%) diff --git a/mpesa_core/.env.example b/.env.example similarity index 100% rename from mpesa_core/.env.example rename to .env.example diff --git a/Cargo.toml b/Cargo.toml index 16797cd5b..52a79b060 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,37 @@ -[workspace] -members = ["mpesa_derive", "mpesa_core"] \ No newline at end of file +[package] +name = "mpesa" +version = "0.4.2" +authors = ["Collins Muriuki "] +edition = "2018" +description = "A wrapper around the M-PESA API in Rust." +keywords = ["api", "mpesa", "mobile"] +repository = "https://github.com/collinsmuriuki/mpesa-rust" +readme = "../README.md" +license = "MIT" + +[dependencies] +serde_json = "1.0" +serde_repr = "0.1" +dotenv = "0.15.0" +openssl = "0.10.30" +base64 = "0.13.0" +failure = "0.1.5" +failure_derive = "0.1.5" +chrono = "0.4" + +[dependencies.reqwest] +version = "0.11.3" +features = ["blocking", "json"] + +[dependencies.serde] +version = "1.0.114" +features = ["derive"] + +[features] +default = ["b2b", "b2c" ,"account_balance", "c2b_register", "c2b_simulate", "express_request"] +b2b = [] +b2c = [] +account_balance = [] +c2b_register = [] +c2b_simulate = [] +express_request = [] \ No newline at end of file diff --git a/mpesa_core/Cargo.toml b/mpesa_core/Cargo.toml deleted file mode 100644 index 85c59a4c9..000000000 --- a/mpesa_core/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "mpesa" -version = "0.4.2" -authors = ["Collins Muriuki "] -edition = "2018" -description = "A wrapper around the M-PESA API in Rust." -keywords = ["api", "mpesa", "mobile"] -repository = "https://github.com/collinsmuriuki/mpesa-rust" -readme = "../README.md" -license = "MIT" - -[dependencies] -serde_json = "1.0" -serde_repr = "0.1" -dotenv = "0.15.0" -openssl = "0.10.30" -base64 = "0.13.0" -failure = "0.1.5" -failure_derive = "0.1.5" -chrono = "0.4" - -[dependencies.mpesa_derive] -version = "0.3.0" -path = "../mpesa_derive" - -[dependencies.reqwest] -version = "0.11.3" -features = ["blocking", "json"] - -[dependencies.serde] -version = "1.0.114" -features = ["derive"] - -[features] -default = ["b2b", "b2c" ,"account_balance", "c2b_register", "c2b_simulate", "express_request"] -b2b = [] -b2c = [] -account_balance = [] -c2b_register = [] -c2b_simulate = [] -express_request = [] \ No newline at end of file diff --git a/mpesa_core/src/mpesa_security.rs b/mpesa_core/src/mpesa_security.rs deleted file mode 100644 index 1233e75ad..000000000 --- a/mpesa_core/src/mpesa_security.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::client::MpesaResult; - -/// Trait responsible for implementation of security configs for Mpesa -pub trait MpesaSecurity { - /// Generates 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. - /// Returns base64 encoded string. - /// - /// # Error - /// Returns `EncryptionError` variant of `MpesaError` - fn gen_security_credentials(&self) -> MpesaResult; -} diff --git a/mpesa_derive/Cargo.toml b/mpesa_derive/Cargo.toml deleted file mode 100644 index b108612f1..000000000 --- a/mpesa_derive/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "mpesa_derive" -version = "0.3.0" -authors = ["Collins Muriuki "] -edition = "2018" -description = "`MpesaSecurity` trait derive macro crate." -repository = "https://github.com/collinsmuriuki/mpesa-rust" -license = "MIT" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -proc-macro = true - -[dependencies] -syn = "1.0" -quote = "1.0" \ No newline at end of file diff --git a/mpesa_derive/src/lib.rs b/mpesa_derive/src/lib.rs deleted file mode 100644 index 0a401240a..000000000 --- a/mpesa_derive/src/lib.rs +++ /dev/null @@ -1,44 +0,0 @@ -extern crate proc_macro; - -use proc_macro::TokenStream; -use quote::quote; - -#[proc_macro_derive(MpesaSecurity)] -pub fn mpesa_security_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse(input).expect("Error parsing input"); - - impl_mpesa_security(&ast) -} - -fn impl_mpesa_security(ast: &syn::DeriveInput) -> TokenStream { - let name = &ast.ident; - - let gen = quote! { - use openssl::x509::X509; - use openssl::rsa::Padding; - use base64::encode; - use std::error::Error; - use crate::MpesaError; - - impl MpesaSecurity for #name { - fn gen_security_credentials(&self) -> Result { - let pem = self.environment().get_certificate().as_bytes(); - let cert = X509::from_pem(pem)?; - // getting the public and rsa keys - let pub_key = cert.public_key()?; - let rsa_key = pub_key.rsa()?; - // configuring the buffer - let buf_len = pub_key.size(); - let mut buffer = vec![0; buf_len]; - - rsa_key.public_encrypt( - self.initiator_password().as_bytes(), - &mut buffer, - Padding::PKCS1, - )?; - Ok(encode(buffer)) - } - } - }; - gen.into() -} diff --git a/mpesa_core/src/client.rs b/src/client.rs similarity index 98% rename from mpesa_core/src/client.rs rename to src/client.rs index 94bcb3d7b..755a67b1e 100644 --- a/mpesa_core/src/client.rs +++ b/src/client.rs @@ -1,10 +1,14 @@ +use crate::MpesaError; + use super::environment::Environment; use super::services::{ - AccountBalanceBuilder, B2bBuilder, B2cBuilder, C2bRegisterBuilder, C2bSimulateBuilder, + AccountBalanceBuilder, + B2bBuilder, + B2cBuilder, + C2bRegisterBuilder, + C2bSimulateBuilder, MpesaExpressRequestBuilder, }; -use crate::MpesaSecurity; -use mpesa_derive::*; use reqwest::blocking::Client; use serde_json::Value; use std::cell::RefCell; @@ -13,7 +17,7 @@ use std::cell::RefCell; pub type MpesaResult = Result; /// Mpesa client that will facilitate communication with the Safaricom API -#[derive(Debug, MpesaSecurity)] +#[derive(Debug)] pub struct Mpesa { client_key: String, client_secret: String, diff --git a/mpesa_core/src/constants.rs b/src/constants.rs similarity index 100% rename from mpesa_core/src/constants.rs rename to src/constants.rs diff --git a/mpesa_core/src/environment.rs b/src/environment.rs similarity index 100% rename from mpesa_core/src/environment.rs rename to src/environment.rs diff --git a/mpesa_core/src/errors.rs b/src/errors.rs similarity index 100% rename from mpesa_core/src/errors.rs rename to src/errors.rs diff --git a/mpesa_core/src/lib.rs b/src/lib.rs similarity index 100% rename from mpesa_core/src/lib.rs rename to src/lib.rs diff --git a/src/mpesa_security.rs b/src/mpesa_security.rs new file mode 100644 index 000000000..63c509279 --- /dev/null +++ b/src/mpesa_security.rs @@ -0,0 +1,37 @@ +use crate::client::MpesaResult; +use openssl::x509::X509; +use openssl::rsa::Padding; +use base64::encode; +use crate::{MpesaError, Mpesa}; + +/// Trait responsible for implementation of security configs for Mpesa +pub trait MpesaSecurity { + /// Generates 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. + /// Returns base64 encoded string. + /// + /// # Error + /// Returns `EncryptionError` variant of `MpesaError` + fn gen_security_credentials(&self) -> MpesaResult; +} + +impl MpesaSecurity for Mpesa { + fn gen_security_credentials(&self) -> Result { + let pem = self.environment().get_certificate().as_bytes(); + let cert = X509::from_pem(pem)?; + // getting the public and rsa keys + let pub_key = cert.public_key()?; + let rsa_key = pub_key.rsa()?; + // configuring the buffer + let buf_len = pub_key.size(); + let mut buffer = vec![0; buf_len]; + + rsa_key.public_encrypt( + self.initiator_password().as_bytes(), + &mut buffer, + Padding::PKCS1, + )?; + Ok(encode(buffer)) + } +} \ No newline at end of file diff --git a/mpesa_core/src/services/account_balance.rs b/src/services/account_balance.rs similarity index 100% rename from mpesa_core/src/services/account_balance.rs rename to src/services/account_balance.rs diff --git a/mpesa_core/src/services/b2b.rs b/src/services/b2b.rs similarity index 100% rename from mpesa_core/src/services/b2b.rs rename to src/services/b2b.rs diff --git a/mpesa_core/src/services/b2c.rs b/src/services/b2c.rs similarity index 100% rename from mpesa_core/src/services/b2c.rs rename to src/services/b2c.rs diff --git a/mpesa_core/src/services/c2b_register.rs b/src/services/c2b_register.rs similarity index 100% rename from mpesa_core/src/services/c2b_register.rs rename to src/services/c2b_register.rs diff --git a/mpesa_core/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs similarity index 100% rename from mpesa_core/src/services/c2b_simulate.rs rename to src/services/c2b_simulate.rs diff --git a/mpesa_core/src/services/express_request.rs b/src/services/express_request.rs similarity index 100% rename from mpesa_core/src/services/express_request.rs rename to src/services/express_request.rs diff --git a/mpesa_core/src/services/mod.rs b/src/services/mod.rs similarity index 100% rename from mpesa_core/src/services/mod.rs rename to src/services/mod.rs diff --git a/mpesa_core/tests/account_balance_test.rs b/tests/account_balance_test.rs similarity index 100% rename from mpesa_core/tests/account_balance_test.rs rename to tests/account_balance_test.rs diff --git a/mpesa_core/tests/b2b_test.rs b/tests/b2b_test.rs similarity index 100% rename from mpesa_core/tests/b2b_test.rs rename to tests/b2b_test.rs diff --git a/mpesa_core/tests/b2c_test.rs b/tests/b2c_test.rs similarity index 100% rename from mpesa_core/tests/b2c_test.rs rename to tests/b2c_test.rs diff --git a/mpesa_core/tests/c2b_register_test.rs b/tests/c2b_register_test.rs similarity index 100% rename from mpesa_core/tests/c2b_register_test.rs rename to tests/c2b_register_test.rs diff --git a/mpesa_core/tests/c2b_simulate_test.rs b/tests/c2b_simulate_test.rs similarity index 100% rename from mpesa_core/tests/c2b_simulate_test.rs rename to tests/c2b_simulate_test.rs diff --git a/mpesa_core/tests/stk_push_test.rs b/tests/stk_push_test.rs similarity index 100% rename from mpesa_core/tests/stk_push_test.rs rename to tests/stk_push_test.rs From f55fc12b77ef44954a77337dad6f9fa6031f1b8c Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 14:47:07 +0300 Subject: [PATCH 002/140] Update mpesa security trait docs --- src/client.rs | 6 +----- src/mpesa_security.rs | 10 ++++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/client.rs b/src/client.rs index 755a67b1e..df6652cef 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,11 +2,7 @@ use crate::MpesaError; use super::environment::Environment; use super::services::{ - AccountBalanceBuilder, - B2bBuilder, - B2cBuilder, - C2bRegisterBuilder, - C2bSimulateBuilder, + AccountBalanceBuilder, B2bBuilder, B2cBuilder, C2bRegisterBuilder, C2bSimulateBuilder, MpesaExpressRequestBuilder, }; use reqwest::blocking::Client; diff --git a/src/mpesa_security.rs b/src/mpesa_security.rs index 63c509279..8bad85e51 100644 --- a/src/mpesa_security.rs +++ b/src/mpesa_security.rs @@ -1,10 +1,8 @@ use crate::client::MpesaResult; -use openssl::x509::X509; +use crate::{Mpesa, MpesaError}; use openssl::rsa::Padding; -use base64::encode; -use crate::{MpesaError, Mpesa}; +use openssl::x509::X509; -/// Trait responsible for implementation of security configs for Mpesa pub trait MpesaSecurity { /// Generates security credentials /// M-Pesa Core authenticates a transaction by decrypting the security credentials. @@ -32,6 +30,6 @@ impl MpesaSecurity for Mpesa { &mut buffer, Padding::PKCS1, )?; - Ok(encode(buffer)) + Ok(base64::encode(buffer)) } -} \ No newline at end of file +} From bea056979960d69edc921fc4e6b2d074717837f4 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 15:07:22 +0300 Subject: [PATCH 003/140] Make openssl optional dependency --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 52a79b060..e53e6ac19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ license = "MIT" serde_json = "1.0" serde_repr = "0.1" dotenv = "0.15.0" -openssl = "0.10.30" +openssl = {version = "0.10.30", optional = true} base64 = "0.13.0" failure = "0.1.5" failure_derive = "0.1.5" @@ -29,8 +29,8 @@ features = ["derive"] [features] default = ["b2b", "b2c" ,"account_balance", "c2b_register", "c2b_simulate", "express_request"] -b2b = [] -b2c = [] +b2b = ["dep:openssl"] +b2c = ["dep:openssl"] account_balance = [] c2b_register = [] c2b_simulate = [] From a285f7042ebed309a723d4a8d962cc7c3d1d4fc7 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 15:47:15 +0300 Subject: [PATCH 004/140] Prefer unwrap_or_else to unwrap_or --- src/services/account_balance.rs | 12 ++++++------ src/services/b2b.rs | 20 ++++++++++---------- src/services/b2c.rs | 18 ++++++++++-------- src/services/c2b_register.rs | 8 ++++---- src/services/c2b_simulate.rs | 8 ++++---- src/services/express_request.rs | 24 +++++++++++++++--------- 6 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index 2beb17a5a..799f18ee1 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -150,16 +150,16 @@ impl<'a> AccountBalanceBuilder<'a> { let credentials = self.client.gen_security_credentials()?; let payload = AccountBalancePayload { - command_id: self.command_id.unwrap_or(CommandId::AccountBalance), - party_a: self.party_a.unwrap_or("None"), + command_id: self.command_id.unwrap_or_else(|| CommandId::AccountBalance), + party_a: self.party_a.unwrap_or_else(|| "None"), identifier_type: &self .identifier_type - .unwrap_or(IdentifierTypes::ShortCode) + .unwrap_or_else(|| IdentifierTypes::ShortCode) .to_string(), - remarks: self.remarks.unwrap_or("None"), + remarks: self.remarks.unwrap_or_else(|| "None"), initiator: self.initiator_name, - queue_time_out_url: self.queue_timeout_url.unwrap_or("None"), - result_url: self.result_url.unwrap_or("None"), + queue_time_out_url: self.queue_timeout_url.unwrap_or_else(|| "None"), + result_url: self.result_url.unwrap_or_else(|| "None"), security_credential: &credentials, }; diff --git a/src/services/b2b.rs b/src/services/b2b.rs index a78cac808..25d8a1910 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -212,22 +212,22 @@ impl<'a> B2bBuilder<'a> { security_credential: &credentials, command_id: self .command_id - .unwrap_or(CommandId::BusinessToBusinessTransfer), - amount: self.amount.unwrap_or(10), - party_a: self.party_a.unwrap_or(""), + .unwrap_or_else(|| CommandId::BusinessToBusinessTransfer), + amount: self.amount.unwrap_or_else(|| 10), + party_a: self.party_a.unwrap_or_else(|| ""), sender_identifier_type: &self .sender_id - .unwrap_or(IdentifierTypes::ShortCode) + .unwrap_or_else(|| IdentifierTypes::ShortCode) .to_string(), - party_b: self.party_b.unwrap_or(""), + party_b: self.party_b.unwrap_or_else(|| ""), reciever_identifier_type: &self .receiver_id - .unwrap_or(IdentifierTypes::ShortCode) + .unwrap_or_else(|| IdentifierTypes::ShortCode) .to_string(), - remarks: self.remarks.unwrap_or("None"), - queue_time_out_url: self.queue_timeout_url.unwrap_or(""), - result_url: self.result_url.unwrap_or(""), - account_reference: self.account_ref.unwrap_or(""), + remarks: self.remarks.unwrap_or_else(|| "None"), + queue_time_out_url: self.queue_timeout_url.unwrap_or_else(|| ""), + result_url: self.result_url.unwrap_or_else(|| ""), + account_reference: self.account_ref.unwrap_or_else(|| ""), }; let response = self diff --git a/src/services/b2c.rs b/src/services/b2c.rs index 6eabe08b7..ef696608f 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -186,14 +186,16 @@ impl<'a> B2cBuilder<'a> { let payload = B2cPayload { initiator_name: self.initiator_name, security_credential: &credentials, - command_id: self.command_id.unwrap_or(CommandId::BusinessPayment), - amount: self.amount.unwrap_or(10), - party_a: self.party_a.unwrap_or("None"), - party_b: self.party_b.unwrap_or("None"), - remarks: self.remarks.unwrap_or("None"), - queue_time_out_url: self.queue_timeout_url.unwrap_or("None"), - result_url: self.result_url.unwrap_or("None"), - occasion: self.occasion.unwrap_or("None"), + command_id: self + .command_id + .unwrap_or_else(|| CommandId::BusinessPayment), + amount: self.amount.unwrap_or_else(|| 10), + party_a: self.party_a.unwrap_or_else(|| "None"), + party_b: self.party_b.unwrap_or_else(|| "None"), + remarks: self.remarks.unwrap_or_else(|| "None"), + queue_time_out_url: self.queue_timeout_url.unwrap_or_else(|| "None"), + result_url: self.result_url.unwrap_or_else(|| "None"), + occasion: self.occasion.unwrap_or_else(|| "None"), }; let response = self diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index 38b37806e..e14d2e25b 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -105,10 +105,10 @@ impl<'a> C2bRegisterBuilder<'a> { ); let payload = C2bRegisterPayload { - validation_url: self.validation_url.unwrap_or("None"), - confirmation_url: self.confirmation_url.unwrap_or("None"), - response_type: self.response_type.unwrap_or(ResponseType::Complete), - short_code: self.short_code.unwrap_or("None"), + validation_url: self.validation_url.unwrap_or_else(|| "None"), + confirmation_url: self.confirmation_url.unwrap_or_else(|| "None"), + response_type: self.response_type.unwrap_or_else(|| ResponseType::Complete), + short_code: self.short_code.unwrap_or_else(|| "None"), }; let response = self diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index e5a04bbcf..6ea00c7ad 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -115,10 +115,10 @@ impl<'a> C2bSimulateBuilder<'a> { let payload = C2bSimulatePayload { command_id: self.command_id.unwrap_or(CommandId::CustomerPayBillOnline), - amount: self.amount.unwrap_or(10), - msisdn: self.msisdn.unwrap_or("None"), - bill_ref_number: self.bill_ref_number.unwrap_or("None"), - short_code: self.short_code.unwrap_or("None"), + amount: self.amount.unwrap_or_else(|| 10), + msisdn: self.msisdn.unwrap_or_else(|| "None"), + bill_ref_number: self.bill_ref_number.unwrap_or_else(|| "None"), + short_code: self.short_code.unwrap_or_else(|| "None"), }; let response = self diff --git a/src/services/express_request.rs b/src/services/express_request.rs index aee455cc7..ee19001eb 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -5,6 +5,10 @@ use chrono::prelude::Local; use serde::{Deserialize, Serialize}; use serde_json::Value; +/// [test credentials](https://developer.safaricom.co.ke/test_credentials) +static DEFAULT_PASSKEY: &'static str = + "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919"; + #[derive(Debug, Serialize)] struct MpesaExpressRequestPayload<'a> { #[serde(rename(serialize = "BusinessShortCode"))] @@ -86,7 +90,7 @@ impl<'a> MpesaExpressRequestBuilder<'a> { if let Some(key) = self.pass_key { return key; } - "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919" + DEFAULT_PASSKEY } /// Utility method to generate base64 encoded password as per Safaricom's [specifications](https://developer.safaricom.co.ke/docs#lipa-na-m-pesa-online-payment) @@ -202,16 +206,18 @@ impl<'a> MpesaExpressRequestBuilder<'a> { business_short_code: self.business_short_code, password: &password, timestamp: ×tamp, - amount: self.amount.unwrap_or(10), - party_a: self.party_a.unwrap_or(self.phone_number.unwrap_or("None")), - party_b: self.party_b.unwrap_or(self.business_short_code), - phone_number: self.phone_number.unwrap_or("None"), - call_back_url: self.callback_url.unwrap_or("None"), - account_reference: self.account_ref.unwrap_or("None"), + amount: self.amount.unwrap_or_else(|| 10), + party_a: self + .party_a + .unwrap_or_else(|| self.phone_number.unwrap_or_else(|| "None")), + party_b: self.party_b.unwrap_or_else(|| self.business_short_code), + phone_number: self.phone_number.unwrap_or_else(|| "None"), + call_back_url: self.callback_url.unwrap_or_else(|| "None"), + account_reference: self.account_ref.unwrap_or_else(|| "None"), transaction_type: self .transaction_type - .unwrap_or(CommandId::CustomerPayBillOnline), - transaction_desc: self.transaction_desc.unwrap_or("None"), + .unwrap_or_else(|| CommandId::CustomerPayBillOnline), + transaction_desc: self.transaction_desc.unwrap_or_else(|| "None"), }; let response = self From 7a58a72b4d7b9a9aee5b0656080d49369ae0bbe6 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 17:08:34 +0300 Subject: [PATCH 005/140] Ignore c2b register calls due to them being depreciated --- src/lib.rs | 4 ++-- tests/c2b_register_test.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ef1a4763a..3d2dce519 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,8 +142,8 @@ //! assert!(response.is_ok()) //! ``` //! -//! * C2B Register -//! ```rust +//! * C2B Register (depreciated) +//! ```ignore //! use mpesa::{Mpesa, MpesaResult, C2bRegisterResponse}; //! use serde_json::Value; //! use std::env; diff --git a/tests/c2b_register_test.rs b/tests/c2b_register_test.rs index 62dac1acc..c59c25c45 100644 --- a/tests/c2b_register_test.rs +++ b/tests/c2b_register_test.rs @@ -3,6 +3,7 @@ use mpesa::Mpesa; use std::env; #[test] +#[ignore = "depreciated"] fn c2b_register_test() { dotenv::dotenv().ok(); From 816eb1dad6a01ed7caee22aecf2a95fd4ee81506 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 17:27:42 +0300 Subject: [PATCH 006/140] Update client and service definitions; depreciate v1 c2b register --- src/client.rs | 5 ++++- src/services/account_balance.rs | 21 +++++++++---------- src/services/b2b.rs | 36 ++++++++++++++++----------------- src/services/b2c.rs | 32 ++++++++++++++--------------- src/services/c2b_simulate.rs | 20 +++++++++--------- src/services/express_request.rs | 31 +++++++++++++--------------- 6 files changed, 72 insertions(+), 73 deletions(-) diff --git a/src/client.rs b/src/client.rs index df6652cef..4eec9de7a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -9,6 +9,8 @@ use reqwest::blocking::Client; use serde_json::Value; use std::cell::RefCell; +static DEFAULT_INITIATOR_PASSWORD: &'static str = "Safcom496!"; + /// `Result` enum type alias pub type MpesaResult = Result; @@ -59,7 +61,7 @@ impl<'a> Mpesa { if let Some(p) = &*self.initiator_password.borrow() { return p.to_owned(); } - "Safcom496!".to_owned() + DEFAULT_INITIATOR_PASSWORD.to_owned() } /// Optional in development but required for production, you will need to call this method and set your production initiator password. @@ -190,6 +192,7 @@ impl<'a> Mpesa { /// .send(); /// ``` #[cfg(feature = "c2b_register")] + #[deprecated] pub fn c2b_register(&'a self) -> C2bRegisterBuilder<'a> { C2bRegisterBuilder::new(self) } diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index 799f18ee1..99eaa4686 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -13,16 +13,16 @@ struct AccountBalancePayload<'a> { security_credential: &'a str, #[serde(rename(serialize = "CommandID"))] command_id: CommandId, - #[serde(rename(serialize = "PartyA"))] - party_a: &'a str, + #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] + party_a: Option<&'a str>, #[serde(rename(serialize = "IdentifierType"))] identifier_type: &'a str, #[serde(rename(serialize = "Remarks"))] remarks: &'a str, - #[serde(rename(serialize = "QueueTimeOutURL"))] - queue_time_out_url: &'a str, - #[serde(rename(serialize = "ResultURL"))] - result_url: &'a str, + #[serde(rename(serialize = "QueueTimeOutURL"), skip_serializing_if = "Option::is_none")] + queue_time_out_url: Option<&'a str>, + #[serde(rename(serialize = "ResultURL"), skip_serializing_if = "Option::is_none")] + result_url: Option<&'a str>, } #[derive(Debug, Deserialize, Clone)] @@ -151,15 +151,15 @@ impl<'a> AccountBalanceBuilder<'a> { let payload = AccountBalancePayload { command_id: self.command_id.unwrap_or_else(|| CommandId::AccountBalance), - party_a: self.party_a.unwrap_or_else(|| "None"), + party_a: self.party_a, identifier_type: &self .identifier_type .unwrap_or_else(|| IdentifierTypes::ShortCode) .to_string(), remarks: self.remarks.unwrap_or_else(|| "None"), initiator: self.initiator_name, - queue_time_out_url: self.queue_timeout_url.unwrap_or_else(|| "None"), - result_url: self.result_url.unwrap_or_else(|| "None"), + queue_time_out_url: self.queue_timeout_url, + result_url: self.result_url, security_credential: &credentials, }; @@ -169,8 +169,7 @@ impl<'a> AccountBalanceBuilder<'a> { .post(&url) .bearer_auth(self.client.auth()?) .json(&payload) - .send()? - .error_for_status()?; + .send()?; if response.status().is_success() { let value: AccountBalanceResponse = response.json()?; diff --git a/src/services/b2b.rs b/src/services/b2b.rs index 25d8a1910..a16e47842 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -15,22 +15,22 @@ struct B2bPayload<'a> { command_id: CommandId, #[serde(rename(serialize = "Amount"))] amount: u32, - #[serde(rename(serialize = "PartyA"))] - party_a: &'a str, + #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] + party_a: Option<&'a str>, #[serde(rename(serialize = "SenderIdentifierType"))] sender_identifier_type: &'a str, - #[serde(rename(serialize = "PartyB"))] - party_b: &'a str, + #[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")] + party_b: Option<&'a str>, #[serde(rename(serialize = "RecieverIdentifierType"))] reciever_identifier_type: &'a str, #[serde(rename(serialize = "Remarks"))] remarks: &'a str, - #[serde(rename(serialize = "QueueTimeOutURL"))] - queue_time_out_url: &'a str, - #[serde(rename(serialize = "ResultURL"))] - result_url: &'a str, - #[serde(rename(serialize = "AccountReference"))] - account_reference: &'a str, + #[serde(rename(serialize = "QueueTimeOutURL"), skip_serializing_if = "Option::is_none")] + queue_time_out_url: Option<&'a str>, + #[serde(rename(serialize = "ResultURL"), skip_serializing_if = "Option::is_none")] + result_url: Option<&'a str>, + #[serde(rename(serialize = "AccountReference"), skip_serializing_if = "Option::is_none")] + account_reference: Option<&'a str>, } #[derive(Debug, Deserialize, Clone)] @@ -213,21 +213,22 @@ impl<'a> B2bBuilder<'a> { command_id: self .command_id .unwrap_or_else(|| CommandId::BusinessToBusinessTransfer), - amount: self.amount.unwrap_or_else(|| 10), - party_a: self.party_a.unwrap_or_else(|| ""), + // TODO: Can this be improved? + amount: self.amount.unwrap_or_default(), + party_a: self.party_a, sender_identifier_type: &self .sender_id .unwrap_or_else(|| IdentifierTypes::ShortCode) .to_string(), - party_b: self.party_b.unwrap_or_else(|| ""), + party_b: self.party_b, reciever_identifier_type: &self .receiver_id .unwrap_or_else(|| IdentifierTypes::ShortCode) .to_string(), remarks: self.remarks.unwrap_or_else(|| "None"), - queue_time_out_url: self.queue_timeout_url.unwrap_or_else(|| ""), - result_url: self.result_url.unwrap_or_else(|| ""), - account_reference: self.account_ref.unwrap_or_else(|| ""), + queue_time_out_url: self.queue_timeout_url, + result_url: self.result_url, + account_reference: self.account_ref, }; let response = self @@ -236,8 +237,7 @@ impl<'a> B2bBuilder<'a> { .post(&url) .bearer_auth(self.client.auth()?) .json(&payload) - .send()? - .error_for_status()?; + .send()?; if response.status().is_success() { let value: B2bResponse = response.json()?; diff --git a/src/services/b2c.rs b/src/services/b2c.rs index ef696608f..a8b7f047c 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -14,18 +14,18 @@ struct B2cPayload<'a> { command_id: CommandId, #[serde(rename(serialize = "Amount"))] amount: u32, - #[serde(rename(serialize = "PartyA"))] - party_a: &'a str, - #[serde(rename(serialize = "PartyB"))] - party_b: &'a str, + #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] + party_a: Option<&'a str>, + #[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")] + party_b: Option<&'a str>, #[serde(rename(serialize = "Remarks"))] remarks: &'a str, - #[serde(rename(serialize = "QueueTimeOutURL"))] - queue_time_out_url: &'a str, - #[serde(rename(serialize = "ResultURL"))] - result_url: &'a str, - #[serde(rename(serialize = "Occasion"))] - occasion: &'a str, + #[serde(rename(serialize = "QueueTimeOutURL"), skip_serializing_if = "Option::is_none")] + queue_time_out_url: Option<&'a str>, + #[serde(rename(serialize = "ResultURL"), skip_serializing_if = "Option::is_none")] + result_url: Option<&'a str>, + #[serde(rename(serialize = "Occasion"), skip_serializing_if = "Option::is_none")] + occasion: Option<&'a str>, } #[derive(Debug, Deserialize, Clone)] @@ -189,13 +189,13 @@ impl<'a> B2cBuilder<'a> { command_id: self .command_id .unwrap_or_else(|| CommandId::BusinessPayment), - amount: self.amount.unwrap_or_else(|| 10), - party_a: self.party_a.unwrap_or_else(|| "None"), - party_b: self.party_b.unwrap_or_else(|| "None"), + amount: self.amount.unwrap_or_default(), + party_a: self.party_a, + party_b: self.party_b, remarks: self.remarks.unwrap_or_else(|| "None"), - queue_time_out_url: self.queue_timeout_url.unwrap_or_else(|| "None"), - result_url: self.result_url.unwrap_or_else(|| "None"), - occasion: self.occasion.unwrap_or_else(|| "None"), + queue_time_out_url: self.queue_timeout_url, + result_url: self.result_url, + occasion: self.occasion, }; let response = self diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index 6ea00c7ad..1b57c4fe0 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -12,17 +12,17 @@ struct C2bSimulatePayload<'a> { command_id: CommandId, #[serde(rename(serialize = "Amount"))] amount: u32, - #[serde(rename(serialize = "Msisdn"))] - msisdn: &'a str, - #[serde(rename(serialize = "BillRefNumber"))] - bill_ref_number: &'a str, - #[serde(rename(serialize = "ShortCode"))] - short_code: &'a str, + #[serde(rename(serialize = "Msisdn"), skip_serializing_if = "Option::is_none")] + msisdn: Option<&'a str>, + #[serde(rename(serialize = "BillRefNumber"), skip_serializing_if = "Option::is_none")] + bill_ref_number: Option<&'a str>, + #[serde(rename(serialize = "ShortCode"), skip_serializing_if = "Option::is_none")] + short_code: Option<&'a str>, } #[derive(Debug, Clone, Deserialize)] pub struct C2bSimulateResponse { - #[serde(rename(deserialize = "ConversationID"), skip_serializing_if = "None")] + #[serde(rename(deserialize = "ConversationID"), skip_serializing_if = "Option::is_none")] pub conversation_id: Option, #[serde(rename(deserialize = "OriginatorCoversationID"))] pub originator_coversation_id: String, @@ -116,9 +116,9 @@ impl<'a> C2bSimulateBuilder<'a> { let payload = C2bSimulatePayload { command_id: self.command_id.unwrap_or(CommandId::CustomerPayBillOnline), amount: self.amount.unwrap_or_else(|| 10), - msisdn: self.msisdn.unwrap_or_else(|| "None"), - bill_ref_number: self.bill_ref_number.unwrap_or_else(|| "None"), - short_code: self.short_code.unwrap_or_else(|| "None"), + msisdn: self.msisdn, + bill_ref_number: self.bill_ref_number, + short_code: self.short_code, }; let response = self diff --git a/src/services/express_request.rs b/src/services/express_request.rs index ee19001eb..82c7d6e47 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -21,14 +21,14 @@ struct MpesaExpressRequestPayload<'a> { transaction_type: CommandId, #[serde(rename(serialize = "Amount"))] amount: u32, - #[serde(rename(serialize = "PartyA"))] - party_a: &'a str, - #[serde(rename(serialize = "PartyB"))] - party_b: &'a str, - #[serde(rename(serialize = "PhoneNumber"))] - phone_number: &'a str, - #[serde(rename(serialize = "CallBackURL"))] - call_back_url: &'a str, + #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] + party_a: Option<&'a str>, + #[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")] + party_b: Option<&'a str>, + #[serde(rename(serialize = "PhoneNumber"), skip_serializing_if = "Option::is_none")] + phone_number: Option<&'a str>, + #[serde(rename(serialize = "CallBackURL"), skip_serializing_if = "Option::is_none")] + call_back_url: Option<&'a str>, #[serde(rename(serialize = "AccountReference"))] account_reference: &'a str, #[serde(rename(serialize = "TransactionDesc"))] @@ -206,13 +206,11 @@ impl<'a> MpesaExpressRequestBuilder<'a> { business_short_code: self.business_short_code, password: &password, timestamp: ×tamp, - amount: self.amount.unwrap_or_else(|| 10), - party_a: self - .party_a - .unwrap_or_else(|| self.phone_number.unwrap_or_else(|| "None")), - party_b: self.party_b.unwrap_or_else(|| self.business_short_code), - phone_number: self.phone_number.unwrap_or_else(|| "None"), - call_back_url: self.callback_url.unwrap_or_else(|| "None"), + amount: self.amount.unwrap_or_default(), + party_a: if self.party_a.is_some() {self.party_a} else {self.phone_number}, + party_b: if self.party_b.is_some() {self.party_b} else {Some(self.business_short_code)}, + phone_number: self.phone_number, + call_back_url: self.callback_url, account_reference: self.account_ref.unwrap_or_else(|| "None"), transaction_type: self .transaction_type @@ -226,8 +224,7 @@ impl<'a> MpesaExpressRequestBuilder<'a> { .post(&url) .bearer_auth(self.client.auth()?) .json(&payload) - .send()? - .error_for_status()?; + .send()?; if response.status().is_success() { let value: MpesaExpressRequestResponse = response.json()?; From d805c77899ac9b1743c8325defdce2bd031f9858 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 17:29:46 +0300 Subject: [PATCH 007/140] Allow depreciated --- tests/c2b_register_test.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/c2b_register_test.rs b/tests/c2b_register_test.rs index c59c25c45..c4d094ace 100644 --- a/tests/c2b_register_test.rs +++ b/tests/c2b_register_test.rs @@ -4,6 +4,7 @@ use std::env; #[test] #[ignore = "depreciated"] +#[allow(deprecated)] fn c2b_register_test() { dotenv::dotenv().ok(); From 46465492b3359c49e89c5248929ba98678e0e2f3 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 17:37:38 +0300 Subject: [PATCH 008/140] Reformat code; fix clippy warnings --- src/client.rs | 2 +- src/services/account_balance.rs | 10 ++++++++-- src/services/b2b.rs | 15 ++++++++++++--- src/services/b2c.rs | 15 ++++++++++++--- src/services/c2b_simulate.rs | 15 ++++++++++++--- src/services/express_request.rs | 25 +++++++++++++++++++------ 6 files changed, 64 insertions(+), 18 deletions(-) diff --git a/src/client.rs b/src/client.rs index 4eec9de7a..b041da9b2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -9,7 +9,7 @@ use reqwest::blocking::Client; use serde_json::Value; use std::cell::RefCell; -static DEFAULT_INITIATOR_PASSWORD: &'static str = "Safcom496!"; +static DEFAULT_INITIATOR_PASSWORD: &str = "Safcom496!"; /// `Result` enum type alias pub type MpesaResult = Result; diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index 99eaa4686..511aef3fa 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -19,9 +19,15 @@ struct AccountBalancePayload<'a> { identifier_type: &'a str, #[serde(rename(serialize = "Remarks"))] remarks: &'a str, - #[serde(rename(serialize = "QueueTimeOutURL"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "QueueTimeOutURL"), + skip_serializing_if = "Option::is_none" + )] queue_time_out_url: Option<&'a str>, - #[serde(rename(serialize = "ResultURL"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "ResultURL"), + skip_serializing_if = "Option::is_none" + )] result_url: Option<&'a str>, } diff --git a/src/services/b2b.rs b/src/services/b2b.rs index a16e47842..c09cbca92 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -25,11 +25,20 @@ struct B2bPayload<'a> { reciever_identifier_type: &'a str, #[serde(rename(serialize = "Remarks"))] remarks: &'a str, - #[serde(rename(serialize = "QueueTimeOutURL"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "QueueTimeOutURL"), + skip_serializing_if = "Option::is_none" + )] queue_time_out_url: Option<&'a str>, - #[serde(rename(serialize = "ResultURL"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "ResultURL"), + skip_serializing_if = "Option::is_none" + )] result_url: Option<&'a str>, - #[serde(rename(serialize = "AccountReference"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "AccountReference"), + skip_serializing_if = "Option::is_none" + )] account_reference: Option<&'a str>, } diff --git a/src/services/b2c.rs b/src/services/b2c.rs index a8b7f047c..c839e5fe1 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -20,11 +20,20 @@ struct B2cPayload<'a> { party_b: Option<&'a str>, #[serde(rename(serialize = "Remarks"))] remarks: &'a str, - #[serde(rename(serialize = "QueueTimeOutURL"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "QueueTimeOutURL"), + skip_serializing_if = "Option::is_none" + )] queue_time_out_url: Option<&'a str>, - #[serde(rename(serialize = "ResultURL"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "ResultURL"), + skip_serializing_if = "Option::is_none" + )] result_url: Option<&'a str>, - #[serde(rename(serialize = "Occasion"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "Occasion"), + skip_serializing_if = "Option::is_none" + )] occasion: Option<&'a str>, } diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index 1b57c4fe0..64b5037e4 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -14,15 +14,24 @@ struct C2bSimulatePayload<'a> { amount: u32, #[serde(rename(serialize = "Msisdn"), skip_serializing_if = "Option::is_none")] msisdn: Option<&'a str>, - #[serde(rename(serialize = "BillRefNumber"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "BillRefNumber"), + skip_serializing_if = "Option::is_none" + )] bill_ref_number: Option<&'a str>, - #[serde(rename(serialize = "ShortCode"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "ShortCode"), + skip_serializing_if = "Option::is_none" + )] short_code: Option<&'a str>, } #[derive(Debug, Clone, Deserialize)] pub struct C2bSimulateResponse { - #[serde(rename(deserialize = "ConversationID"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(deserialize = "ConversationID"), + skip_serializing_if = "Option::is_none" + )] pub conversation_id: Option, #[serde(rename(deserialize = "OriginatorCoversationID"))] pub originator_coversation_id: String, diff --git a/src/services/express_request.rs b/src/services/express_request.rs index 82c7d6e47..3ab3c4e9b 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -6,8 +6,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; /// [test credentials](https://developer.safaricom.co.ke/test_credentials) -static DEFAULT_PASSKEY: &'static str = - "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919"; +static DEFAULT_PASSKEY: &str = "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919"; #[derive(Debug, Serialize)] struct MpesaExpressRequestPayload<'a> { @@ -25,9 +24,15 @@ struct MpesaExpressRequestPayload<'a> { party_a: Option<&'a str>, #[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")] party_b: Option<&'a str>, - #[serde(rename(serialize = "PhoneNumber"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "PhoneNumber"), + skip_serializing_if = "Option::is_none" + )] phone_number: Option<&'a str>, - #[serde(rename(serialize = "CallBackURL"), skip_serializing_if = "Option::is_none")] + #[serde( + rename(serialize = "CallBackURL"), + skip_serializing_if = "Option::is_none" + )] call_back_url: Option<&'a str>, #[serde(rename(serialize = "AccountReference"))] account_reference: &'a str, @@ -207,8 +212,16 @@ impl<'a> MpesaExpressRequestBuilder<'a> { password: &password, timestamp: ×tamp, amount: self.amount.unwrap_or_default(), - party_a: if self.party_a.is_some() {self.party_a} else {self.phone_number}, - party_b: if self.party_b.is_some() {self.party_b} else {Some(self.business_short_code)}, + party_a: if self.party_a.is_some() { + self.party_a + } else { + self.phone_number + }, + party_b: if self.party_b.is_some() { + self.party_b + } else { + Some(self.business_short_code) + }, phone_number: self.phone_number, call_back_url: self.callback_url, account_reference: self.account_ref.unwrap_or_else(|| "None"), From 88369e87756dfe5118b99fe07098e76dc375b29a Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 17:42:47 +0300 Subject: [PATCH 009/140] Remove redundant mpesa security trait --- src/client.rs | 22 +++++++++++++++++++++ src/lib.rs | 4 +--- src/mpesa_security.rs | 35 --------------------------------- src/services/account_balance.rs | 2 +- src/services/b2b.rs | 1 - src/services/b2c.rs | 2 +- 6 files changed, 25 insertions(+), 41 deletions(-) delete mode 100644 src/mpesa_security.rs diff --git a/src/client.rs b/src/client.rs index b041da9b2..043d7cd76 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,6 +5,8 @@ use super::services::{ AccountBalanceBuilder, B2bBuilder, B2cBuilder, C2bRegisterBuilder, C2bSimulateBuilder, MpesaExpressRequestBuilder, }; +use openssl::rsa::Padding; +use openssl::x509::X509; use reqwest::blocking::Client; use serde_json::Value; use std::cell::RefCell; @@ -270,3 +272,23 @@ impl<'a> Mpesa { MpesaExpressRequestBuilder::new(self, business_short_code) } } + +impl Mpesa { + pub fn gen_security_credentials(&self) -> Result { + let pem = self.environment().get_certificate().as_bytes(); + let cert = X509::from_pem(pem)?; + // getting the public and rsa keys + let pub_key = cert.public_key()?; + let rsa_key = pub_key.rsa()?; + // configuring the buffer + let buf_len = pub_key.size(); + let mut buffer = vec![0; buf_len]; + + rsa_key.public_encrypt( + self.initiator_password().as_bytes(), + &mut buffer, + Padding::PKCS1, + )?; + Ok(base64::encode(buffer)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 3d2dce519..d0f7eeea0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -215,7 +215,7 @@ //! //! * Mpesa Express Request / STK push/ Lipa na M-PESA online //! -//! ```rust +//! ```ignore //! use mpesa::{Mpesa, MpesaResult, MpesaExpressRequestResponse}; //! use std::env; //! use dotenv::dotenv; @@ -252,14 +252,12 @@ mod client; mod constants; pub mod environment; mod errors; -mod mpesa_security; pub mod services; pub use client::{Mpesa, MpesaResult}; pub use constants::{CommandId, IdentifierTypes, ResponseType}; pub use environment::Environment::{self, Production, Sandbox}; pub use errors::MpesaError; -use mpesa_security::MpesaSecurity; pub use services::{ AccountBalanceResponse, B2bResponse, B2cResponse, C2bRegisterResponse, C2bSimulateResponse, MpesaExpressRequestResponse, diff --git a/src/mpesa_security.rs b/src/mpesa_security.rs deleted file mode 100644 index 8bad85e51..000000000 --- a/src/mpesa_security.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::client::MpesaResult; -use crate::{Mpesa, MpesaError}; -use openssl::rsa::Padding; -use openssl::x509::X509; - -pub trait MpesaSecurity { - /// Generates 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. - /// Returns base64 encoded string. - /// - /// # Error - /// Returns `EncryptionError` variant of `MpesaError` - fn gen_security_credentials(&self) -> MpesaResult; -} - -impl MpesaSecurity for Mpesa { - fn gen_security_credentials(&self) -> Result { - let pem = self.environment().get_certificate().as_bytes(); - let cert = X509::from_pem(pem)?; - // getting the public and rsa keys - let pub_key = cert.public_key()?; - let rsa_key = pub_key.rsa()?; - // configuring the buffer - let buf_len = pub_key.size(); - let mut buffer = vec![0; buf_len]; - - rsa_key.public_encrypt( - self.initiator_password().as_bytes(), - &mut buffer, - Padding::PKCS1, - )?; - Ok(base64::encode(buffer)) - } -} diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index 511aef3fa..7165214eb 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -1,6 +1,6 @@ use crate::client::MpesaResult; use crate::constants::{CommandId, IdentifierTypes}; -use crate::{Mpesa, MpesaError, MpesaSecurity}; +use crate::{Mpesa, MpesaError}; use serde::{Deserialize, Serialize}; use serde_json::Value; diff --git a/src/services/b2b.rs b/src/services/b2b.rs index c09cbca92..9dd3f3efc 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -1,7 +1,6 @@ use crate::client::{Mpesa, MpesaResult}; use crate::constants::{CommandId, IdentifierTypes}; use crate::errors::MpesaError; -use crate::MpesaSecurity; use serde::{Deserialize, Serialize}; use serde_json::Value; diff --git a/src/services/b2c.rs b/src/services/b2c.rs index c839e5fe1..2e0e38d8c 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -1,5 +1,5 @@ use crate::client::MpesaResult; -use crate::{CommandId, Mpesa, MpesaError, MpesaSecurity}; +use crate::{CommandId, Mpesa, MpesaError}; use serde::{Deserialize, Serialize}; use serde_json::Value; From c713713b78d8bc02312f47112d8630bb8ab5f7bc Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 17:45:12 +0300 Subject: [PATCH 010/140] Make openssl required dependency for account_balance feature --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e53e6ac19..84546d667 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ features = ["derive"] default = ["b2b", "b2c" ,"account_balance", "c2b_register", "c2b_simulate", "express_request"] b2b = ["dep:openssl"] b2c = ["dep:openssl"] -account_balance = [] +account_balance = ["dep:openssl"] c2b_register = [] c2b_simulate = [] express_request = [] \ No newline at end of file From 09ae414f4db378ce66dadcb1e5d75a390ebcb55c Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 17:52:40 +0300 Subject: [PATCH 011/140] Fix clippy warnings; ignore others --- src/client.rs | 3 ++- src/constants.rs | 4 ++-- src/services/account_balance.rs | 1 + src/services/b2b.rs | 1 + src/services/b2c.rs | 1 + src/services/c2b_register.rs | 1 + src/services/c2b_simulate.rs | 1 + src/services/express_request.rs | 1 + 8 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 043d7cd76..988f12e91 100644 --- a/src/client.rs +++ b/src/client.rs @@ -100,6 +100,7 @@ impl<'a> Mpesa { /// /// # Errors /// Returns a `MpesaError` on failure + #[allow(clippy::single_char_pattern)] pub(crate) fn auth(&self) -> MpesaResult { let url = format!( "{}/oauth/v1/generate?grant_type=client_credentials", @@ -115,7 +116,7 @@ impl<'a> Mpesa { // hence why we need strip out double quotes `"` from the deserialized value // example: "value" -> value let value: Value = resp.json()?; - return Ok(value["access_token"].to_string().replace("\"", "")); + return Ok(value["access_token"].to_string().replace('\"', "")); } Err(MpesaError::Message( "Could not authenticate to Safaricom, please check your credentials", diff --git a/src/constants.rs b/src/constants.rs index 8a7ce96ce..ddb2e3375 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -22,7 +22,7 @@ pub enum CommandId { impl Display for CommandId { fn fmt(&self, f: &mut Formatter) -> FmtResult { - write!(f, "{}", self) + write!(f, "{:?}", self) } } @@ -82,6 +82,6 @@ pub enum ResponseType { impl Display for ResponseType { fn fmt(&self, f: &mut Formatter) -> FmtResult { - write!(f, "{}", self) + write!(f, "{:?}", self) } } diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index 7165214eb..fba4191ad 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -147,6 +147,7 @@ impl<'a> AccountBalanceBuilder<'a> { /// /// # Errors /// Returns a `MpesaError` on failure + #[allow(clippy::unnecessary_lazy_evaluations)] pub fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/accountbalance/v1/query", diff --git a/src/services/b2b.rs b/src/services/b2b.rs index 9dd3f3efc..3c01d61ba 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -208,6 +208,7 @@ impl<'a> B2bBuilder<'a> { /// /// # Errors /// Returns a `MpesaError` on failure + #[allow(clippy::unnecessary_lazy_evaluations)] pub fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/b2b/v1/paymentrequest", diff --git a/src/services/b2c.rs b/src/services/b2c.rs index 2e0e38d8c..7668df086 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -185,6 +185,7 @@ impl<'a> B2cBuilder<'a> { /// /// # Errors /// Returns a `MpesaError` on failure. + #[allow(clippy::unnecessary_lazy_evaluations)] pub fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/b2c/v1/paymentrequest", diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index e14d2e25b..f0a20859c 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -98,6 +98,7 @@ impl<'a> C2bRegisterBuilder<'a> { /// /// # Errors /// Returns a `MpesaError` on failure + #[allow(clippy::unnecessary_lazy_evaluations)] pub fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/c2b/v1/registerurl", diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index 64b5037e4..fd12e8bdf 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -116,6 +116,7 @@ impl<'a> C2bSimulateBuilder<'a> { /// /// # Errors /// Returns a `MpesaError` on failure + #[allow(clippy::unnecessary_lazy_evaluations)] pub fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/c2b/v1/simulate", diff --git a/src/services/express_request.rs b/src/services/express_request.rs index 3ab3c4e9b..e54250416 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -199,6 +199,7 @@ impl<'a> MpesaExpressRequestBuilder<'a> { /// # Errors /// Returns a `MpesaError` on failure #[allow(clippy::or_fun_call)] + #[allow(clippy::unnecessary_lazy_evaluations)] pub fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/stkpush/v1/processrequest", From 3e410f12ae70d282f7e6d5d791b39ac42163d3d1 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 17:55:05 +0300 Subject: [PATCH 012/140] Remove all error for status calls --- src/services/b2c.rs | 3 +-- src/services/c2b_register.rs | 3 +-- src/services/c2b_simulate.rs | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/services/b2c.rs b/src/services/b2c.rs index 7668df086..ee9f041d9 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -214,8 +214,7 @@ impl<'a> B2cBuilder<'a> { .post(&url) .bearer_auth(self.client.auth()?) .json(&payload) - .send()? - .error_for_status()?; + .send()?; if response.status().is_success() { let value: B2cResponse = response.json()?; diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index f0a20859c..53db1d89c 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -118,8 +118,7 @@ impl<'a> C2bRegisterBuilder<'a> { .post(&url) .bearer_auth(self.client.auth()?) .json(&payload) - .send()? - .error_for_status()?; + .send()?; if response.status().is_success() { let value: C2bRegisterResponse = response.json()?; diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index fd12e8bdf..387116a4e 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -137,8 +137,7 @@ impl<'a> C2bSimulateBuilder<'a> { .post(&url) .bearer_auth(self.client.auth()?) .json(&payload) - .send()? - .error_for_status()?; + .send()?; if response.status().is_success() { let value: C2bSimulateResponse = response.json()?; From 2218dbb01db7da19a1df820fb9e0b7df916da280 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 17:58:07 +0300 Subject: [PATCH 013/140] gen_security_credentials only public in crate context --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 988f12e91..69a437a8a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -275,7 +275,7 @@ impl<'a> Mpesa { } impl Mpesa { - pub fn gen_security_credentials(&self) -> Result { + pub(crate) fn gen_security_credentials(&self) -> Result { let pem = self.environment().get_certificate().as_bytes(); let cert = X509::from_pem(pem)?; // getting the public and rsa keys From 41c98f91bef67fc1ca75a0fcc7dc421859b41381 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 18:10:50 +0300 Subject: [PATCH 014/140] Remove redundant comment --- src/services/b2b.rs | 4 +--- src/services/b2c.rs | 4 +--- src/services/c2b_simulate.rs | 6 ++---- src/services/express_request.rs | 6 ++---- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/services/b2b.rs b/src/services/b2b.rs index 3c01d61ba..7b5f7f310 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -180,10 +180,8 @@ impl<'a> B2bBuilder<'a> { self } + /// Adds an `amount` to the request /// This is a required field - /// - /// # Errors - /// If the amount is less than 10? pub fn amount(mut self, amount: u32) -> B2bBuilder<'a> { self.amount = Some(amount); self diff --git a/src/services/b2c.rs b/src/services/b2c.rs index ee9f041d9..c2d224cf2 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -133,10 +133,8 @@ impl<'a> B2cBuilder<'a> { self } + /// Adds an `amount` to the request /// This is a required field - /// - /// # Errors - /// If the amount is less than 10? pub fn amount(mut self, amount: u32) -> B2cBuilder<'a> { self.amount = Some(amount); self diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index 387116a4e..7ad49b5dc 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -71,10 +71,8 @@ impl<'a> C2bSimulateBuilder<'a> { self } - /// Adds the amount being transacted. This is a required field - /// - /// # Errors - /// If invalid amount, less than 10? + /// Adds an `amount` to the request + /// This is a required field pub fn amount(mut self, amount: u32) -> C2bSimulateBuilder<'a> { self.amount = Some(amount); self diff --git a/src/services/express_request.rs b/src/services/express_request.rs index e54250416..bbf7327e4 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -125,10 +125,8 @@ impl<'a> MpesaExpressRequestBuilder<'a> { self } - /// Amount to be transacted - /// - /// # Errors - /// If `amount` is invalid + /// Adds an `amount` to the request + /// This is a required field pub fn amount(mut self, amount: u32) -> MpesaExpressRequestBuilder<'a> { self.amount = Some(amount); self From d64ad85e45df29990d11612ec7770ead5c07b77e Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 18:14:15 +0300 Subject: [PATCH 015/140] Update docs --- src/services/account_balance.rs | 4 ++-- src/services/b2b.rs | 4 ++-- src/services/b2c.rs | 4 ++-- src/services/c2b_register.rs | 2 +- src/services/c2b_simulate.rs | 4 ++-- src/services/express_request.rs | 4 +++- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index fba4191ad..4d69c2676 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -139,10 +139,10 @@ impl<'a> AccountBalanceBuilder<'a> { self } - /// **AccountBalance API** + /// # AccountBalance API /// /// Enquire the balance on an M-Pesa BuyGoods (Till Number). - /// A successful request returns a `serde_json::Value` type. + /// A successful request returns a `C2bRegisterResponse` type. /// See more [here](https://developer.safaricom.co.ke/docs#account-balance-api) /// /// # Errors diff --git a/src/services/b2b.rs b/src/services/b2b.rs index 7b5f7f310..40f2a8c18 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -193,7 +193,7 @@ impl<'a> B2bBuilder<'a> { self } - /// **B2B API** + /// # B2B API /// /// Sends b2b payment request. /// @@ -202,7 +202,7 @@ impl<'a> B2bBuilder<'a> { /// business initiating the transaction and the both businesses involved in the transaction /// See more [here](https://developer.safaricom.co.ke/docs?shell#b2b-api) /// - /// A successful request returns a `serde_json::Value` type + /// A successful request returns a `B2bResponse` type /// /// # Errors /// Returns a `MpesaError` on failure diff --git a/src/services/b2c.rs b/src/services/b2c.rs index c2d224cf2..b18cde1fe 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -170,7 +170,7 @@ impl<'a> B2cBuilder<'a> { self } - /// **B2C API** + /// # B2C API /// /// Sends b2c payment request. /// @@ -179,7 +179,7 @@ impl<'a> B2cBuilder<'a> { /// valid and verified B2C M-Pesa Short code. /// See more [here](https://developer.safaricom.co.ke/docs?shell#b2c-api) /// - /// A successful request returns a `serde_json::Value` type + /// A successful request returns a `B2cResponse` type /// /// # Errors /// Returns a `MpesaError` on failure. diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index 53db1d89c..e733f4328 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -94,7 +94,7 @@ impl<'a> C2bRegisterBuilder<'a> { /// /// The response expected is the success code the 3rd party /// - /// A successful request returns a `serde_json::Value` type + /// A successful request returns a `C2bRegisterResponse` type /// /// # Errors /// Returns a `MpesaError` on failure diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index 7ad49b5dc..0b07ef066 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -103,14 +103,14 @@ impl<'a> C2bSimulateBuilder<'a> { self } - /// **C2B Simulate API** + /// # C2B Simulate API /// /// Make payment requests from Client to Business /// /// This enables you to receive the payment requests in real time. /// See more [here](https://developer.safaricom.co.ke/c2b/apis/post/simulate) /// - /// A successful request returns a `serde_json::Value` type + /// A successful request returns a `C2bSimulateResponse` type /// /// # Errors /// Returns a `MpesaError` on failure diff --git a/src/services/express_request.rs b/src/services/express_request.rs index bbf7327e4..a70c3ae7f 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -190,9 +190,11 @@ impl<'a> MpesaExpressRequestBuilder<'a> { self } - /// *Lipa na M-Pesa Online Payment / Mpesa Express/ Stk push* + /// # 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 /// /// # Errors /// Returns a `MpesaError` on failure From 3277aba1f8995f553bb904266e4526f5953c4d96 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 18:14:47 +0300 Subject: [PATCH 016/140] Update readme --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index d6bea7953..e3c6ebc20 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,6 @@ Documentation - - mpesa travis-ci - License: MIT @@ -198,5 +195,5 @@ More will be added progressively, pull requests welcome Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/collinsmuriuki/mpesa-rust/issues). You can also take a look at the [contributing guide](CONTRIBUTING.md). -Copyright © 2021 [Collins Muriuki](https://github.com/collinsmuriuki).
+Copyright © 2022 [Collins Muriuki](https://github.com/collinsmuriuki).
This project is [MIT](LICENSE) licensed. From b7a2ab246537a58c224b2d133beac5217d71eead Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 18:16:03 +0300 Subject: [PATCH 017/140] Reformat code --- src/services/express_request.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/express_request.rs b/src/services/express_request.rs index a70c3ae7f..7a6051009 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -193,7 +193,7 @@ impl<'a> MpesaExpressRequestBuilder<'a> { /// # 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 /// /// # Errors From fac273a43805c9a2c162245051a48a4e3b52489e Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 18:21:33 +0300 Subject: [PATCH 018/140] Add pre-commit --- .pre-commit-config.yaml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..425e6f793 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +fail_fast: true +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: check-toml + + - repo: local + hooks: + - id: run-cargo-fmt + name: Cargo fmt + entry: /bin/bash -c "cargo fmt --all -- --check" + language: script + files: \.x$ + always_run: true + - id: run-cargo-clippy + name: Cargo clippy + entry: /bin/bash -c "cargo clippy -- -D warnings" + language: script + files: \.x$ + always_run: true + - id: run-cargo-test + name: Cargo test + entry: /bin/bash -c "cargo test --no-fail-fast" + language: script + files: \.x$ + always_run: true From e8f5f546268cc1bbdac2ddd5c992efcd6e514d42 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 18:26:35 +0300 Subject: [PATCH 019/140] Make base_url only public in the context of the crate; remove redundant doc test --- src/environment.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/environment.rs b/src/environment.rs index 03f1f32ec..b3ec86c01 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -52,16 +52,7 @@ impl TryFrom<&'static str> for Environment { impl Environment { /// Matches to intended base_url depending on Environment variant - /// - /// ## Example - /// ``` - /// use mpesa::Environment; - /// - /// let env: Environment = Environment::Production; - /// let base_url: &str = env.base_url(); - /// assert_eq!("https://api.safaricom.co.ke", base_url); - /// ``` - pub fn base_url(&self) -> &'static str { + pub(crate) fn base_url(&self) -> &'static str { match self { Environment::Production => "https://api.safaricom.co.ke", Environment::Sandbox => "https://sandbox.safaricom.co.ke", From b0e514794268288f117e583c28297f4dcead0c6a Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 18:33:26 +0300 Subject: [PATCH 020/140] Fix 400 error in c2b register; remove depreceiated attribute --- src/client.rs | 1 - src/constants.rs | 2 +- src/lib.rs | 2 +- src/services/c2b_register.rs | 4 +++- tests/c2b_register_test.rs | 5 +++-- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 69a437a8a..408b407c7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -195,7 +195,6 @@ impl<'a> Mpesa { /// .send(); /// ``` #[cfg(feature = "c2b_register")] - #[deprecated] pub fn c2b_register(&'a self) -> C2bRegisterBuilder<'a> { C2bRegisterBuilder::new(self) } diff --git a/src/constants.rs b/src/constants.rs index ddb2e3375..058c8b71b 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -76,7 +76,7 @@ impl Display for MpesaResponseCode { #[derive(Debug, Serialize, Deserialize)] /// C2B Register Response types pub enum ResponseType { - Complete, + Completed, Cancelled, } diff --git a/src/lib.rs b/src/lib.rs index d0f7eeea0..e406d75aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,7 +142,7 @@ //! assert!(response.is_ok()) //! ``` //! -//! * C2B Register (depreciated) +//! * C2B Register //! ```ignore //! use mpesa::{Mpesa, MpesaResult, C2bRegisterResponse}; //! use serde_json::Value; diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index e733f4328..95f6ebb11 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -108,7 +108,9 @@ impl<'a> C2bRegisterBuilder<'a> { let payload = C2bRegisterPayload { validation_url: self.validation_url.unwrap_or_else(|| "None"), confirmation_url: self.confirmation_url.unwrap_or_else(|| "None"), - response_type: self.response_type.unwrap_or_else(|| ResponseType::Complete), + response_type: self + .response_type + .unwrap_or_else(|| ResponseType::Completed), short_code: self.short_code.unwrap_or_else(|| "None"), }; diff --git a/tests/c2b_register_test.rs b/tests/c2b_register_test.rs index c4d094ace..e97da8c64 100644 --- a/tests/c2b_register_test.rs +++ b/tests/c2b_register_test.rs @@ -3,8 +3,7 @@ use mpesa::Mpesa; use std::env; #[test] -#[ignore = "depreciated"] -#[allow(deprecated)] +#[ignore = "c2b_register always fails on sandbox with status 503"] fn c2b_register_test() { dotenv::dotenv().ok(); @@ -21,5 +20,7 @@ fn c2b_register_test() { .validation_url("https://testdomain.com/valid") .send(); + println!("{response:?}"); + assert!(response.is_ok()) } From f4e2467461b946cab6da6b4eb892af5deea8e092 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 18:38:38 +0300 Subject: [PATCH 021/140] chrono dependency only necessary for express_request feature --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 84546d667..783cf9d93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ openssl = {version = "0.10.30", optional = true} base64 = "0.13.0" failure = "0.1.5" failure_derive = "0.1.5" -chrono = "0.4" +chrono = {version = "0.4", optional = true} [dependencies.reqwest] version = "0.11.3" @@ -34,4 +34,4 @@ b2c = ["dep:openssl"] account_balance = ["dep:openssl"] c2b_register = [] c2b_simulate = [] -express_request = [] \ No newline at end of file +express_request = ["dep:chrono"] \ No newline at end of file From 6cfe690013002dacdde110f7af636e3531fbe237 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 18:39:44 +0300 Subject: [PATCH 022/140] Update readme path on cargo toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 783cf9d93..8a938cc6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" description = "A wrapper around the M-PESA API in Rust." keywords = ["api", "mpesa", "mobile"] repository = "https://github.com/collinsmuriuki/mpesa-rust" -readme = "../README.md" +readme = "./README.md" license = "MIT" [dependencies] From 05a08ae8a3a174f2517dbe4a03d59f3b6d450e74 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 14 Nov 2022 18:44:17 +0300 Subject: [PATCH 023/140] Further clean up dependencies --- Cargo.toml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8a938cc6f..fcb83c8c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,26 +12,28 @@ license = "MIT" [dependencies] serde_json = "1.0" serde_repr = "0.1" -dotenv = "0.15.0" -openssl = {version = "0.10.30", optional = true} -base64 = "0.13.0" -failure = "0.1.5" -failure_derive = "0.1.5" +openssl = {version = "0.10", optional = true} +base64 = {version = "0.13", optional = true} +failure = "0.1" +failure_derive = "0.1" chrono = {version = "0.4", optional = true} [dependencies.reqwest] -version = "0.11.3" +version = "0.11" features = ["blocking", "json"] [dependencies.serde] -version = "1.0.114" +version = "1.0" features = ["derive"] +[dev-dependencies] +dotenv = "0.15" + [features] default = ["b2b", "b2c" ,"account_balance", "c2b_register", "c2b_simulate", "express_request"] -b2b = ["dep:openssl"] -b2c = ["dep:openssl"] -account_balance = ["dep:openssl"] +b2b = ["dep:openssl", "dep:base64"] +b2c = ["dep:openssl", "dep:base64"] +account_balance = ["dep:openssl", "dep:base64"] c2b_register = [] c2b_simulate = [] -express_request = ["dep:chrono"] \ No newline at end of file +express_request = ["dep:chrono", "dep:base64"] \ No newline at end of file From 9737bfc01dbac15e5335be5924d24fa2d676fa69 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 10:24:06 +0300 Subject: [PATCH 024/140] Redocument gen_security_credentials --- src/client.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 408b407c7..bb3d5d223 100644 --- a/src/client.rs +++ b/src/client.rs @@ -274,7 +274,14 @@ impl<'a> Mpesa { } impl Mpesa { - pub(crate) fn gen_security_credentials(&self) -> Result { + /// Generates 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. + /// Returns base64 encoded string. + /// + /// # Errors + /// Returns `EncryptionError` variant of `MpesaError` + pub(crate) fn gen_security_credentials(&self) -> MpesaResult { let pem = self.environment().get_certificate().as_bytes(); let cert = X509::from_pem(pem)?; // getting the public and rsa keys From d3b08f145db4239de28cdfd6ac4508a406b49732 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 10:25:50 +0300 Subject: [PATCH 025/140] Document global variables --- src/client.rs | 1 + src/services/express_request.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index bb3d5d223..58d88e8f7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,6 +11,7 @@ use reqwest::blocking::Client; use serde_json::Value; use std::cell::RefCell; +/// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) static DEFAULT_INITIATOR_PASSWORD: &str = "Safcom496!"; /// `Result` enum type alias diff --git a/src/services/express_request.rs b/src/services/express_request.rs index 7a6051009..3ec620a0c 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -5,7 +5,7 @@ use chrono::prelude::Local; use serde::{Deserialize, Serialize}; use serde_json::Value; -/// [test credentials](https://developer.safaricom.co.ke/test_credentials) +/// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) static DEFAULT_PASSKEY: &str = "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919"; #[derive(Debug, Serialize)] From 97187a328f6c0a1fd96e6e0d1c31eeca8214c56f Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 10:36:27 +0300 Subject: [PATCH 026/140] Add temporary todo list --- TODO.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..f87f9602d --- /dev/null +++ b/TODO.md @@ -0,0 +1,7 @@ +# TODO + +- [x] Unify `mpesa_derive` and `mpesa_core` +- [ ] Add missing services: `reversal`, `transaction_status`, `bill_manager`, `dynamic_qr_code`, `c2b_simulate_v2` +- [x] Clean up `Cargo.toml`: Correctly use Cargo features and declare optional and dev dependencies +- [ ] Convert library to async and update tests +- [ ] Migrate to `thiserror` and remove `failure` \ No newline at end of file From 3f7b771252fe13716bea06c4eea61f9ad2d9af70 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 12:25:48 +0300 Subject: [PATCH 027/140] Move todo to contributing --- CONTRIBUTING.md | 64 +++++++------------------------------------------ TODO.md | 4 +++- 2 files changed, 11 insertions(+), 57 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57afdbd73..b5557a4cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,56 +1,8 @@ -# Contributing guide - -> Instructions for local project setup and contribution. - -## Rust - -You will need Rust installed to run this project. You can find a guide to install on any os [here](https://www.rust-lang.org/tools/install) - -## Install - -```sh -git clone https://github.com/collinsmuriuki/mpesa-rust.git -cd mpesa-rust -``` - -## App Keys -Copy your safaricom credentials to an `.env` file in the root of the project, check `.env.example` for reference or simply run. - -See [here](https://developer.safaricom.co.ke/docs#developer-sign-up) if you need to acquire app keys. -```sh -echo CLIENT_KEY="" >> .env -echo CLIENT_SECRET="" >> .env -``` - -## Test Credentials -You can get test credentials [here](https://developer.safaricom.co.ke/test_credentials) - -## Run tests - -```sh -cargo test -``` - -## RoadMap - -- [x] Create Mpesa Client struct -- [x] Implement Auth -- [x] Error handling -- [x] Generate security credentials -- [x] Implement B2C payment -- [x] Implement B2B payment -- [x] Query transaction status -- [x] Simulate C2B Payment -- [ ] Query status of Lipa na M-Pesa -- [x] Initiate Lipa na M-Pesa online w/ STK push -- [x] Register C2B Confirmation and Validation URLs -- [x] Integration tests -- [ ] Rewrite in async -- [x] Publish on https://crates.io -- [x] Setup travis-ci -- [ ] Improve documentation - - -## Pull Requests - -Fork the repo and create a feature branch. Push your changes and make a PR. \ No newline at end of file +# TODO + +- [x] Unify `mpesa_derive` and `mpesa_core` +- [ ] Add missing services: `reversal`, `transaction_status`, `bill_manager`, `dynamic_qr_code`, `c2b_simulate_v2` +- [x] Clean up `Cargo.toml`: Correctly use Cargo features and declare optional and dev dependencies +- [ ] Convert library to async and update tests +- [ ] Migrate to `thiserror` and remove `failure` +- [ ] Refine tests: test more edge cases \ No newline at end of file diff --git a/TODO.md b/TODO.md index f87f9602d..484c3e913 100644 --- a/TODO.md +++ b/TODO.md @@ -4,4 +4,6 @@ - [ ] Add missing services: `reversal`, `transaction_status`, `bill_manager`, `dynamic_qr_code`, `c2b_simulate_v2` - [x] Clean up `Cargo.toml`: Correctly use Cargo features and declare optional and dev dependencies - [ ] Convert library to async and update tests -- [ ] Migrate to `thiserror` and remove `failure` \ No newline at end of file +- [ ] Migrate to `thiserror` and remove `failure` +- [ ] Refine tests: test more edge cases +- [ ] Address security issues in cargo security audit \ No newline at end of file From 0ae6518ab0ab4f013c252a9fa7849c714c1646ab Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 16:42:21 +0300 Subject: [PATCH 028/140] Remove todo markdown file --- TODO.md | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 484c3e913..000000000 --- a/TODO.md +++ /dev/null @@ -1,9 +0,0 @@ -# TODO - -- [x] Unify `mpesa_derive` and `mpesa_core` -- [ ] Add missing services: `reversal`, `transaction_status`, `bill_manager`, `dynamic_qr_code`, `c2b_simulate_v2` -- [x] Clean up `Cargo.toml`: Correctly use Cargo features and declare optional and dev dependencies -- [ ] Convert library to async and update tests -- [ ] Migrate to `thiserror` and remove `failure` -- [ ] Refine tests: test more edge cases -- [ ] Address security issues in cargo security audit \ No newline at end of file From a85edc257632222ab4c0ab316b8210d8cee65ab2 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 16:54:11 +0300 Subject: [PATCH 029/140] Add derive_environment macro and implement TryFrom for Environment --- src/environment.rs | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/environment.rs b/src/environment.rs index b3ec86c01..ef62e0e58 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -22,46 +22,53 @@ pub enum Environment { Sandbox, } -impl FromStr for Environment { - type Err = MpesaError; - - fn from_str(s: &str) -> Result { - match s { +macro_rules! derive_environment { + ($v:expr) => { + match $v { "production" | "Production" | "PRODUCTION" => Ok(Self::Production), "sandbox" | "Sandbox" | "SANDBOX" => Ok(Self::Sandbox), _ => Err(MpesaError::Message( "Could not parse the provided environment name", )), } + }; +} + +impl FromStr for Environment { + type Err = MpesaError; + + fn from_str(s: &str) -> Result { + derive_environment!(s) } } -impl TryFrom<&'static str> for Environment { +impl TryFrom<&str> for Environment { type Error = MpesaError; - fn try_from(v: &'static str) -> Result { - match v { - "production" | "Production" | "PRODUCTION" => Ok(Self::Production), - "sandbox" | "Sandbox" | "SANDBOX" => Ok(Self::Sandbox), - _ => Err(MpesaError::Message( - "Could not parse the provided environment name", - )), - } + fn try_from(v: &str) -> Result { + derive_environment!(v) + } +} + +impl TryFrom for Environment { + type Error = MpesaError; + + fn try_from(v: String) -> Result { + derive_environment!(v.as_str()) } } impl Environment { /// Matches to intended base_url depending on Environment variant - pub(crate) fn base_url(&self) -> &'static str { + pub(crate) fn base_url(&self) -> &str { match self { Environment::Production => "https://api.safaricom.co.ke", Environment::Sandbox => "https://sandbox.safaricom.co.ke", } } - /// Match to X509 public key certificate based on - /// environment variant - pub fn get_certificate(&self) -> &'static str { + /// Match to X509 public key certificate based on environment + pub fn get_certificate(&self) -> &str { match self { Environment::Production => { r#"-----BEGIN CERTIFICATE----- From 3e87ed00384b597d8cf0393b42c1a1a8ea5e0371 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 17:04:46 +0300 Subject: [PATCH 030/140] Limit get_certificate to crate context --- src/environment.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/environment.rs b/src/environment.rs index ef62e0e58..3f2f5d5b6 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -18,7 +18,9 @@ use std::{convert::TryFrom, str::FromStr}; /// and the base url /// Required to construct a new `Mpesa` struct pub enum Environment { + /// Production environment Production, + /// Sandbox environment: for testing and development purposes Sandbox, } @@ -59,7 +61,7 @@ impl TryFrom for Environment { } impl Environment { - /// Matches to intended base_url depending on Environment variant + /// Matches to base_url based on `Environment` variant pub(crate) fn base_url(&self) -> &str { match self { Environment::Production => "https://api.safaricom.co.ke", @@ -67,8 +69,8 @@ impl Environment { } } - /// Match to X509 public key certificate based on environment - pub fn get_certificate(&self) -> &str { + /// Match to X509 public key certificate based on `Environment` + pub(crate) fn get_certificate(&self) -> &str { match self { Environment::Production => { r#"-----BEGIN CERTIFICATE----- From 8109eaf64579d24c068e93537924e7f6201d1636 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 17:05:53 +0300 Subject: [PATCH 031/140] Update docs and tests --- src/lib.rs | 2 +- tests/account_balance_test.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e406d75aa..0e2354fb2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//!## About +//!## mpesa-rust //! //! An unofficial Rust wrapper around the [Safaricom API](https://developer.safaricom.co.ke/docs?shell#introduction) for accessing M-Pesa services. //! diff --git a/tests/account_balance_test.rs b/tests/account_balance_test.rs index 920faab60..7e40d68bd 100644 --- a/tests/account_balance_test.rs +++ b/tests/account_balance_test.rs @@ -1,5 +1,5 @@ use dotenv; -use mpesa::{Environment, Mpesa}; +use mpesa::{Mpesa, Sandbox}; use std::env; #[test] @@ -9,7 +9,7 @@ fn account_balance_test() { let client = Mpesa::new( env::var("CLIENT_KEY").unwrap(), env::var("CLIENT_SECRET").unwrap(), - Environment::Sandbox, + Sandbox, ); let response = client From 3a714ea621ea046529a9eebc8ef85391199431c5 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 17:20:36 +0300 Subject: [PATCH 032/140] Add environment test cases; move certificates to dedicated files and dir --- src/certificates/production | 38 ++++++++++++ src/certificates/sandbox | 35 +++++++++++ src/environment.rs | 116 ++++++++++++------------------------ 3 files changed, 110 insertions(+), 79 deletions(-) create mode 100644 src/certificates/production create mode 100644 src/certificates/sandbox diff --git a/src/certificates/production b/src/certificates/production new file mode 100644 index 000000000..13e811765 --- /dev/null +++ b/src/certificates/production @@ -0,0 +1,38 @@ +-----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----- \ No newline at end of file diff --git a/src/certificates/sandbox b/src/certificates/sandbox new file mode 100644 index 000000000..6fd6123bd --- /dev/null +++ b/src/certificates/sandbox @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGKzCCBROgAwIBAgIQDL7NH8cxSdUpl0ihH0A1wTANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E +aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwODI3MDAwMDAwWhcN +MTkwNDA0MTIwMDAwWjBuMQswCQYDVQQGEwJLRTEQMA4GA1UEBxMHTmFpcm9iaTEW +MBQGA1UEChMNU2FmYXJpY29tIFBMQzETMBEGA1UECxMKRGlnaXRhbCBJVDEgMB4G +A1UEAxMXc2FuZGJveC5zYWZhcmljb20uY28ua2UwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC78yeC/wLoZY6TJeqc4g/9eAKIpeCwEsjX09pD8ZxAGXqT +Oi7ssdIGJBPmJZNeEVyf8ocFhisCuLngJ9Z5e/AvH52PhrEFmVu2D03zSf4C+rhZ +ndEKP6G79pUAb/bemOliU9zM8xYYkpCRzPWUzk6zSDarg0ZDLw5FrtZj/VJ9YEDL +WGgAfwExEgSN3wjyUlJ2UwI3wqQXLka0VNFWoZxUH5j436gbSWRIL6NJUmrq8V8S +aTEPz3eJHj3NOToDu245c7VKdF/KExyZjRjD2p5I+Aip80TXzKlZj6DjMb3DlfXF +Hsnu0+1uJE701mvKX7BiscxKr8tCRphL63as4dqvAgMBAAGjggLkMIIC4DAfBgNV +HSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUzZmY7ZORLw9w +qRbAQN5m9lJ28qMwIgYDVR0RBBswGYIXc2FuZGJveC5zYWZhcmljb20uY28ua2Uw +DgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBr +BgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1z +aGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Et +c2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcC +ARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYB +BQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w +RgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy +dFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwCQYDVR0TBAIwADCCAQUGCisGAQQB1nkC +BAIEgfYEgfMA8QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB +ZXs1FvEAAAQDAEcwRQIgBzVMkm7SNprjJ1GBqiXIc9rNzY+y7gt6s/O02oMkyFoC +IQDBuThGlpmUKpeZoHhK6HGwB4jDMIecmKaOcMS18R2jxwB3AId1v+dZfPiMQ5lf +vfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZXs1F8IAAAQDAEgwRgIhAIRq2XFiC+RS +uDCYq8ICJg0QafSV+e9BLpJnElEdaSjiAiEAyiiW4vxwv4cWcAXE6FAipctyUBs6 +bE5QyaCnmNpoDiQwDQYJKoZIhvcNAQELBQADggEBAB0YoWve9Sxhb0PBS3Hc46Rf +a7H1jhHuwE+UyscSQsdJdk8uPAgDuKRZMvJPGEaCkNHm36NfcaXXFjPOl7LI1d1a +9zqSP0xeZBI6cF0x96WuQGrI9/WR2tfxjmaUSp8a/aJ6n+tZA28eJZNPrIaMm+6j +gh7AkKnqcf+g8F/MvCCVdNAiVMdz6UpCscf6BRPHNZ5ifvChGh7aUKjrVLLuF4Ls +HE05qm6HNyV5eTa6wvcbc4ewguN1UDZvPWetSyfBk10Wbpor4znQ4TJ3Y9uCvsJH +41ldblDvZZ2z4kB2UYQ7iBkPlJSxSOaFgW/GGDXq49sz/995xzhVITHxh2SdLkI= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/src/environment.rs b/src/environment.rs index 3f2f5d5b6..fb5facb19 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -72,85 +72,43 @@ impl Environment { /// Match to X509 public key certificate based on `Environment` pub(crate) fn get_certificate(&self) -> &str { match self { - Environment::Production => { - r#"-----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----- -"# - } - Environment::Sandbox => { - r#"-----BEGIN CERTIFICATE----- -MIIGKzCCBROgAwIBAgIQDL7NH8cxSdUpl0ihH0A1wTANBgkqhkiG9w0BAQsFADBN -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E -aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwODI3MDAwMDAwWhcN -MTkwNDA0MTIwMDAwWjBuMQswCQYDVQQGEwJLRTEQMA4GA1UEBxMHTmFpcm9iaTEW -MBQGA1UEChMNU2FmYXJpY29tIFBMQzETMBEGA1UECxMKRGlnaXRhbCBJVDEgMB4G -A1UEAxMXc2FuZGJveC5zYWZhcmljb20uY28ua2UwggEiMA0GCSqGSIb3DQEBAQUA -A4IBDwAwggEKAoIBAQC78yeC/wLoZY6TJeqc4g/9eAKIpeCwEsjX09pD8ZxAGXqT -Oi7ssdIGJBPmJZNeEVyf8ocFhisCuLngJ9Z5e/AvH52PhrEFmVu2D03zSf4C+rhZ -ndEKP6G79pUAb/bemOliU9zM8xYYkpCRzPWUzk6zSDarg0ZDLw5FrtZj/VJ9YEDL -WGgAfwExEgSN3wjyUlJ2UwI3wqQXLka0VNFWoZxUH5j436gbSWRIL6NJUmrq8V8S -aTEPz3eJHj3NOToDu245c7VKdF/KExyZjRjD2p5I+Aip80TXzKlZj6DjMb3DlfXF -Hsnu0+1uJE701mvKX7BiscxKr8tCRphL63as4dqvAgMBAAGjggLkMIIC4DAfBgNV -HSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUzZmY7ZORLw9w -qRbAQN5m9lJ28qMwIgYDVR0RBBswGYIXc2FuZGJveC5zYWZhcmljb20uY28ua2Uw -DgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBr -BgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1z -aGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Et -c2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcC -ARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYB -BQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w -RgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy -dFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwCQYDVR0TBAIwADCCAQUGCisGAQQB1nkC -BAIEgfYEgfMA8QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB -ZXs1FvEAAAQDAEcwRQIgBzVMkm7SNprjJ1GBqiXIc9rNzY+y7gt6s/O02oMkyFoC -IQDBuThGlpmUKpeZoHhK6HGwB4jDMIecmKaOcMS18R2jxwB3AId1v+dZfPiMQ5lf -vfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZXs1F8IAAAQDAEgwRgIhAIRq2XFiC+RS -uDCYq8ICJg0QafSV+e9BLpJnElEdaSjiAiEAyiiW4vxwv4cWcAXE6FAipctyUBs6 -bE5QyaCnmNpoDiQwDQYJKoZIhvcNAQELBQADggEBAB0YoWve9Sxhb0PBS3Hc46Rf -a7H1jhHuwE+UyscSQsdJdk8uPAgDuKRZMvJPGEaCkNHm36NfcaXXFjPOl7LI1d1a -9zqSP0xeZBI6cF0x96WuQGrI9/WR2tfxjmaUSp8a/aJ6n+tZA28eJZNPrIaMm+6j -gh7AkKnqcf+g8F/MvCCVdNAiVMdz6UpCscf6BRPHNZ5ifvChGh7aUKjrVLLuF4Ls -HE05qm6HNyV5eTa6wvcbc4ewguN1UDZvPWetSyfBk10Wbpor4znQ4TJ3Y9uCvsJH -41ldblDvZZ2z4kB2UYQ7iBkPlJSxSOaFgW/GGDXq49sz/995xzhVITHxh2SdLkI= ------END CERTIFICATE----- - "# - } + Environment::Production => include_str!("./certificates/production"), + Environment::Sandbox => include_str!("./certificates/sandbox"), } } } + +#[cfg(test)] +mod tests { + use std::convert::TryInto; + + use super::*; + + #[test] + fn test_valid_string_is_parsed_as_environment() { + let accepted_production_values = vec!["production", "Production", "PRODUCTION"]; + let accepted_sandbox_values = vec!["sandbox", "Sandbox", "SANDBOX"]; + accepted_production_values.into_iter().for_each(|v| { + let environment: Environment = v.parse().unwrap(); + assert_eq!(environment.base_url(), "https://api.safaricom.co.ke"); + assert_eq!( + environment.get_certificate(), + include_str!("./certificates/production") + ) + }); + accepted_sandbox_values.into_iter().for_each(|v| { + let environment: Environment = v.parse().unwrap(); + assert_eq!(environment.base_url(), "https://sandbox.safaricom.co.ke"); + assert_eq!( + environment.get_certificate(), + include_str!("./certificates/sandbox") + ) + }) + } + + #[test] + #[should_panic] + fn test_invalid_string_panics() { + let _: Environment = "foo_bar".try_into().unwrap(); + } +} From 31f97562f17c64fa1f7a19cd96d1d3e49dcf7a4f Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 17:26:31 +0300 Subject: [PATCH 033/140] Update environment tests --- src/environment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/environment.rs b/src/environment.rs index fb5facb19..45168f326 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -97,7 +97,7 @@ mod tests { ) }); accepted_sandbox_values.into_iter().for_each(|v| { - let environment: Environment = v.parse().unwrap(); + let environment: Environment = v.try_into().unwrap(); assert_eq!(environment.base_url(), "https://sandbox.safaricom.co.ke"); assert_eq!( environment.get_certificate(), From 458a5d679fa7dbf99a67c46cd72059bc1e8049be Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 17:57:03 +0300 Subject: [PATCH 034/140] Rename macro --- src/environment.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/environment.rs b/src/environment.rs index 45168f326..a9e18c47b 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -24,7 +24,7 @@ pub enum Environment { Sandbox, } -macro_rules! derive_environment { +macro_rules! environment_from_string { ($v:expr) => { match $v { "production" | "Production" | "PRODUCTION" => Ok(Self::Production), @@ -40,7 +40,7 @@ impl FromStr for Environment { type Err = MpesaError; fn from_str(s: &str) -> Result { - derive_environment!(s) + environment_from_string!(s) } } @@ -48,7 +48,7 @@ impl TryFrom<&str> for Environment { type Error = MpesaError; fn try_from(v: &str) -> Result { - derive_environment!(v) + environment_from_string!(v) } } @@ -56,7 +56,7 @@ impl TryFrom for Environment { type Error = MpesaError; fn try_from(v: String) -> Result { - derive_environment!(v.as_str()) + environment_from_string!(v.as_str()) } } From 597b1be08fb7e6893c415a3fc4e265debb0c3ae6 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 18:03:32 +0300 Subject: [PATCH 035/140] Add lazy evaluation on c2b_simulate send method --- src/services/c2b_simulate.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index 0b07ef066..7e66ad81b 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -122,7 +122,9 @@ impl<'a> C2bSimulateBuilder<'a> { ); let payload = C2bSimulatePayload { - command_id: self.command_id.unwrap_or(CommandId::CustomerPayBillOnline), + command_id: self + .command_id + .unwrap_or_else(|| CommandId::CustomerPayBillOnline), amount: self.amount.unwrap_or_else(|| 10), msisdn: self.msisdn, bill_ref_number: self.bill_ref_number, From 339cdff10f0167626f93783acf013acff1b2e2a6 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 18:33:24 +0300 Subject: [PATCH 036/140] Update cargo config toml --- .cargo/config.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 7a8d7abf6..81ae92510 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,10 +1,5 @@ [build] rustdoc = "rustdoc" -[profile.release] -lto = "fat" -debug = 1 -codegen-units = 1 - [alias] t = ["test", "--all-features", "--no-fail-fast"] \ No newline at end of file From c298aace77db576d7db0bcdd6330e57633b791fb Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 18:35:00 +0300 Subject: [PATCH 037/140] Remove redundant docs shield from readme --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index e3c6ebc20..b823b5c7d 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,6 @@ Version - - Documentation - License: MIT From 2fcf5436e13c74e71ab92ef012af0abb8030ebdc Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 19:16:34 +0300 Subject: [PATCH 038/140] Convert library to async rust --- Cargo.toml | 3 +- README.md | 24 ++- src/client.rs | 15 +- src/lib.rs | 286 ++++++++++++++++++-------------- src/services/account_balance.rs | 11 +- src/services/b2b.rs | 11 +- src/services/b2c.rs | 11 +- src/services/c2b_register.rs | 11 +- src/services/c2b_simulate.rs | 11 +- src/services/express_request.rs | 11 +- tests/account_balance_test.rs | 7 +- tests/b2b_test.rs | 7 +- tests/b2c_test.rs | 7 +- tests/c2b_register_test.rs | 9 +- tests/c2b_simulate_test.rs | 7 +- tests/stk_push_test.rs | 7 +- 16 files changed, 244 insertions(+), 194 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fcb83c8c9..3fb37f361 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ features = ["derive"] [dev-dependencies] dotenv = "0.15" +tokio = {version = "1", features = ["rt", "macros"]} [features] default = ["b2b", "b2c" ,"account_balance", "c2b_register", "c2b_simulate", "express_request"] @@ -36,4 +37,4 @@ b2c = ["dep:openssl", "dep:base64"] account_balance = ["dep:openssl", "dep:base64"] c2b_register = [] c2b_simulate = [] -express_request = ["dep:chrono", "dep:base64"] \ No newline at end of file +express_request = ["dep:chrono", "dep:base64"] diff --git a/README.md b/README.md index b823b5c7d..2a592c809 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ let client = Mpesa::new( Environment::Sandbox, ); -assert!(client.is_connected()) +assert!(client.is_connected().await) ``` Since the `Environment` enum implements `FromStr` and `TryFrom`, you can pass the name of the environment as a `&str` and call the `parse()` or `try_into()` @@ -76,7 +76,7 @@ let client = Mpesa::new( env::var("CLIENT_SECRET")?, "sandbox".parse()?, // "production" ); -assert!(client.is_connected()) +assert!(client.is_connected().await) ``` If you intend to use in production, you will need to call a the `set_initiator_password` method from `Mpesa` after initially @@ -94,7 +94,7 @@ let client = Mpesa::new( client.set_initiator_password("new_password"); -assert!(client.is_connected()) +assert!(client.is_connected().await) ``` ### Services @@ -111,7 +111,8 @@ let response = client .result_url("https://testdomain.com/ok") .timeout_url("https://testdomain.com/err") .amount(1000) - .send(); + .send() + .await; assert!(response.is_ok()) ``` @@ -126,7 +127,8 @@ let response = client .timeout_url("https://testdomain.com/err") .account_ref("254708374149") .amount(1000) - .send(); + .send() + .await; assert!(response.is_ok()) ``` @@ -138,7 +140,8 @@ let response = client .short_code("600496") .confirmation_url("https://testdomain.com/true") .validation_url("https://testdomain.com/valid") - .send(); + .send() + .await; assert!(response.is_ok()) ``` @@ -151,7 +154,8 @@ let response = client .short_code("600496") .msisdn("254700000000") .amount(1000) - .send(); + .send() + .await; assert!(response.is_ok()) ``` @@ -163,7 +167,8 @@ let response = client .result_url("https://testdomain.com/ok") .timeout_url("https://testdomain.com/err") .party_a("600496") - .send(); + .send() + .await; assert!(response.is_ok()) ``` @@ -175,7 +180,8 @@ let response = client .phone_number("254708374149") .amount(500) .callback_url("https://test.example.com/api") - .send(); + .send() + .await; assert!(response.is_ok()) ``` diff --git a/src/client.rs b/src/client.rs index 58d88e8f7..8602397fe 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,7 +7,7 @@ use super::services::{ }; use openssl::rsa::Padding; use openssl::x509::X509; -use reqwest::blocking::Client; +use reqwest::Client; use serde_json::Value; use std::cell::RefCell; @@ -40,7 +40,7 @@ impl<'a> Mpesa { /// ``` pub fn new(client_key: String, client_secret: String, environment: Environment) -> Self { let http_client = Client::builder() - .connect_timeout(std::time::Duration::from_millis(10000)) + .connect_timeout(std::time::Duration::from_millis(10_000)) .build() // TODO: Potentialy return a `Result` enum from Mpesa::new? .expect("Error building http client"); @@ -85,8 +85,8 @@ impl<'a> Mpesa { } /// Checks if the client can be authenticated - pub fn is_connected(&self) -> bool { - self.auth().is_ok() + pub async fn is_connected(&self) -> bool { + self.auth().await.is_ok() } /// **Safaricom Oauth** @@ -102,7 +102,7 @@ impl<'a> Mpesa { /// # Errors /// Returns a `MpesaError` on failure #[allow(clippy::single_char_pattern)] - pub(crate) fn auth(&self) -> MpesaResult { + pub(crate) async fn auth(&self) -> MpesaResult { let url = format!( "{}/oauth/v1/generate?grant_type=client_credentials", self.environment.base_url() @@ -111,12 +111,13 @@ impl<'a> Mpesa { .http_client .get(&url) .basic_auth(&self.client_key, Some(&self.client_secret)) - .send()?; + .send() + .await?; if resp.status().is_success() { // TODO: Needs custom return type: currently not casting the response to a custom type // hence why we need strip out double quotes `"` from the deserialized value // example: "value" -> value - let value: Value = resp.json()?; + let value: Value = resp.json().await?; return Ok(value["access_token"].to_string().replace('\"', "")); } Err(MpesaError::Message( diff --git a/src/lib.rs b/src/lib.rs index 0e2354fb2..3f2a3bfcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,181 +36,210 @@ //! * Only calling `unwrap` for demonstration purposes. Errors are handled appropriately in the lib via the `MpesaError` enum. //! * Use of `dotenv` is optional. //! -//! ```rust +//! ```rust,no_run //! use mpesa::{Mpesa, Environment}; //! use std::env; //! use dotenv::dotenv; //! -//! dotenv().ok(); -//! -//! let client: Mpesa = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! Environment::Sandbox, -//! ); -//! assert!(client.is_connected()) +//! #[tokio::main] +//! async fn main() { +//! dotenv().ok(); +//! +//! let client: Mpesa = Mpesa::new( +//! env::var("CLIENT_KEY").unwrap(), +//! env::var("CLIENT_SECRET").unwrap(), +//! Environment::Sandbox, +//! ); +//! assert!(client.is_connected().await) +//! } //! ``` //! //! Since the `Environment` enum implements `FromStr` and `TryFrom`, you can pass the name of the environment as a `&str` and call the `parse()` or `try_into()` //! method to create an `Environment` type from the string slice (Pascal case or Uppercase string slices also valid): //! -//! ```rust +//! ```rust,no_run //! use mpesa::Mpesa; //! use std::env; //! use dotenv::dotenv; //! -//! dotenv().ok(); -//! -//! let client: Mpesa = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), // "production" -//! ); -//! assert!(client.is_connected()) +//! #[tokio::main] +//! async fn main() { +//! dotenv().ok(); +//! +//! let client: Mpesa = Mpesa::new( +//! env::var("CLIENT_KEY").unwrap(), +//! env::var("CLIENT_SECRET").unwrap(), +//! "sandbox".parse().unwrap(), // "production" +//! ); +//! assert!(client.is_connected().await) +//! } //! ``` //! If you intend to use in production, you will need to call a the `set_initiator_password` method from `Mpesa` after initially //! creating the client. Here you provide your initiator password, which overrides the default password used in sandbox `"Safcom496!"`: //! -//! ```rust +//! ```rust,no_run //! use mpesa::Mpesa; //! use std::env; //! use dotenv::dotenv; //! -//! dotenv().ok(); +//! #[tokio::main] +//! async fn main() { +//! dotenv().ok(); //! -//! let client: Mpesa = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), -//! ); +//! let client: Mpesa = Mpesa::new( +//! env::var("CLIENT_KEY").unwrap(), +//! env::var("CLIENT_SECRET").unwrap(), +//! "sandbox".parse().unwrap(), // "production" +//! ); //! -//! client.set_initiator_password("new_password"); +//! client.set_initiator_password("new_password"); //! -//! assert!(client.is_connected()) +//! assert!(client.is_connected().await) +//! } //! ``` //! //!### Services //! The following services are currently available from the `Mpesa` client as methods that return builders: //! * B2C -//! ```rust +//! ```rust,no_run //! use mpesa::{Mpesa, MpesaResult, B2cResponse}; //! use std::env; //! use dotenv::dotenv; //! -//! dotenv().ok(); -//! -//! let client: Mpesa = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), -//! ); -//! -//! let response: MpesaResult = client -//! .b2c("testapi496") -//! .party_a("600496") -//! .party_b("254708374149") -//! .result_url("https://testdomain.com/ok") -//! .timeout_url("https://testdomain.com/err") -//! .amount(1000) -//! .send(); -//! assert!(response.is_ok()) +//! #[tokio::main] +//! async fn main() { +//! dotenv().ok(); +//! +//! let client: Mpesa = Mpesa::new( +//! env::var("CLIENT_KEY").unwrap(), +//! env::var("CLIENT_SECRET").unwrap(), +//! "sandbox".parse().unwrap(), // "production" +//! ); +//! +//! let response: MpesaResult = client +//! .b2c("testapi496") +//! .party_a("600496") +//! .party_b("254708374149") +//! .result_url("https://testdomain.com/ok") +//! .timeout_url("https://testdomain.com/err") +//! .amount(1000) +//! .send() +//! .await; +//! assert!(response.is_ok()) +//! } //! ``` //! //! * B2B -//! ```rust +//! ```rust,no_run //! use mpesa::{Mpesa, MpesaResult, B2bResponse}; //! use std::env; //! use dotenv::dotenv; //! -//! dotenv().ok(); -//! -//! let client: Mpesa = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), -//! ); -//! -//! let response: MpesaResult = client -//! .b2b("testapi496") -//! .party_a("600496") -//! .party_b("600000") -//! .result_url("https://testdomain.com/ok") -//! .timeout_url("https://testdomain.com/err") -//! .account_ref("254708374149") -//! .amount(1000) -//! .send(); +//! #[tokio::main] +//! async fn main() { +//! dotenv().ok(); +//! +//! let client: Mpesa = Mpesa::new( +//! env::var("CLIENT_KEY").unwrap(), +//! env::var("CLIENT_SECRET").unwrap(), +//! "sandbox".parse().unwrap(), +//! ); +//! +//! let response: MpesaResult = client +//! .b2b("testapi496") +//! .party_a("600496") +//! .party_b("600000") +//! .result_url("https://testdomain.com/ok") +//! .timeout_url("https://testdomain.com/err") +//! .account_ref("254708374149") +//! .amount(1000) +//! .send() +//! .await; //! assert!(response.is_ok()) +//! } //! ``` //! //! * C2B Register -//! ```ignore +//! ```rust,no_run //! use mpesa::{Mpesa, MpesaResult, C2bRegisterResponse}; //! use serde_json::Value; //! use std::env; //! use dotenv::dotenv; //! -//! dotenv().ok(); -//! -//! let client: Mpesa = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), -//! ); -//! -//! let response: MpesaResult = client -//! .c2b_register() -//! .short_code("600496") -//! .confirmation_url("https://testdomain.com/true") -//! .validation_url("https://testdomain.com/valid") -//! .send(); -//! assert!(response.is_ok()) +//! #[tokio::main] +//! async fn main() { +//! dotenv().ok(); +//! +//! let client: Mpesa = Mpesa::new( +//! env::var("CLIENT_KEY").unwrap(), +//! env::var("CLIENT_SECRET").unwrap(), +//! "sandbox".parse().unwrap(), // "production" +//! ); +//! +//! let response: MpesaResult = client +//! .c2b_register() +//! .short_code("600496") +//! .confirmation_url("https://testdomain.com/true") +//! .validation_url("https://testdomain.com/valid") +//! .send() +//! .await; +//! assert!(response.is_ok()) +//! } //! ``` //! //! * C2B Simulate -//! ```rust +//! ```rust,no_run //! use mpesa::{Mpesa, MpesaResult, C2bSimulateResponse}; //! use std::env; //! use dotenv::dotenv; //! -//! dotenv().ok(); -//! -//! let client: Mpesa = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), -//! ); -//! -//! let response: MpesaResult = client -//! .c2b_simulate() -//! .short_code("600496") -//! .msisdn("254700000000") -//! .amount(1000) -//! .send(); -//! assert!(response.is_ok()) +//! #[tokio::main] +//! async fn main() { +//! dotenv().ok(); +//! +//! let client: Mpesa = Mpesa::new( +//! env::var("CLIENT_KEY").unwrap(), +//! env::var("CLIENT_SECRET").unwrap(), +//! "sandbox".parse().unwrap(), +//! ); +//! +//! let response: MpesaResult = client +//! .c2b_simulate() +//! .short_code("600496") +//! .msisdn("254700000000") +//! .amount(1000) +//! .send() +//! .await; +//! assert!(response.is_ok()) +//! } //! ``` //! //! * Account Balance //! -//! ```rust +//! ```rust,no_run //! use mpesa::{Mpesa, MpesaResult, AccountBalanceResponse}; //! use std::env; //! use dotenv::dotenv; //! -//! dotenv().ok(); -//! -//! let client: Mpesa = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), -//! ); -//! -//! let response: MpesaResult = client -//! .account_balance("testapi496") -//! .result_url("https://testdomain.com/ok") -//! .timeout_url("https://testdomain.com/err") -//! .party_a("600496") -//! .send(); -//! assert!(response.is_ok()) +//! #[tokio::main] +//! async fn main() { +//! dotenv().ok(); +//! +//! let client: Mpesa = Mpesa::new( +//! env::var("CLIENT_KEY").unwrap(), +//! env::var("CLIENT_SECRET").unwrap(), +//! "sandbox".parse().unwrap(), +//! ); +//! +//! let response: MpesaResult = client +//! .account_balance("testapi496") +//! .result_url("https://testdomain.com/ok") +//! .timeout_url("https://testdomain.com/err") +//! .party_a("600496") +//! .send() +//! .await; +//! assert!(response.is_ok()) +//! } //! ``` //! //! * Mpesa Express Request / STK push/ Lipa na M-PESA online @@ -220,22 +249,25 @@ //! use std::env; //! use dotenv::dotenv; //! -//! dotenv().ok(); -//! -//! let client: Mpesa = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), -//! ); -//! -//! let response: MpesaResult = client -//! .express_request("174379") -//! .phone_number("254708374149") -//! .amount(500) -//! .callback_url("https://testdomain.com/ok") -//! .send(); -//! -//! assert!(response.is_ok()) +//! #[tokio::main] +//! async fn main() { +//! dotenv().ok(); +//! +//! let client: Mpesa = Mpesa::new( +//! env::var("CLIENT_KEY").unwrap(), +//! env::var("CLIENT_SECRET").unwrap(), +//! "sandbox".parse().unwrap(), +//! ); +//! +//! let response: MpesaResult = client +//! .express_request("174379") +//! .phone_number("254708374149") +//! .amount(500) +//! .callback_url("https://testdomain.com/ok") +//! .send() +//! .await; +//! assert!(response.is_ok()) +//! } //! ``` //! More will be added progressively, pull requests welcome //!## Author diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index 4d69c2676..1f09687bd 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -148,7 +148,7 @@ impl<'a> AccountBalanceBuilder<'a> { /// # Errors /// Returns a `MpesaError` on failure #[allow(clippy::unnecessary_lazy_evaluations)] - pub fn send(self) -> MpesaResult { + pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/accountbalance/v1/query", self.client.environment().base_url() @@ -174,16 +174,17 @@ impl<'a> AccountBalanceBuilder<'a> { .client .http_client .post(&url) - .bearer_auth(self.client.auth()?) + .bearer_auth(self.client.auth().await?) .json(&payload) - .send()?; + .send() + .await?; if response.status().is_success() { - let value: AccountBalanceResponse = response.json()?; + let value: AccountBalanceResponse = response.json().await?; return Ok(value); } - let value: Value = response.json()?; + let value: Value = response.json().await?; Err(MpesaError::AccountBalanceError(value)) } } diff --git a/src/services/b2b.rs b/src/services/b2b.rs index 40f2a8c18..b466712be 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -207,7 +207,7 @@ impl<'a> B2bBuilder<'a> { /// # Errors /// Returns a `MpesaError` on failure #[allow(clippy::unnecessary_lazy_evaluations)] - pub fn send(self) -> MpesaResult { + pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/b2b/v1/paymentrequest", self.client.environment().base_url() @@ -242,16 +242,17 @@ impl<'a> B2bBuilder<'a> { .client .http_client .post(&url) - .bearer_auth(self.client.auth()?) + .bearer_auth(self.client.auth().await?) .json(&payload) - .send()?; + .send() + .await?; if response.status().is_success() { - let value: B2bResponse = response.json()?; + let value: B2bResponse = response.json().await?; return Ok(value); } - let value: Value = response.json()?; + let value: Value = response.json().await?; Err(MpesaError::B2bError(value)) } } diff --git a/src/services/b2c.rs b/src/services/b2c.rs index b18cde1fe..82be5c088 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -184,7 +184,7 @@ impl<'a> B2cBuilder<'a> { /// # Errors /// Returns a `MpesaError` on failure. #[allow(clippy::unnecessary_lazy_evaluations)] - pub fn send(self) -> MpesaResult { + pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/b2c/v1/paymentrequest", self.client.environment().base_url() @@ -210,16 +210,17 @@ impl<'a> B2cBuilder<'a> { .client .http_client .post(&url) - .bearer_auth(self.client.auth()?) + .bearer_auth(self.client.auth().await?) .json(&payload) - .send()?; + .send() + .await?; if response.status().is_success() { - let value: B2cResponse = response.json()?; + let value: B2cResponse = response.json().await?; return Ok(value); } - let value: Value = response.json()?; + let value: Value = response.json().await?; Err(MpesaError::B2cError(value)) } } diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index 95f6ebb11..856135c6f 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -99,7 +99,7 @@ impl<'a> C2bRegisterBuilder<'a> { /// # Errors /// Returns a `MpesaError` on failure #[allow(clippy::unnecessary_lazy_evaluations)] - pub fn send(self) -> MpesaResult { + pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/c2b/v1/registerurl", self.client.environment().base_url() @@ -118,16 +118,17 @@ impl<'a> C2bRegisterBuilder<'a> { .client .http_client .post(&url) - .bearer_auth(self.client.auth()?) + .bearer_auth(self.client.auth().await?) .json(&payload) - .send()?; + .send() + .await?; if response.status().is_success() { - let value: C2bRegisterResponse = response.json()?; + let value: C2bRegisterResponse = response.json().await?; return Ok(value); } - let value: Value = response.json()?; + let value: Value = response.json().await?; Err(MpesaError::C2bRegisterError(value)) } } diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index 7e66ad81b..dfa209e66 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -115,7 +115,7 @@ impl<'a> C2bSimulateBuilder<'a> { /// # Errors /// Returns a `MpesaError` on failure #[allow(clippy::unnecessary_lazy_evaluations)] - pub fn send(self) -> MpesaResult { + pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/c2b/v1/simulate", self.client.environment().base_url() @@ -135,16 +135,17 @@ impl<'a> C2bSimulateBuilder<'a> { .client .http_client .post(&url) - .bearer_auth(self.client.auth()?) + .bearer_auth(self.client.auth().await?) .json(&payload) - .send()?; + .send() + .await?; if response.status().is_success() { - let value: C2bSimulateResponse = response.json()?; + let value: C2bSimulateResponse = response.json().await?; return Ok(value); } - let value: Value = response.json()?; + let value: Value = response.json().await?; Err(MpesaError::C2bSimulateError(value)) } } diff --git a/src/services/express_request.rs b/src/services/express_request.rs index 3ec620a0c..e2126e7bd 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -200,7 +200,7 @@ impl<'a> MpesaExpressRequestBuilder<'a> { /// Returns a `MpesaError` on failure #[allow(clippy::or_fun_call)] #[allow(clippy::unnecessary_lazy_evaluations)] - pub fn send(self) -> MpesaResult { + pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/stkpush/v1/processrequest", self.client.environment().base_url() @@ -236,16 +236,17 @@ impl<'a> MpesaExpressRequestBuilder<'a> { .client .http_client .post(&url) - .bearer_auth(self.client.auth()?) + .bearer_auth(self.client.auth().await?) .json(&payload) - .send()?; + .send() + .await?; if response.status().is_success() { - let value: MpesaExpressRequestResponse = response.json()?; + let value: MpesaExpressRequestResponse = response.json().await?; return Ok(value); } - let value: Value = response.json()?; + let value: Value = response.json().await?; Err(MpesaError::MpesaExpressRequestError(value)) } } diff --git a/tests/account_balance_test.rs b/tests/account_balance_test.rs index 7e40d68bd..9287b692f 100644 --- a/tests/account_balance_test.rs +++ b/tests/account_balance_test.rs @@ -2,8 +2,8 @@ use dotenv; use mpesa::{Mpesa, Sandbox}; use std::env; -#[test] -fn account_balance_test() { +#[tokio::test] +async fn account_balance_test() { dotenv::dotenv().ok(); let client = Mpesa::new( @@ -17,7 +17,8 @@ fn account_balance_test() { .result_url("https://testdomain.com/ok") .timeout_url("https://testdomain.com/err") .party_a("600496") - .send(); + .send() + .await; assert!(response.is_ok()) } diff --git a/tests/b2b_test.rs b/tests/b2b_test.rs index 0261a2d74..44e3a5b36 100644 --- a/tests/b2b_test.rs +++ b/tests/b2b_test.rs @@ -2,8 +2,8 @@ use dotenv; use mpesa::Mpesa; use std::env; -#[test] -fn b2b_test() { +#[tokio::test] +async fn b2b_test() { dotenv::dotenv().ok(); let client = Mpesa::new( @@ -20,7 +20,8 @@ fn b2b_test() { .timeout_url("https://testdomain.com/err") .account_ref("254708374149") .amount(1000) - .send(); + .send() + .await; assert!(response.is_ok()) } diff --git a/tests/b2c_test.rs b/tests/b2c_test.rs index 0e84e162f..bb380c38f 100644 --- a/tests/b2c_test.rs +++ b/tests/b2c_test.rs @@ -2,8 +2,8 @@ use dotenv; use mpesa::{Environment, Mpesa}; use std::env; -#[test] -fn b2c_test() { +#[tokio::test] +async fn b2c_test() { dotenv::dotenv().ok(); let client = Mpesa::new( @@ -19,7 +19,8 @@ fn b2c_test() { .result_url("https://testdomain.com/ok") .timeout_url("https://testdomain.com/err") .amount(1000) - .send(); + .send() + .await; assert!(response.is_ok()) } diff --git a/tests/c2b_register_test.rs b/tests/c2b_register_test.rs index e97da8c64..c7dc58e8d 100644 --- a/tests/c2b_register_test.rs +++ b/tests/c2b_register_test.rs @@ -2,9 +2,9 @@ use dotenv; use mpesa::Mpesa; use std::env; -#[test] +#[tokio::test] #[ignore = "c2b_register always fails on sandbox with status 503"] -fn c2b_register_test() { +async fn c2b_register_test() { dotenv::dotenv().ok(); let client = Mpesa::new( @@ -18,9 +18,8 @@ fn c2b_register_test() { .short_code("600496") .confirmation_url("https://testdomain.com/true") .validation_url("https://testdomain.com/valid") - .send(); - - println!("{response:?}"); + .send() + .await; assert!(response.is_ok()) } diff --git a/tests/c2b_simulate_test.rs b/tests/c2b_simulate_test.rs index ef7175639..52d8a523e 100644 --- a/tests/c2b_simulate_test.rs +++ b/tests/c2b_simulate_test.rs @@ -2,8 +2,8 @@ use dotenv; use mpesa::{Environment, Mpesa}; use std::env; -#[test] -fn c2b_simulate_test() { +#[tokio::test] +async fn c2b_simulate_test() { dotenv::dotenv().ok(); let client = Mpesa::new( @@ -17,7 +17,8 @@ fn c2b_simulate_test() { .short_code("600496") .msisdn("254700000000") .amount(1000) - .send(); + .send() + .await; assert!(response.is_ok()) } diff --git a/tests/stk_push_test.rs b/tests/stk_push_test.rs index a3d483272..3e7e8c8ba 100644 --- a/tests/stk_push_test.rs +++ b/tests/stk_push_test.rs @@ -2,8 +2,8 @@ use dotenv; use mpesa::Mpesa; use std::env; -#[test] -fn stk_push_test() { +#[tokio::test] +async fn stk_push_test() { dotenv::dotenv().ok(); let client = Mpesa::new( @@ -17,7 +17,8 @@ fn stk_push_test() { .phone_number("254708374149") .amount(500) .callback_url("https://test.example.com/api") - .send(); + .send() + .await; assert!(response.is_ok()) } From 7b3e45c8a3783d445d593e0649c15817a0c12c2d Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 19:24:11 +0300 Subject: [PATCH 039/140] Update docs --- CONTRIBUTING.md | 2 +- README.md | 9 ++++++++- src/lib.rs | 12 ++++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b5557a4cc..362841bba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,6 +3,6 @@ - [x] Unify `mpesa_derive` and `mpesa_core` - [ ] Add missing services: `reversal`, `transaction_status`, `bill_manager`, `dynamic_qr_code`, `c2b_simulate_v2` - [x] Clean up `Cargo.toml`: Correctly use Cargo features and declare optional and dev dependencies -- [ ] Convert library to async and update tests +- [x] Convert library to async and update tests - [ ] Migrate to `thiserror` and remove `failure` - [ ] Refine tests: test more edge cases \ No newline at end of file diff --git a/README.md b/README.md index 2a592c809..18093c289 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,14 @@ An unofficial Rust wrapper around the [Safaricom API](https://developer.safarico mpesa = "0.4.2" ``` -Optionally, you can disable default-features, which is basically the entire suite of MPESA APIs to conditionally select from either `["b2b", "b2c" ,"account_balance", "c2b_register", "c2b_simulate", "express_request"]` services. +Optionally, you can disable default-features, which is basically the entire suite of MPESA APIs to conditionally select from either: +- `b2b` +- `b2c` +- `account_balance` +- `c2b_register` +- `c2b_simulate` +- `express_request` + Example: ```toml diff --git a/src/lib.rs b/src/lib.rs index 3f2a3bfcf..413e05f7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,14 @@ //! [dependencies] //! mpesa = "0.4.2" //! ``` -//! Optionally, you can disable default-features, which is basically the entire suite of MPESA APIs to conditionally select from either `["b2b", "b2c" ,"account_balance", "c2b_register", "c2b_simulate", "express_request"]` services. +//! Optionally, you can disable default-features, which is basically the entire suite of MPESA APIs to conditionally select from either: +//! - `b2b` +//! - `b2c` +//! - `account_balance` +//! - `c2b_register` +//! - `c2b_simulate` +//! - `express_request` +//! //! Example: //! //! ```toml @@ -155,7 +162,7 @@ //! .amount(1000) //! .send() //! .await; -//! assert!(response.is_ok()) +//! assert!(response.is_ok()) //! } //! ``` //! @@ -270,6 +277,7 @@ //! } //! ``` //! More will be added progressively, pull requests welcome +//! //!## Author //! //! **Collins Muriuki** From 34b28c5dd4fd08a1c652b82ead76f46705e0a34d Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 19:30:49 +0300 Subject: [PATCH 040/140] Update ci pipelines --- .github/workflows/release-core.yml | 3 +-- .github/workflows/release-derive.yml | 27 --------------------------- src/lib.rs | 4 ++-- 3 files changed, 3 insertions(+), 31 deletions(-) delete mode 100644 .github/workflows/release-derive.yml diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index f51fb163b..d5444af51 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -3,7 +3,7 @@ name: publish to crates.io on: push: tags: - - 'core-0.*' + - '1.*' env: CARGO_TERM_COLOR: always @@ -21,7 +21,6 @@ jobs: override: true - name: Publish run: | - cd mpesa_core cargo doc cargo login ${{ secrets.CARGO_REGISTRY_TOKEN }} cargo publish diff --git a/.github/workflows/release-derive.yml b/.github/workflows/release-derive.yml deleted file mode 100644 index f812091ad..000000000 --- a/.github/workflows/release-derive.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: publish to crates.io - -on: - push: - tags: - - 'derive-0.*' - -env: - CARGO_TERM_COLOR: always - -jobs: - release_mpesa_core: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - name: Publish - run: | - cd mpesa_derive - cargo doc - cargo login ${{ secrets.CARGO_REGISTRY_TOKEN }} - cargo publish diff --git a/src/lib.rs b/src/lib.rs index 413e05f7a..53548bfd6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ //! - `c2b_register` //! - `c2b_simulate` //! - `express_request` -//! +//! //! Example: //! //! ```toml @@ -277,7 +277,7 @@ //! } //! ``` //! More will be added progressively, pull requests welcome -//! +//! //!## Author //! //! **Collins Muriuki** From 79819f015b5cbea26405f9d8339490b5602de9c2 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 20:15:02 +0300 Subject: [PATCH 041/140] Add todo coment on MpesaResponseCode enum --- src/constants.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/constants.rs b/src/constants.rs index 058c8b71b..e08c0210f 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -43,6 +43,7 @@ impl Display for IdentifierTypes { } } +/// TODO: Enable deserializing of json numbers/ strings to `MpesaResponseCode` /// M-pesa result and response codes #[derive(Debug, Copy, Clone, Deserialize_repr)] #[repr(u16)] From 1eaddac1009f947bf7d3380da293871e8fd47a79 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 15 Nov 2022 20:18:16 +0300 Subject: [PATCH 042/140] Update services docs --- src/services/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/services/mod.rs b/src/services/mod.rs index 3d8cc46d8..69b98b5b1 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -11,9 +11,6 @@ //! 4. [C2B Register](https://developer.safaricom.co.ke/docs?shell#c2b-api) //! 5. [C2B Simulate](https://developer.safaricom.co.ke/docs#account-balance-api) //! 6. [Mpesa Express/ STK Push](https://developer.safaricom.co.ke/docs#lipa-na-m-pesa-online-payment) -//! -//! Also worth noting I am using `reqwest::blocking::Client` to make http requests. Research ongoing -//! on how to make this crate async. mod account_balance; mod b2b; From b4b03c36e4066d40eb28b7df87dfa9c59e120a66 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 13:52:52 +0300 Subject: [PATCH 043/140] Reorganize testsl add a helper macro to get a test mpesa client --- .gitignore | 3 +- Cargo.toml | 17 +++++-- src/client.rs | 10 ++++ src/services/mod.rs | 2 + src/services/transaction_reversal.rs | 31 +++++++++++ .../{ => mpesa-rust}/account_balance_test.rs | 12 +---- tests/{ => mpesa-rust}/b2b_test.rs | 12 +---- tests/{ => mpesa-rust}/b2c_test.rs | 12 +---- tests/{ => mpesa-rust}/c2b_register_test.rs | 12 +---- tests/{ => mpesa-rust}/c2b_simulate_test.rs | 12 +---- tests/mpesa-rust/helpers.rs | 51 +++++++++++++++++++ tests/mpesa-rust/main.rs | 7 +++ tests/{ => mpesa-rust}/stk_push_test.rs | 0 13 files changed, 125 insertions(+), 56 deletions(-) create mode 100644 src/services/transaction_reversal.rs rename tests/{ => mpesa-rust}/account_balance_test.rs (58%) rename tests/{ => mpesa-rust}/b2b_test.rs (61%) rename tests/{ => mpesa-rust}/b2c_test.rs (58%) rename tests/{ => mpesa-rust}/c2b_register_test.rs (61%) rename tests/{ => mpesa-rust}/c2b_simulate_test.rs (50%) create mode 100644 tests/mpesa-rust/helpers.rs create mode 100644 tests/mpesa-rust/main.rs rename tests/{ => mpesa-rust}/stk_push_test.rs (100%) diff --git a/.gitignore b/.gitignore index 5c8ba229d..24a2d2013 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ /.idea Cargo.lock .env -.DS_Store -main.rs \ No newline at end of file +.DS_Store \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 3fb37f361..dfe92db4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,10 +31,19 @@ dotenv = "0.15" tokio = {version = "1", features = ["rt", "macros"]} [features] -default = ["b2b", "b2c" ,"account_balance", "c2b_register", "c2b_simulate", "express_request"] -b2b = ["dep:openssl", "dep:base64"] -b2c = ["dep:openssl", "dep:base64"] +default = [ + "account_balance", + "b2b", + "b2c", + "c2b_register", + "c2b_simulate", + "express_request", + "transaction_reversal" +] +b2b = ["dep:openssl", "dep:base64", "transaction_reversal"] +b2c = ["dep:openssl", "dep:base64", "transaction_reversal"] account_balance = ["dep:openssl", "dep:base64"] c2b_register = [] -c2b_simulate = [] +c2b_simulate = ["transaction_reversal"] express_request = ["dep:chrono", "dep:base64"] +transaction_reversal = ["dep:openssl", "dep:base64"] \ No newline at end of file diff --git a/src/client.rs b/src/client.rs index 8602397fe..13100f49b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,3 +1,4 @@ +use crate::services::TransactionReversalBuilder; use crate::MpesaError; use super::environment::Environment; @@ -273,6 +274,15 @@ impl<'a> Mpesa { ) -> MpesaExpressRequestBuilder<'a> { MpesaExpressRequestBuilder::new(self, business_short_code) } + + ///**Transaction Reversal Builder** + /// Reverses a B2B, B2C or C2B M-Pesa transaction. + /// + /// See more from the Safaricom API docs [here](https://developer.safaricom.co.ke/Documentation) + #[cfg(feature = "transaction_reversal")] + pub fn transaction_reversal(&'a self) -> TransactionReversalBuilder { + todo!() + } } impl Mpesa { diff --git a/src/services/mod.rs b/src/services/mod.rs index 69b98b5b1..bfce53490 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -18,6 +18,7 @@ mod b2c; mod c2b_register; mod c2b_simulate; mod express_request; +mod transaction_reversal; pub use account_balance::{AccountBalanceBuilder, AccountBalanceResponse}; pub use b2b::{B2bBuilder, B2bResponse}; @@ -25,3 +26,4 @@ pub use b2c::{B2cBuilder, B2cResponse}; pub use c2b_register::{C2bRegisterBuilder, C2bRegisterResponse}; pub use c2b_simulate::{C2bSimulateBuilder, C2bSimulateResponse}; pub use express_request::{MpesaExpressRequestBuilder, MpesaExpressRequestResponse}; +pub use transaction_reversal::{TransactionReversalBuilder, TransactionReversalResponse}; diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs new file mode 100644 index 000000000..de618efef --- /dev/null +++ b/src/services/transaction_reversal.rs @@ -0,0 +1,31 @@ +use serde::Serialize; + +use crate::CommandId; + +#[derive(Debug, Serialize)] +pub struct TransactionReversalPayload<'a> { + #[serde(rename(serialize = "Initiator"))] + initiator: &'a str, + #[serde(rename(serialize = "SecurityCredential"))] + security_credentials: &'a str, + #[serde(rename(serialize = "CommandID"))] + command_id: CommandId, + #[serde(rename(serialize = "TransactionID"))] + transaction_id: &'a str, + #[serde(rename(serialize = "ReceiverParty"))] + receiver_party: &'a str, + #[serde(rename(serialize = "ReceiverIdentifierType"))] + receiver_identifier_type: &'a str, + #[serde(rename(serialize = "ResultURL"))] + result_url: &'a str, + #[serde(rename(serialize = "QueueTimeOutURL"))] + queue_timeout_url: &'a str, + #[serde(rename(serialize = "Remarks"))] + remarks: &'a str, + #[serde(rename(serialize = "Occasion"))] + ocassion: &'a str, +} + +pub struct TransactionReversalBuilder; + +pub struct TransactionReversalResponse; diff --git a/tests/account_balance_test.rs b/tests/mpesa-rust/account_balance_test.rs similarity index 58% rename from tests/account_balance_test.rs rename to tests/mpesa-rust/account_balance_test.rs index 9287b692f..aa8770327 100644 --- a/tests/account_balance_test.rs +++ b/tests/mpesa-rust/account_balance_test.rs @@ -1,16 +1,8 @@ -use dotenv; -use mpesa::{Mpesa, Sandbox}; -use std::env; +use crate::get_mpesa_client; #[tokio::test] async fn account_balance_test() { - dotenv::dotenv().ok(); - - let client = Mpesa::new( - env::var("CLIENT_KEY").unwrap(), - env::var("CLIENT_SECRET").unwrap(), - Sandbox, - ); + let client = get_mpesa_client!(); let response = client .account_balance("testapi496") diff --git a/tests/b2b_test.rs b/tests/mpesa-rust/b2b_test.rs similarity index 61% rename from tests/b2b_test.rs rename to tests/mpesa-rust/b2b_test.rs index 44e3a5b36..521fa1c52 100644 --- a/tests/b2b_test.rs +++ b/tests/mpesa-rust/b2b_test.rs @@ -1,16 +1,8 @@ -use dotenv; -use mpesa::Mpesa; -use std::env; +use crate::get_mpesa_client; #[tokio::test] async fn b2b_test() { - dotenv::dotenv().ok(); - - let client = Mpesa::new( - env::var("CLIENT_KEY").unwrap(), - env::var("CLIENT_SECRET").unwrap(), - "sandbox".parse().unwrap(), - ); + let client = get_mpesa_client!(); let response = client .b2b("testapi496") diff --git a/tests/b2c_test.rs b/tests/mpesa-rust/b2c_test.rs similarity index 58% rename from tests/b2c_test.rs rename to tests/mpesa-rust/b2c_test.rs index bb380c38f..f2223ce53 100644 --- a/tests/b2c_test.rs +++ b/tests/mpesa-rust/b2c_test.rs @@ -1,16 +1,8 @@ -use dotenv; -use mpesa::{Environment, Mpesa}; -use std::env; +use crate::get_mpesa_client; #[tokio::test] async fn b2c_test() { - dotenv::dotenv().ok(); - - let client = Mpesa::new( - env::var("CLIENT_KEY").unwrap(), - env::var("CLIENT_SECRET").unwrap(), - Environment::Sandbox, - ); + let client = get_mpesa_client!(); let response = client .b2c("testapi496") diff --git a/tests/c2b_register_test.rs b/tests/mpesa-rust/c2b_register_test.rs similarity index 61% rename from tests/c2b_register_test.rs rename to tests/mpesa-rust/c2b_register_test.rs index c7dc58e8d..d6b00fd46 100644 --- a/tests/c2b_register_test.rs +++ b/tests/mpesa-rust/c2b_register_test.rs @@ -1,17 +1,9 @@ -use dotenv; -use mpesa::Mpesa; -use std::env; +use crate::get_mpesa_client; #[tokio::test] #[ignore = "c2b_register always fails on sandbox with status 503"] async fn c2b_register_test() { - dotenv::dotenv().ok(); - - let client = Mpesa::new( - env::var("CLIENT_KEY").unwrap(), - env::var("CLIENT_SECRET").unwrap(), - "sandbox".parse().unwrap(), - ); + let client = get_mpesa_client!(); let response = client .c2b_register() diff --git a/tests/c2b_simulate_test.rs b/tests/mpesa-rust/c2b_simulate_test.rs similarity index 50% rename from tests/c2b_simulate_test.rs rename to tests/mpesa-rust/c2b_simulate_test.rs index 52d8a523e..c04ca6e76 100644 --- a/tests/c2b_simulate_test.rs +++ b/tests/mpesa-rust/c2b_simulate_test.rs @@ -1,16 +1,8 @@ -use dotenv; -use mpesa::{Environment, Mpesa}; -use std::env; +use crate::get_mpesa_client; #[tokio::test] async fn c2b_simulate_test() { - dotenv::dotenv().ok(); - - let client = Mpesa::new( - env::var("CLIENT_KEY").unwrap(), - env::var("CLIENT_SECRET").unwrap(), - Environment::Sandbox, - ); + let client = get_mpesa_client!(); let response = client .c2b_simulate() diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs new file mode 100644 index 000000000..92afb7c14 --- /dev/null +++ b/tests/mpesa-rust/helpers.rs @@ -0,0 +1,51 @@ +#[macro_export] +macro_rules! get_mpesa_client { + () => {{ + dotenv::dotenv().ok(); + mpesa::Mpesa::new( + std::env::var("CLIENT_KEY").unwrap(), + std::env::var("CLIENT_SECRET").unwrap(), + "sandbox".parse().unwrap(), + ) + }}; + + ($client_key:expr, $client_secret:expr) => {{ + dotenv::dotenv().ok(); + mpesa::Mpesa::new($client_key, $client_secret, "sandbox".parse().unwrap()) + }}; + + ($client_key:expr, $client_secret:expr, $environment:expr) => {{ + dotenv::dotenv().ok(); + mpesa::Mpesa::new($client_key, $client_secret, $environment) + }}; +} + +#[cfg(test)] +mod tests { + use crate::get_mpesa_client; + + #[tokio::test] + async fn test_client_is_created_successfuly_with_correct_credentials() { + let client = get_mpesa_client!(); + assert!(client.is_connected().await); + } + + #[tokio::test] + async fn test_client_will_not_authenticate_with_wrong_credentials() { + let client = get_mpesa_client!( + "not a client key".to_string(), + "not a client secret".to_string() + ); + assert!(!client.is_connected().await); + } + + #[tokio::test] + async fn test_client_will_not_authenticate_with_sandbox_credentials_in_production() { + let client = get_mpesa_client!( + std::env::var("CLIENT_KEY").unwrap(), + std::env::var("CLIENT_SECRET").unwrap(), + "production".parse().unwrap() + ); + assert!(!client.is_connected().await); + } +} diff --git a/tests/mpesa-rust/main.rs b/tests/mpesa-rust/main.rs new file mode 100644 index 000000000..214d81dab --- /dev/null +++ b/tests/mpesa-rust/main.rs @@ -0,0 +1,7 @@ +mod account_balance_test; +mod b2b_test; +mod b2c_test; +mod c2b_register_test; +mod c2b_simulate_test; +mod helpers; +mod stk_push_test; diff --git a/tests/stk_push_test.rs b/tests/mpesa-rust/stk_push_test.rs similarity index 100% rename from tests/stk_push_test.rs rename to tests/mpesa-rust/stk_push_test.rs From ebfe4e9425ed5c7f2e5c7378ca19bf8e5dbf1e3f Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 15:07:18 +0300 Subject: [PATCH 044/140] Major update to how api environments are handled; add provision for mock servers --- README.md | 19 ++++++++--- src/client.rs | 26 ++++++++-------- src/environment.rs | 13 ++++++-- src/lib.rs | 52 ++++++++++++++++--------------- src/services/account_balance.rs | 27 +++++++++------- src/services/b2b.rs | 33 ++++++++++---------- src/services/b2c.rs | 29 ++++++++--------- src/services/c2b_register.rs | 17 +++++----- src/services/c2b_simulate.rs | 19 +++++------ src/services/express_request.rs | 33 ++++++++++++-------- tests/mpesa-rust/helpers.rs | 35 ++++++++++++++++++--- tests/mpesa-rust/stk_push_test.rs | 12 ++----- 12 files changed, 184 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index 18093c289..e06b63775 100644 --- a/README.md +++ b/README.md @@ -75,15 +75,24 @@ Since the `Environment` enum implements `FromStr` and `TryFrom`, you can pass th method to create an `Environment` type from the string slice (Pascal case and Uppercase string slices also valid): ```rust -use mpesa::Mpesa; +use mpesa::{Mpesa, Environment}; +use std::str::FromStr; +use std::convert::TryFrom; use std::env; -let client = Mpesa::new( +let client0 = Mpesa::new( env::var("CLIENT_KEY")?, env::var("CLIENT_SECRET")?, - "sandbox".parse()?, // "production" + Environment::from_str("sandbox").unwrap() ); -assert!(client.is_connected().await) + +let client1 = Mpesa::new( + env::var("CLIENT_KEY")?, + env::var("CLIENT_SECRET")?, + Environment::try_from("sandbox").unwrap() +); +assert!(client0.is_connected().await) +assert!(client1.is_connected().await) ``` If you intend to use in production, you will need to call a the `set_initiator_password` method from `Mpesa` after initially @@ -96,7 +105,7 @@ use std::env; let client = Mpesa::new( env::var("CLIENT_KEY")?, env::var("CLIENT_SECRET")?, - "production".parse()?, + Environment::Sandbox, ); client.set_initiator_password("new_password"); diff --git a/src/client.rs b/src/client.rs index 13100f49b..e02da4c4e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,7 +1,7 @@ +use crate::environment::ApiEnvironment; use crate::services::TransactionReversalBuilder; use crate::MpesaError; -use super::environment::Environment; use super::services::{ AccountBalanceBuilder, B2bBuilder, B2cBuilder, C2bRegisterBuilder, C2bSimulateBuilder, MpesaExpressRequestBuilder, @@ -20,15 +20,15 @@ pub type MpesaResult = Result; /// Mpesa client that will facilitate communication with the Safaricom API #[derive(Debug)] -pub struct Mpesa { +pub struct Mpesa { client_key: String, client_secret: String, initiator_password: RefCell>, - environment: Environment, + environment: Env, pub(crate) http_client: Client, } -impl<'a> Mpesa { +impl<'a, Env: ApiEnvironment> Mpesa { /// Constructs a new `Mpesa` instance. /// /// # Example @@ -39,7 +39,7 @@ impl<'a> Mpesa { /// "sandbox".parse().unwrap(), /// ); /// ``` - pub fn new(client_key: String, client_secret: String, environment: Environment) -> Self { + pub fn new(client_key: String, client_secret: String, environment: Env) -> Self { let http_client = Client::builder() .connect_timeout(std::time::Duration::from_millis(10_000)) .build() @@ -55,7 +55,7 @@ impl<'a> Mpesa { } /// Gets the current `Environment` - pub(crate) fn environment(&'a self) -> &Environment { + pub(crate) fn environment(&'a self) -> &Env { &self.environment } @@ -149,7 +149,7 @@ impl<'a> Mpesa { /// .send(); /// ``` #[cfg(feature = "b2c")] - pub fn b2c(&'a self, initiator_name: &'a str) -> B2cBuilder<'a> { + pub fn b2c(&'a self, initiator_name: &'a str) -> B2cBuilder<'a, Env> { B2cBuilder::new(self, initiator_name) } @@ -177,7 +177,7 @@ impl<'a> Mpesa { /// .send(); /// ``` #[cfg(feature = "b2b")] - pub fn b2b(&'a self, initiator_name: &'a str) -> B2bBuilder<'a> { + pub fn b2b(&'a self, initiator_name: &'a str) -> B2bBuilder<'a, Env> { B2bBuilder::new(self, initiator_name) } @@ -198,7 +198,7 @@ impl<'a> Mpesa { /// .send(); /// ``` #[cfg(feature = "c2b_register")] - pub fn c2b_register(&'a self) -> C2bRegisterBuilder<'a> { + pub fn c2b_register(&'a self) -> C2bRegisterBuilder<'a, Env> { C2bRegisterBuilder::new(self) } @@ -219,7 +219,7 @@ impl<'a> Mpesa { /// .send(); /// ``` #[cfg(feature = "c2b_simulate")] - pub fn c2b_simulate(&'a self) -> C2bSimulateBuilder<'a> { + pub fn c2b_simulate(&'a self) -> C2bSimulateBuilder<'a, Env> { C2bSimulateBuilder::new(self) } @@ -243,7 +243,7 @@ impl<'a> Mpesa { /// .send(); /// ``` #[cfg(feature = "account_balance")] - pub fn account_balance(&'a self, initiator_name: &'a str) -> AccountBalanceBuilder<'a> { + pub fn account_balance(&'a self, initiator_name: &'a str) -> AccountBalanceBuilder<'a, Env> { AccountBalanceBuilder::new(self, initiator_name) } @@ -271,7 +271,7 @@ impl<'a> Mpesa { pub fn express_request( &'a self, business_short_code: &'a str, - ) -> MpesaExpressRequestBuilder<'a> { + ) -> MpesaExpressRequestBuilder<'a, Env> { MpesaExpressRequestBuilder::new(self, business_short_code) } @@ -285,7 +285,7 @@ impl<'a> Mpesa { } } -impl Mpesa { +impl Mpesa { /// Generates 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. diff --git a/src/environment.rs b/src/environment.rs index a9e18c47b..398b68962 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -24,6 +24,13 @@ pub enum Environment { Sandbox, } +/// Expected behavior of an `Mpesa` client environment +/// This abstraction exists to make it possible to mock the MPESA api server for tests +pub trait ApiEnvironment { + fn base_url(&self) -> &'static str; + fn get_certificate(&self) -> &'static str; +} + macro_rules! environment_from_string { ($v:expr) => { match $v { @@ -60,9 +67,9 @@ impl TryFrom for Environment { } } -impl Environment { +impl ApiEnvironment for Environment { /// Matches to base_url based on `Environment` variant - pub(crate) fn base_url(&self) -> &str { + fn base_url(&self) -> &'static str { match self { Environment::Production => "https://api.safaricom.co.ke", Environment::Sandbox => "https://sandbox.safaricom.co.ke", @@ -70,7 +77,7 @@ impl Environment { } /// Match to X509 public key certificate based on `Environment` - pub(crate) fn get_certificate(&self) -> &str { + fn get_certificate(&self) -> &'static str { match self { Environment::Production => include_str!("./certificates/production"), Environment::Sandbox => include_str!("./certificates/sandbox"), diff --git a/src/lib.rs b/src/lib.rs index 53548bfd6..f871aea1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,7 @@ //! async fn main() { //! dotenv().ok(); //! -//! let client: Mpesa = Mpesa::new( +//! let client = Mpesa::new( //! env::var("CLIENT_KEY").unwrap(), //! env::var("CLIENT_SECRET").unwrap(), //! Environment::Sandbox, @@ -65,18 +65,19 @@ //! method to create an `Environment` type from the string slice (Pascal case or Uppercase string slices also valid): //! //! ```rust,no_run -//! use mpesa::Mpesa; +//! use mpesa::{Mpesa, Environment}; //! use std::env; //! use dotenv::dotenv; +//! use std::str::FromStr; //! //! #[tokio::main] //! async fn main() { //! dotenv().ok(); //! -//! let client: Mpesa = Mpesa::new( +//! let client = Mpesa::new( //! env::var("CLIENT_KEY").unwrap(), //! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), // "production" +//! Environment::from_str("sandbox").unwrap() //! ); //! assert!(client.is_connected().await) //! } @@ -85,7 +86,7 @@ //! creating the client. Here you provide your initiator password, which overrides the default password used in sandbox `"Safcom496!"`: //! //! ```rust,no_run -//! use mpesa::Mpesa; +//! use mpesa::{Mpesa, Environment}; //! use std::env; //! use dotenv::dotenv; //! @@ -93,10 +94,10 @@ //! async fn main() { //! dotenv().ok(); //! -//! let client: Mpesa = Mpesa::new( +//! let client = Mpesa::new( //! env::var("CLIENT_KEY").unwrap(), //! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), // "production" +//! Environment::Sandbox //! ); //! //! client.set_initiator_password("new_password"); @@ -109,7 +110,7 @@ //! The following services are currently available from the `Mpesa` client as methods that return builders: //! * B2C //! ```rust,no_run -//! use mpesa::{Mpesa, MpesaResult, B2cResponse}; +//! use mpesa::{Mpesa, Environment, MpesaResult, B2cResponse}; //! use std::env; //! use dotenv::dotenv; //! @@ -117,10 +118,10 @@ //! async fn main() { //! dotenv().ok(); //! -//! let client: Mpesa = Mpesa::new( +//! let client = Mpesa::new( //! env::var("CLIENT_KEY").unwrap(), //! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), // "production" +//! Environment::Sandbox //! ); //! //! let response: MpesaResult = client @@ -138,7 +139,7 @@ //! //! * B2B //! ```rust,no_run -//! use mpesa::{Mpesa, MpesaResult, B2bResponse}; +//! use mpesa::{Mpesa, Environment, MpesaResult, B2bResponse}; //! use std::env; //! use dotenv::dotenv; //! @@ -146,10 +147,10 @@ //! async fn main() { //! dotenv().ok(); //! -//! let client: Mpesa = Mpesa::new( +//! let client = Mpesa::new( //! env::var("CLIENT_KEY").unwrap(), //! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), +//! Environment::Sandbox //! ); //! //! let response: MpesaResult = client @@ -168,7 +169,7 @@ //! //! * C2B Register //! ```rust,no_run -//! use mpesa::{Mpesa, MpesaResult, C2bRegisterResponse}; +//! use mpesa::{Mpesa, Environment, MpesaResult, C2bRegisterResponse}; //! use serde_json::Value; //! use std::env; //! use dotenv::dotenv; @@ -177,10 +178,10 @@ //! async fn main() { //! dotenv().ok(); //! -//! let client: Mpesa = Mpesa::new( +//! let client = Mpesa::new( //! env::var("CLIENT_KEY").unwrap(), //! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), // "production" +//! Environment::Sandbox //! ); //! //! let response: MpesaResult = client @@ -196,7 +197,7 @@ //! //! * C2B Simulate //! ```rust,no_run -//! use mpesa::{Mpesa, MpesaResult, C2bSimulateResponse}; +//! use mpesa::{Mpesa, Environment, MpesaResult, C2bSimulateResponse}; //! use std::env; //! use dotenv::dotenv; //! @@ -204,10 +205,10 @@ //! async fn main() { //! dotenv().ok(); //! -//! let client: Mpesa = Mpesa::new( +//! let client = Mpesa::new( //! env::var("CLIENT_KEY").unwrap(), //! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), +//! Environment::Sandbox //! ); //! //! let response: MpesaResult = client @@ -224,7 +225,7 @@ //! * Account Balance //! //! ```rust,no_run -//! use mpesa::{Mpesa, MpesaResult, AccountBalanceResponse}; +//! use mpesa::{Mpesa, MpesaResult, Environment, AccountBalanceResponse}; //! use std::env; //! use dotenv::dotenv; //! @@ -232,10 +233,10 @@ //! async fn main() { //! dotenv().ok(); //! -//! let client: Mpesa = Mpesa::new( +//! let client = Mpesa::new( //! env::var("CLIENT_KEY").unwrap(), //! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), +//! Environment::Sandbox //! ); //! //! let response: MpesaResult = client @@ -252,7 +253,7 @@ //! * Mpesa Express Request / STK push/ Lipa na M-PESA online //! //! ```ignore -//! use mpesa::{Mpesa, MpesaResult, MpesaExpressRequestResponse}; +//! use mpesa::{Mpesa, MpesaResult, Environment, MpesaExpressRequestResponse}; //! use std::env; //! use dotenv::dotenv; //! @@ -260,10 +261,10 @@ //! async fn main() { //! dotenv().ok(); //! -//! let client: Mpesa = Mpesa::new( +//! let client = Mpesa::new( //! env::var("CLIENT_KEY").unwrap(), //! env::var("CLIENT_SECRET").unwrap(), -//! "sandbox".parse().unwrap(), +//! Environment::Sandbox //! ); //! //! let response: MpesaResult = client @@ -296,6 +297,7 @@ pub mod services; pub use client::{Mpesa, MpesaResult}; pub use constants::{CommandId, IdentifierTypes, ResponseType}; +pub use environment::ApiEnvironment; pub use environment::Environment::{self, Production, Sandbox}; pub use errors::MpesaError; pub use services::{ diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index 1f09687bd..f47d19180 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -1,5 +1,6 @@ use crate::client::MpesaResult; use crate::constants::{CommandId, IdentifierTypes}; +use crate::environment::ApiEnvironment; use crate::{Mpesa, MpesaError}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -43,9 +44,9 @@ pub struct AccountBalanceResponse { pub response_description: String, } #[derive(Debug)] -pub struct AccountBalanceBuilder<'a> { +pub struct AccountBalanceBuilder<'a, Env: ApiEnvironment> { initiator_name: &'a str, - client: &'a Mpesa, + client: &'a Mpesa, command_id: Option, party_a: Option<&'a str>, identifier_type: Option, @@ -54,10 +55,10 @@ pub struct AccountBalanceBuilder<'a> { result_url: Option<&'a str>, } -impl<'a> AccountBalanceBuilder<'a> { +impl<'a, Env: ApiEnvironment> AccountBalanceBuilder<'a, Env> { /// Creates a new `AccountBalanceBuilder`. /// Requires an `initiator_name`, the credential/ username used to authenticate the transaction request - pub fn new(client: &'a Mpesa, initiator_name: &'a str) -> AccountBalanceBuilder<'a> { + pub fn new(client: &'a Mpesa, initiator_name: &'a str) -> AccountBalanceBuilder<'a, Env> { AccountBalanceBuilder { initiator_name, client, @@ -75,7 +76,7 @@ impl<'a> AccountBalanceBuilder<'a> { /// /// # Errors /// If `CommandId` is invalid - pub fn command_id(mut self, command_id: CommandId) -> AccountBalanceBuilder<'a> { + pub fn command_id(mut self, command_id: CommandId) -> AccountBalanceBuilder<'a, Env> { self.command_id = Some(command_id); self } @@ -85,7 +86,7 @@ impl<'a> AccountBalanceBuilder<'a> { /// /// # Errors /// If `Party A` is not provided or invalid - pub fn party_a(mut self, party_a: &'a str) -> AccountBalanceBuilder<'a> { + pub fn party_a(mut self, party_a: &'a str) -> AccountBalanceBuilder<'a, Env> { self.party_a = Some(party_a); self } @@ -98,14 +99,14 @@ impl<'a> AccountBalanceBuilder<'a> { pub fn identifier_type( mut self, identifier_type: IdentifierTypes, - ) -> AccountBalanceBuilder<'a> { + ) -> AccountBalanceBuilder<'a, Env> { self.identifier_type = Some(identifier_type); self } /// Adds `Remarks`, a comment sent along transaction. /// Optional field that defaults to `"None"` if no value is provided - pub fn remarks(mut self, remarks: &'a str) -> AccountBalanceBuilder<'a> { + pub fn remarks(mut self, remarks: &'a str) -> AccountBalanceBuilder<'a, Env> { self.remarks = Some(remarks); self } @@ -114,7 +115,7 @@ impl<'a> AccountBalanceBuilder<'a> { /// /// # Error /// If `QueueTimeoutUrl` is invalid or not provided - pub fn timeout_url(mut self, timeout_url: &'a str) -> AccountBalanceBuilder<'a> { + pub fn timeout_url(mut self, timeout_url: &'a str) -> AccountBalanceBuilder<'a, Env> { self.queue_timeout_url = Some(timeout_url); self } @@ -123,7 +124,7 @@ impl<'a> AccountBalanceBuilder<'a> { /// /// # Error /// If `ResultUrl` is invalid or not provided - pub fn result_url(mut self, result_url: &'a str) -> AccountBalanceBuilder<'a> { + pub fn result_url(mut self, result_url: &'a str) -> AccountBalanceBuilder<'a, Env> { self.result_url = Some(result_url); self } @@ -133,7 +134,11 @@ impl<'a> AccountBalanceBuilder<'a> { /// # Error /// If either `QueueTimeoutUrl` and `ResultUrl` is invalid or not provided #[deprecated] - pub fn urls(mut self, timeout_url: &'a str, result_url: &'a str) -> AccountBalanceBuilder<'a> { + pub fn urls( + mut self, + timeout_url: &'a str, + result_url: &'a str, + ) -> AccountBalanceBuilder<'a, Env> { self.queue_timeout_url = Some(timeout_url); self.result_url = Some(result_url); self diff --git a/src/services/b2b.rs b/src/services/b2b.rs index b466712be..30d78c83e 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -1,5 +1,6 @@ use crate::client::{Mpesa, MpesaResult}; use crate::constants::{CommandId, IdentifierTypes}; +use crate::environment::ApiEnvironment; use crate::errors::MpesaError; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -55,9 +56,9 @@ pub struct B2bResponse { #[derive(Debug)] /// B2B transaction builder struct -pub struct B2bBuilder<'a> { +pub struct B2bBuilder<'a, Env: ApiEnvironment> { initiator_name: &'a str, - client: &'a Mpesa, + client: &'a Mpesa, command_id: Option, amount: Option, party_a: Option<&'a str>, @@ -70,10 +71,10 @@ pub struct B2bBuilder<'a> { account_ref: Option<&'a str>, } -impl<'a> B2bBuilder<'a> { +impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { /// Creates a new B2B builder /// Requires an `initiator_name`, the credential/ username used to authenticate the transaction request - pub fn new(client: &'a Mpesa, initiator_name: &'a str) -> B2bBuilder<'a> { + pub fn new(client: &'a Mpesa, initiator_name: &'a str) -> B2bBuilder<'a, Env> { B2bBuilder { client, initiator_name, @@ -94,7 +95,7 @@ impl<'a> B2bBuilder<'a> { /// /// # Errors /// If invalid `CommandId` is provided - pub fn command_id(mut self, command_id: CommandId) -> B2bBuilder<'a> { + pub fn command_id(mut self, command_id: CommandId) -> B2bBuilder<'a, Env> { self.command_id = Some(command_id); self } @@ -104,7 +105,7 @@ impl<'a> B2bBuilder<'a> { /// /// # Errors /// If `Party A` is invalid or not provided - pub fn party_a(mut self, party_a: &'a str) -> B2bBuilder<'a> { + pub fn party_a(mut self, party_a: &'a str) -> B2bBuilder<'a, Env> { self.party_a = Some(party_a); self } @@ -114,7 +115,7 @@ impl<'a> B2bBuilder<'a> { /// /// # Errors /// If `Party B` is invalid or not provided - pub fn party_b(mut self, party_b: &'a str) -> B2bBuilder<'a> { + pub fn party_b(mut self, party_b: &'a str) -> B2bBuilder<'a, Env> { self.party_b = Some(party_b); self } @@ -125,7 +126,7 @@ impl<'a> B2bBuilder<'a> { /// # Errors /// If either `Party A` or `Party B` is invalid or not provided #[deprecated] - pub fn parties(mut self, party_a: &'a str, party_b: &'a str) -> B2bBuilder<'a> { + pub fn parties(mut self, party_a: &'a str, party_b: &'a str) -> B2bBuilder<'a, Env> { self.party_a = Some(party_a); self.party_b = Some(party_b); self @@ -135,7 +136,7 @@ impl<'a> B2bBuilder<'a> { /// /// # Error /// If `QueueTimeoutUrl` is invalid or not provided - pub fn timeout_url(mut self, timeout_url: &'a str) -> B2bBuilder<'a> { + pub fn timeout_url(mut self, timeout_url: &'a str) -> B2bBuilder<'a, Env> { self.queue_timeout_url = Some(timeout_url); self } @@ -144,7 +145,7 @@ impl<'a> B2bBuilder<'a> { /// /// # Error /// If `ResultUrl` is invalid or not provided - pub fn result_url(mut self, result_url: &'a str) -> B2bBuilder<'a> { + pub fn result_url(mut self, result_url: &'a str) -> B2bBuilder<'a, Env> { self.result_url = Some(result_url); self } @@ -154,7 +155,7 @@ impl<'a> B2bBuilder<'a> { /// # Error /// If either `QueueTimeoutUrl` and `ResultUrl` is invalid or not provided #[deprecated] - pub fn urls(mut self, timeout_url: &'a str, result_url: &'a str) -> B2bBuilder<'a> { + pub fn urls(mut self, timeout_url: &'a str, result_url: &'a str) -> B2bBuilder<'a, Env> { // TODO: validate urls self.queue_timeout_url = Some(timeout_url); self.result_url = Some(result_url); @@ -162,19 +163,19 @@ impl<'a> B2bBuilder<'a> { } /// Adds `sender_id`. Will default to `IdentifierTypes::ShortCode` if not explicitly provided - pub fn sender_id(mut self, sender_id: IdentifierTypes) -> B2bBuilder<'a> { + pub fn sender_id(mut self, sender_id: IdentifierTypes) -> B2bBuilder<'a, Env> { self.sender_id = Some(sender_id); self } /// Adds `receiver_id`. Will default to `IdentifierTypes::ShortCode` if not explicitly provided - pub fn receiver_id(mut self, receiver_id: IdentifierTypes) -> B2bBuilder<'a> { + pub fn receiver_id(mut self, receiver_id: IdentifierTypes) -> B2bBuilder<'a, Env> { self.receiver_id = Some(receiver_id); self } /// Adds `account_ref`. This field is required - pub fn account_ref(mut self, account_ref: &'a str) -> B2bBuilder<'a> { + pub fn account_ref(mut self, account_ref: &'a str) -> B2bBuilder<'a, Env> { // TODO: add validation self.account_ref = Some(account_ref); self @@ -182,13 +183,13 @@ impl<'a> B2bBuilder<'a> { /// Adds an `amount` to the request /// This is a required field - pub fn amount(mut self, amount: u32) -> B2bBuilder<'a> { + pub fn amount(mut self, amount: u32) -> B2bBuilder<'a, Env> { self.amount = Some(amount); self } /// Adds `remarks`. This field is optional, will default to "None" if not explicitly passed - pub fn remarks(mut self, remarks: &'a str) -> B2bBuilder<'a> { + pub fn remarks(mut self, remarks: &'a str) -> B2bBuilder<'a, Env> { self.remarks = Some(remarks); self } diff --git a/src/services/b2c.rs b/src/services/b2c.rs index 82be5c088..00e5830e0 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -1,4 +1,5 @@ use crate::client::MpesaResult; +use crate::environment::ApiEnvironment; use crate::{CommandId, Mpesa, MpesaError}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -51,9 +52,9 @@ pub struct B2cResponse { #[derive(Debug)] /// B2C transaction builder struct -pub struct B2cBuilder<'a> { +pub struct B2cBuilder<'a, Env: ApiEnvironment> { initiator_name: &'a str, - client: &'a Mpesa, + client: &'a Mpesa, command_id: Option, amount: Option, party_a: Option<&'a str>, @@ -64,10 +65,10 @@ pub struct B2cBuilder<'a> { occasion: Option<&'a str>, } -impl<'a> B2cBuilder<'a> { +impl<'a, Env: ApiEnvironment> B2cBuilder<'a, Env> { /// Create a new B2C builder. /// Requires an `initiator_name`, the credential/ username used to authenticate the transaction request - pub fn new(client: &'a Mpesa, initiator_name: &'a str) -> B2cBuilder<'a> { + pub fn new(client: &'a Mpesa, initiator_name: &'a str) -> B2cBuilder<'a, Env> { B2cBuilder { client, initiator_name, @@ -83,7 +84,7 @@ impl<'a> B2cBuilder<'a> { } /// Adds the `CommandId`. Defaults to `CommandId::BusinessPayment` if not explicitly provided. - pub fn command_id(mut self, command_id: CommandId) -> B2cBuilder<'a> { + pub fn command_id(mut self, command_id: CommandId) -> B2cBuilder<'a, Env> { self.command_id = Some(command_id); self } @@ -93,7 +94,7 @@ impl<'a> B2cBuilder<'a> { /// /// # Errors /// If `Party A` is invalid or not provided - pub fn party_a(mut self, party_a: &'a str) -> B2cBuilder<'a> { + pub fn party_a(mut self, party_a: &'a str) -> B2cBuilder<'a, Env> { self.party_a = Some(party_a); self } @@ -103,7 +104,7 @@ impl<'a> B2cBuilder<'a> { /// /// # Errors /// If `Party B` is invalid or not provided - pub fn party_b(mut self, party_b: &'a str) -> B2cBuilder<'a> { + pub fn party_b(mut self, party_b: &'a str) -> B2cBuilder<'a, Env> { self.party_b = Some(party_b); self } @@ -114,7 +115,7 @@ impl<'a> B2cBuilder<'a> { /// # Errors /// If either `Party A` or `Party B` is invalid or not provided #[deprecated] - pub fn parties(mut self, party_a: &'a str, party_b: &'a str) -> B2cBuilder<'a> { + pub fn parties(mut self, party_a: &'a str, party_b: &'a str) -> B2cBuilder<'a, Env> { // TODO: add validation self.party_a = Some(party_a); self.party_b = Some(party_b); @@ -122,20 +123,20 @@ impl<'a> B2cBuilder<'a> { } /// Adds `Remarks`. This is an optional field, will default to "None" if not explicitly provided - pub fn remarks(mut self, remarks: &'a str) -> B2cBuilder<'a> { + pub fn remarks(mut self, remarks: &'a str) -> B2cBuilder<'a, Env> { self.remarks = Some(remarks); self } /// Adds `Occasion`. This is an optional field, will default to an empty string - pub fn occasion(mut self, occasion: &'a str) -> B2cBuilder<'a> { + pub fn occasion(mut self, occasion: &'a str) -> B2cBuilder<'a, Env> { self.occasion = Some(occasion); self } /// Adds an `amount` to the request /// This is a required field - pub fn amount(mut self, amount: u32) -> B2cBuilder<'a> { + pub fn amount(mut self, amount: u32) -> B2cBuilder<'a, Env> { self.amount = Some(amount); self } @@ -144,7 +145,7 @@ impl<'a> B2cBuilder<'a> { /// /// # Error /// If `QueueTimeoutUrl` is invalid or not provided - pub fn timeout_url(mut self, timeout_url: &'a str) -> B2cBuilder<'a> { + pub fn timeout_url(mut self, timeout_url: &'a str) -> B2cBuilder<'a, Env> { self.queue_timeout_url = Some(timeout_url); self } @@ -153,7 +154,7 @@ impl<'a> B2cBuilder<'a> { /// /// # Error /// If `ResultUrl` is invalid or not provided - pub fn result_url(mut self, result_url: &'a str) -> B2cBuilder<'a> { + pub fn result_url(mut self, result_url: &'a str) -> B2cBuilder<'a, Env> { self.result_url = Some(result_url); self } @@ -163,7 +164,7 @@ impl<'a> B2cBuilder<'a> { /// # Error /// If either `QueueTimeoutUrl` and `ResultUrl` is invalid or not provided #[deprecated] - pub fn urls(mut self, timeout_url: &'a str, result_url: &'a str) -> B2cBuilder<'a> { + pub fn urls(mut self, timeout_url: &'a str, result_url: &'a str) -> B2cBuilder<'a, Env> { // TODO: validate urls; will probably return a `Result` from this self.queue_timeout_url = Some(timeout_url); self.result_url = Some(result_url); diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index 856135c6f..83939ad09 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -1,5 +1,6 @@ use crate::client::{Mpesa, MpesaResult}; use crate::constants::ResponseType; +use crate::environment::ApiEnvironment; use crate::errors::MpesaError; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -29,17 +30,17 @@ pub struct C2bRegisterResponse { #[derive(Debug)] /// C2B Register builder -pub struct C2bRegisterBuilder<'a> { - client: &'a Mpesa, +pub struct C2bRegisterBuilder<'a, Env: ApiEnvironment> { + client: &'a Mpesa, validation_url: Option<&'a str>, confirmation_url: Option<&'a str>, response_type: Option, short_code: Option<&'a str>, } -impl<'a> C2bRegisterBuilder<'a> { +impl<'a, Env: ApiEnvironment> C2bRegisterBuilder<'a, Env> { /// Creates a new C2B Builder - pub fn new(client: &'a Mpesa) -> C2bRegisterBuilder<'a> { + pub fn new(client: &'a Mpesa) -> C2bRegisterBuilder<'a, Env> { C2bRegisterBuilder { client, validation_url: None, @@ -53,7 +54,7 @@ impl<'a> C2bRegisterBuilder<'a> { /// /// # Error /// If `ValidationURL` is invalid or not provided - pub fn validation_url(mut self, validation_url: &'a str) -> C2bRegisterBuilder<'a> { + pub fn validation_url(mut self, validation_url: &'a str) -> C2bRegisterBuilder<'a, Env> { self.validation_url = Some(validation_url); self } @@ -62,13 +63,13 @@ impl<'a> C2bRegisterBuilder<'a> { /// /// # Error /// If `ConfirmationUrl` is invalid or not provided - pub fn confirmation_url(mut self, confirmation_url: &'a str) -> C2bRegisterBuilder<'a> { + pub fn confirmation_url(mut self, confirmation_url: &'a str) -> C2bRegisterBuilder<'a, Env> { self.confirmation_url = Some(confirmation_url); self } /// Adds `ResponseType` for timeout. Will default to `ResponseType::Complete` if not explicitly provided - pub fn response_type(mut self, response_type: ResponseType) -> C2bRegisterBuilder<'a> { + pub fn response_type(mut self, response_type: ResponseType) -> C2bRegisterBuilder<'a, Env> { self.response_type = Some(response_type); self } @@ -77,7 +78,7 @@ impl<'a> C2bRegisterBuilder<'a> { /// /// # Error /// If `ShortCode` is invalid - pub fn short_code(mut self, short_code: &'a str) -> C2bRegisterBuilder<'a> { + pub fn short_code(mut self, short_code: &'a str) -> C2bRegisterBuilder<'a, Env> { self.short_code = Some(short_code); self } diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index dfa209e66..1127da8b9 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -1,5 +1,6 @@ use crate::client::{Mpesa, MpesaResult}; use crate::constants::CommandId; +use crate::environment::ApiEnvironment; use crate::errors::MpesaError; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -40,8 +41,8 @@ pub struct C2bSimulateResponse { } #[derive(Debug)] -pub struct C2bSimulateBuilder<'a> { - client: &'a Mpesa, +pub struct C2bSimulateBuilder<'a, Env: ApiEnvironment> { + client: &'a Mpesa, command_id: Option, amount: Option, msisdn: Option<&'a str>, @@ -49,9 +50,9 @@ pub struct C2bSimulateBuilder<'a> { short_code: Option<&'a str>, } -impl<'a> C2bSimulateBuilder<'a> { +impl<'a, Env: ApiEnvironment> C2bSimulateBuilder<'a, Env> { /// Creates a new C2B Simulate builder - pub fn new(client: &'a Mpesa) -> C2bSimulateBuilder<'a> { + pub fn new(client: &'a Mpesa) -> C2bSimulateBuilder<'a, Env> { C2bSimulateBuilder { client, command_id: None, @@ -66,14 +67,14 @@ impl<'a> C2bSimulateBuilder<'a> { /// /// # Errors /// If `CommandId` is not valid - pub fn command_id(mut self, command_id: CommandId) -> C2bSimulateBuilder<'a> { + pub fn command_id(mut self, command_id: CommandId) -> C2bSimulateBuilder<'a, Env> { self.command_id = Some(command_id); self } /// Adds an `amount` to the request /// This is a required field - pub fn amount(mut self, amount: u32) -> C2bSimulateBuilder<'a> { + pub fn amount(mut self, amount: u32) -> C2bSimulateBuilder<'a, Env> { self.amount = Some(amount); self } @@ -83,7 +84,7 @@ impl<'a> C2bSimulateBuilder<'a> { /// /// # Errors /// If `MSISDN` is invalid - pub fn msisdn(mut self, msisdn: &'a str) -> C2bSimulateBuilder<'a> { + pub fn msisdn(mut self, msisdn: &'a str) -> C2bSimulateBuilder<'a, Env> { self.msisdn = Some(msisdn); self } @@ -92,13 +93,13 @@ impl<'a> C2bSimulateBuilder<'a> { /// /// # Errors /// If Till or PayBill number is invalid - pub fn short_code(mut self, short_code: &'a str) -> C2bSimulateBuilder<'a> { + pub fn short_code(mut self, short_code: &'a str) -> C2bSimulateBuilder<'a, Env> { self.short_code = Some(short_code); self } /// Adds Bull reference number. This field is optional and will by default be `"None"`. - pub fn bill_ref_number(mut self, bill_ref_number: &'a str) -> C2bSimulateBuilder<'a> { + pub fn bill_ref_number(mut self, bill_ref_number: &'a str) -> C2bSimulateBuilder<'a, Env> { self.bill_ref_number = Some(bill_ref_number); self } diff --git a/src/services/express_request.rs b/src/services/express_request.rs index e2126e7bd..8ca44fd0d 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -1,5 +1,6 @@ use crate::client::{Mpesa, MpesaResult}; use crate::constants::CommandId; +use crate::environment::ApiEnvironment; use crate::errors::MpesaError; use chrono::prelude::Local; use serde::{Deserialize, Serialize}; @@ -54,9 +55,9 @@ pub struct MpesaExpressRequestResponse { pub response_description: String, } -pub struct MpesaExpressRequestBuilder<'a> { +pub struct MpesaExpressRequestBuilder<'a, Env: ApiEnvironment> { business_short_code: &'a str, - client: &'a Mpesa, + client: &'a Mpesa, transaction_type: Option, amount: Option, party_a: Option<&'a str>, @@ -68,8 +69,11 @@ pub struct MpesaExpressRequestBuilder<'a> { pass_key: Option<&'a str>, } -impl<'a> MpesaExpressRequestBuilder<'a> { - pub fn new(client: &'a Mpesa, business_short_code: &'a str) -> MpesaExpressRequestBuilder<'a> { +impl<'a, Env: ApiEnvironment> MpesaExpressRequestBuilder<'a, Env> { + pub fn new( + client: &'a Mpesa, + business_short_code: &'a str, + ) -> MpesaExpressRequestBuilder<'a, Env> { MpesaExpressRequestBuilder { client, business_short_code, @@ -120,14 +124,14 @@ impl<'a> MpesaExpressRequestBuilder<'a> { /// /// # Errors /// If thee `pass_key` is invalid - pub fn pass_key(mut self, pass_key: &'a str) -> MpesaExpressRequestBuilder<'a> { + pub fn pass_key(mut self, pass_key: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { self.pass_key = Some(pass_key); self } /// Adds an `amount` to the request /// This is a required field - pub fn amount(mut self, amount: u32) -> MpesaExpressRequestBuilder<'a> { + pub fn amount(mut self, amount: u32) -> MpesaExpressRequestBuilder<'a, Env> { self.amount = Some(amount); self } @@ -136,7 +140,7 @@ impl<'a> MpesaExpressRequestBuilder<'a> { /// /// # Errors /// If `phone_number` is invalid - pub fn phone_number(mut self, phone_number: &'a str) -> MpesaExpressRequestBuilder<'a> { + pub fn phone_number(mut self, phone_number: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { self.phone_number = Some(phone_number); self } @@ -145,7 +149,7 @@ impl<'a> MpesaExpressRequestBuilder<'a> { /// /// # Errors /// If the `callback_url` is invalid - pub fn callback_url(mut self, callback_url: &'a str) -> MpesaExpressRequestBuilder<'a> { + pub fn callback_url(mut self, callback_url: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { self.callback_url = Some(callback_url); self } @@ -154,7 +158,7 @@ impl<'a> MpesaExpressRequestBuilder<'a> { /// /// # Errors /// If `party_a` is invalid - pub fn party_a(mut self, party_a: &'a str) -> MpesaExpressRequestBuilder<'a> { + pub fn party_a(mut self, party_a: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { self.party_a = Some(party_a); self } @@ -163,13 +167,13 @@ impl<'a> MpesaExpressRequestBuilder<'a> { /// /// # Errors /// If `party_b` is invalid - pub fn party_b(mut self, party_b: &'a str) -> MpesaExpressRequestBuilder<'a> { + pub fn party_b(mut self, party_b: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { self.party_b = Some(party_b); self } /// Optional - Used with M-Pesa PayBills. - pub fn account_ref(mut self, account_ref: &'a str) -> MpesaExpressRequestBuilder<'a> { + pub fn account_ref(mut self, account_ref: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { self.account_ref = Some(account_ref); self } @@ -178,14 +182,17 @@ impl<'a> MpesaExpressRequestBuilder<'a> { /// /// # Errors /// If the `CommandId` is invalid - pub fn transaction_type(mut self, command_id: CommandId) -> MpesaExpressRequestBuilder<'a> { + pub fn transaction_type( + mut self, + command_id: CommandId, + ) -> MpesaExpressRequestBuilder<'a, Env> { self.transaction_type = Some(command_id); self } /// A description of the transaction. /// Optional - defaults to "None" - pub fn transaction_desc(mut self, description: &'a str) -> MpesaExpressRequestBuilder<'a> { + pub fn transaction_desc(mut self, description: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { self.transaction_desc = Some(description); self } diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index 92afb7c14..52867df6f 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -1,22 +1,49 @@ +use mpesa::{ApiEnvironment, Mpesa}; + +pub struct TestEnvironment; + +// TODO: Implement mock server for testing +impl ApiEnvironment for TestEnvironment { + fn base_url(&self) -> &'static str { + let _client = Mpesa::new("foo".to_string(), "bar".to_string(), TestEnvironment); + "https://mock_server_url.com" + } + + fn get_certificate(&self) -> &'static str { + include_str!("../../src/certificates/sandbox") + } +} + #[macro_export] macro_rules! get_mpesa_client { () => {{ + use std::str::FromStr; dotenv::dotenv().ok(); mpesa::Mpesa::new( std::env::var("CLIENT_KEY").unwrap(), std::env::var("CLIENT_SECRET").unwrap(), - "sandbox".parse().unwrap(), + mpesa::Environment::from_str("sandbox").unwrap(), ) }}; ($client_key:expr, $client_secret:expr) => {{ + use std::str::FromStr; dotenv::dotenv().ok(); - mpesa::Mpesa::new($client_key, $client_secret, "sandbox".parse().unwrap()) + mpesa::Mpesa::new( + $client_key, + $client_secret, + mpesa::Environment::from_str("sandbox").unwrap(), + ) }}; ($client_key:expr, $client_secret:expr, $environment:expr) => {{ + use std::str::FromStr; dotenv::dotenv().ok(); - mpesa::Mpesa::new($client_key, $client_secret, $environment) + mpesa::Mpesa::new( + $client_key, + $client_secret, + mpesa::Environment::from_str($environment).unwrap(), + ) }}; } @@ -44,7 +71,7 @@ mod tests { let client = get_mpesa_client!( std::env::var("CLIENT_KEY").unwrap(), std::env::var("CLIENT_SECRET").unwrap(), - "production".parse().unwrap() + "production" ); assert!(!client.is_connected().await); } diff --git a/tests/mpesa-rust/stk_push_test.rs b/tests/mpesa-rust/stk_push_test.rs index 3e7e8c8ba..532bd75ad 100644 --- a/tests/mpesa-rust/stk_push_test.rs +++ b/tests/mpesa-rust/stk_push_test.rs @@ -1,16 +1,8 @@ -use dotenv; -use mpesa::Mpesa; -use std::env; +use crate::get_mpesa_client; #[tokio::test] async fn stk_push_test() { - dotenv::dotenv().ok(); - - let client = Mpesa::new( - env::var("CLIENT_KEY").unwrap(), - env::var("CLIENT_SECRET").unwrap(), - "sandbox".parse().unwrap(), - ); + let client = get_mpesa_client!(); let response = client .express_request("174379") From c8814b45e66fb49cb67efc989486f73a5e356f91 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 15:09:37 +0300 Subject: [PATCH 045/140] Update docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e06b63775..a8c8d80d3 100644 --- a/README.md +++ b/README.md @@ -83,13 +83,13 @@ use std::env; let client0 = Mpesa::new( env::var("CLIENT_KEY")?, env::var("CLIENT_SECRET")?, - Environment::from_str("sandbox").unwrap() + Environment::from_str("sandbox").unwrap() // "Sandbox" and "SANDBOX" also valid ); let client1 = Mpesa::new( env::var("CLIENT_KEY")?, env::var("CLIENT_SECRET")?, - Environment::try_from("sandbox").unwrap() + Environment::try_from("production").unwrap() // "Production" and "PRODUCTION" also valid ); assert!(client0.is_connected().await) assert!(client1.is_connected().await) From 31d4b3bde3355e15dab18cd3facc7255247436bc Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 15:35:17 +0300 Subject: [PATCH 046/140] Add wiremock; modify TestEnvironment struct --- Cargo.toml | 1 + src/environment.rs | 8 ++++---- tests/mpesa-rust/helpers.rs | 26 ++++++++++++++++++++------ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dfe92db4d..b92224601 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ features = ["derive"] [dev-dependencies] dotenv = "0.15" tokio = {version = "1", features = ["rt", "macros"]} +wiremock = "0.5" [features] default = [ diff --git a/src/environment.rs b/src/environment.rs index 398b68962..38ea7ebdc 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -27,8 +27,8 @@ pub enum Environment { /// Expected behavior of an `Mpesa` client environment /// This abstraction exists to make it possible to mock the MPESA api server for tests pub trait ApiEnvironment { - fn base_url(&self) -> &'static str; - fn get_certificate(&self) -> &'static str; + fn base_url(&self) -> &str; + fn get_certificate(&self) -> &str; } macro_rules! environment_from_string { @@ -69,7 +69,7 @@ impl TryFrom for Environment { impl ApiEnvironment for Environment { /// Matches to base_url based on `Environment` variant - fn base_url(&self) -> &'static str { + fn base_url(&self) -> &str { match self { Environment::Production => "https://api.safaricom.co.ke", Environment::Sandbox => "https://sandbox.safaricom.co.ke", @@ -77,7 +77,7 @@ impl ApiEnvironment for Environment { } /// Match to X509 public key certificate based on `Environment` - fn get_certificate(&self) -> &'static str { + fn get_certificate(&self) -> &str { match self { Environment::Production => include_str!("./certificates/production"), Environment::Sandbox => include_str!("./certificates/sandbox"), diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index 52867df6f..908caa395 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -1,15 +1,29 @@ -use mpesa::{ApiEnvironment, Mpesa}; +use mpesa::ApiEnvironment; +use wiremock::MockServer; -pub struct TestEnvironment; +pub struct TestEnvironment { + pub server: MockServer, + pub server_url: String, +} + +impl TestEnvironment { + #[allow(unused)] + pub async fn new() -> Self { + let mock_server = MockServer::start().await; + TestEnvironment { + server_url: mock_server.uri(), + server: mock_server, + } + } +} // TODO: Implement mock server for testing impl ApiEnvironment for TestEnvironment { - fn base_url(&self) -> &'static str { - let _client = Mpesa::new("foo".to_string(), "bar".to_string(), TestEnvironment); - "https://mock_server_url.com" + fn base_url(&self) -> &str { + &self.server_url } - fn get_certificate(&self) -> &'static str { + fn get_certificate(&self) -> &str { include_str!("../../src/certificates/sandbox") } } From 4d07d88c5e8b7a763017669a892561e8a623cdea Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 15:41:58 +0300 Subject: [PATCH 047/140] Update get_mpesa_client macro --- tests/mpesa-rust/helpers.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index 908caa395..04c893ac5 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -31,33 +31,40 @@ impl ApiEnvironment for TestEnvironment { #[macro_export] macro_rules! get_mpesa_client { () => {{ + use mpesa::{Environment, Mpesa}; use std::str::FromStr; dotenv::dotenv().ok(); - mpesa::Mpesa::new( + let client = Mpesa::new( std::env::var("CLIENT_KEY").unwrap(), std::env::var("CLIENT_SECRET").unwrap(), - mpesa::Environment::from_str("sandbox").unwrap(), - ) + Environment::from_str("sandbox").unwrap(), + ); + client }}; ($client_key:expr, $client_secret:expr) => {{ + use mpesa::{Environment, Mpesa}; use std::str::FromStr; dotenv::dotenv().ok(); - mpesa::Mpesa::new( + let client = Mpesa::new( $client_key, $client_secret, - mpesa::Environment::from_str("sandbox").unwrap(), - ) + Environment::from_str("sandbox").unwrap(), + ); + client }}; ($client_key:expr, $client_secret:expr, $environment:expr) => {{ + use mpesa::{Environment, Mpesa}; use std::str::FromStr; dotenv::dotenv().ok(); - mpesa::Mpesa::new( + let client = Mpesa::new( $client_key, $client_secret, - mpesa::Environment::from_str($environment).unwrap(), - ) + Environment::from_str($environment).unwrap(), + ); + + client }}; } From ea7ead7a971aadec8a34f123c78a9e25bd1db29d Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 16:11:46 +0300 Subject: [PATCH 048/140] Update docs --- README.md | 44 ++++++++++++++++++++++++++++++++++++++------ src/lib.rs | 8 ++------ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a8c8d80d3..b17390905 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,6 @@ You will first need to create an instance of the `Mpesa` instance (the client). environment. It's worth noting that these credentials are only valid in the sandbox environment. To go live and get production keys read the docs [here](https://developer.safaricom.co.ke/docs?javascript#going-live). -_NOTE_: - -- Only calling `unwrap` for demonstration purposes. Errors are handled appropriately in the lib via the `MpesaError` enum. - These are the following ways you can instantiate `Mpesa`: ```rust @@ -71,8 +67,8 @@ let client = Mpesa::new( assert!(client.is_connected().await) ``` -Since the `Environment` enum implements `FromStr` and `TryFrom`, you can pass the name of the environment as a `&str` and call the `parse()` or `try_into()` -method to create an `Environment` type from the string slice (Pascal case and Uppercase string slices also valid): +Since the `Environment` enum implements `FromStr` and `TryFrom` for `String` and `&str` types, you can call `Environment::from_str` or `Environment::try_from` to create an `Environment` type. This is ideal if the environment values are +stored in a `.env` or any other configuration file: ```rust use mpesa::{Mpesa, Environment}; @@ -95,6 +91,42 @@ assert!(client0.is_connected().await) assert!(client1.is_connected().await) ``` +The `Mpesa` struct's `environment` parameter is generic over any type that implements the `ApiEnvironment` trait. This trait +expects the following methods to be implemented for a given type: + +```rust +pub trait ApiEnvironment { + fn base_url(&self) -> &str; + fn get_certificate(&self) -> &str; +} +``` + +This trait allows you to create your own type to pass to the `environment` parameter. With this in place, you are able to mock http requests (for testing purposes) from the MPESA api by returning a mock server uri from the `base_url` method as well as using your own certificates, required to sign select requests to the MPESA api, by providing your own `get_certificate` implementation. + +See the example below: + +```rust +pub struct TestEnvironment; + +impl ApiEnvironment for TestEnvironment { + fn base_url(&self) -> &str { + // your base url here + "https://your_mock_url.com" + } + + fn get_certificate(&self) -> &str { + // your certificate here + r#"..."# + } +} + +let client = Mpesa::new( + env::var("CLIENT_KEY")?, + env::var("CLIENT_SECRET")?, + TestEnvironment // ✔ valid +); +``` + If you intend to use in production, you will need to call a the `set_initiator_password` method from `Mpesa` after initially creating the client. Here you provide your initiator password, which overrides the default password used in sandbox `"Safcom496!"`: diff --git a/src/lib.rs b/src/lib.rs index f871aea1c..269fb0a70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,10 +39,6 @@ //! //! These are the following ways you can instantiate `Mpesa`: //! -//! _NOTE_: -//! * Only calling `unwrap` for demonstration purposes. Errors are handled appropriately in the lib via the `MpesaError` enum. -//! * Use of `dotenv` is optional. -//! //! ```rust,no_run //! use mpesa::{Mpesa, Environment}; //! use std::env; @@ -61,8 +57,8 @@ //! } //! ``` //! -//! Since the `Environment` enum implements `FromStr` and `TryFrom`, you can pass the name of the environment as a `&str` and call the `parse()` or `try_into()` -//! method to create an `Environment` type from the string slice (Pascal case or Uppercase string slices also valid): +//! Since the `Environment` enum implements `FromStr` and `TryFrom` for `String` and `&str` types, you can call `Environment::from_str` or `Environment::try_from` to create an `Environment` type. This is ideal if the environment values are +//! stored in a `.env` or any other configuration file //! //! ```rust,no_run //! use mpesa::{Mpesa, Environment}; From 3e1daeb3ea186eee9ba1897c485faf09ed3c59b1 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 16:14:02 +0300 Subject: [PATCH 049/140] Make cargo features independent from each other --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b92224601..ad390d594 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,10 +41,10 @@ default = [ "express_request", "transaction_reversal" ] -b2b = ["dep:openssl", "dep:base64", "transaction_reversal"] -b2c = ["dep:openssl", "dep:base64", "transaction_reversal"] +b2b = ["dep:openssl", "dep:base64"] +b2c = ["dep:openssl", "dep:base64"] account_balance = ["dep:openssl", "dep:base64"] c2b_register = [] -c2b_simulate = ["transaction_reversal"] +c2b_simulate = [] express_request = ["dep:chrono", "dep:base64"] transaction_reversal = ["dep:openssl", "dep:base64"] \ No newline at end of file From 14966c356ab3b25d3c03d900992478ca2eb400ad Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 16:56:50 +0300 Subject: [PATCH 050/140] Update docs --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b17390905..24ead720a 100644 --- a/README.md +++ b/README.md @@ -103,15 +103,20 @@ pub trait ApiEnvironment { This trait allows you to create your own type to pass to the `environment` parameter. With this in place, you are able to mock http requests (for testing purposes) from the MPESA api by returning a mock server uri from the `base_url` method as well as using your own certificates, required to sign select requests to the MPESA api, by providing your own `get_certificate` implementation. -See the example below: +See the example below (and [here](./src/environment.rs) so see how the trait is implemented for the `Environment` enum): ```rust -pub struct TestEnvironment; +use mpesa::{Mpesa, ApiEnvironment}; +use std::str::FromStr; +use std::convert::TryFrom; +use std::env; -impl ApiEnvironment for TestEnvironment { +pub struct MyCustomEnvironment; + +impl ApiEnvironment for MyCustomEnvironment { fn base_url(&self) -> &str { // your base url here - "https://your_mock_url.com" + "https://your_base_url.com" } fn get_certificate(&self) -> &str { @@ -120,10 +125,10 @@ impl ApiEnvironment for TestEnvironment { } } -let client = Mpesa::new( +let client: Mpesa = Mpesa::new( env::var("CLIENT_KEY")?, env::var("CLIENT_SECRET")?, - TestEnvironment // ✔ valid + MyCustomEnvironment // ✔ valid ); ``` From fdcd877615da9179ac5f17d30118f7b32cc72065 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 17:10:39 +0300 Subject: [PATCH 051/140] Unify Mpesa impl blocks --- src/client.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index e02da4c4e..8fd729e47 100644 --- a/src/client.rs +++ b/src/client.rs @@ -283,9 +283,7 @@ impl<'a, Env: ApiEnvironment> Mpesa { pub fn transaction_reversal(&'a self) -> TransactionReversalBuilder { todo!() } -} -impl Mpesa { /// Generates 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. From b6036c01adcadf4a98908da39bbe56c17bce22de Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 17:40:18 +0300 Subject: [PATCH 052/140] Update get_mpesa_client and its unittests --- tests/mpesa-rust/helpers.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index 04c893ac5..ca341456e 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -58,12 +58,7 @@ macro_rules! get_mpesa_client { use mpesa::{Environment, Mpesa}; use std::str::FromStr; dotenv::dotenv().ok(); - let client = Mpesa::new( - $client_key, - $client_secret, - Environment::from_str($environment).unwrap(), - ); - + let client = Mpesa::new($client_key, $client_secret, $environment); client }}; } @@ -92,7 +87,7 @@ mod tests { let client = get_mpesa_client!( std::env::var("CLIENT_KEY").unwrap(), std::env::var("CLIENT_SECRET").unwrap(), - "production" + Environment::from_str("production").unwrap() ); assert!(!client.is_connected().await); } From 51068f34e0b5f3c3d4f070576acc5626e8dbf0ec Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 18:25:19 +0300 Subject: [PATCH 053/140] Override default user agent --- src/client.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8fd729e47..05d6d4794 100644 --- a/src/client.rs +++ b/src/client.rs @@ -14,6 +14,7 @@ use std::cell::RefCell; /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) static DEFAULT_INITIATOR_PASSWORD: &str = "Safcom496!"; +static CARGO_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); /// `Result` enum type alias pub type MpesaResult = Result; @@ -36,14 +37,16 @@ impl<'a, Env: ApiEnvironment> Mpesa { /// let client: Mpesa = Mpesa::new( /// env::var("CLIENT_KEY").unwrap(), /// env::var("CLIENT_SECRET").unwrap(), - /// "sandbox".parse().unwrap(), + /// Environment::Sandbox, /// ); /// ``` pub fn new(client_key: String, client_secret: String, environment: Env) -> Self { let http_client = Client::builder() .connect_timeout(std::time::Duration::from_millis(10_000)) - .build() + .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, From 5fd776545c69ebc08779e7fedf39af3ddf58cf3c Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 18:29:43 +0300 Subject: [PATCH 054/140] Update docs --- README.md | 28 ++++++++++++---------------- src/client.rs | 5 +++-- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 24ead720a..4638c0187 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,10 @@ These are the following ways you can instantiate `Mpesa`: ```rust use mpesa::{Mpesa, Environment}; -use std::env; let client = Mpesa::new( - env::var("CLIENT_KEY")?, - env::var("CLIENT_SECRET")?, + env!("CLIENT_KEY")?, + env!("CLIENT_SECRET")?, Environment::Sandbox, ); @@ -74,18 +73,17 @@ stored in a `.env` or any other configuration file: use mpesa::{Mpesa, Environment}; use std::str::FromStr; use std::convert::TryFrom; -use std::env; let client0 = Mpesa::new( - env::var("CLIENT_KEY")?, - env::var("CLIENT_SECRET")?, - Environment::from_str("sandbox").unwrap() // "Sandbox" and "SANDBOX" also valid + env!("CLIENT_KEY")?, + env!("CLIENT_SECRET")?, + Environment::from_str("sandbox")? // "Sandbox" and "SANDBOX" also valid ); let client1 = Mpesa::new( - env::var("CLIENT_KEY")?, - env::var("CLIENT_SECRET")?, - Environment::try_from("production").unwrap() // "Production" and "PRODUCTION" also valid + env!("CLIENT_KEY")?, + env!("CLIENT_SECRET")?, + Environment::try_from("production")? // "Production" and "PRODUCTION" also valid ); assert!(client0.is_connected().await) assert!(client1.is_connected().await) @@ -109,7 +107,6 @@ See the example below (and [here](./src/environment.rs) so see how the trait is use mpesa::{Mpesa, ApiEnvironment}; use std::str::FromStr; use std::convert::TryFrom; -use std::env; pub struct MyCustomEnvironment; @@ -126,8 +123,8 @@ impl ApiEnvironment for MyCustomEnvironment { } let client: Mpesa = Mpesa::new( - env::var("CLIENT_KEY")?, - env::var("CLIENT_SECRET")?, + env!("CLIENT_KEY"), + env!("CLIENT_SECRET"), MyCustomEnvironment // ✔ valid ); ``` @@ -137,11 +134,10 @@ creating the client. Here you provide your initiator password, which overrides t ```rust use mpesa::Mpesa; -use std::env; let client = Mpesa::new( - env::var("CLIENT_KEY")?, - env::var("CLIENT_SECRET")?, + env!("CLIENT_KEY")?, + env!("CLIENT_SECRET")?, Environment::Sandbox, ); diff --git a/src/client.rs b/src/client.rs index 05d6d4794..40d89410e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -14,6 +14,7 @@ use std::cell::RefCell; /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) static DEFAULT_INITIATOR_PASSWORD: &str = "Safcom496!"; +/// Get current package version from metadata static CARGO_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); /// `Result` enum type alias @@ -35,8 +36,8 @@ impl<'a, Env: ApiEnvironment> Mpesa { /// # Example /// ```ignore /// let client: Mpesa = Mpesa::new( - /// env::var("CLIENT_KEY").unwrap(), - /// env::var("CLIENT_SECRET").unwrap(), + /// env!("CLIENT_KEY").unwrap(), + /// env!("CLIENT_SECRET").unwrap(), /// Environment::Sandbox, /// ); /// ``` From 2ef73fb35c201f61744c30656d8db82fa8936313 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 18:35:18 +0300 Subject: [PATCH 055/140] Replace failure with thiserror; close #38 --- CONTRIBUTING.md | 2 +- Cargo.toml | 17 ++++++------- src/errors.rs | 67 +++++++++++++------------------------------------ 3 files changed, 26 insertions(+), 60 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 362841bba..768c46bd7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,5 +4,5 @@ - [ ] Add missing services: `reversal`, `transaction_status`, `bill_manager`, `dynamic_qr_code`, `c2b_simulate_v2` - [x] Clean up `Cargo.toml`: Correctly use Cargo features and declare optional and dev dependencies - [x] Convert library to async and update tests -- [ ] Migrate to `thiserror` and remove `failure` +- [x] Migrate to `thiserror` and remove `failure` - [ ] Refine tests: test more edge cases \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index ad390d594..a2e1e6b23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,21 +10,20 @@ readme = "./README.md" license = "MIT" [dependencies] -serde_json = "1.0" -serde_repr = "0.1" -openssl = {version = "0.10", optional = true} base64 = {version = "0.13", optional = true} -failure = "0.1" -failure_derive = "0.1" chrono = {version = "0.4", optional = true} +openssl = {version = "0.10", optional = true} +serde_json = "1.0" +serde_repr = "0.1" +thiserror = "1.0.37" [dependencies.reqwest] -version = "0.11" features = ["blocking", "json"] +version = "0.11" [dependencies.serde] -version = "1.0" features = ["derive"] +version = "1.0" [dev-dependencies] dotenv = "0.15" @@ -41,10 +40,10 @@ default = [ "express_request", "transaction_reversal" ] +account_balance = ["dep:openssl", "dep:base64"] b2b = ["dep:openssl", "dep:base64"] b2c = ["dep:openssl", "dep:base64"] -account_balance = ["dep:openssl", "dep:base64"] c2b_register = [] c2b_simulate = [] express_request = ["dep:chrono", "dep:base64"] -transaction_reversal = ["dep:openssl", "dep:base64"] \ No newline at end of file +transaction_reversal = ["dep:openssl", "dep:base64"] diff --git a/src/errors.rs b/src/errors.rs index 27b7a2b47..bfa076320 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,63 +1,30 @@ -use failure_derive::*; use std::env::VarError; -#[derive(Debug, Fail)] /// Mpesa error stack +#[derive(thiserror::Error, Debug)] pub enum MpesaError { - #[fail(display = "Error Authenticating: {}", 0)] + #[error("Authentication request failed: {0}")] AuthenticationError(serde_json::Value), - #[fail(display = "Error performing B2B transaction: {}", 0)] + #[error("B2B request failed: {0}")] B2bError(serde_json::Value), - #[fail(display = "Error performing B2C transaction: {}", 0)] + #[error("B2C request failed: {0}")] B2cError(serde_json::Value), - #[fail(display = "Error performing C2B registration: {}", 0)] + #[error("C2B register request failed: {0}")] C2bRegisterError(serde_json::Value), - #[fail(display = "Error performing C2B simulation: {}", 0)] + #[error("C2B simulate request failed: {0}")] C2bSimulateError(serde_json::Value), - #[fail(display = "Error getting account balance: {}", 0)] + #[error("Account Balance request failed: {0}")] AccountBalanceError(serde_json::Value), - #[fail(display = "Error making mpesa express request: {}", 0)] + #[error("Mpesa Express request/ STK push failed: {0}")] MpesaExpressRequestError(serde_json::Value), - #[fail(display = "Network Error: {}", 0)] - NetworkError(reqwest::Error), - #[fail(display = "Error parsing JSON data: {}", 0)] - ParseError(serde_json::Error), - #[fail(display = "Error getting environmental variables: {}", 0)] - EnvironmentalVariableError(VarError), - #[fail(display = "Error extracting X509 from pem: {}", 0)] - EncryptionError(openssl::error::ErrorStack), - #[fail(display = "Error: {}", 0)] + #[error("An error has occured while performing the http request")] + NetworkError(#[from] reqwest::Error), + #[error("An error has occured while serializig/ deserializing")] + ParseError(#[from] serde_json::Error), + #[error("An error has occured while retreiving an environmental variable")] + EnvironmentalVariableError(#[from] VarError), + #[error("An error has occurred while generating security credentials")] + EncryptionError(#[from] openssl::error::ErrorStack), + #[error("{0}")] Message(&'static str), - #[fail(display = "Error: {:#?}", 0)] - ErrorResponse(serde_json::Value), -} - -impl From for MpesaError { - fn from(e: serde_json::Error) -> Self { - MpesaError::ParseError(e) - } -} - -impl From for MpesaError { - fn from(e: reqwest::Error) -> Self { - MpesaError::NetworkError(e) - } -} - -impl From for MpesaError { - fn from(e: VarError) -> Self { - MpesaError::EnvironmentalVariableError(e) - } -} - -impl From<&'static str> for MpesaError { - fn from(e: &'static str) -> Self { - MpesaError::Message(e) - } -} - -impl From for MpesaError { - fn from(e: openssl::error::ErrorStack) -> Self { - MpesaError::EncryptionError(e) - } } From 49bb243c6aac7a30fae27287b2e00cbb90cca64a Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 18:54:20 +0300 Subject: [PATCH 056/140] Ignore select rustsec report codes; close #39 and #40 --- .github/workflows/audit-cron.yml | 1 + .github/workflows/audit.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/audit-cron.yml b/.github/workflows/audit-cron.yml index 5e31924c8..ddf678121 100644 --- a/.github/workflows/audit-cron.yml +++ b/.github/workflows/audit-cron.yml @@ -10,4 +10,5 @@ jobs: - uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} + args: --ignore RUSTSEC-2020-0159 RUSTSEC-2020-0071 continue-on-error: true diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 67b5edeb5..eae58d002 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -12,4 +12,5 @@ jobs: - uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} + args: --ignore RUSTSEC-2020-0159 RUSTSEC-2020-0071 continue-on-error: true From ee4781237c35303d4d68986dbc720b00418f8a09 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 19:09:03 +0300 Subject: [PATCH 057/140] Add test to assert that set_initiator_password overrides the default initiator password --- src/client.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/client.rs b/src/client.rs index 40d89410e..37e78c53d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -313,3 +313,22 @@ impl<'a, Env: ApiEnvironment> Mpesa { Ok(base64::encode(buffer)) } } + +#[cfg(test)] +mod tests { + use crate::Sandbox; + + use super::*; + + #[test] + fn test_setting_initator_password() { + let client = Mpesa::new( + "client_key".to_string(), + "client_secret".to_string(), + Sandbox, + ); + assert_eq!(client.initiator_password(), DEFAULT_INITIATOR_PASSWORD); + client.set_initiator_password("foo_bar"); + assert_eq!(client.initiator_password(), "foo_bar".to_string()); + } +} From d08ed9628bb641a8dd9cfe466354f36e2b700f6d Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 19:17:53 +0300 Subject: [PATCH 058/140] Add test to assert that overriden base url and certificate are used with custom env --- src/client.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/client.rs b/src/client.rs index 37e78c53d..cdf193303 100644 --- a/src/client.rs +++ b/src/client.rs @@ -331,4 +331,27 @@ mod tests { client.set_initiator_password("foo_bar"); assert_eq!(client.initiator_password(), "foo_bar".to_string()); } + + struct TestEnvironment; + + impl ApiEnvironment for TestEnvironment { + fn base_url(&self) -> &str { + "https://example.com" + } + + fn get_certificate(&self) -> &str { + "certificate" + } + } + + #[test] + fn test_custom_environment() { + let client = Mpesa::new( + "client_key".to_string(), + "client_secret".to_string(), + TestEnvironment, + ); + assert_eq!(client.environment().base_url(), "https://example.com"); + assert_eq!(client.environment().get_certificate(), "certificate"); + } } From 2a4bfd2f5528bd79c420f5235ec28f4c0b2502f1 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 19:29:44 +0300 Subject: [PATCH 059/140] Add extra security advisory id to ignore option in ci workflows --- .github/workflows/audit-cron.yml | 2 +- .github/workflows/audit.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/audit-cron.yml b/.github/workflows/audit-cron.yml index ddf678121..eb81669e7 100644 --- a/.github/workflows/audit-cron.yml +++ b/.github/workflows/audit-cron.yml @@ -10,5 +10,5 @@ jobs: - uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --ignore RUSTSEC-2020-0159 RUSTSEC-2020-0071 + args: --ignore RUSTSEC-2020-0159 RUSTSEC-2020-0071 RUSTSEC-2021-0141 continue-on-error: true diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index eae58d002..adf6e072c 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -12,5 +12,5 @@ jobs: - uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --ignore RUSTSEC-2020-0159 RUSTSEC-2020-0071 + args: --ignore RUSTSEC-2020-0159 RUSTSEC-2020-0071 RUSTSEC-2021-0141 continue-on-error: true From a85dd4a2b247109401cf5bcac4501d58f7348975 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 19:33:12 +0300 Subject: [PATCH 060/140] Update library version in docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4638c0187..d259f95ef 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ An unofficial Rust wrapper around the [Safaricom API](https://developer.safarico ```toml [dependencies] -mpesa = "0.4.2" +mpesa = { git = "https://github.com/collinsmuriuki/mpesa-rust" } ``` Optionally, you can disable default-features, which is basically the entire suite of MPESA APIs to conditionally select from either: @@ -34,7 +34,7 @@ Example: ```toml [dependencies] -mpesa = { version = "0.4.2", default_features = false, features = ["b2b", "express_request"] } +mpesa = { git = "https://github.com/collinsmuriuki/mpesa-rust", default_features = false, features = ["b2b", "express_request"] } ``` In your lib or binary crate: From f533a91f300c5b4b13efcb1b9ea9d999411e2759 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 19:39:09 +0300 Subject: [PATCH 061/140] Update badges --- README.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d259f95ef..6c39d6aa3 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,8 @@ # mpesa-rust -

- - Version - - - License: MIT - -

+[![Rust](https://github.com/collinsmuriuki/mpesa-rust/actions/workflows/general.yml/badge.svg)](https://github.com/collinsmuriuki/mpesa-rust/actions/workflows/general.yml) +[![](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) +[![](https://img.shields.io/crates/v/mpesa)](https://crates.io/crates/mpesa) ## About From 6d0bb1280e494c40d61af2594c9035893ae5d344 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 19:55:57 +0300 Subject: [PATCH 062/140] Update pre-commit hooks --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 425e6f793..4ce278dc4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,17 +11,17 @@ repos: name: Cargo fmt entry: /bin/bash -c "cargo fmt --all -- --check" language: script - files: \.x$ + files: ^.*\.rs$ always_run: true - id: run-cargo-clippy name: Cargo clippy entry: /bin/bash -c "cargo clippy -- -D warnings" language: script - files: \.x$ + files: ^.*\.rs$ always_run: true - id: run-cargo-test name: Cargo test entry: /bin/bash -c "cargo test --no-fail-fast" language: script - files: \.x$ + files: ^.*\.rs$ always_run: true From 0d4a74c3b41d9002571b508fc198e3d69c37aa59 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 20:42:27 +0300 Subject: [PATCH 063/140] Remove redundant reqwest blocking feature; add rt-multi-thread feature to tokio --- Cargo.toml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a2e1e6b23..05d81a19d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,21 +13,15 @@ license = "MIT" base64 = {version = "0.13", optional = true} chrono = {version = "0.4", optional = true} openssl = {version = "0.10", optional = true} +reqwest = {version = "0.11", features = ["json"]} +serde = {version="1.0", features= ["derive"]} serde_json = "1.0" serde_repr = "0.1" thiserror = "1.0.37" -[dependencies.reqwest] -features = ["blocking", "json"] -version = "0.11" - -[dependencies.serde] -features = ["derive"] -version = "1.0" - [dev-dependencies] dotenv = "0.15" -tokio = {version = "1", features = ["rt", "macros"]} +tokio = {version = "1", features = ["rt", "rt-multi-thread", "macros"]} wiremock = "0.5" [features] From 3215f787e49cc0518b18b9bdd36fa0d11e612431 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 20:59:59 +0300 Subject: [PATCH 064/140] Remove re-exported response structs; update docs --- src/lib.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 269fb0a70..bcfa62a37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,7 +106,7 @@ //! The following services are currently available from the `Mpesa` client as methods that return builders: //! * B2C //! ```rust,no_run -//! use mpesa::{Mpesa, Environment, MpesaResult, B2cResponse}; +//! use mpesa::{Mpesa, Environment}; //! use std::env; //! use dotenv::dotenv; //! @@ -120,7 +120,7 @@ //! Environment::Sandbox //! ); //! -//! let response: MpesaResult = client +//! let response = client //! .b2c("testapi496") //! .party_a("600496") //! .party_b("254708374149") @@ -135,7 +135,7 @@ //! //! * B2B //! ```rust,no_run -//! use mpesa::{Mpesa, Environment, MpesaResult, B2bResponse}; +//! use mpesa::{Mpesa, Environment}; //! use std::env; //! use dotenv::dotenv; //! @@ -149,7 +149,7 @@ //! Environment::Sandbox //! ); //! -//! let response: MpesaResult = client +//! let response = client //! .b2b("testapi496") //! .party_a("600496") //! .party_b("600000") @@ -165,7 +165,7 @@ //! //! * C2B Register //! ```rust,no_run -//! use mpesa::{Mpesa, Environment, MpesaResult, C2bRegisterResponse}; +//! use mpesa::{Mpesa, Environment}; //! use serde_json::Value; //! use std::env; //! use dotenv::dotenv; @@ -180,7 +180,7 @@ //! Environment::Sandbox //! ); //! -//! let response: MpesaResult = client +//! let response = client //! .c2b_register() //! .short_code("600496") //! .confirmation_url("https://testdomain.com/true") @@ -193,7 +193,7 @@ //! //! * C2B Simulate //! ```rust,no_run -//! use mpesa::{Mpesa, Environment, MpesaResult, C2bSimulateResponse}; +//! use mpesa::{Mpesa, Environment}; //! use std::env; //! use dotenv::dotenv; //! @@ -207,7 +207,7 @@ //! Environment::Sandbox //! ); //! -//! let response: MpesaResult = client +//! let response = client //! .c2b_simulate() //! .short_code("600496") //! .msisdn("254700000000") @@ -221,7 +221,7 @@ //! * Account Balance //! //! ```rust,no_run -//! use mpesa::{Mpesa, MpesaResult, Environment, AccountBalanceResponse}; +//! use mpesa::{Mpesa, Environment}; //! use std::env; //! use dotenv::dotenv; //! @@ -235,7 +235,7 @@ //! Environment::Sandbox //! ); //! -//! let response: MpesaResult = client +//! let response = client //! .account_balance("testapi496") //! .result_url("https://testdomain.com/ok") //! .timeout_url("https://testdomain.com/err") @@ -249,7 +249,7 @@ //! * Mpesa Express Request / STK push/ Lipa na M-PESA online //! //! ```ignore -//! use mpesa::{Mpesa, MpesaResult, Environment, MpesaExpressRequestResponse}; +//! use mpesa::{Mpesa, Environment}; //! use std::env; //! use dotenv::dotenv; //! @@ -263,7 +263,7 @@ //! Environment::Sandbox //! ); //! -//! let response: MpesaResult = client +//! let response = client //! .express_request("174379") //! .phone_number("254708374149") //! .amount(500) @@ -296,7 +296,3 @@ pub use constants::{CommandId, IdentifierTypes, ResponseType}; pub use environment::ApiEnvironment; pub use environment::Environment::{self, Production, Sandbox}; pub use errors::MpesaError; -pub use services::{ - AccountBalanceResponse, B2bResponse, B2cResponse, C2bRegisterResponse, C2bSimulateResponse, - MpesaExpressRequestResponse, -}; From 9e256844b4d7b5138826cc4b5a96da70506fc56b Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 21:18:22 +0300 Subject: [PATCH 065/140] Add feature flags to services module --- src/services/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/services/mod.rs b/src/services/mod.rs index bfce53490..acd388429 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -20,10 +20,17 @@ mod c2b_simulate; mod express_request; mod transaction_reversal; +#[cfg(feature = "account_balance")] pub use account_balance::{AccountBalanceBuilder, AccountBalanceResponse}; +#[cfg(feature = "b2b")] pub use b2b::{B2bBuilder, B2bResponse}; +#[cfg(feature = "b2c")] pub use b2c::{B2cBuilder, B2cResponse}; +#[cfg(feature = "c2b_register")] pub use c2b_register::{C2bRegisterBuilder, C2bRegisterResponse}; +#[cfg(feature = "c2b_simulate")] pub use c2b_simulate::{C2bSimulateBuilder, C2bSimulateResponse}; +#[cfg(feature = "express_request")] pub use express_request::{MpesaExpressRequestBuilder, MpesaExpressRequestResponse}; +#[cfg(feature = "transaction_reversal")] pub use transaction_reversal::{TransactionReversalBuilder, TransactionReversalResponse}; From 21a23243641a1b4319d56cd6f6f613decc945fa7 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 21:48:02 +0300 Subject: [PATCH 066/140] Make Mpesa new method parameters generic over Strings and string slices --- src/client.rs | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/client.rs b/src/client.rs index cdf193303..76a0ad76b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -36,12 +36,12 @@ impl<'a, Env: ApiEnvironment> Mpesa { /// # Example /// ```ignore /// let client: Mpesa = Mpesa::new( - /// env!("CLIENT_KEY").unwrap(), - /// env!("CLIENT_SECRET").unwrap(), + /// env!("CLIENT_KEY"), + /// env!("CLIENT_SECRET"), /// Environment::Sandbox, /// ); /// ``` - pub fn new(client_key: String, client_secret: String, environment: Env) -> Self { + pub fn new>(client_key: S, client_secret: S, environment: Env) -> Self { let http_client = Client::builder() .connect_timeout(std::time::Duration::from_millis(10_000)) .user_agent(format!("mpesa-rust@{}", CARGO_PACKAGE_VERSION)) @@ -50,8 +50,8 @@ impl<'a, Env: ApiEnvironment> Mpesa { .build() .expect("Error building http client"); Self { - client_key, - client_secret, + client_key: client_key.into(), + client_secret: client_secret.into(), initiator_password: RefCell::new(None), environment, http_client, @@ -322,11 +322,7 @@ mod tests { #[test] fn test_setting_initator_password() { - let client = Mpesa::new( - "client_key".to_string(), - "client_secret".to_string(), - Sandbox, - ); + let client = Mpesa::new("client_key", "client_secret", Sandbox); assert_eq!(client.initiator_password(), DEFAULT_INITIATOR_PASSWORD); client.set_initiator_password("foo_bar"); assert_eq!(client.initiator_password(), "foo_bar".to_string()); @@ -346,11 +342,7 @@ mod tests { #[test] fn test_custom_environment() { - let client = Mpesa::new( - "client_key".to_string(), - "client_secret".to_string(), - TestEnvironment, - ); + let client = Mpesa::new("client_key", "client_secret", TestEnvironment); assert_eq!(client.environment().base_url(), "https://example.com"); assert_eq!(client.environment().get_certificate(), "certificate"); } From 495d770a9d2ee52f5c025aff4f1583b613dd87d1 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 21:52:26 +0300 Subject: [PATCH 067/140] Update docs --- README.md | 18 ++++++++++-------- src/client.rs | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6c39d6aa3..85cebc5b5 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,8 @@ These are the following ways you can instantiate `Mpesa`: use mpesa::{Mpesa, Environment}; let client = Mpesa::new( - env!("CLIENT_KEY")?, - env!("CLIENT_SECRET")?, + env!("CLIENT_KEY"), + env!("CLIENT_SECRET"), Environment::Sandbox, ); @@ -70,14 +70,14 @@ use std::str::FromStr; use std::convert::TryFrom; let client0 = Mpesa::new( - env!("CLIENT_KEY")?, - env!("CLIENT_SECRET")?, + env!("CLIENT_KEY"), + env!("CLIENT_SECRET"), Environment::from_str("sandbox")? // "Sandbox" and "SANDBOX" also valid ); let client1 = Mpesa::new( - env!("CLIENT_KEY")?, - env!("CLIENT_SECRET")?, + env!("CLIENT_KEY"), + env!("CLIENT_SECRET"), Environment::try_from("production")? // "Production" and "PRODUCTION" also valid ); assert!(client0.is_connected().await) @@ -122,6 +122,8 @@ let client: Mpesa = Mpesa::new( env!("CLIENT_SECRET"), MyCustomEnvironment // ✔ valid ); + +//... ``` If you intend to use in production, you will need to call a the `set_initiator_password` method from `Mpesa` after initially @@ -131,8 +133,8 @@ creating the client. Here you provide your initiator password, which overrides t use mpesa::Mpesa; let client = Mpesa::new( - env!("CLIENT_KEY")?, - env!("CLIENT_SECRET")?, + env!("CLIENT_KEY"), + env!("CLIENT_SECRET"), Environment::Sandbox, ); diff --git a/src/client.rs b/src/client.rs index 76a0ad76b..27d6711b1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -80,7 +80,7 @@ impl<'a, Env: ApiEnvironment> Mpesa { /// let client: Mpesa = Mpesa::new( /// env::var("CLIENT_KEY").unwrap(), /// env::var("CLIENT_SECRET").unwrap(), - /// "sandbox".parse().unwrap(), + /// Environment::Sandbox, /// ); /// /// client.set_initiator_password("your_initiator_password"); From bc29c8cd2d2280d27a07bd11575ed8e87e0f5bc5 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Wed, 16 Nov 2022 22:02:08 +0300 Subject: [PATCH 068/140] Make set_initiator_password generic over Strings and string slices --- src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 27d6711b1..2493a70e8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -85,8 +85,8 @@ impl<'a, Env: ApiEnvironment> Mpesa { /// /// client.set_initiator_password("your_initiator_password"); /// ``` - pub fn set_initiator_password(&self, initiator_password: &str) { - *self.initiator_password.borrow_mut() = Some(initiator_password.to_string()); + pub fn set_initiator_password>(&self, initiator_password: S) { + *self.initiator_password.borrow_mut() = Some(initiator_password.into()); } /// Checks if the client can be authenticated From 573abd25c73c935123b64f660be923781cd739e1 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Thu, 17 Nov 2022 07:06:07 +0300 Subject: [PATCH 069/140] Make amount methods in service builders generic over any number that implements Into --- src/services/b2b.rs | 8 ++++---- src/services/b2c.rs | 8 ++++---- src/services/c2b_simulate.rs | 10 +++++----- src/services/express_request.rs | 11 +++++++---- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/services/b2b.rs b/src/services/b2b.rs index 30d78c83e..a58d2f829 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -14,7 +14,7 @@ struct B2bPayload<'a> { #[serde(rename(serialize = "CommandID"))] command_id: CommandId, #[serde(rename(serialize = "Amount"))] - amount: u32, + amount: f64, #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] party_a: Option<&'a str>, #[serde(rename(serialize = "SenderIdentifierType"))] @@ -60,7 +60,7 @@ pub struct B2bBuilder<'a, Env: ApiEnvironment> { initiator_name: &'a str, client: &'a Mpesa, command_id: Option, - amount: Option, + amount: Option, party_a: Option<&'a str>, sender_id: Option, party_b: Option<&'a str>, @@ -183,8 +183,8 @@ impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { /// Adds an `amount` to the request /// This is a required field - pub fn amount(mut self, amount: u32) -> B2bBuilder<'a, Env> { - self.amount = Some(amount); + pub fn amount>(mut self, amount: Number) -> B2bBuilder<'a, Env> { + self.amount = Some(amount.into()); self } diff --git a/src/services/b2c.rs b/src/services/b2c.rs index 00e5830e0..1a6844299 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -14,7 +14,7 @@ struct B2cPayload<'a> { #[serde(rename(serialize = "CommandID"))] command_id: CommandId, #[serde(rename(serialize = "Amount"))] - amount: u32, + amount: f64, #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] party_a: Option<&'a str>, #[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")] @@ -56,7 +56,7 @@ pub struct B2cBuilder<'a, Env: ApiEnvironment> { initiator_name: &'a str, client: &'a Mpesa, command_id: Option, - amount: Option, + amount: Option, party_a: Option<&'a str>, party_b: Option<&'a str>, remarks: Option<&'a str>, @@ -136,8 +136,8 @@ impl<'a, Env: ApiEnvironment> B2cBuilder<'a, Env> { /// Adds an `amount` to the request /// This is a required field - pub fn amount(mut self, amount: u32) -> B2cBuilder<'a, Env> { - self.amount = Some(amount); + pub fn amount>(mut self, amount: Number) -> B2cBuilder<'a, Env> { + self.amount = Some(amount.into()); self } diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index 1127da8b9..7c587711a 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -12,7 +12,7 @@ struct C2bSimulatePayload<'a> { #[serde(rename(serialize = "CommandID"))] command_id: CommandId, #[serde(rename(serialize = "Amount"))] - amount: u32, + amount: f64, #[serde(rename(serialize = "Msisdn"), skip_serializing_if = "Option::is_none")] msisdn: Option<&'a str>, #[serde( @@ -44,7 +44,7 @@ pub struct C2bSimulateResponse { pub struct C2bSimulateBuilder<'a, Env: ApiEnvironment> { client: &'a Mpesa, command_id: Option, - amount: Option, + amount: Option, msisdn: Option<&'a str>, bill_ref_number: Option<&'a str>, short_code: Option<&'a str>, @@ -74,8 +74,8 @@ impl<'a, Env: ApiEnvironment> C2bSimulateBuilder<'a, Env> { /// Adds an `amount` to the request /// This is a required field - pub fn amount(mut self, amount: u32) -> C2bSimulateBuilder<'a, Env> { - self.amount = Some(amount); + pub fn amount>(mut self, amount: Number) -> C2bSimulateBuilder<'a, Env> { + self.amount = Some(amount.into()); self } @@ -126,7 +126,7 @@ impl<'a, Env: ApiEnvironment> C2bSimulateBuilder<'a, Env> { command_id: self .command_id .unwrap_or_else(|| CommandId::CustomerPayBillOnline), - amount: self.amount.unwrap_or_else(|| 10), + amount: self.amount.unwrap_or_default(), msisdn: self.msisdn, bill_ref_number: self.bill_ref_number, short_code: self.short_code, diff --git a/src/services/express_request.rs b/src/services/express_request.rs index 8ca44fd0d..30ea0f65f 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -20,7 +20,7 @@ struct MpesaExpressRequestPayload<'a> { #[serde(rename(serialize = "TransactionType"))] transaction_type: CommandId, #[serde(rename(serialize = "Amount"))] - amount: u32, + amount: f64, #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] party_a: Option<&'a str>, #[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")] @@ -59,7 +59,7 @@ pub struct MpesaExpressRequestBuilder<'a, Env: ApiEnvironment> { business_short_code: &'a str, client: &'a Mpesa, transaction_type: Option, - amount: Option, + amount: Option, party_a: Option<&'a str>, party_b: Option<&'a str>, phone_number: Option<&'a str>, @@ -131,8 +131,11 @@ impl<'a, Env: ApiEnvironment> MpesaExpressRequestBuilder<'a, Env> { /// Adds an `amount` to the request /// This is a required field - pub fn amount(mut self, amount: u32) -> MpesaExpressRequestBuilder<'a, Env> { - self.amount = Some(amount); + pub fn amount>( + mut self, + amount: Number, + ) -> MpesaExpressRequestBuilder<'a, Env> { + self.amount = Some(amount.into()); self } From c0a4d5901f0c7a617ebe9fe27425484fc30689ff Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Thu, 17 Nov 2022 07:50:52 +0300 Subject: [PATCH 070/140] Update how access_token is derived from response body --- src/client.rs | 28 +++++++++++++++------------- src/services/account_balance.rs | 5 ++--- src/services/b2b.rs | 5 ++--- src/services/b2c.rs | 5 ++--- src/services/c2b_register.rs | 5 ++--- src/services/c2b_simulate.rs | 5 ++--- src/services/express_request.rs | 5 ++--- 7 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/client.rs b/src/client.rs index 2493a70e8..45bd55553 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,7 +8,7 @@ use super::services::{ }; use openssl::rsa::Padding; use openssl::x509::X509; -use reqwest::Client; +use reqwest::Client as HttpClient; use serde_json::Value; use std::cell::RefCell; @@ -27,7 +27,7 @@ pub struct Mpesa { client_secret: String, initiator_password: RefCell>, environment: Env, - pub(crate) http_client: Client, + pub(crate) http_client: HttpClient, } impl<'a, Env: ApiEnvironment> Mpesa { @@ -42,7 +42,7 @@ impl<'a, Env: ApiEnvironment> Mpesa { /// ); /// ``` pub fn new>(client_key: S, client_secret: S, environment: Env) -> Self { - let http_client = Client::builder() + let http_client = HttpClient::builder() .connect_timeout(std::time::Duration::from_millis(10_000)) .user_agent(format!("mpesa-rust@{}", CARGO_PACKAGE_VERSION)) // TODO: Potentialy return a `Result` enum from Mpesa::new? @@ -112,22 +112,24 @@ impl<'a, Env: ApiEnvironment> Mpesa { "{}/oauth/v1/generate?grant_type=client_credentials", self.environment.base_url() ); - let resp = self + let response = self .http_client .get(&url) .basic_auth(&self.client_key, Some(&self.client_secret)) .send() .await?; - if resp.status().is_success() { - // TODO: Needs custom return type: currently not casting the response to a custom type - // hence why we need strip out double quotes `"` from the deserialized value - // example: "value" -> value - let value: Value = resp.json().await?; - return Ok(value["access_token"].to_string().replace('\"', "")); + if response.status().is_success() { + let value = response.json::().await?; + let access_token = value + .get("access_token") + .ok_or_else(|| MpesaError::AuthenticationError(value.clone()))?; + let access_token = access_token + .as_str() + .ok_or_else(|| MpesaError::AuthenticationError(value.clone()))?; + return Ok(access_token.to_string()); } - Err(MpesaError::Message( - "Could not authenticate to Safaricom, please check your credentials", - )) + let value = response.json::().await?; + Err(MpesaError::AuthenticationError(value)) } /// **B2C Builder** diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index f47d19180..e7a4c6c5f 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -3,7 +3,6 @@ use crate::constants::{CommandId, IdentifierTypes}; use crate::environment::ApiEnvironment; use crate::{Mpesa, MpesaError}; use serde::{Deserialize, Serialize}; -use serde_json::Value; #[derive(Debug, Serialize)] /// Account Balance payload @@ -185,11 +184,11 @@ impl<'a, Env: ApiEnvironment> AccountBalanceBuilder<'a, Env> { .await?; if response.status().is_success() { - let value: AccountBalanceResponse = response.json().await?; + let value = response.json::<_>().await?; return Ok(value); } - let value: Value = response.json().await?; + let value = response.json().await?; Err(MpesaError::AccountBalanceError(value)) } } diff --git a/src/services/b2b.rs b/src/services/b2b.rs index a58d2f829..c602d83e5 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -3,7 +3,6 @@ use crate::constants::{CommandId, IdentifierTypes}; use crate::environment::ApiEnvironment; use crate::errors::MpesaError; use serde::{Deserialize, Serialize}; -use serde_json::Value; #[derive(Debug, Serialize)] struct B2bPayload<'a> { @@ -249,11 +248,11 @@ impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { .await?; if response.status().is_success() { - let value: B2bResponse = response.json().await?; + let value = response.json::<_>().await?; return Ok(value); } - let value: Value = response.json().await?; + let value = response.json().await?; Err(MpesaError::B2bError(value)) } } diff --git a/src/services/b2c.rs b/src/services/b2c.rs index 1a6844299..4400f2014 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -2,7 +2,6 @@ use crate::client::MpesaResult; use crate::environment::ApiEnvironment; use crate::{CommandId, Mpesa, MpesaError}; use serde::{Deserialize, Serialize}; -use serde_json::Value; #[derive(Debug, Serialize)] /// Payload to allow for b2c transactions: @@ -217,11 +216,11 @@ impl<'a, Env: ApiEnvironment> B2cBuilder<'a, Env> { .await?; if response.status().is_success() { - let value: B2cResponse = response.json().await?; + let value = response.json::<_>().await?; return Ok(value); } - let value: Value = response.json().await?; + let value = response.json().await?; Err(MpesaError::B2cError(value)) } } diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index 83939ad09..9bb1d4c90 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -3,7 +3,6 @@ use crate::constants::ResponseType; use crate::environment::ApiEnvironment; use crate::errors::MpesaError; use serde::{Deserialize, Serialize}; -use serde_json::Value; #[derive(Debug, Serialize)] /// Payload to register the 3rd party’s confirmation and validation URLs to M-Pesa @@ -125,11 +124,11 @@ impl<'a, Env: ApiEnvironment> C2bRegisterBuilder<'a, Env> { .await?; if response.status().is_success() { - let value: C2bRegisterResponse = response.json().await?; + let value = response.json::<_>().await?; return Ok(value); } - let value: Value = response.json().await?; + let value = response.json().await?; Err(MpesaError::C2bRegisterError(value)) } } diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index 7c587711a..941ad58f8 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -3,7 +3,6 @@ use crate::constants::CommandId; use crate::environment::ApiEnvironment; use crate::errors::MpesaError; use serde::{Deserialize, Serialize}; -use serde_json::Value; #[derive(Debug, Serialize)] /// Payload to make payment requests from C2B. @@ -142,11 +141,11 @@ impl<'a, Env: ApiEnvironment> C2bSimulateBuilder<'a, Env> { .await?; if response.status().is_success() { - let value: C2bSimulateResponse = response.json().await?; + let value = response.json::<_>().await?; return Ok(value); } - let value: Value = response.json().await?; + let value = response.json().await?; Err(MpesaError::C2bSimulateError(value)) } } diff --git a/src/services/express_request.rs b/src/services/express_request.rs index 30ea0f65f..4bd56be3c 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -4,7 +4,6 @@ use crate::environment::ApiEnvironment; use crate::errors::MpesaError; use chrono::prelude::Local; use serde::{Deserialize, Serialize}; -use serde_json::Value; /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) static DEFAULT_PASSKEY: &str = "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919"; @@ -252,11 +251,11 @@ impl<'a, Env: ApiEnvironment> MpesaExpressRequestBuilder<'a, Env> { .await?; if response.status().is_success() { - let value: MpesaExpressRequestResponse = response.json().await?; + let value = response.json::<_>().await?; return Ok(value); } - let value: Value = response.json().await?; + let value = response.json().await?; Err(MpesaError::MpesaExpressRequestError(value)) } } From 15b5bd4096bf15c9810b1e19f9872554352c9da4 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Thu, 17 Nov 2022 07:56:39 +0300 Subject: [PATCH 071/140] Add test for gen_security_credentials --- src/client.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client.rs b/src/client.rs index 45bd55553..7ab95850a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -338,6 +338,7 @@ mod tests { } fn get_certificate(&self) -> &str { + // not a valid pem "certificate" } } @@ -348,4 +349,11 @@ mod tests { assert_eq!(client.environment().base_url(), "https://example.com"); assert_eq!(client.environment().get_certificate(), "certificate"); } + + #[test] + #[should_panic] + fn test_gen_security_credentials_fails_with_invalid_pem() { + let client = Mpesa::new("client_key", "client_secret", TestEnvironment); + let _ = client.gen_security_credentials().unwrap(); + } } From 2ede5286dc4ca52422de0b78e1e22b3555ff3ef5 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Thu, 17 Nov 2022 08:25:59 +0300 Subject: [PATCH 072/140] Rename lifetime parameter --- src/client.rs | 27 +++++---- src/services/account_balance.rs | 55 ++++++++++--------- src/services/b2b.rs | 76 ++++++++++++++------------ src/services/b2c.rs | 68 +++++++++++++---------- src/services/c2b_register.rs | 36 +++++++----- src/services/c2b_simulate.rs | 35 ++++++------ src/services/express_request.rs | 82 ++++++++++++++++------------ src/services/transaction_reversal.rs | 20 +++---- 8 files changed, 221 insertions(+), 178 deletions(-) diff --git a/src/client.rs b/src/client.rs index 7ab95850a..73972b711 100644 --- a/src/client.rs +++ b/src/client.rs @@ -30,7 +30,7 @@ pub struct Mpesa { pub(crate) http_client: HttpClient, } -impl<'a, Env: ApiEnvironment> Mpesa { +impl<'mpesa, Env: ApiEnvironment> Mpesa { /// Constructs a new `Mpesa` instance. /// /// # Example @@ -59,13 +59,13 @@ impl<'a, Env: ApiEnvironment> Mpesa { } /// Gets the current `Environment` - pub(crate) fn environment(&'a self) -> &Env { + pub(crate) fn environment(&'mpesa self) -> &Env { &self.environment } /// Gets the initiator password as a byte slice /// If `None`, the default password is b"Safcom496!" - pub(crate) fn initiator_password(&'a self) -> String { + pub(crate) fn initiator_password(&'mpesa self) -> String { if let Some(p) = &*self.initiator_password.borrow() { return p.to_owned(); } @@ -155,7 +155,7 @@ impl<'a, Env: ApiEnvironment> Mpesa { /// .send(); /// ``` #[cfg(feature = "b2c")] - pub fn b2c(&'a self, initiator_name: &'a str) -> B2cBuilder<'a, Env> { + pub fn b2c(&'mpesa self, initiator_name: &'mpesa str) -> B2cBuilder<'mpesa, Env> { B2cBuilder::new(self, initiator_name) } @@ -183,7 +183,7 @@ impl<'a, Env: ApiEnvironment> Mpesa { /// .send(); /// ``` #[cfg(feature = "b2b")] - pub fn b2b(&'a self, initiator_name: &'a str) -> B2bBuilder<'a, Env> { + pub fn b2b(&'mpesa self, initiator_name: &'mpesa str) -> B2bBuilder<'mpesa, Env> { B2bBuilder::new(self, initiator_name) } @@ -204,7 +204,7 @@ impl<'a, Env: ApiEnvironment> Mpesa { /// .send(); /// ``` #[cfg(feature = "c2b_register")] - pub fn c2b_register(&'a self) -> C2bRegisterBuilder<'a, Env> { + pub fn c2b_register(&'mpesa self) -> C2bRegisterBuilder<'mpesa, Env> { C2bRegisterBuilder::new(self) } @@ -225,7 +225,7 @@ impl<'a, Env: ApiEnvironment> Mpesa { /// .send(); /// ``` #[cfg(feature = "c2b_simulate")] - pub fn c2b_simulate(&'a self) -> C2bSimulateBuilder<'a, Env> { + pub fn c2b_simulate(&'mpesa self) -> C2bSimulateBuilder<'mpesa, Env> { C2bSimulateBuilder::new(self) } @@ -249,7 +249,10 @@ impl<'a, Env: ApiEnvironment> Mpesa { /// .send(); /// ``` #[cfg(feature = "account_balance")] - pub fn account_balance(&'a self, initiator_name: &'a str) -> AccountBalanceBuilder<'a, Env> { + pub fn account_balance( + &'mpesa self, + initiator_name: &'mpesa str, + ) -> AccountBalanceBuilder<'mpesa, Env> { AccountBalanceBuilder::new(self, initiator_name) } @@ -275,9 +278,9 @@ impl<'a, Env: ApiEnvironment> Mpesa { /// ``` #[cfg(feature = "express_request")] pub fn express_request( - &'a self, - business_short_code: &'a str, - ) -> MpesaExpressRequestBuilder<'a, Env> { + &'mpesa self, + business_short_code: &'mpesa str, + ) -> MpesaExpressRequestBuilder<'mpesa, Env> { MpesaExpressRequestBuilder::new(self, business_short_code) } @@ -286,7 +289,7 @@ impl<'a, 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(&'a self) -> TransactionReversalBuilder { + pub fn transaction_reversal(&'mpesa self) -> TransactionReversalBuilder { todo!() } diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index e7a4c6c5f..78bc718d7 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -6,29 +6,29 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize)] /// Account Balance payload -struct AccountBalancePayload<'a> { +struct AccountBalancePayload<'mpesa> { #[serde(rename(serialize = "Initiator"))] - initiator: &'a str, + initiator: &'mpesa str, #[serde(rename(serialize = "SecurityCredential"))] - security_credential: &'a str, + security_credential: &'mpesa str, #[serde(rename(serialize = "CommandID"))] command_id: CommandId, #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] - party_a: Option<&'a str>, + party_a: Option<&'mpesa str>, #[serde(rename(serialize = "IdentifierType"))] - identifier_type: &'a str, + identifier_type: &'mpesa str, #[serde(rename(serialize = "Remarks"))] - remarks: &'a str, + remarks: &'mpesa str, #[serde( rename(serialize = "QueueTimeOutURL"), skip_serializing_if = "Option::is_none" )] - queue_time_out_url: Option<&'a str>, + queue_time_out_url: Option<&'mpesa str>, #[serde( rename(serialize = "ResultURL"), skip_serializing_if = "Option::is_none" )] - result_url: Option<&'a str>, + result_url: Option<&'mpesa str>, } #[derive(Debug, Deserialize, Clone)] @@ -43,21 +43,24 @@ pub struct AccountBalanceResponse { pub response_description: String, } #[derive(Debug)] -pub struct AccountBalanceBuilder<'a, Env: ApiEnvironment> { - initiator_name: &'a str, - client: &'a Mpesa, +pub struct AccountBalanceBuilder<'mpesa, Env: ApiEnvironment> { + initiator_name: &'mpesa str, + client: &'mpesa Mpesa, command_id: Option, - party_a: Option<&'a str>, + party_a: Option<&'mpesa str>, identifier_type: Option, - remarks: Option<&'a str>, - queue_timeout_url: Option<&'a str>, - result_url: Option<&'a str>, + remarks: Option<&'mpesa str>, + queue_timeout_url: Option<&'mpesa str>, + result_url: Option<&'mpesa str>, } -impl<'a, Env: ApiEnvironment> AccountBalanceBuilder<'a, Env> { +impl<'mpesa, Env: ApiEnvironment> AccountBalanceBuilder<'mpesa, Env> { /// Creates a new `AccountBalanceBuilder`. /// Requires an `initiator_name`, the credential/ username used to authenticate the transaction request - pub fn new(client: &'a Mpesa, initiator_name: &'a str) -> AccountBalanceBuilder<'a, Env> { + pub fn new( + client: &'mpesa Mpesa, + initiator_name: &'mpesa str, + ) -> AccountBalanceBuilder<'mpesa, Env> { AccountBalanceBuilder { initiator_name, client, @@ -75,7 +78,7 @@ impl<'a, Env: ApiEnvironment> AccountBalanceBuilder<'a, Env> { /// /// # Errors /// If `CommandId` is invalid - pub fn command_id(mut self, command_id: CommandId) -> AccountBalanceBuilder<'a, Env> { + pub fn command_id(mut self, command_id: CommandId) -> AccountBalanceBuilder<'mpesa, Env> { self.command_id = Some(command_id); self } @@ -85,7 +88,7 @@ impl<'a, Env: ApiEnvironment> AccountBalanceBuilder<'a, Env> { /// /// # Errors /// If `Party A` is not provided or invalid - pub fn party_a(mut self, party_a: &'a str) -> AccountBalanceBuilder<'a, Env> { + pub fn party_a(mut self, party_a: &'mpesa str) -> AccountBalanceBuilder<'mpesa, Env> { self.party_a = Some(party_a); self } @@ -98,14 +101,14 @@ impl<'a, Env: ApiEnvironment> AccountBalanceBuilder<'a, Env> { pub fn identifier_type( mut self, identifier_type: IdentifierTypes, - ) -> AccountBalanceBuilder<'a, Env> { + ) -> AccountBalanceBuilder<'mpesa, Env> { self.identifier_type = Some(identifier_type); self } /// Adds `Remarks`, a comment sent along transaction. /// Optional field that defaults to `"None"` if no value is provided - pub fn remarks(mut self, remarks: &'a str) -> AccountBalanceBuilder<'a, Env> { + pub fn remarks(mut self, remarks: &'mpesa str) -> AccountBalanceBuilder<'mpesa, Env> { self.remarks = Some(remarks); self } @@ -114,7 +117,7 @@ impl<'a, Env: ApiEnvironment> AccountBalanceBuilder<'a, Env> { /// /// # Error /// If `QueueTimeoutUrl` is invalid or not provided - pub fn timeout_url(mut self, timeout_url: &'a str) -> AccountBalanceBuilder<'a, Env> { + pub fn timeout_url(mut self, timeout_url: &'mpesa str) -> AccountBalanceBuilder<'mpesa, Env> { self.queue_timeout_url = Some(timeout_url); self } @@ -123,7 +126,7 @@ impl<'a, Env: ApiEnvironment> AccountBalanceBuilder<'a, Env> { /// /// # Error /// If `ResultUrl` is invalid or not provided - pub fn result_url(mut self, result_url: &'a str) -> AccountBalanceBuilder<'a, Env> { + pub fn result_url(mut self, result_url: &'mpesa str) -> AccountBalanceBuilder<'mpesa, Env> { self.result_url = Some(result_url); self } @@ -135,9 +138,9 @@ impl<'a, Env: ApiEnvironment> AccountBalanceBuilder<'a, Env> { #[deprecated] pub fn urls( mut self, - timeout_url: &'a str, - result_url: &'a str, - ) -> AccountBalanceBuilder<'a, Env> { + timeout_url: &'mpesa str, + result_url: &'mpesa str, + ) -> AccountBalanceBuilder<'mpesa, Env> { self.queue_timeout_url = Some(timeout_url); self.result_url = Some(result_url); self diff --git a/src/services/b2b.rs b/src/services/b2b.rs index c602d83e5..a8dfba33e 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -5,40 +5,40 @@ use crate::errors::MpesaError; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize)] -struct B2bPayload<'a> { +struct B2bPayload<'mpesa> { #[serde(rename(serialize = "Initiator"))] - initiator: &'a str, + initiator: &'mpesa str, #[serde(rename(serialize = "SecurityCredential"))] - security_credential: &'a str, + security_credential: &'mpesa str, #[serde(rename(serialize = "CommandID"))] command_id: CommandId, #[serde(rename(serialize = "Amount"))] amount: f64, #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] - party_a: Option<&'a str>, + party_a: Option<&'mpesa str>, #[serde(rename(serialize = "SenderIdentifierType"))] - sender_identifier_type: &'a str, + sender_identifier_type: &'mpesa str, #[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")] - party_b: Option<&'a str>, + party_b: Option<&'mpesa str>, #[serde(rename(serialize = "RecieverIdentifierType"))] - reciever_identifier_type: &'a str, + reciever_identifier_type: &'mpesa str, #[serde(rename(serialize = "Remarks"))] - remarks: &'a str, + remarks: &'mpesa str, #[serde( rename(serialize = "QueueTimeOutURL"), skip_serializing_if = "Option::is_none" )] - queue_time_out_url: Option<&'a str>, + queue_time_out_url: Option<&'mpesa str>, #[serde( rename(serialize = "ResultURL"), skip_serializing_if = "Option::is_none" )] - result_url: Option<&'a str>, + result_url: Option<&'mpesa str>, #[serde( rename(serialize = "AccountReference"), skip_serializing_if = "Option::is_none" )] - account_reference: Option<&'a str>, + account_reference: Option<&'mpesa str>, } #[derive(Debug, Deserialize, Clone)] @@ -55,25 +55,25 @@ pub struct B2bResponse { #[derive(Debug)] /// B2B transaction builder struct -pub struct B2bBuilder<'a, Env: ApiEnvironment> { - initiator_name: &'a str, - client: &'a Mpesa, +pub struct B2bBuilder<'mpesa, Env: ApiEnvironment> { + initiator_name: &'mpesa str, + client: &'mpesa Mpesa, command_id: Option, amount: Option, - party_a: Option<&'a str>, + party_a: Option<&'mpesa str>, sender_id: Option, - party_b: Option<&'a str>, + party_b: Option<&'mpesa str>, receiver_id: Option, - remarks: Option<&'a str>, - queue_timeout_url: Option<&'a str>, - result_url: Option<&'a str>, - account_ref: Option<&'a str>, + remarks: Option<&'mpesa str>, + queue_timeout_url: Option<&'mpesa str>, + result_url: Option<&'mpesa str>, + account_ref: Option<&'mpesa str>, } -impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { +impl<'mpesa, Env: ApiEnvironment> B2bBuilder<'mpesa, Env> { /// Creates a new B2B builder /// Requires an `initiator_name`, the credential/ username used to authenticate the transaction request - pub fn new(client: &'a Mpesa, initiator_name: &'a str) -> B2bBuilder<'a, Env> { + pub fn new(client: &'mpesa Mpesa, initiator_name: &'mpesa str) -> B2bBuilder<'mpesa, Env> { B2bBuilder { client, initiator_name, @@ -94,7 +94,7 @@ impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { /// /// # Errors /// If invalid `CommandId` is provided - pub fn command_id(mut self, command_id: CommandId) -> B2bBuilder<'a, Env> { + pub fn command_id(mut self, command_id: CommandId) -> B2bBuilder<'mpesa, Env> { self.command_id = Some(command_id); self } @@ -104,7 +104,7 @@ impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { /// /// # Errors /// If `Party A` is invalid or not provided - pub fn party_a(mut self, party_a: &'a str) -> B2bBuilder<'a, Env> { + pub fn party_a(mut self, party_a: &'mpesa str) -> B2bBuilder<'mpesa, Env> { self.party_a = Some(party_a); self } @@ -114,7 +114,7 @@ impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { /// /// # Errors /// If `Party B` is invalid or not provided - pub fn party_b(mut self, party_b: &'a str) -> B2bBuilder<'a, Env> { + pub fn party_b(mut self, party_b: &'mpesa str) -> B2bBuilder<'mpesa, Env> { self.party_b = Some(party_b); self } @@ -125,7 +125,11 @@ impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { /// # Errors /// If either `Party A` or `Party B` is invalid or not provided #[deprecated] - pub fn parties(mut self, party_a: &'a str, party_b: &'a str) -> B2bBuilder<'a, Env> { + pub fn parties( + mut self, + party_a: &'mpesa str, + party_b: &'mpesa str, + ) -> B2bBuilder<'mpesa, Env> { self.party_a = Some(party_a); self.party_b = Some(party_b); self @@ -135,7 +139,7 @@ impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { /// /// # Error /// If `QueueTimeoutUrl` is invalid or not provided - pub fn timeout_url(mut self, timeout_url: &'a str) -> B2bBuilder<'a, Env> { + pub fn timeout_url(mut self, timeout_url: &'mpesa str) -> B2bBuilder<'mpesa, Env> { self.queue_timeout_url = Some(timeout_url); self } @@ -144,7 +148,7 @@ impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { /// /// # Error /// If `ResultUrl` is invalid or not provided - pub fn result_url(mut self, result_url: &'a str) -> B2bBuilder<'a, Env> { + pub fn result_url(mut self, result_url: &'mpesa str) -> B2bBuilder<'mpesa, Env> { self.result_url = Some(result_url); self } @@ -154,7 +158,11 @@ impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { /// # Error /// If either `QueueTimeoutUrl` and `ResultUrl` is invalid or not provided #[deprecated] - pub fn urls(mut self, timeout_url: &'a str, result_url: &'a str) -> B2bBuilder<'a, Env> { + pub fn urls( + mut self, + timeout_url: &'mpesa str, + result_url: &'mpesa str, + ) -> B2bBuilder<'mpesa, Env> { // TODO: validate urls self.queue_timeout_url = Some(timeout_url); self.result_url = Some(result_url); @@ -162,19 +170,19 @@ impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { } /// Adds `sender_id`. Will default to `IdentifierTypes::ShortCode` if not explicitly provided - pub fn sender_id(mut self, sender_id: IdentifierTypes) -> B2bBuilder<'a, Env> { + pub fn sender_id(mut self, sender_id: IdentifierTypes) -> B2bBuilder<'mpesa, Env> { self.sender_id = Some(sender_id); self } /// Adds `receiver_id`. Will default to `IdentifierTypes::ShortCode` if not explicitly provided - pub fn receiver_id(mut self, receiver_id: IdentifierTypes) -> B2bBuilder<'a, Env> { + pub fn receiver_id(mut self, receiver_id: IdentifierTypes) -> B2bBuilder<'mpesa, Env> { self.receiver_id = Some(receiver_id); self } /// Adds `account_ref`. This field is required - pub fn account_ref(mut self, account_ref: &'a str) -> B2bBuilder<'a, Env> { + pub fn account_ref(mut self, account_ref: &'mpesa str) -> B2bBuilder<'mpesa, Env> { // TODO: add validation self.account_ref = Some(account_ref); self @@ -182,13 +190,13 @@ impl<'a, Env: ApiEnvironment> B2bBuilder<'a, Env> { /// Adds an `amount` to the request /// This is a required field - pub fn amount>(mut self, amount: Number) -> B2bBuilder<'a, Env> { + pub fn amount>(mut self, amount: Number) -> B2bBuilder<'mpesa, Env> { self.amount = Some(amount.into()); self } /// Adds `remarks`. This field is optional, will default to "None" if not explicitly passed - pub fn remarks(mut self, remarks: &'a str) -> B2bBuilder<'a, Env> { + pub fn remarks(mut self, remarks: &'mpesa str) -> B2bBuilder<'mpesa, Env> { self.remarks = Some(remarks); self } diff --git a/src/services/b2c.rs b/src/services/b2c.rs index 4400f2014..97021aba4 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -5,36 +5,36 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize)] /// Payload to allow for b2c transactions: -struct B2cPayload<'a> { +struct B2cPayload<'mpesa> { #[serde(rename(serialize = "InitiatorName"))] - initiator_name: &'a str, + initiator_name: &'mpesa str, #[serde(rename(serialize = "SecurityCredential"))] - security_credential: &'a str, + security_credential: &'mpesa str, #[serde(rename(serialize = "CommandID"))] command_id: CommandId, #[serde(rename(serialize = "Amount"))] amount: f64, #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] - party_a: Option<&'a str>, + party_a: Option<&'mpesa str>, #[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")] - party_b: Option<&'a str>, + party_b: Option<&'mpesa str>, #[serde(rename(serialize = "Remarks"))] - remarks: &'a str, + remarks: &'mpesa str, #[serde( rename(serialize = "QueueTimeOutURL"), skip_serializing_if = "Option::is_none" )] - queue_time_out_url: Option<&'a str>, + queue_time_out_url: Option<&'mpesa str>, #[serde( rename(serialize = "ResultURL"), skip_serializing_if = "Option::is_none" )] - result_url: Option<&'a str>, + result_url: Option<&'mpesa str>, #[serde( rename(serialize = "Occasion"), skip_serializing_if = "Option::is_none" )] - occasion: Option<&'a str>, + occasion: Option<&'mpesa str>, } #[derive(Debug, Deserialize, Clone)] @@ -51,23 +51,23 @@ pub struct B2cResponse { #[derive(Debug)] /// B2C transaction builder struct -pub struct B2cBuilder<'a, Env: ApiEnvironment> { - initiator_name: &'a str, - client: &'a Mpesa, +pub struct B2cBuilder<'mpesa, Env: ApiEnvironment> { + initiator_name: &'mpesa str, + client: &'mpesa Mpesa, command_id: Option, amount: Option, - party_a: Option<&'a str>, - party_b: Option<&'a str>, - remarks: Option<&'a str>, - queue_timeout_url: Option<&'a str>, - result_url: Option<&'a str>, - occasion: Option<&'a str>, + party_a: Option<&'mpesa str>, + party_b: Option<&'mpesa str>, + remarks: Option<&'mpesa str>, + queue_timeout_url: Option<&'mpesa str>, + result_url: Option<&'mpesa str>, + occasion: Option<&'mpesa str>, } -impl<'a, Env: ApiEnvironment> B2cBuilder<'a, Env> { +impl<'mpesa, Env: ApiEnvironment> B2cBuilder<'mpesa, Env> { /// Create a new B2C builder. /// Requires an `initiator_name`, the credential/ username used to authenticate the transaction request - pub fn new(client: &'a Mpesa, initiator_name: &'a str) -> B2cBuilder<'a, Env> { + pub fn new(client: &'mpesa Mpesa, initiator_name: &'mpesa str) -> B2cBuilder<'mpesa, Env> { B2cBuilder { client, initiator_name, @@ -83,7 +83,7 @@ impl<'a, Env: ApiEnvironment> B2cBuilder<'a, Env> { } /// Adds the `CommandId`. Defaults to `CommandId::BusinessPayment` if not explicitly provided. - pub fn command_id(mut self, command_id: CommandId) -> B2cBuilder<'a, Env> { + pub fn command_id(mut self, command_id: CommandId) -> B2cBuilder<'mpesa, Env> { self.command_id = Some(command_id); self } @@ -93,7 +93,7 @@ impl<'a, Env: ApiEnvironment> B2cBuilder<'a, Env> { /// /// # Errors /// If `Party A` is invalid or not provided - pub fn party_a(mut self, party_a: &'a str) -> B2cBuilder<'a, Env> { + pub fn party_a(mut self, party_a: &'mpesa str) -> B2cBuilder<'mpesa, Env> { self.party_a = Some(party_a); self } @@ -103,7 +103,7 @@ impl<'a, Env: ApiEnvironment> B2cBuilder<'a, Env> { /// /// # Errors /// If `Party B` is invalid or not provided - pub fn party_b(mut self, party_b: &'a str) -> B2cBuilder<'a, Env> { + pub fn party_b(mut self, party_b: &'mpesa str) -> B2cBuilder<'mpesa, Env> { self.party_b = Some(party_b); self } @@ -114,7 +114,11 @@ impl<'a, Env: ApiEnvironment> B2cBuilder<'a, Env> { /// # Errors /// If either `Party A` or `Party B` is invalid or not provided #[deprecated] - pub fn parties(mut self, party_a: &'a str, party_b: &'a str) -> B2cBuilder<'a, Env> { + pub fn parties( + mut self, + party_a: &'mpesa str, + party_b: &'mpesa str, + ) -> B2cBuilder<'mpesa, Env> { // TODO: add validation self.party_a = Some(party_a); self.party_b = Some(party_b); @@ -122,20 +126,20 @@ impl<'a, Env: ApiEnvironment> B2cBuilder<'a, Env> { } /// Adds `Remarks`. This is an optional field, will default to "None" if not explicitly provided - pub fn remarks(mut self, remarks: &'a str) -> B2cBuilder<'a, Env> { + pub fn remarks(mut self, remarks: &'mpesa str) -> B2cBuilder<'mpesa, Env> { self.remarks = Some(remarks); self } /// Adds `Occasion`. This is an optional field, will default to an empty string - pub fn occasion(mut self, occasion: &'a str) -> B2cBuilder<'a, Env> { + pub fn occasion(mut self, occasion: &'mpesa str) -> B2cBuilder<'mpesa, Env> { self.occasion = Some(occasion); self } /// Adds an `amount` to the request /// This is a required field - pub fn amount>(mut self, amount: Number) -> B2cBuilder<'a, Env> { + pub fn amount>(mut self, amount: Number) -> B2cBuilder<'mpesa, Env> { self.amount = Some(amount.into()); self } @@ -144,7 +148,7 @@ impl<'a, Env: ApiEnvironment> B2cBuilder<'a, Env> { /// /// # Error /// If `QueueTimeoutUrl` is invalid or not provided - pub fn timeout_url(mut self, timeout_url: &'a str) -> B2cBuilder<'a, Env> { + pub fn timeout_url(mut self, timeout_url: &'mpesa str) -> B2cBuilder<'mpesa, Env> { self.queue_timeout_url = Some(timeout_url); self } @@ -153,7 +157,7 @@ impl<'a, Env: ApiEnvironment> B2cBuilder<'a, Env> { /// /// # Error /// If `ResultUrl` is invalid or not provided - pub fn result_url(mut self, result_url: &'a str) -> B2cBuilder<'a, Env> { + pub fn result_url(mut self, result_url: &'mpesa str) -> B2cBuilder<'mpesa, Env> { self.result_url = Some(result_url); self } @@ -163,7 +167,11 @@ impl<'a, Env: ApiEnvironment> B2cBuilder<'a, Env> { /// # Error /// If either `QueueTimeoutUrl` and `ResultUrl` is invalid or not provided #[deprecated] - pub fn urls(mut self, timeout_url: &'a str, result_url: &'a str) -> B2cBuilder<'a, Env> { + pub fn urls( + mut self, + timeout_url: &'mpesa str, + result_url: &'mpesa str, + ) -> B2cBuilder<'mpesa, Env> { // TODO: validate urls; will probably return a `Result` from this self.queue_timeout_url = Some(timeout_url); self.result_url = Some(result_url); diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index 9bb1d4c90..0d401a58c 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -6,15 +6,15 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize)] /// Payload to register the 3rd party’s confirmation and validation URLs to M-Pesa -struct C2bRegisterPayload<'a> { +struct C2bRegisterPayload<'mpesa> { #[serde(rename(serialize = "ValidationURL"))] - validation_url: &'a str, + validation_url: &'mpesa str, #[serde(rename(serialize = "ConfirmationURL"))] - confirmation_url: &'a str, + confirmation_url: &'mpesa str, #[serde(rename(serialize = "ResponseType"))] response_type: ResponseType, #[serde(rename(serialize = "ShortCode"))] - short_code: &'a str, + short_code: &'mpesa str, } #[derive(Debug, Deserialize, Clone)] @@ -29,17 +29,17 @@ pub struct C2bRegisterResponse { #[derive(Debug)] /// C2B Register builder -pub struct C2bRegisterBuilder<'a, Env: ApiEnvironment> { - client: &'a Mpesa, - validation_url: Option<&'a str>, - confirmation_url: Option<&'a str>, +pub struct C2bRegisterBuilder<'mpesa, Env: ApiEnvironment> { + client: &'mpesa Mpesa, + validation_url: Option<&'mpesa str>, + confirmation_url: Option<&'mpesa str>, response_type: Option, - short_code: Option<&'a str>, + short_code: Option<&'mpesa str>, } -impl<'a, Env: ApiEnvironment> C2bRegisterBuilder<'a, Env> { +impl<'mpesa, Env: ApiEnvironment> C2bRegisterBuilder<'mpesa, Env> { /// Creates a new C2B Builder - pub fn new(client: &'a Mpesa) -> C2bRegisterBuilder<'a, Env> { + pub fn new(client: &'mpesa Mpesa) -> C2bRegisterBuilder<'mpesa, Env> { C2bRegisterBuilder { client, validation_url: None, @@ -53,7 +53,10 @@ impl<'a, Env: ApiEnvironment> C2bRegisterBuilder<'a, Env> { /// /// # Error /// If `ValidationURL` is invalid or not provided - pub fn validation_url(mut self, validation_url: &'a str) -> C2bRegisterBuilder<'a, Env> { + pub fn validation_url( + mut self, + validation_url: &'mpesa str, + ) -> C2bRegisterBuilder<'mpesa, Env> { self.validation_url = Some(validation_url); self } @@ -62,13 +65,16 @@ impl<'a, Env: ApiEnvironment> C2bRegisterBuilder<'a, Env> { /// /// # Error /// If `ConfirmationUrl` is invalid or not provided - pub fn confirmation_url(mut self, confirmation_url: &'a str) -> C2bRegisterBuilder<'a, Env> { + pub fn confirmation_url( + mut self, + confirmation_url: &'mpesa str, + ) -> C2bRegisterBuilder<'mpesa, Env> { self.confirmation_url = Some(confirmation_url); self } /// Adds `ResponseType` for timeout. Will default to `ResponseType::Complete` if not explicitly provided - pub fn response_type(mut self, response_type: ResponseType) -> C2bRegisterBuilder<'a, Env> { + pub fn response_type(mut self, response_type: ResponseType) -> C2bRegisterBuilder<'mpesa, Env> { self.response_type = Some(response_type); self } @@ -77,7 +83,7 @@ impl<'a, Env: ApiEnvironment> C2bRegisterBuilder<'a, Env> { /// /// # Error /// If `ShortCode` is invalid - pub fn short_code(mut self, short_code: &'a str) -> C2bRegisterBuilder<'a, Env> { + pub fn short_code(mut self, short_code: &'mpesa str) -> C2bRegisterBuilder<'mpesa, Env> { self.short_code = Some(short_code); self } diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index 941ad58f8..bd0568f67 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -7,23 +7,23 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize)] /// Payload to make payment requests from C2B. /// See more: https://developer.safaricom.co.ke/docs#c2b-api -struct C2bSimulatePayload<'a> { +struct C2bSimulatePayload<'mpesa> { #[serde(rename(serialize = "CommandID"))] command_id: CommandId, #[serde(rename(serialize = "Amount"))] amount: f64, #[serde(rename(serialize = "Msisdn"), skip_serializing_if = "Option::is_none")] - msisdn: Option<&'a str>, + msisdn: Option<&'mpesa str>, #[serde( rename(serialize = "BillRefNumber"), skip_serializing_if = "Option::is_none" )] - bill_ref_number: Option<&'a str>, + bill_ref_number: Option<&'mpesa str>, #[serde( rename(serialize = "ShortCode"), skip_serializing_if = "Option::is_none" )] - short_code: Option<&'a str>, + short_code: Option<&'mpesa str>, } #[derive(Debug, Clone, Deserialize)] @@ -40,18 +40,18 @@ pub struct C2bSimulateResponse { } #[derive(Debug)] -pub struct C2bSimulateBuilder<'a, Env: ApiEnvironment> { - client: &'a Mpesa, +pub struct C2bSimulateBuilder<'mpesa, Env: ApiEnvironment> { + client: &'mpesa Mpesa, command_id: Option, amount: Option, - msisdn: Option<&'a str>, - bill_ref_number: Option<&'a str>, - short_code: Option<&'a str>, + msisdn: Option<&'mpesa str>, + bill_ref_number: Option<&'mpesa str>, + short_code: Option<&'mpesa str>, } -impl<'a, Env: ApiEnvironment> C2bSimulateBuilder<'a, Env> { +impl<'mpesa, Env: ApiEnvironment> C2bSimulateBuilder<'mpesa, Env> { /// Creates a new C2B Simulate builder - pub fn new(client: &'a Mpesa) -> C2bSimulateBuilder<'a, Env> { + pub fn new(client: &'mpesa Mpesa) -> C2bSimulateBuilder<'mpesa, Env> { C2bSimulateBuilder { client, command_id: None, @@ -66,14 +66,14 @@ impl<'a, Env: ApiEnvironment> C2bSimulateBuilder<'a, Env> { /// /// # Errors /// If `CommandId` is not valid - pub fn command_id(mut self, command_id: CommandId) -> C2bSimulateBuilder<'a, Env> { + pub fn command_id(mut self, command_id: CommandId) -> C2bSimulateBuilder<'mpesa, Env> { self.command_id = Some(command_id); self } /// Adds an `amount` to the request /// This is a required field - pub fn amount>(mut self, amount: Number) -> C2bSimulateBuilder<'a, Env> { + pub fn amount>(mut self, amount: Number) -> C2bSimulateBuilder<'mpesa, Env> { self.amount = Some(amount.into()); self } @@ -83,7 +83,7 @@ impl<'a, Env: ApiEnvironment> C2bSimulateBuilder<'a, Env> { /// /// # Errors /// If `MSISDN` is invalid - pub fn msisdn(mut self, msisdn: &'a str) -> C2bSimulateBuilder<'a, Env> { + pub fn msisdn(mut self, msisdn: &'mpesa str) -> C2bSimulateBuilder<'mpesa, Env> { self.msisdn = Some(msisdn); self } @@ -92,13 +92,16 @@ impl<'a, Env: ApiEnvironment> C2bSimulateBuilder<'a, Env> { /// /// # Errors /// If Till or PayBill number is invalid - pub fn short_code(mut self, short_code: &'a str) -> C2bSimulateBuilder<'a, Env> { + pub fn short_code(mut self, short_code: &'mpesa str) -> C2bSimulateBuilder<'mpesa, Env> { self.short_code = Some(short_code); self } /// Adds Bull reference number. This field is optional and will by default be `"None"`. - pub fn bill_ref_number(mut self, bill_ref_number: &'a str) -> C2bSimulateBuilder<'a, Env> { + pub fn bill_ref_number( + mut self, + bill_ref_number: &'mpesa str, + ) -> C2bSimulateBuilder<'mpesa, Env> { self.bill_ref_number = Some(bill_ref_number); self } diff --git a/src/services/express_request.rs b/src/services/express_request.rs index 4bd56be3c..6bb3971ff 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -9,35 +9,35 @@ use serde::{Deserialize, Serialize}; static DEFAULT_PASSKEY: &str = "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919"; #[derive(Debug, Serialize)] -struct MpesaExpressRequestPayload<'a> { +struct MpesaExpressRequestPayload<'mpesa> { #[serde(rename(serialize = "BusinessShortCode"))] - business_short_code: &'a str, + business_short_code: &'mpesa str, #[serde(rename(serialize = "Password"))] - password: &'a str, + password: &'mpesa str, #[serde(rename(serialize = "Timestamp"))] - timestamp: &'a str, + timestamp: &'mpesa str, #[serde(rename(serialize = "TransactionType"))] transaction_type: CommandId, #[serde(rename(serialize = "Amount"))] amount: f64, #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] - party_a: Option<&'a str>, + party_a: Option<&'mpesa str>, #[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")] - party_b: Option<&'a str>, + party_b: Option<&'mpesa str>, #[serde( rename(serialize = "PhoneNumber"), skip_serializing_if = "Option::is_none" )] - phone_number: Option<&'a str>, + phone_number: Option<&'mpesa str>, #[serde( rename(serialize = "CallBackURL"), skip_serializing_if = "Option::is_none" )] - call_back_url: Option<&'a str>, + call_back_url: Option<&'mpesa str>, #[serde(rename(serialize = "AccountReference"))] - account_reference: &'a str, + account_reference: &'mpesa str, #[serde(rename(serialize = "TransactionDesc"))] - transaction_desc: &'a str, + transaction_desc: &'mpesa str, } #[derive(Debug, Clone, Deserialize)] @@ -54,25 +54,25 @@ pub struct MpesaExpressRequestResponse { pub response_description: String, } -pub struct MpesaExpressRequestBuilder<'a, Env: ApiEnvironment> { - business_short_code: &'a str, - client: &'a Mpesa, +pub struct MpesaExpressRequestBuilder<'mpesa, Env: ApiEnvironment> { + business_short_code: &'mpesa str, + client: &'mpesa Mpesa, transaction_type: Option, amount: Option, - party_a: Option<&'a str>, - party_b: Option<&'a str>, - phone_number: Option<&'a str>, - callback_url: Option<&'a str>, - account_ref: Option<&'a str>, - transaction_desc: Option<&'a str>, - pass_key: Option<&'a str>, + party_a: Option<&'mpesa str>, + party_b: Option<&'mpesa str>, + phone_number: Option<&'mpesa str>, + callback_url: Option<&'mpesa str>, + account_ref: Option<&'mpesa str>, + transaction_desc: Option<&'mpesa str>, + pass_key: Option<&'mpesa str>, } -impl<'a, Env: ApiEnvironment> MpesaExpressRequestBuilder<'a, Env> { +impl<'mpesa, Env: ApiEnvironment> MpesaExpressRequestBuilder<'mpesa, Env> { pub fn new( - client: &'a Mpesa, - business_short_code: &'a str, - ) -> MpesaExpressRequestBuilder<'a, Env> { + client: &'mpesa Mpesa, + business_short_code: &'mpesa str, + ) -> MpesaExpressRequestBuilder<'mpesa, Env> { MpesaExpressRequestBuilder { client, business_short_code, @@ -89,12 +89,12 @@ impl<'a, Env: ApiEnvironment> MpesaExpressRequestBuilder<'a, Env> { } /// Public method get the `business_short_code` - pub fn business_short_code(&'a self) -> &'a str { + pub fn business_short_code(&'mpesa self) -> &'mpesa str { self.business_short_code } /// Retrieves the production passkey if present or defaults to the key provided in Safaricom's [test credentials](https://developer.safaricom.co.ke/test_credentials) - fn get_pass_key(&'a self) -> &'a str { + fn get_pass_key(&'mpesa self) -> &'mpesa str { if let Some(key) = self.pass_key { return key; } @@ -123,7 +123,7 @@ impl<'a, Env: ApiEnvironment> MpesaExpressRequestBuilder<'a, Env> { /// /// # Errors /// If thee `pass_key` is invalid - pub fn pass_key(mut self, pass_key: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { + pub fn pass_key(mut self, pass_key: &'mpesa str) -> MpesaExpressRequestBuilder<'mpesa, Env> { self.pass_key = Some(pass_key); self } @@ -133,7 +133,7 @@ impl<'a, Env: ApiEnvironment> MpesaExpressRequestBuilder<'a, Env> { pub fn amount>( mut self, amount: Number, - ) -> MpesaExpressRequestBuilder<'a, Env> { + ) -> MpesaExpressRequestBuilder<'mpesa, Env> { self.amount = Some(amount.into()); self } @@ -142,7 +142,10 @@ impl<'a, Env: ApiEnvironment> MpesaExpressRequestBuilder<'a, Env> { /// /// # Errors /// If `phone_number` is invalid - pub fn phone_number(mut self, phone_number: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { + pub fn phone_number( + mut self, + phone_number: &'mpesa str, + ) -> MpesaExpressRequestBuilder<'mpesa, Env> { self.phone_number = Some(phone_number); self } @@ -151,7 +154,10 @@ impl<'a, Env: ApiEnvironment> MpesaExpressRequestBuilder<'a, Env> { /// /// # Errors /// If the `callback_url` is invalid - pub fn callback_url(mut self, callback_url: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { + pub fn callback_url( + mut self, + callback_url: &'mpesa str, + ) -> MpesaExpressRequestBuilder<'mpesa, Env> { self.callback_url = Some(callback_url); self } @@ -160,7 +166,7 @@ impl<'a, Env: ApiEnvironment> MpesaExpressRequestBuilder<'a, Env> { /// /// # Errors /// If `party_a` is invalid - pub fn party_a(mut self, party_a: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { + pub fn party_a(mut self, party_a: &'mpesa str) -> MpesaExpressRequestBuilder<'mpesa, Env> { self.party_a = Some(party_a); self } @@ -169,13 +175,16 @@ impl<'a, Env: ApiEnvironment> MpesaExpressRequestBuilder<'a, Env> { /// /// # Errors /// If `party_b` is invalid - pub fn party_b(mut self, party_b: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { + pub fn party_b(mut self, party_b: &'mpesa str) -> MpesaExpressRequestBuilder<'mpesa, Env> { self.party_b = Some(party_b); self } /// Optional - Used with M-Pesa PayBills. - pub fn account_ref(mut self, account_ref: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { + pub fn account_ref( + mut self, + account_ref: &'mpesa str, + ) -> MpesaExpressRequestBuilder<'mpesa, Env> { self.account_ref = Some(account_ref); self } @@ -187,14 +196,17 @@ impl<'a, Env: ApiEnvironment> MpesaExpressRequestBuilder<'a, Env> { pub fn transaction_type( mut self, command_id: CommandId, - ) -> MpesaExpressRequestBuilder<'a, Env> { + ) -> MpesaExpressRequestBuilder<'mpesa, Env> { self.transaction_type = Some(command_id); self } /// A description of the transaction. /// Optional - defaults to "None" - pub fn transaction_desc(mut self, description: &'a str) -> MpesaExpressRequestBuilder<'a, Env> { + pub fn transaction_desc( + mut self, + description: &'mpesa str, + ) -> MpesaExpressRequestBuilder<'mpesa, Env> { self.transaction_desc = Some(description); self } diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index de618efef..5364f9b1b 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -3,27 +3,27 @@ use serde::Serialize; use crate::CommandId; #[derive(Debug, Serialize)] -pub struct TransactionReversalPayload<'a> { +pub struct TransactionReversalPayload<'mpesa> { #[serde(rename(serialize = "Initiator"))] - initiator: &'a str, + initiator: &'mpesa str, #[serde(rename(serialize = "SecurityCredential"))] - security_credentials: &'a str, + security_credentials: &'mpesa str, #[serde(rename(serialize = "CommandID"))] command_id: CommandId, #[serde(rename(serialize = "TransactionID"))] - transaction_id: &'a str, + transaction_id: &'mpesa str, #[serde(rename(serialize = "ReceiverParty"))] - receiver_party: &'a str, + receiver_party: &'mpesa str, #[serde(rename(serialize = "ReceiverIdentifierType"))] - receiver_identifier_type: &'a str, + receiver_identifier_type: &'mpesa str, #[serde(rename(serialize = "ResultURL"))] - result_url: &'a str, + result_url: &'mpesa str, #[serde(rename(serialize = "QueueTimeOutURL"))] - queue_timeout_url: &'a str, + queue_timeout_url: &'mpesa str, #[serde(rename(serialize = "Remarks"))] - remarks: &'a str, + remarks: &'mpesa str, #[serde(rename(serialize = "Occasion"))] - ocassion: &'a str, + ocassion: &'mpesa str, } pub struct TransactionReversalBuilder; From 65efb81bd9006ccc041ccaad26883ccde063a2fa Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Thu, 17 Nov 2022 11:31:37 +0300 Subject: [PATCH 073/140] Update initiator_password function --- src/client.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index 73972b711..0f073c771 100644 --- a/src/client.rs +++ b/src/client.rs @@ -66,10 +66,10 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { /// Gets the initiator password as a byte slice /// If `None`, the default password is b"Safcom496!" pub(crate) fn initiator_password(&'mpesa self) -> String { - if let Some(p) = &*self.initiator_password.borrow() { - return p.to_owned(); - } - DEFAULT_INITIATOR_PASSWORD.to_owned() + let Some(p) = &*self.initiator_password.borrow() else { + return DEFAULT_INITIATOR_PASSWORD.to_owned() + }; + p.to_owned() } /// Optional in development but required for production, you will need to call this method and set your production initiator password. From 6124536ded2577067c88b433df60c69d69302bc6 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Thu, 17 Nov 2022 11:37:22 +0300 Subject: [PATCH 074/140] Update client imports --- src/client.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0f073c771..d89da91b0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,11 +1,9 @@ use crate::environment::ApiEnvironment; -use crate::services::TransactionReversalBuilder; -use crate::MpesaError; - -use super::services::{ +use crate::services::{ AccountBalanceBuilder, B2bBuilder, B2cBuilder, C2bRegisterBuilder, C2bSimulateBuilder, - MpesaExpressRequestBuilder, + MpesaExpressRequestBuilder, TransactionReversalBuilder, }; +use crate::MpesaError; use openssl::rsa::Padding; use openssl::x509::X509; use reqwest::Client as HttpClient; From b4ee4a0e23f8202b64d4e2f89b8aed8a86e4c8ad Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Thu, 17 Nov 2022 21:09:39 +0300 Subject: [PATCH 075/140] Initial bring up of transaction reversal implementation --- src/client.rs | 9 +- src/constants.rs | 2 +- src/services/transaction_reversal.rs | 137 ++++++++++++++++++++++++++- 3 files changed, 142 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index d89da91b0..0e6eb8781 100644 --- a/src/client.rs +++ b/src/client.rs @@ -287,8 +287,13 @@ 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) -> TransactionReversalBuilder { - todo!() + pub fn transaction_reversal( + &'mpesa self, + transaction_id: &'mpesa str, + amount: f64, + receiver_party: &'mpesa str, + ) -> TransactionReversalBuilder<'mpesa, Env> { + TransactionReversalBuilder::new(self, transaction_id, amount, receiver_party) } /// Generates security credentials diff --git a/src/constants.rs b/src/constants.rs index e08c0210f..401b9859f 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -3,7 +3,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use std::fmt::{Display, Formatter, Result as FmtResult}; /// Mpesa command ids -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub enum CommandId { TransactionReversal, SalaryPayment, diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index 5364f9b1b..c4bdc38d5 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -1,6 +1,11 @@ +use serde::Deserialize; use serde::Serialize; +use crate::ApiEnvironment; use crate::CommandId; +use crate::Mpesa; +use crate::MpesaError; +use crate::MpesaResult; #[derive(Debug, Serialize)] pub struct TransactionReversalPayload<'mpesa> { @@ -23,9 +28,135 @@ pub struct TransactionReversalPayload<'mpesa> { #[serde(rename(serialize = "Remarks"))] remarks: &'mpesa str, #[serde(rename(serialize = "Occasion"))] - ocassion: &'mpesa str, + occasion: &'mpesa str, + #[serde(rename(serialize = "Amount"))] + amount: f64, } -pub struct TransactionReversalBuilder; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransactionReversalResponse { + #[serde(rename(deserialize = "ConversationID"))] + conversation_id: String, + #[serde(rename(deserialize = "OriginatorConversationID"))] + originator_conversation_id: String, + #[serde(rename(deserialize = "ResponseDescription"))] + response_description: String, +} + +#[derive(Debug)] +pub struct TransactionReversalBuilder<'mpesa, Env: ApiEnvironment> { + client: &'mpesa Mpesa, + initiator: Option<&'mpesa str>, + security_credentials: Option<&'mpesa str>, + command_id: Option, + transaction_id: Option<&'mpesa str>, + receiver_party: Option<&'mpesa str>, + receiver_identifier_type: Option<&'mpesa str>, + result_url: Option<&'mpesa str>, + queue_timeout_url: Option<&'mpesa str>, + remarks: Option<&'mpesa str>, + occasion: Option<&'mpesa str>, + amount: Option, +} + +impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { + pub fn new( + client: &'mpesa Mpesa, + transaction_id: &'mpesa str, + amount: f64, + receiver_party: &'mpesa str, + ) -> TransactionReversalBuilder<'mpesa, Env> { + TransactionReversalBuilder { + client, + initiator: None, + security_credentials: None, + command_id: None, + transaction_id: Some(transaction_id), + receiver_party: Some(receiver_party), + receiver_identifier_type: None, + result_url: None, + queue_timeout_url: None, + remarks: None, + occasion: None, + amount: Some(amount), + } + } + + pub fn command_id(mut self, command_id: CommandId) -> Self { + self.command_id = Some(command_id); + self + } + + pub fn transaction_id(mut self, transaction_id: &'mpesa str) -> Self { + self.transaction_id = Some(transaction_id); + self + } + + pub fn receiver_party(mut self, receiver_party: &'mpesa str) -> Self { + self.receiver_party = Some(receiver_party); + self + } + + pub fn receiver_identifier_type(mut self, receiver_identifier_type: &'mpesa str) -> Self { + self.receiver_identifier_type = Some(receiver_identifier_type); + self + } + + pub fn result_url(mut self, result_url: &'mpesa str) -> Self { + self.result_url = Some(result_url); + self + } -pub struct TransactionReversalResponse; + pub fn queue_timeout_url(mut self, queue_timeout_url: &'mpesa str) -> Self { + self.queue_timeout_url = Some(queue_timeout_url); + self + } + + pub fn remarks(mut self, remarks: &'mpesa str) -> Self { + self.remarks = Some(remarks); + self + } + + pub fn occasion(mut self, occasion: &'mpesa str) -> Self { + self.occasion = Some(occasion); + self + } + + pub async fn send(&self) -> MpesaResult { + let url = format!( + "{}/reversal/v1/request", + self.client.environment().base_url() + ); + + let payload = TransactionReversalPayload { + initiator: self.initiator.unwrap(), + security_credentials: self.security_credentials.unwrap(), + command_id: self.command_id.clone().unwrap(), + transaction_id: self.transaction_id.unwrap(), + receiver_party: self.receiver_party.unwrap(), + receiver_identifier_type: self.receiver_identifier_type.unwrap(), + result_url: self.result_url.unwrap(), + queue_timeout_url: self.queue_timeout_url.unwrap(), + remarks: self.remarks.unwrap(), + occasion: self.occasion.unwrap(), + amount: self.amount.unwrap_or_default(), + }; + + let response = self + .client + .http_client + .post(&url) + .bearer_auth(self.client.auth().await?) + .json(&payload) + .send() + .await?; + + if !response.status().is_success() { + let value = response.json().await?; + return Err(MpesaError::MpesaExpressRequestError(value)); + }; + + let response = response.json::<_>().await?; + Ok(response) + } +} From 954a09a6d32de6f8f46b6c48e410265b7cdfeb4d Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Sat, 19 Nov 2022 14:49:43 +0300 Subject: [PATCH 076/140] Finished implementation of transaction reversal --- src/client.rs | 6 +- src/errors.rs | 2 + src/services/transaction_reversal.rs | 123 +++++++++++++----- tests/mpesa-rust/main.rs | 1 + tests/mpesa-rust/transaction_reversal_test.rs | 19 +++ 5 files changed, 114 insertions(+), 37 deletions(-) create mode 100644 tests/mpesa-rust/transaction_reversal_test.rs diff --git a/src/client.rs b/src/client.rs index 0e6eb8781..cb9e1ab85 100644 --- a/src/client.rs +++ b/src/client.rs @@ -289,11 +289,9 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { #[cfg(feature = "transaction_reversal")] pub fn transaction_reversal( &'mpesa self, - transaction_id: &'mpesa str, - amount: f64, - receiver_party: &'mpesa str, + initiator_name: &'mpesa str, ) -> TransactionReversalBuilder<'mpesa, Env> { - TransactionReversalBuilder::new(self, transaction_id, amount, receiver_party) + TransactionReversalBuilder::new(self, initiator_name) } /// Generates security credentials diff --git a/src/errors.rs b/src/errors.rs index bfa076320..b36fa78b3 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -17,6 +17,8 @@ pub enum MpesaError { AccountBalanceError(serde_json::Value), #[error("Mpesa Express request/ STK push failed: {0}")] MpesaExpressRequestError(serde_json::Value), + #[error("Mpesa Transaction reversal failed: {0}")] + MpesaTransactionReversalError(serde_json::Value), #[error("An error has occured while performing the http request")] NetworkError(#[from] reqwest::Error), #[error("An error has occured while serializig/ deserializing")] diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index c4bdc38d5..3a44fda3a 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -20,15 +20,15 @@ pub struct TransactionReversalPayload<'mpesa> { #[serde(rename(serialize = "ReceiverParty"))] receiver_party: &'mpesa str, #[serde(rename(serialize = "ReceiverIdentifierType"))] - receiver_identifier_type: &'mpesa str, + receiver_identifier_type: Option<&'mpesa str>, #[serde(rename(serialize = "ResultURL"))] - result_url: &'mpesa str, + result_url: Option<&'mpesa str>, #[serde(rename(serialize = "QueueTimeOutURL"))] - queue_timeout_url: &'mpesa str, + timeout_url: Option<&'mpesa str>, #[serde(rename(serialize = "Remarks"))] - remarks: &'mpesa str, + remarks: Option<&'mpesa str>, #[serde(rename(serialize = "Occasion"))] - occasion: &'mpesa str, + occasion: Option<&'mpesa str>, #[serde(rename(serialize = "Amount"))] amount: f64, } @@ -43,17 +43,17 @@ pub struct TransactionReversalResponse { response_description: String, } +/// Creates new `TransactionReversalBuilder` #[derive(Debug)] pub struct TransactionReversalBuilder<'mpesa, Env: ApiEnvironment> { client: &'mpesa Mpesa, - initiator: Option<&'mpesa str>, - security_credentials: Option<&'mpesa str>, + initiator: &'mpesa str, command_id: Option, transaction_id: Option<&'mpesa str>, receiver_party: Option<&'mpesa str>, receiver_identifier_type: Option<&'mpesa str>, result_url: Option<&'mpesa str>, - queue_timeout_url: Option<&'mpesa str>, + timeout_url: Option<&'mpesa str>, remarks: Option<&'mpesa str>, occasion: Option<&'mpesa str>, amount: Option, @@ -62,84 +62,141 @@ pub struct TransactionReversalBuilder<'mpesa, Env: ApiEnvironment> { impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { pub fn new( client: &'mpesa Mpesa, - transaction_id: &'mpesa str, - amount: f64, - receiver_party: &'mpesa str, + initiator: &'mpesa str, ) -> TransactionReversalBuilder<'mpesa, Env> { TransactionReversalBuilder { client, - initiator: None, - security_credentials: None, + initiator, command_id: None, - transaction_id: Some(transaction_id), - receiver_party: Some(receiver_party), + transaction_id: None, + receiver_party: None, receiver_identifier_type: None, result_url: None, - queue_timeout_url: None, + timeout_url: None, remarks: None, occasion: None, - amount: Some(amount), + 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 required field pub fn receiver_identifier_type(mut self, receiver_identifier_type: &'mpesa str) -> 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 } - pub fn queue_timeout_url(mut self, queue_timeout_url: &'mpesa str) -> Self { - self.queue_timeout_url = Some(queue_timeout_url); + /// 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 required field pub fn remarks(mut self, remarks: &'mpesa str) -> Self { self.remarks = Some(remarks); self } - + /// Occasion of the transaction + /// This is an optional Parameter pub fn occasion(mut self, occasion: &'mpesa str) -> Self { self.occasion = Some(occasion); self } + /// Adds an `amount` to the request + /// + /// This is a required field + pub fn amount(mut self, amount: f64) -> Self { + self.amount = Some(amount); + self + } + + /// # Transaction Reversal API + /// + /// Requests for transaction reversal + /// + /// This API enables reversal of a B2B, B2C or C2B M-Pesa transaction + /// Required parameters: + /// + /// `transaction_id`: This is the Mpesa Transaction ID of the transaction which you wish to reverse + /// + /// `amount` : The amount transacted in the transaction to be reversed , down to the cent + /// + /// `receiver_party`: Your organization's short code. + /// + /// See more from the Safaricom API docs [here](https://developer.safaricom.co.ke/Documentation) + /// + /// A successful request returns a `TransactionReversalResponse` type + /// + /// # Errors + /// Returns a `MpesaError` on failure. pub async fn send(&self) -> MpesaResult { let url = format!( "{}/reversal/v1/request", self.client.environment().base_url() ); + let credentials = self.client.gen_security_credentials()?; + let payload = TransactionReversalPayload { - initiator: self.initiator.unwrap(), - security_credentials: self.security_credentials.unwrap(), - command_id: self.command_id.clone().unwrap(), - transaction_id: self.transaction_id.unwrap(), - receiver_party: self.receiver_party.unwrap(), - receiver_identifier_type: self.receiver_identifier_type.unwrap(), - result_url: self.result_url.unwrap(), - queue_timeout_url: self.queue_timeout_url.unwrap(), - remarks: self.remarks.unwrap(), - occasion: self.occasion.unwrap(), - amount: self.amount.unwrap_or_default(), + initiator: self.initiator, + security_credentials: &credentials, + command_id: self + .command_id + .clone() + .unwrap_or(CommandId::TransactionReversal), + transaction_id: self + .transaction_id + .expect("transaction_id is required field"), + receiver_party: self + .receiver_party + .expect("receiver_party is required field"), + receiver_identifier_type: self.receiver_identifier_type, + result_url: self.result_url, + timeout_url: self.timeout_url, + remarks: self.remarks, + occasion: self.occasion, + amount: self.amount.expect("amount is required parameter"), }; let response = self @@ -153,7 +210,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { if !response.status().is_success() { let value = response.json().await?; - return Err(MpesaError::MpesaExpressRequestError(value)); + return Err(MpesaError::MpesaTransactionReversalError(value)); }; let response = response.json::<_>().await?; diff --git a/tests/mpesa-rust/main.rs b/tests/mpesa-rust/main.rs index 214d81dab..c00401fdc 100644 --- a/tests/mpesa-rust/main.rs +++ b/tests/mpesa-rust/main.rs @@ -5,3 +5,4 @@ mod c2b_register_test; mod c2b_simulate_test; mod helpers; mod stk_push_test; +mod transaction_reversal_test; diff --git a/tests/mpesa-rust/transaction_reversal_test.rs b/tests/mpesa-rust/transaction_reversal_test.rs new file mode 100644 index 000000000..53c361ca0 --- /dev/null +++ b/tests/mpesa-rust/transaction_reversal_test.rs @@ -0,0 +1,19 @@ +use crate::get_mpesa_client; + +#[tokio::test] +async fn stk_push_test() { + let client = get_mpesa_client!(); + + let response = client + .transaction_reversal("testapi496") + .result_url("https://testdomain.com/ok") + .timeout_url("https://testdomain.com/err") + .transaction_id("OEI2AK4Q16") + .amount(1.0) + .receiver_party("600983") + .remarks("wrong recipient") + .send() + .await; + + assert!(response.is_ok()) +} From 07d9219b345aa1727128afc6f392b2f31cc7bc31 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Sat, 19 Nov 2022 18:04:13 +0300 Subject: [PATCH 077/140] Update src/services/transaction_reversal.rs Co-authored-by: Collins Muriuki --- src/services/transaction_reversal.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index 3a44fda3a..5cbc04f07 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -136,6 +136,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { self.remarks = Some(remarks); self } + /// Occasion of the transaction /// This is an optional Parameter pub fn occasion(mut self, occasion: &'mpesa str) -> Self { From cb9fbaa466d77ca865cd6f6f61b79e767fd69d22 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Sat, 19 Nov 2022 18:04:20 +0300 Subject: [PATCH 078/140] Update src/services/transaction_reversal.rs Co-authored-by: Collins Muriuki --- src/services/transaction_reversal.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index 5cbc04f07..3045ee601 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -129,6 +129,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { self.timeout_url = Some(timeout_url); self } + /// Comments that are sent along with the transaction. /// /// This is required field From a1f3d01658a599e21ca85af0b64206b74db77452 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Sat, 19 Nov 2022 18:09:34 +0300 Subject: [PATCH 079/140] Remove clone on CommandID and small refactor --- src/constants.rs | 2 +- src/services/transaction_reversal.rs | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index 401b9859f..e08c0210f 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -3,7 +3,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use std::fmt::{Display, Formatter, Result as FmtResult}; /// Mpesa command ids -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize)] pub enum CommandId { TransactionReversal, SalaryPayment, diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index 3045ee601..a56040569 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -43,7 +43,6 @@ pub struct TransactionReversalResponse { response_description: String, } -/// Creates new `TransactionReversalBuilder` #[derive(Debug)] pub struct TransactionReversalBuilder<'mpesa, Env: ApiEnvironment> { client: &'mpesa Mpesa, @@ -60,6 +59,7 @@ pub struct TransactionReversalBuilder<'mpesa, Env: ApiEnvironment> { } impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { + /// Creates new `TransactionReversalBuilder` pub fn new( client: &'mpesa Mpesa, initiator: &'mpesa str, @@ -129,7 +129,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { self.timeout_url = Some(timeout_url); self } - + /// Comments that are sent along with the transaction. /// /// This is required field @@ -137,7 +137,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { self.remarks = Some(remarks); self } - + /// Occasion of the transaction /// This is an optional Parameter pub fn occasion(mut self, occasion: &'mpesa str) -> Self { @@ -148,8 +148,8 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { /// Adds an `amount` to the request /// /// This is a required field - pub fn amount(mut self, amount: f64) -> Self { - self.amount = Some(amount); + pub fn amount>(mut self, amount: Number) -> Self { + self.amount = Some(amount.into()); self } @@ -172,7 +172,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { /// /// # Errors /// Returns a `MpesaError` on failure. - pub async fn send(&self) -> MpesaResult { + pub async fn send(self) -> MpesaResult { let url = format!( "{}/reversal/v1/request", self.client.environment().base_url() @@ -183,10 +183,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { let payload = TransactionReversalPayload { initiator: self.initiator, security_credentials: &credentials, - command_id: self - .command_id - .clone() - .unwrap_or(CommandId::TransactionReversal), + command_id: self.command_id.unwrap_or(CommandId::TransactionReversal), transaction_id: self .transaction_id .expect("transaction_id is required field"), From 30d212667cdc4764cdad6399a96c5bfa20c813be Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Sat, 19 Nov 2022 18:58:09 +0300 Subject: [PATCH 080/140] Fix error name and url --- src/services/transaction_reversal.rs | 2 +- tests/mpesa-rust/transaction_reversal_test.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index a56040569..d54a2b7a4 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -174,7 +174,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { /// Returns a `MpesaError` on failure. pub async fn send(self) -> MpesaResult { let url = format!( - "{}/reversal/v1/request", + "{}/mpesa/reversal/v1/request", self.client.environment().base_url() ); diff --git a/tests/mpesa-rust/transaction_reversal_test.rs b/tests/mpesa-rust/transaction_reversal_test.rs index 53c361ca0..c487f9b9d 100644 --- a/tests/mpesa-rust/transaction_reversal_test.rs +++ b/tests/mpesa-rust/transaction_reversal_test.rs @@ -1,7 +1,7 @@ use crate::get_mpesa_client; #[tokio::test] -async fn stk_push_test() { +async fn transaction_reversal_test() { let client = get_mpesa_client!(); let response = client @@ -9,6 +9,7 @@ async fn stk_push_test() { .result_url("https://testdomain.com/ok") .timeout_url("https://testdomain.com/err") .transaction_id("OEI2AK4Q16") + .receiver_identifier_type("5") .amount(1.0) .receiver_party("600983") .remarks("wrong recipient") From 36fa4517495be15b50ff418f9027755385ff5b01 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Sat, 19 Nov 2022 19:37:04 +0300 Subject: [PATCH 081/140] Use IdentifierType and return error incase of missing param --- src/services/transaction_reversal.rs | 17 ++++++++++------- tests/mpesa-rust/transaction_reversal_test.rs | 7 ++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index d54a2b7a4..076f5a055 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -3,6 +3,7 @@ use serde::Serialize; use crate::ApiEnvironment; use crate::CommandId; +use crate::IdentifierTypes; use crate::Mpesa; use crate::MpesaError; use crate::MpesaResult; @@ -20,7 +21,7 @@ pub struct TransactionReversalPayload<'mpesa> { #[serde(rename(serialize = "ReceiverParty"))] receiver_party: &'mpesa str, #[serde(rename(serialize = "ReceiverIdentifierType"))] - receiver_identifier_type: Option<&'mpesa str>, + receiver_identifier_type: Option, #[serde(rename(serialize = "ResultURL"))] result_url: Option<&'mpesa str>, #[serde(rename(serialize = "QueueTimeOutURL"))] @@ -50,7 +51,7 @@ pub struct TransactionReversalBuilder<'mpesa, Env: ApiEnvironment> { command_id: Option, transaction_id: Option<&'mpesa str>, receiver_party: Option<&'mpesa str>, - receiver_identifier_type: Option<&'mpesa str>, + receiver_identifier_type: Option, result_url: Option<&'mpesa str>, timeout_url: Option<&'mpesa str>, remarks: Option<&'mpesa str>, @@ -107,7 +108,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { /// Type of organization receiving the transaction /// /// This is required field - pub fn receiver_identifier_type(mut self, receiver_identifier_type: &'mpesa str) -> Self { + pub fn receiver_identifier_type(mut self, receiver_identifier_type: IdentifierTypes) -> Self { self.receiver_identifier_type = Some(receiver_identifier_type); self } @@ -138,7 +139,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { self } - /// Occasion of the transaction + /// Adds any additional information to be associated with the transaction. /// This is an optional Parameter pub fn occasion(mut self, occasion: &'mpesa str) -> Self { self.occasion = Some(occasion); @@ -186,16 +187,18 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { command_id: self.command_id.unwrap_or(CommandId::TransactionReversal), transaction_id: self .transaction_id - .expect("transaction_id is required field"), + .ok_or(MpesaError::Message("transaction_id is required field"))?, receiver_party: self .receiver_party - .expect("receiver_party is required field"), + .ok_or(MpesaError::Message("receiver_party is required field"))?, receiver_identifier_type: self.receiver_identifier_type, result_url: self.result_url, timeout_url: self.timeout_url, remarks: self.remarks, occasion: self.occasion, - amount: self.amount.expect("amount is required parameter"), + amount: self + .amount + .ok_or(MpesaError::Message("amount is required field"))?, }; let response = self diff --git a/tests/mpesa-rust/transaction_reversal_test.rs b/tests/mpesa-rust/transaction_reversal_test.rs index c487f9b9d..39aa7d867 100644 --- a/tests/mpesa-rust/transaction_reversal_test.rs +++ b/tests/mpesa-rust/transaction_reversal_test.rs @@ -1,3 +1,5 @@ +use mpesa::IdentifierTypes; + use crate::get_mpesa_client; #[tokio::test] @@ -9,12 +11,11 @@ async fn transaction_reversal_test() { .result_url("https://testdomain.com/ok") .timeout_url("https://testdomain.com/err") .transaction_id("OEI2AK4Q16") - .receiver_identifier_type("5") + .receiver_identifier_type(IdentifierTypes::ShortCode) .amount(1.0) - .receiver_party("600983") + .receiver_party("600610") .remarks("wrong recipient") .send() .await; - assert!(response.is_ok()) } From fe4c91bb62156ce5c40a98fcbe55334c88c7a583 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Sat, 19 Nov 2022 19:52:35 +0300 Subject: [PATCH 082/140] Fix tests --- src/services/transaction_reversal.rs | 2 +- tests/mpesa-rust/transaction_reversal_test.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index 076f5a055..fce593aa8 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -20,7 +20,7 @@ pub struct TransactionReversalPayload<'mpesa> { transaction_id: &'mpesa str, #[serde(rename(serialize = "ReceiverParty"))] receiver_party: &'mpesa str, - #[serde(rename(serialize = "ReceiverIdentifierType"))] + #[serde(rename(serialize = "RecieverIdentifierType"))] receiver_identifier_type: Option, #[serde(rename(serialize = "ResultURL"))] result_url: Option<&'mpesa str>, diff --git a/tests/mpesa-rust/transaction_reversal_test.rs b/tests/mpesa-rust/transaction_reversal_test.rs index 39aa7d867..a7a8cabfc 100644 --- a/tests/mpesa-rust/transaction_reversal_test.rs +++ b/tests/mpesa-rust/transaction_reversal_test.rs @@ -13,7 +13,7 @@ async fn transaction_reversal_test() { .transaction_id("OEI2AK4Q16") .receiver_identifier_type(IdentifierTypes::ShortCode) .amount(1.0) - .receiver_party("600610") + .receiver_party("600111") .remarks("wrong recipient") .send() .await; From 6120d43a83aafedfb25ce11b6e73220cf45c3f14 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Sat, 19 Nov 2022 20:07:04 +0300 Subject: [PATCH 083/140] Reversal implemented --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 768c46bd7..fb7f5e0c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # TODO - [x] Unify `mpesa_derive` and `mpesa_core` -- [ ] Add missing services: `reversal`, `transaction_status`, `bill_manager`, `dynamic_qr_code`, `c2b_simulate_v2` +- [ ] Add missing services: `transaction_status`, `bill_manager`, `dynamic_qr_code`, `c2b_simulate_v2` - [x] Clean up `Cargo.toml`: Correctly use Cargo features and declare optional and dev dependencies - [x] Convert library to async and update tests - [x] Migrate to `thiserror` and remove `failure` From 0db0cd8ec480ac5ebe6c446ff8d67869ab9aca16 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 22 Nov 2022 13:44:01 +0300 Subject: [PATCH 084/140] Update docs --- README.md | 17 +++++++++++++++++ src/lib.rs | 35 ++++++++++++++++++++++++++++++++++- src/services/mod.rs | 1 + 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 85cebc5b5..4f02312c4 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Optionally, you can disable default-features, which is basically the entire suit - `c2b_register` - `c2b_simulate` - `express_request` +- `transaction_reversal` Example: @@ -231,6 +232,22 @@ let response = client assert!(response.is_ok()) ``` +- Transaction Reversal: + +```rust +let response = client + .transaction_reversal("testapi496") + .result_url("https://testdomain.com/ok") + .timeout_url("https://testdomain.com/err") + .transaction_id("OEI2AK4Q16") + .receiver_identifier_type(IdentifierTypes::ShortCode) + .amount(100.0) + .receiver_party("600111") + .send() + .await; +assert!(response.is_ok()) +``` + More will be added progressively, pull requests welcome ## Author diff --git a/src/lib.rs b/src/lib.rs index bcfa62a37..fdbb2738f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ //! - `c2b_register` //! - `c2b_simulate` //! - `express_request` +//! - `transaction_reversal` //! //! Example: //! @@ -248,7 +249,7 @@ //! //! * Mpesa Express Request / STK push/ Lipa na M-PESA online //! -//! ```ignore +//! ```rust,no_run //! use mpesa::{Mpesa, Environment}; //! use std::env; //! use dotenv::dotenv; @@ -273,6 +274,38 @@ //! assert!(response.is_ok()) //! } //! ``` +//! +//! * Transaction Reversal +//! +//! ```rust,no_run +//! use mpesa::{Mpesa, Environment, IdentifierTypes}; +//! use std::env; +//! use dotenv::dotenv; +//! +//! #[tokio::main] +//! async fn main() { +//! dotenv().ok(); +//! +//! let client = Mpesa::new( +//! env::var("CLIENT_KEY").unwrap(), +//! env::var("CLIENT_SECRET").unwrap(), +//! Environment::Sandbox +//! ); +//! +//! let response = client +//! .transaction_reversal("testapi496") +//! .result_url("https://testdomain.com/ok") +//! .timeout_url("https://testdomain.com/err") +//! .transaction_id("OEI2AK4Q16") +//! .receiver_identifier_type(IdentifierTypes::ShortCode) +//! .amount(100.0) +//! .receiver_party("600111") +//! .send() +//! .await; +//! assert!(response.is_ok()); +//!} +//! ``` +//! //! More will be added progressively, pull requests welcome //! //!## Author diff --git a/src/services/mod.rs b/src/services/mod.rs index acd388429..b3aade93a 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -11,6 +11,7 @@ //! 4. [C2B Register](https://developer.safaricom.co.ke/docs?shell#c2b-api) //! 5. [C2B Simulate](https://developer.safaricom.co.ke/docs#account-balance-api) //! 6. [Mpesa Express/ STK Push](https://developer.safaricom.co.ke/docs#lipa-na-m-pesa-online-payment) +//! 7. [Transaction Reversal](https://developer.safaricom.co.ke/docs#reversal) mod account_balance; mod b2b; From 7e5b1c07ca1decd5563e541e2a8de4914b6219ad Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 22 Nov 2022 16:16:16 +0300 Subject: [PATCH 085/140] Update initiator_password docs --- src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index cb9e1ab85..36d54f6c2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -61,8 +61,8 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { &self.environment } - /// Gets the initiator password as a byte slice - /// If `None`, the default password is b"Safcom496!" + /// Gets the initiator password + /// If `None`, the default password is `"Safcom496!"` pub(crate) fn initiator_password(&'mpesa self) -> String { let Some(p) = &*self.initiator_password.borrow() else { return DEFAULT_INITIATOR_PASSWORD.to_owned() From 5aa5c457799512673145c40401a26c6d00fd6fc8 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 22 Nov 2022 21:38:24 +0300 Subject: [PATCH 086/140] Add ci job to sync docs; update publish job --- .github/workflows/release-core.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index d5444af51..7ef844a85 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -9,7 +9,7 @@ env: CARGO_TERM_COLOR: always jobs: - release_mpesa_core: + publish_to_crate_io: runs-on: ubuntu-latest @@ -19,8 +19,28 @@ jobs: with: toolchain: stable override: true - - name: Publish + - name: Publish to crates.io run: | cargo doc cargo login ${{ secrets.CARGO_REGISTRY_TOKEN }} cargo publish + + sync_docs_to_gh_pages: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - name: Sync Docs + run: | + cargo doc + pip install ghp-import + echo '' > target/doc/index.html + ghp-import -n target/doc + git push -qf https://github.com/collinsmuriuki/mpesa-rust.git gh-pages From c27e9cf1aefcbb8ed5b57e964bcf49eabc7e24e7 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 22 Nov 2022 16:11:32 +0300 Subject: [PATCH 087/140] Remove unnecessary environment method --- src/client.rs | 8 ++++---- src/services/account_balance.rs | 2 +- src/services/b2b.rs | 2 +- src/services/b2c.rs | 2 +- src/services/c2b_register.rs | 2 +- src/services/c2b_simulate.rs | 2 +- src/services/express_request.rs | 2 +- src/services/transaction_reversal.rs | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/client.rs b/src/client.rs index 36d54f6c2..77cb16621 100644 --- a/src/client.rs +++ b/src/client.rs @@ -24,7 +24,7 @@ pub struct Mpesa { client_key: String, client_secret: String, initiator_password: RefCell>, - environment: Env, + pub(crate) environment: Env, pub(crate) http_client: HttpClient, } @@ -302,7 +302,7 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { /// # Errors /// Returns `EncryptionError` variant of `MpesaError` pub(crate) fn gen_security_credentials(&self) -> MpesaResult { - let pem = self.environment().get_certificate().as_bytes(); + let pem = self.environment.get_certificate().as_bytes(); let cert = X509::from_pem(pem)?; // getting the public and rsa keys let pub_key = cert.public_key()?; @@ -350,8 +350,8 @@ mod tests { #[test] fn test_custom_environment() { let client = Mpesa::new("client_key", "client_secret", TestEnvironment); - assert_eq!(client.environment().base_url(), "https://example.com"); - assert_eq!(client.environment().get_certificate(), "certificate"); + assert_eq!(client.environment.base_url(), "https://example.com"); + assert_eq!(client.environment.get_certificate(), "certificate"); } #[test] diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index 78bc718d7..4905af021 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -158,7 +158,7 @@ impl<'mpesa, Env: ApiEnvironment> AccountBalanceBuilder<'mpesa, Env> { pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/accountbalance/v1/query", - self.client.environment().base_url() + self.client.environment.base_url() ); let credentials = self.client.gen_security_credentials()?; diff --git a/src/services/b2b.rs b/src/services/b2b.rs index a8dfba33e..f44b14ecd 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -218,7 +218,7 @@ impl<'mpesa, Env: ApiEnvironment> B2bBuilder<'mpesa, Env> { pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/b2b/v1/paymentrequest", - self.client.environment().base_url() + self.client.environment.base_url() ); let credentials = self.client.gen_security_credentials()?; diff --git a/src/services/b2c.rs b/src/services/b2c.rs index 97021aba4..1109f2385 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -195,7 +195,7 @@ impl<'mpesa, Env: ApiEnvironment> B2cBuilder<'mpesa, Env> { pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/b2c/v1/paymentrequest", - self.client.environment().base_url() + self.client.environment.base_url() ); let credentials = self.client.gen_security_credentials()?; diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index 0d401a58c..e6466943a 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -108,7 +108,7 @@ impl<'mpesa, Env: ApiEnvironment> C2bRegisterBuilder<'mpesa, Env> { pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/c2b/v1/registerurl", - self.client.environment().base_url() + self.client.environment.base_url() ); let payload = C2bRegisterPayload { diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index bd0568f67..34f0b0358 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -121,7 +121,7 @@ impl<'mpesa, Env: ApiEnvironment> C2bSimulateBuilder<'mpesa, Env> { pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/c2b/v1/simulate", - self.client.environment().base_url() + self.client.environment.base_url() ); let payload = C2bSimulatePayload { diff --git a/src/services/express_request.rs b/src/services/express_request.rs index 6bb3971ff..6bac078dc 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -224,7 +224,7 @@ impl<'mpesa, Env: ApiEnvironment> MpesaExpressRequestBuilder<'mpesa, Env> { pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/stkpush/v1/processrequest", - self.client.environment().base_url() + self.client.environment.base_url() ); let (password, timestamp) = self.generate_password_and_timestamp(); diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index fce593aa8..5fd4bb28a 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -176,7 +176,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/reversal/v1/request", - self.client.environment().base_url() + self.client.environment.base_url() ); let credentials = self.client.gen_security_credentials()?; From 9ed8b1552ff0f069581926ac8e19bd880f2ce84a Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 22 Nov 2022 21:55:07 +0300 Subject: [PATCH 088/140] Remove dead code --- src/client.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index 77cb16621..0ad29af33 100644 --- a/src/client.rs +++ b/src/client.rs @@ -56,11 +56,6 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { } } - /// Gets the current `Environment` - pub(crate) fn environment(&'mpesa self) -> &Env { - &self.environment - } - /// Gets the initiator password /// If `None`, the default password is `"Safcom496!"` pub(crate) fn initiator_password(&'mpesa self) -> String { From 950fb423a3fee21d093c6b0af22c8e776b6024b2 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 22 Nov 2022 22:11:39 +0300 Subject: [PATCH 089/140] Make transaction reversal fields public --- src/services/transaction_reversal.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index 5fd4bb28a..1e34758f1 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -37,11 +37,11 @@ pub struct TransactionReversalPayload<'mpesa> { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransactionReversalResponse { #[serde(rename(deserialize = "ConversationID"))] - conversation_id: String, + pub conversation_id: String, #[serde(rename(deserialize = "OriginatorConversationID"))] - originator_conversation_id: String, + pub originator_conversation_id: String, #[serde(rename(deserialize = "ResponseDescription"))] - response_description: String, + pub response_description: String, } #[derive(Debug)] From 773f49bd2fe747c42f17e783d5c3c15bf8c9c223 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Tue, 22 Nov 2022 22:17:46 +0300 Subject: [PATCH 090/140] Implementation of transaction status --- Cargo.toml | 6 +- src/client.rs | 11 +- src/errors.rs | 2 + src/services/mod.rs | 3 + src/services/transaction_status.rs | 185 ++++++++++++++++++++ tests/mpesa-rust/main.rs | 1 + tests/mpesa-rust/transaction_status_test.rs | 21 +++ 7 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 src/services/transaction_status.rs create mode 100644 tests/mpesa-rust/transaction_status_test.rs diff --git a/Cargo.toml b/Cargo.toml index 05d81a19d..90a91d1df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,8 @@ default = [ "c2b_register", "c2b_simulate", "express_request", - "transaction_reversal" + "transaction_reversal", + "transaction_status" ] account_balance = ["dep:openssl", "dep:base64"] b2b = ["dep:openssl", "dep:base64"] @@ -40,4 +41,5 @@ b2c = ["dep:openssl", "dep:base64"] c2b_register = [] c2b_simulate = [] express_request = ["dep:chrono", "dep:base64"] -transaction_reversal = ["dep:openssl", "dep:base64"] +transaction_reversal = ["dep:openssl"] +transaction_status= ["dep:openssl"] diff --git a/src/client.rs b/src/client.rs index 36d54f6c2..71be219cb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,7 +1,7 @@ use crate::environment::ApiEnvironment; use crate::services::{ AccountBalanceBuilder, B2bBuilder, B2cBuilder, C2bRegisterBuilder, C2bSimulateBuilder, - MpesaExpressRequestBuilder, TransactionReversalBuilder, + MpesaExpressRequestBuilder, TransactionReversalBuilder, TransactionStatusBuilder, }; use crate::MpesaError; use openssl::rsa::Padding; @@ -294,6 +294,15 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { TransactionReversalBuilder::new(self, initiator_name) } + // TODO::Add docs + #[cfg(feature = "transaction_status")] + pub fn transaction_status( + &'mpesa self, + initiator_name: &'mpesa str, + ) -> TransactionStatusBuilder<'mpesa, Env> { + TransactionStatusBuilder::new(self, initiator_name) + } + /// Generates 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. diff --git a/src/errors.rs b/src/errors.rs index b36fa78b3..a3c39e46c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -19,6 +19,8 @@ pub enum MpesaError { MpesaExpressRequestError(serde_json::Value), #[error("Mpesa Transaction reversal failed: {0}")] MpesaTransactionReversalError(serde_json::Value), + #[error("Mpesa Transaction status failed: {0}")] + MpesaTransactionStatusError(serde_json::Value), #[error("An error has occured while performing the http request")] NetworkError(#[from] reqwest::Error), #[error("An error has occured while serializig/ deserializing")] diff --git a/src/services/mod.rs b/src/services/mod.rs index b3aade93a..3354f35ac 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -20,6 +20,7 @@ mod c2b_register; mod c2b_simulate; mod express_request; mod transaction_reversal; +mod transaction_status; #[cfg(feature = "account_balance")] pub use account_balance::{AccountBalanceBuilder, AccountBalanceResponse}; @@ -35,3 +36,5 @@ pub use c2b_simulate::{C2bSimulateBuilder, C2bSimulateResponse}; pub use express_request::{MpesaExpressRequestBuilder, MpesaExpressRequestResponse}; #[cfg(feature = "transaction_reversal")] pub use transaction_reversal::{TransactionReversalBuilder, TransactionReversalResponse}; +#[cfg(feature = "transaction_status")] +pub use transaction_status::{TransactionStatusBuilder, TransactionStatusResponse}; diff --git a/src/services/transaction_status.rs b/src/services/transaction_status.rs new file mode 100644 index 000000000..d60d7d3d0 --- /dev/null +++ b/src/services/transaction_status.rs @@ -0,0 +1,185 @@ +use serde::Deserialize; +use serde::Serialize; + +use crate::ApiEnvironment; +use crate::CommandId; +use crate::IdentifierTypes; +use crate::Mpesa; +use crate::MpesaError; +use crate::MpesaResult; + +#[derive(Debug, Serialize)] +pub struct TransactionStatusPayload<'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 = "PartyA")] + party_a: Option<&'mpesa str>, + #[serde(rename(serialize = "IdentifierType"))] + identifier_type: Option, + #[serde(rename(serialize = "ResultURL"))] + result_url: Option<&'mpesa str>, + #[serde(rename(serialize = "QueueTimeOutURL"))] + timeout_url: Option<&'mpesa str>, + #[serde(rename(serialize = "Remarks"))] + remarks: Option<&'mpesa str>, + #[serde(rename(serialize = "Occasion"))] + occasion: Option<&'mpesa str>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransactionStatusResponse { + #[serde(rename(deserialize = "ConversationID"))] + conversation_id: String, + #[serde(rename(deserialize = "OriginatorConversationID"))] + originator_conversation_id: String, + #[serde(rename(deserialize = "ResponseDescription"))] + response_description: String, +} + +#[derive(Debug)] +pub struct TransactionStatusBuilder<'mpesa, Env: ApiEnvironment> { + client: &'mpesa Mpesa, + initiator: &'mpesa str, + command_id: Option, + transaction_id: Option<&'mpesa str>, + party_a: Option<&'mpesa str>, + identifier_type: Option, + result_url: Option<&'mpesa str>, + timeout_url: Option<&'mpesa str>, + remarks: Option<&'mpesa str>, + occasion: Option<&'mpesa str>, +} + +impl<'mpesa, Env: ApiEnvironment> TransactionStatusBuilder<'mpesa, Env> { + /// Creates new `TransactionStatusBuilder` + pub fn new( + client: &'mpesa Mpesa, + initiator: &'mpesa str, + ) -> TransactionStatusBuilder<'mpesa, Env> { + TransactionStatusBuilder { + client, + initiator, + command_id: None, + transaction_id: None, + party_a: None, + identifier_type: None, + result_url: None, + timeout_url: None, + remarks: None, + occasion: 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 party_a(mut self, party_a: &'mpesa str) -> Self { + self.party_a = Some(party_a); + self + } + + /// Type of organization receiving the transaction + /// + /// This is required field + pub fn identifier_type(mut self, identifier_type: IdentifierTypes) -> Self { + self.identifier_type = Some(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 required field + 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 + pub fn occasion(mut self, occasion: &'mpesa str) -> Self { + self.occasion = Some(occasion); + self + } + + pub async fn send(self) -> MpesaResult { + let url = format!( + "{}/mpesa/transactionstatus/v1/query", + self.client.environment().base_url() + ); + + let credentials = self.client.gen_security_credentials()?; + + let payload = TransactionStatusPayload { + initiator: self.initiator, + security_credentials: &credentials, + command_id: self.command_id.unwrap_or(CommandId::TransactionStatusQuery), + transaction_id: self + .transaction_id + .ok_or(MpesaError::Message("transaction_id is required field"))?, + party_a: self.party_a, + identifier_type: self.identifier_type, + result_url: self.result_url, + timeout_url: self.timeout_url, + remarks: self.remarks, + occasion: self.occasion, + }; + + let response = self + .client + .http_client + .post(&url) + .bearer_auth(self.client.auth().await?) + .json(&payload) + .send() + .await?; + + if !response.status().is_success() { + let value = response.json().await?; + return Err(MpesaError::MpesaTransactionStatusError(value)); + }; + + let response = response.json::<_>().await?; + Ok(response) + } +} diff --git a/tests/mpesa-rust/main.rs b/tests/mpesa-rust/main.rs index c00401fdc..39e4de764 100644 --- a/tests/mpesa-rust/main.rs +++ b/tests/mpesa-rust/main.rs @@ -6,3 +6,4 @@ mod c2b_simulate_test; mod helpers; mod stk_push_test; mod transaction_reversal_test; +mod transaction_status_test; diff --git a/tests/mpesa-rust/transaction_status_test.rs b/tests/mpesa-rust/transaction_status_test.rs new file mode 100644 index 000000000..1d52b04d6 --- /dev/null +++ b/tests/mpesa-rust/transaction_status_test.rs @@ -0,0 +1,21 @@ +use mpesa::IdentifierTypes; + +use crate::get_mpesa_client; + +#[tokio::test] +async fn transaction_reversal_test() { + let client = get_mpesa_client!(); + + let response = client + .transaction_status("testapi496") + .result_url("https://testdomain.com/ok") + .timeout_url("https://testdomain.com/err") + .transaction_id("OEI2AK4Q16") + .identifier_type(IdentifierTypes::ShortCode) + .party_a("600111") + .remarks("status") + .occasion("work") + .send() + .await; + assert!(response.is_ok()) +} From 0e8335a4fbed9995e5f02cf56ffc08384e67e832 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Tue, 22 Nov 2022 22:19:07 +0300 Subject: [PATCH 091/140] Make transaction status field public --- src/services/transaction_status.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/transaction_status.rs b/src/services/transaction_status.rs index d60d7d3d0..1b51f904a 100644 --- a/src/services/transaction_status.rs +++ b/src/services/transaction_status.rs @@ -35,11 +35,11 @@ pub struct TransactionStatusPayload<'mpesa> { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransactionStatusResponse { #[serde(rename(deserialize = "ConversationID"))] - conversation_id: String, + pub conversation_id: String, #[serde(rename(deserialize = "OriginatorConversationID"))] - originator_conversation_id: String, + pub originator_conversation_id: String, #[serde(rename(deserialize = "ResponseDescription"))] - response_description: String, + pub response_description: String, } #[derive(Debug)] From c98e2bddce15204a71ace1a26114f9b6f81e40fe Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Tue, 22 Nov 2022 22:22:07 +0300 Subject: [PATCH 092/140] Change test name --- tests/mpesa-rust/transaction_status_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mpesa-rust/transaction_status_test.rs b/tests/mpesa-rust/transaction_status_test.rs index 1d52b04d6..4a2214f35 100644 --- a/tests/mpesa-rust/transaction_status_test.rs +++ b/tests/mpesa-rust/transaction_status_test.rs @@ -3,7 +3,7 @@ use mpesa::IdentifierTypes; use crate::get_mpesa_client; #[tokio::test] -async fn transaction_reversal_test() { +async fn transaction_status_test() { let client = get_mpesa_client!(); let response = client From 7004a8764af525a9f2f7abfec5b5f5381e0d3b6e Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Wed, 23 Nov 2022 19:17:01 +0300 Subject: [PATCH 093/140] Adds docs --- README.md | 34 +++++++++++++++++++++++------- src/client.rs | 18 ++++++++++++++-- src/services/transaction_status.rs | 18 +++++++++++++++- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4f02312c4..f582ae2fe 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ mpesa = { git = "https://github.com/collinsmuriuki/mpesa-rust" } ``` Optionally, you can disable default-features, which is basically the entire suite of MPESA APIs to conditionally select from either: + - `b2b` - `b2c` - `account_balance` @@ -148,7 +149,7 @@ assert!(client.is_connected().await) The following services are currently available from the `Mpesa` client as methods that return builders: -- B2C +- B2C ```rust let response = client @@ -163,7 +164,7 @@ let response = client assert!(response.is_ok()) ``` -- B2B +- B2B ```rust let response = client @@ -179,7 +180,7 @@ let response = client assert!(response.is_ok()) ``` -- C2B Register +- C2B Register ```rust let response = client @@ -192,7 +193,7 @@ let response = client assert!(response.is_ok()) ``` -- C2B Simulate +- C2B Simulate ```rust @@ -206,7 +207,7 @@ let response = client assert!(response.is_ok()) ``` -- Account Balance +- Account Balance ```rust let response = client @@ -219,7 +220,7 @@ let response = client assert!(response.is_ok()) ``` -- Mpesa Express Request / STK push / Lipa na M-PESA online +- Mpesa Express Request / STK push / Lipa na M-PESA online ```rust let response = client @@ -248,14 +249,31 @@ let response = client assert!(response.is_ok()) ``` +- Transaction Status + +```rust +let response = client + .transaction_status("testapi496") + .result_url("https://testdomain.com/ok") + .timeout_url("https://testdomain.com/err") + .transaction_id("OEI2AK4Q16") + .identifier_type(IdentifierTypes::ShortCode) + .party_a("600111") + .remarks("status") + .occasion("work") + .send() + .await; +assert!(response.is_ok()) +``` + More will be added progressively, pull requests welcome ## Author **Collins Muriuki** -- Twitter: [@collinsmuriuki\_](https://twitter.com/collinsmuriuki_) -- Not affiliated with Safaricom. +- Twitter: [@collinsmuriuki\_](https://twitter.com/collinsmuriuki_) +- Not affiliated with Safaricom. ## Contributing diff --git a/src/client.rs b/src/client.rs index 71be219cb..49f501a56 100644 --- a/src/client.rs +++ b/src/client.rs @@ -293,8 +293,22 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { ) -> TransactionReversalBuilder<'mpesa, Env> { TransactionReversalBuilder::new(self, initiator_name) } - - // TODO::Add docs + ///**Transaction Status Builder** + /// Queries the status of a B2B, B2C or C2B M-Pesa transaction. + /// + /// See more from the Safaricom API docs [here](https://developer.safaricom.co.ke/Documentation) + /// # Example + /// ```ignore + /// let response = client + /// .transaction_status("testapi496") + /// .party_a("600496") + /// .identifier_type(mpesa::IdentifierTypes::ShortCode) // optional, defaults to `IdentifierTypes::ShortCode` + /// .remarks("Your Remarks") // optional, defaults to "None" + /// .result_url("https://testdomain.com/err") + /// .timeout_url("https://testdomain.com/ok") + /// .send() + /// .await; + /// ``` #[cfg(feature = "transaction_status")] pub fn transaction_status( &'mpesa self, diff --git a/src/services/transaction_status.rs b/src/services/transaction_status.rs index 1b51f904a..6a42dcf01 100644 --- a/src/services/transaction_status.rs +++ b/src/services/transaction_status.rs @@ -76,7 +76,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionStatusBuilder<'mpesa, Env> { } } - /// Adds `CommandId`. Defaults to `CommandId::TransactionReversal` if no value explicitly passed + /// Adds `CommandId`. Defaults to `CommandId::TransactionStatus` if no value explicitly passed /// /// # Errors /// If `CommandId` is not valid @@ -142,6 +142,22 @@ impl<'mpesa, Env: ApiEnvironment> TransactionStatusBuilder<'mpesa, Env> { self } + /// # Transaction Status API + /// + /// Requests for the status of a transaction + /// + /// This API enables the status of a B2B, B2C or C2B M-Pesa transaction + /// Required parameters: + /// + /// `transaction_id`: This is the Mpesa Transaction ID of the transaction which you wish to reverse + /// + /// + /// See more from the Safaricom API docs [here](https://developer.safaricom.co.ke/Documentation) + /// + /// A successful request returns a `TransactionStatusResponse` type + /// + /// # Errors + /// Returns a `MpesaError` on failure. pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/transactionstatus/v1/query", From b266a96d2023286b022dec14c796b2982662ca2d Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Wed, 23 Nov 2022 19:21:22 +0300 Subject: [PATCH 094/140] Small fixes --- src/services/transaction_status.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/transaction_status.rs b/src/services/transaction_status.rs index 6a42dcf01..2b7eb72d5 100644 --- a/src/services/transaction_status.rs +++ b/src/services/transaction_status.rs @@ -161,7 +161,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionStatusBuilder<'mpesa, Env> { pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/transactionstatus/v1/query", - self.client.environment().base_url() + self.client.environment.base_url() ); let credentials = self.client.gen_security_credentials()?; From 706124ce5a2f49f2ef2dc77b0e90cb3f72662235 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Fri, 25 Nov 2022 07:13:22 +0300 Subject: [PATCH 095/140] Add base64 dep --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 90a91d1df..0928fdac9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,5 +41,5 @@ b2c = ["dep:openssl", "dep:base64"] c2b_register = [] c2b_simulate = [] express_request = ["dep:chrono", "dep:base64"] -transaction_reversal = ["dep:openssl"] -transaction_status= ["dep:openssl"] +transaction_reversal = ["dep:openssl", "dep:base64"] +transaction_status= ["dep:openssl", "dep:base64"] From 460e43e4673440efb886a7d845b8be5ed7077e70 Mon Sep 17 00:00:00 2001 From: Yasir Date: Fri, 25 Nov 2022 08:08:29 +0300 Subject: [PATCH 096/140] Improvements for `Environment` values (#58) * Implementation of transaction status * Make transaction status field public * Change test name * Adds docs * Small fixes * Add base64 dep * Support more cases for Env --- src/environment.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/environment.rs b/src/environment.rs index 38ea7ebdc..2487120f9 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -34,8 +34,8 @@ pub trait ApiEnvironment { macro_rules! environment_from_string { ($v:expr) => { match $v { - "production" | "Production" | "PRODUCTION" => Ok(Self::Production), - "sandbox" | "Sandbox" | "SANDBOX" => Ok(Self::Sandbox), + "production" => Ok(Self::Production), + "sandbox" => Ok(Self::Sandbox), _ => Err(MpesaError::Message( "Could not parse the provided environment name", )), @@ -47,7 +47,7 @@ impl FromStr for Environment { type Err = MpesaError; fn from_str(s: &str) -> Result { - environment_from_string!(s) + environment_from_string!(s.to_lowercase().as_str()) } } @@ -55,7 +55,7 @@ impl TryFrom<&str> for Environment { type Error = MpesaError; fn try_from(v: &str) -> Result { - environment_from_string!(v) + environment_from_string!(v.to_lowercase().as_str()) } } @@ -63,7 +63,7 @@ impl TryFrom for Environment { type Error = MpesaError; fn try_from(v: String) -> Result { - environment_from_string!(v.as_str()) + environment_from_string!(v.to_lowercase().as_str()) } } @@ -93,8 +93,9 @@ mod tests { #[test] fn test_valid_string_is_parsed_as_environment() { - let accepted_production_values = vec!["production", "Production", "PRODUCTION"]; - let accepted_sandbox_values = vec!["sandbox", "Sandbox", "SANDBOX"]; + let accepted_production_values = + vec!["production", "Production", "PRODUCTION", "prODUctIoN"]; + let accepted_sandbox_values = vec!["sandbox", "Sandbox", "SANDBOX", "sanDBoX"]; accepted_production_values.into_iter().for_each(|v| { let environment: Environment = v.parse().unwrap(); assert_eq!(environment.base_url(), "https://api.safaricom.co.ke"); From 96849201fe03348e18309f3f3cec3efc8633d4b5 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 6 Feb 2023 21:49:52 +0300 Subject: [PATCH 097/140] Use latest version of wiremock --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 0928fdac9..a4f9e3022 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ serde = {version="1.0", features= ["derive"]} serde_json = "1.0" serde_repr = "0.1" thiserror = "1.0.37" +wiremock = "0.5" [dev-dependencies] dotenv = "0.15" From efb0a1929964e3b620e42c1c49d0bbdc423c6ed2 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 08:27:23 +0300 Subject: [PATCH 098/140] Implement black box testing; refactor tests --- src/services/c2b_register.rs | 11 ++++-- src/services/c2b_simulate.rs | 6 ++-- tests/mpesa-rust/account_balance_test.rs | 30 +++++++++++++--- tests/mpesa-rust/b2b_test.rs | 31 +++++++++++++--- tests/mpesa-rust/b2c_test.rs | 30 +++++++++++++--- tests/mpesa-rust/c2b_register_test.rs | 30 ++++++++++++---- tests/mpesa-rust/c2b_simulate_test.rs | 29 ++++++++++++--- tests/mpesa-rust/helpers.rs | 36 +++++++++++-------- tests/mpesa-rust/stk_push_test.rs | 34 +++++++++++++++--- tests/mpesa-rust/transaction_reversal_test.rs | 30 ++++++++++++---- tests/mpesa-rust/transaction_status_test.rs | 28 ++++++++++++--- 11 files changed, 234 insertions(+), 61 deletions(-) diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index e6466943a..6839459a4 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -19,10 +19,15 @@ struct C2bRegisterPayload<'mpesa> { #[derive(Debug, Deserialize, Clone)] pub struct C2bRegisterResponse { - #[serde(rename(deserialize = "ConversationID"), skip_serializing_if = "None")] + #[serde( + rename(deserialize = "ConversationID"), + skip_serializing_if = "Option::is_none" + )] pub conversation_id: Option, - #[serde(rename(deserialize = "OriginatorCoversationID"))] - pub originator_coversation_id: String, + #[serde(rename(deserialize = "OriginatorConverstionID"))] + pub originator_conversation_id: String, + #[serde(rename(deserialize = "ResponseCode"))] + pub response_code: String, #[serde(rename(deserialize = "ResponseDescription"))] pub response_description: String, } diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index 34f0b0358..8665c6c59 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -33,8 +33,10 @@ pub struct C2bSimulateResponse { skip_serializing_if = "Option::is_none" )] pub conversation_id: Option, - #[serde(rename(deserialize = "OriginatorCoversationID"))] - pub originator_coversation_id: String, + #[serde(rename(deserialize = "OriginatorConversationID"))] + pub originator_conversation_id: String, + #[serde(rename(deserialize = "ResponseCode"))] + pub response_code: String, #[serde(rename(deserialize = "ResponseDescription"))] pub response_description: String, } diff --git a/tests/mpesa-rust/account_balance_test.rs b/tests/mpesa-rust/account_balance_test.rs index aa8770327..336916d90 100644 --- a/tests/mpesa-rust/account_balance_test.rs +++ b/tests/mpesa-rust/account_balance_test.rs @@ -1,16 +1,36 @@ use crate::get_mpesa_client; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; #[tokio::test] async fn account_balance_test() { - let client = get_mpesa_client!(); - + 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/accountbalance/v1/query")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(1) + .mount(&server) + .await; let response = client .account_balance("testapi496") .result_url("https://testdomain.com/ok") .timeout_url("https://testdomain.com/err") .party_a("600496") .send() - .await; - - assert!(response.is_ok()) + .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." + ); + assert_eq!(response.response_code, "0"); } diff --git a/tests/mpesa-rust/b2b_test.rs b/tests/mpesa-rust/b2b_test.rs index 521fa1c52..ba13dbb65 100644 --- a/tests/mpesa-rust/b2b_test.rs +++ b/tests/mpesa-rust/b2b_test.rs @@ -1,9 +1,24 @@ +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; + use crate::get_mpesa_client; #[tokio::test] async fn b2b_test() { - let client = get_mpesa_client!(); - + 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/b2b/v1/paymentrequest")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(1) + .mount(&server) + .await; let response = client .b2b("testapi496") .party_a("600496") @@ -13,7 +28,13 @@ async fn b2b_test() { .account_ref("254708374149") .amount(1000) .send() - .await; - - assert!(response.is_ok()) + .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." + ); + assert_eq!(response.response_code, "0"); } diff --git a/tests/mpesa-rust/b2c_test.rs b/tests/mpesa-rust/b2c_test.rs index f2223ce53..617be5f68 100644 --- a/tests/mpesa-rust/b2c_test.rs +++ b/tests/mpesa-rust/b2c_test.rs @@ -1,9 +1,23 @@ use crate::get_mpesa_client; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; #[tokio::test] async fn b2c_test() { - let client = get_mpesa_client!(); - + 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/b2c/v1/paymentrequest")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(1) + .mount(&server) + .await; let response = client .b2c("testapi496") .party_a("600496") @@ -12,7 +26,13 @@ async fn b2c_test() { .timeout_url("https://testdomain.com/err") .amount(1000) .send() - .await; - - assert!(response.is_ok()) + .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." + ); + assert_eq!(response.response_code, "0"); } diff --git a/tests/mpesa-rust/c2b_register_test.rs b/tests/mpesa-rust/c2b_register_test.rs index d6b00fd46..d0fe477d2 100644 --- a/tests/mpesa-rust/c2b_register_test.rs +++ b/tests/mpesa-rust/c2b_register_test.rs @@ -1,17 +1,35 @@ use crate::get_mpesa_client; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; #[tokio::test] -#[ignore = "c2b_register always fails on sandbox with status 503"] async fn c2b_register_test() { - let client = get_mpesa_client!(); - + let (client, server) = get_mpesa_client!(); + let sample_response_body = json!({ + "OriginatorConverstionID": "29464-48063588-1", + "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/c2b/v1/registerurl")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(1) + .mount(&server) + .await; let response = client .c2b_register() .short_code("600496") .confirmation_url("https://testdomain.com/true") .validation_url("https://testdomain.com/valid") .send() - .await; - - assert!(response.is_ok()) + .await + .unwrap(); + assert_eq!(response.originator_conversation_id, "29464-48063588-1"); + assert_eq!( + response.response_description, + "Accept the service request successfully." + ); + assert_eq!(response.response_code, "0"); + assert_eq!(response.conversation_id, None); } diff --git a/tests/mpesa-rust/c2b_simulate_test.rs b/tests/mpesa-rust/c2b_simulate_test.rs index c04ca6e76..9adc2242a 100644 --- a/tests/mpesa-rust/c2b_simulate_test.rs +++ b/tests/mpesa-rust/c2b_simulate_test.rs @@ -1,16 +1,35 @@ use crate::get_mpesa_client; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; #[tokio::test] async fn c2b_simulate_test() { - let client = get_mpesa_client!(); - + let (client, server) = get_mpesa_client!(); + let sample_response_body = json!({ + "OriginatorConversationID": "29464-48063588-1", + "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/c2b/v1/simulate")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(1) + .mount(&server) + .await; let response = client .c2b_simulate() .short_code("600496") .msisdn("254700000000") .amount(1000) .send() - .await; - - assert!(response.is_ok()) + .await + .unwrap(); + assert_eq!(response.originator_conversation_id, "29464-48063588-1"); + assert_eq!( + response.response_description, + "Accept the service request successfully." + ); + assert_eq!(response.response_code, "0"); + assert_eq!(response.conversation_id, None); } diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index ca341456e..0007adc4b 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -2,17 +2,14 @@ use mpesa::ApiEnvironment; use wiremock::MockServer; pub struct TestEnvironment { - pub server: MockServer, pub server_url: String, } impl TestEnvironment { #[allow(unused)] - pub async fn new() -> Self { - let mock_server = MockServer::start().await; + pub async fn new(server: &MockServer) -> Self { TestEnvironment { - server_url: mock_server.uri(), - server: mock_server, + server_url: server.uri(), } } } @@ -31,15 +28,30 @@ impl ApiEnvironment for TestEnvironment { #[macro_export] macro_rules! get_mpesa_client { () => {{ - use mpesa::{Environment, Mpesa}; - use std::str::FromStr; + use crate::helpers::TestEnvironment; + use mpesa::Mpesa; + use wiremock::{MockServer, Mock, ResponseTemplate}; + use serde_json::json; + use wiremock::matchers::{path, query_param, method}; + dotenv::dotenv().ok(); + let server = MockServer::start().await; + let test_environment = TestEnvironment::new(&server).await; let client = Mpesa::new( std::env::var("CLIENT_KEY").unwrap(), std::env::var("CLIENT_SECRET").unwrap(), - Environment::from_str("sandbox").unwrap(), + test_environment, ); - client + Mock::given(method("GET")) + .and(path("/oauth/v1/generate")) + .and(query_param("grant_type", "client_credentials")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "access_token": "dummy_access_token" + }))) + .expect(1) + .mount(&server) + .await; + (client, server) }}; ($client_key:expr, $client_secret:expr) => {{ @@ -67,12 +79,6 @@ macro_rules! get_mpesa_client { mod tests { use crate::get_mpesa_client; - #[tokio::test] - async fn test_client_is_created_successfuly_with_correct_credentials() { - let client = get_mpesa_client!(); - assert!(client.is_connected().await); - } - #[tokio::test] async fn test_client_will_not_authenticate_with_wrong_credentials() { let client = get_mpesa_client!( diff --git a/tests/mpesa-rust/stk_push_test.rs b/tests/mpesa-rust/stk_push_test.rs index 532bd75ad..e15c2d132 100644 --- a/tests/mpesa-rust/stk_push_test.rs +++ b/tests/mpesa-rust/stk_push_test.rs @@ -1,16 +1,40 @@ use crate::get_mpesa_client; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; #[tokio::test] async fn stk_push_test() { - let client = get_mpesa_client!(); - + 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" + }); + 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 response = client .express_request("174379") .phone_number("254708374149") .amount(500) .callback_url("https://test.example.com/api") .send() - .await; - - assert!(response.is_ok()) + .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 accepeted for processing" + ); } diff --git a/tests/mpesa-rust/transaction_reversal_test.rs b/tests/mpesa-rust/transaction_reversal_test.rs index a7a8cabfc..ffac5c5a8 100644 --- a/tests/mpesa-rust/transaction_reversal_test.rs +++ b/tests/mpesa-rust/transaction_reversal_test.rs @@ -1,11 +1,23 @@ -use mpesa::IdentifierTypes; - use crate::get_mpesa_client; +use mpesa::IdentifierTypes; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; #[tokio::test] async fn transaction_reversal_test() { - let client = get_mpesa_client!(); - + 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.", + }); + 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 response = client .transaction_reversal("testapi496") .result_url("https://testdomain.com/ok") @@ -16,6 +28,12 @@ async fn transaction_reversal_test() { .receiver_party("600111") .remarks("wrong recipient") .send() - .await; - assert!(response.is_ok()) + .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." + ); } diff --git a/tests/mpesa-rust/transaction_status_test.rs b/tests/mpesa-rust/transaction_status_test.rs index 4a2214f35..d0a5b999a 100644 --- a/tests/mpesa-rust/transaction_status_test.rs +++ b/tests/mpesa-rust/transaction_status_test.rs @@ -1,11 +1,25 @@ 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_status_test() { - let client = get_mpesa_client!(); - + 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.", + }); + Mock::given(method("POST")) + .and(path("/mpesa/transactionstatus/v1/query")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(1) + .mount(&server) + .await; let response = client .transaction_status("testapi496") .result_url("https://testdomain.com/ok") @@ -16,6 +30,12 @@ async fn transaction_status_test() { .remarks("status") .occasion("work") .send() - .await; - assert!(response.is_ok()) + .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." + ); } From 6fb941aa76ae59c5404ce7d81b721ac043ab5f82 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 08:49:13 +0300 Subject: [PATCH 099/140] Extend stk push tests --- src/services/express_request.rs | 30 +++++------ tests/mpesa-rust/helpers.rs | 6 ++- tests/mpesa-rust/stk_push_test.rs | 89 ++++++++++++++++++++++++++++++- 3 files changed, 108 insertions(+), 17 deletions(-) diff --git a/src/services/express_request.rs b/src/services/express_request.rs index 6bac078dc..170ca0f85 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -24,16 +24,10 @@ struct MpesaExpressRequestPayload<'mpesa> { party_a: Option<&'mpesa str>, #[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")] party_b: Option<&'mpesa str>, - #[serde( - rename(serialize = "PhoneNumber"), - skip_serializing_if = "Option::is_none" - )] - phone_number: Option<&'mpesa str>, - #[serde( - rename(serialize = "CallBackURL"), - skip_serializing_if = "Option::is_none" - )] - call_back_url: Option<&'mpesa str>, + #[serde(rename(serialize = "PhoneNumber"))] + phone_number: &'mpesa str, + #[serde(rename(serialize = "CallBackURL"))] + call_back_url: &'mpesa str, #[serde(rename(serialize = "AccountReference"))] account_reference: &'mpesa str, #[serde(rename(serialize = "TransactionDesc"))] @@ -233,7 +227,9 @@ impl<'mpesa, Env: ApiEnvironment> MpesaExpressRequestBuilder<'mpesa, Env> { business_short_code: self.business_short_code, password: &password, timestamp: ×tamp, - amount: self.amount.unwrap_or_default(), + amount: self + .amount + .ok_or(MpesaError::Message("amount is required"))?, party_a: if self.party_a.is_some() { self.party_a } else { @@ -244,13 +240,17 @@ impl<'mpesa, Env: ApiEnvironment> MpesaExpressRequestBuilder<'mpesa, Env> { } else { Some(self.business_short_code) }, - phone_number: self.phone_number, - call_back_url: self.callback_url, - account_reference: self.account_ref.unwrap_or_else(|| "None"), + phone_number: self + .phone_number + .ok_or(MpesaError::Message("phone_number is required"))?, + call_back_url: self + .callback_url + .ok_or(MpesaError::Message("callback_url is required"))?, + account_reference: self.account_ref.unwrap_or_else(|| stringify!(None)), transaction_type: self .transaction_type .unwrap_or_else(|| CommandId::CustomerPayBillOnline), - transaction_desc: self.transaction_desc.unwrap_or_else(|| "None"), + transaction_desc: self.transaction_desc.unwrap_or_else(|| stringify!(None)), }; let response = self diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index 0007adc4b..564a566d3 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -28,6 +28,10 @@ impl ApiEnvironment for TestEnvironment { #[macro_export] macro_rules! get_mpesa_client { () => {{ + get_mpesa_client!(1) + }}; + + ($expected_requests: expr) => {{ use crate::helpers::TestEnvironment; use mpesa::Mpesa; use wiremock::{MockServer, Mock, ResponseTemplate}; @@ -48,7 +52,7 @@ macro_rules! get_mpesa_client { .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "access_token": "dummy_access_token" }))) - .expect(1) + .expect($expected_requests) .mount(&server) .await; (client, server) diff --git a/tests/mpesa-rust/stk_push_test.rs b/tests/mpesa-rust/stk_push_test.rs index e15c2d132..13e82d9ff 100644 --- a/tests/mpesa-rust/stk_push_test.rs +++ b/tests/mpesa-rust/stk_push_test.rs @@ -1,10 +1,11 @@ use crate::get_mpesa_client; +use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; #[tokio::test] -async fn stk_push_test() { +async fn stk_push_test_success() { let (client, server) = get_mpesa_client!(); let sample_response_body = json!({ "MerchantRequestID": "16813-1590513-1", @@ -38,3 +39,89 @@ async fn stk_push_test() { "Success. Request accepeted for processing" ); } + +#[tokio::test] +async fn stk_push_fails_if_no_amount_is_provided() { + let (client, server) = get_mpesa_client!(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("174379") + .phone_number("254708374149") + .callback_url("https://test.example.com/api") + .send() + .await { + if let MpesaError::Message(msg) = e { + assert_eq!(msg, "amount is required") + }; + } +} + +#[tokio::test] +async fn stk_push_fails_if_no_callback_url_is_provided() { + let (client, server) = get_mpesa_client!(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("174379") + .phone_number("254708374149") + .amount(500) + .send() + .await { + if let MpesaError::Message(msg) = e { + assert_eq!(msg, "callback_url is required") + }; + } +} + + +#[tokio::test] +async fn stk_push_fails_if_no_phone_number_is_provided() { + let (client, server) = get_mpesa_client!(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("174379") + .amount(500) + .callback_url("https://test.example.com/api") + .send() + .await { + if let MpesaError::Message(msg) = e { + assert_eq!(msg, "phone_number is required") + }; + } +} + From 9164c0c57a63379a139e0e45cf403f0b9f5e9ea0 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 08:51:16 +0300 Subject: [PATCH 100/140] Rename happy path tests --- tests/mpesa-rust/account_balance_test.rs | 2 +- tests/mpesa-rust/b2b_test.rs | 2 +- tests/mpesa-rust/b2c_test.rs | 2 +- tests/mpesa-rust/c2b_register_test.rs | 2 +- tests/mpesa-rust/c2b_simulate_test.rs | 2 +- tests/mpesa-rust/stk_push_test.rs | 2 +- tests/mpesa-rust/transaction_reversal_test.rs | 2 +- tests/mpesa-rust/transaction_status_test.rs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/mpesa-rust/account_balance_test.rs b/tests/mpesa-rust/account_balance_test.rs index 336916d90..abcfd4715 100644 --- a/tests/mpesa-rust/account_balance_test.rs +++ b/tests/mpesa-rust/account_balance_test.rs @@ -4,7 +4,7 @@ use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; #[tokio::test] -async fn account_balance_test() { +async fn account_balance_success() { let (client, server) = get_mpesa_client!(); let sample_response_body = json!({ "OriginatorConversationID": "29464-48063588-1", diff --git a/tests/mpesa-rust/b2b_test.rs b/tests/mpesa-rust/b2b_test.rs index ba13dbb65..d86307fbe 100644 --- a/tests/mpesa-rust/b2b_test.rs +++ b/tests/mpesa-rust/b2b_test.rs @@ -5,7 +5,7 @@ use wiremock::{Mock, ResponseTemplate}; use crate::get_mpesa_client; #[tokio::test] -async fn b2b_test() { +async fn b2b_success() { let (client, server) = get_mpesa_client!(); let sample_response_body = json!({ "OriginatorConversationID": "29464-48063588-1", diff --git a/tests/mpesa-rust/b2c_test.rs b/tests/mpesa-rust/b2c_test.rs index 617be5f68..c86baa622 100644 --- a/tests/mpesa-rust/b2c_test.rs +++ b/tests/mpesa-rust/b2c_test.rs @@ -4,7 +4,7 @@ use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; #[tokio::test] -async fn b2c_test() { +async fn b2c_success() { let (client, server) = get_mpesa_client!(); let sample_response_body = json!({ "OriginatorConversationID": "29464-48063588-1", diff --git a/tests/mpesa-rust/c2b_register_test.rs b/tests/mpesa-rust/c2b_register_test.rs index d0fe477d2..754495044 100644 --- a/tests/mpesa-rust/c2b_register_test.rs +++ b/tests/mpesa-rust/c2b_register_test.rs @@ -4,7 +4,7 @@ use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; #[tokio::test] -async fn c2b_register_test() { +async fn c2b_register_success() { let (client, server) = get_mpesa_client!(); let sample_response_body = json!({ "OriginatorConverstionID": "29464-48063588-1", diff --git a/tests/mpesa-rust/c2b_simulate_test.rs b/tests/mpesa-rust/c2b_simulate_test.rs index 9adc2242a..6cd761176 100644 --- a/tests/mpesa-rust/c2b_simulate_test.rs +++ b/tests/mpesa-rust/c2b_simulate_test.rs @@ -4,7 +4,7 @@ use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; #[tokio::test] -async fn c2b_simulate_test() { +async fn c2b_simulate_success() { let (client, server) = get_mpesa_client!(); let sample_response_body = json!({ "OriginatorConversationID": "29464-48063588-1", diff --git a/tests/mpesa-rust/stk_push_test.rs b/tests/mpesa-rust/stk_push_test.rs index 13e82d9ff..80bacc409 100644 --- a/tests/mpesa-rust/stk_push_test.rs +++ b/tests/mpesa-rust/stk_push_test.rs @@ -5,7 +5,7 @@ use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; #[tokio::test] -async fn stk_push_test_success() { +async fn stk_push_success_success() { let (client, server) = get_mpesa_client!(); let sample_response_body = json!({ "MerchantRequestID": "16813-1590513-1", diff --git a/tests/mpesa-rust/transaction_reversal_test.rs b/tests/mpesa-rust/transaction_reversal_test.rs index ffac5c5a8..1b9d9757f 100644 --- a/tests/mpesa-rust/transaction_reversal_test.rs +++ b/tests/mpesa-rust/transaction_reversal_test.rs @@ -5,7 +5,7 @@ use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; #[tokio::test] -async fn transaction_reversal_test() { +async fn transaction_reversal_success() { let (client, server) = get_mpesa_client!(); let sample_response_body = json!({ "OriginatorConversationID": "29464-48063588-1", diff --git a/tests/mpesa-rust/transaction_status_test.rs b/tests/mpesa-rust/transaction_status_test.rs index d0a5b999a..535042961 100644 --- a/tests/mpesa-rust/transaction_status_test.rs +++ b/tests/mpesa-rust/transaction_status_test.rs @@ -7,7 +7,7 @@ use wiremock::{Mock, ResponseTemplate}; use crate::get_mpesa_client; #[tokio::test] -async fn transaction_status_test() { +async fn transaction_status_success() { let (client, server) = get_mpesa_client!(); let sample_response_body = json!({ "OriginatorConversationID": "29464-48063588-1", From 0918cf0d22239d90226921ef144f29c26f30b6ba Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 10:30:25 +0300 Subject: [PATCH 101/140] Further extend integration tests; update api and docs --- src/services/account_balance.rs | 32 ++-- src/services/b2b.rs | 23 ++- src/services/b2c.rs | 53 +++--- src/services/c2b_register.rs | 14 +- src/services/c2b_simulate.rs | 47 +++--- src/services/transaction_reversal.rs | 39 +++-- src/services/transaction_status.rs | 37 +++-- tests/mpesa-rust/account_balance_test.rs | 88 ++++++++++ tests/mpesa-rust/b2b_test.rs | 97 +++++++++++ tests/mpesa-rust/b2c_test.rs | 156 ++++++++++++++++++ tests/mpesa-rust/c2b_register_test.rs | 85 ++++++++++ tests/mpesa-rust/c2b_simulate_test.rs | 122 +++++++++++++- tests/mpesa-rust/helpers.rs | 4 +- tests/mpesa-rust/stk_push_test.rs | 44 ++--- tests/mpesa-rust/transaction_reversal_test.rs | 153 ++++++++++++++++- tests/mpesa-rust/transaction_status_test.rs | 119 ++++++++++++- 16 files changed, 979 insertions(+), 134 deletions(-) diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index 4905af021..b3d7ba4d4 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -13,22 +13,16 @@ struct AccountBalancePayload<'mpesa> { security_credential: &'mpesa str, #[serde(rename(serialize = "CommandID"))] command_id: CommandId, - #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] - party_a: Option<&'mpesa str>, + #[serde(rename(serialize = "PartyA"))] + party_a: &'mpesa str, #[serde(rename(serialize = "IdentifierType"))] identifier_type: &'mpesa str, #[serde(rename(serialize = "Remarks"))] remarks: &'mpesa str, - #[serde( - rename(serialize = "QueueTimeOutURL"), - skip_serializing_if = "Option::is_none" - )] - queue_time_out_url: Option<&'mpesa str>, - #[serde( - rename(serialize = "ResultURL"), - skip_serializing_if = "Option::is_none" - )] - result_url: Option<&'mpesa str>, + #[serde(rename(serialize = "QueueTimeOutURL"))] + queue_time_out_url: &'mpesa str, + #[serde(rename(serialize = "ResultURL"))] + result_url: &'mpesa str, } #[derive(Debug, Deserialize, Clone)] @@ -165,15 +159,21 @@ impl<'mpesa, Env: ApiEnvironment> AccountBalanceBuilder<'mpesa, Env> { let payload = AccountBalancePayload { command_id: self.command_id.unwrap_or_else(|| CommandId::AccountBalance), - party_a: self.party_a, + party_a: self + .party_a + .ok_or(MpesaError::Message("party_a is required"))?, identifier_type: &self .identifier_type .unwrap_or_else(|| IdentifierTypes::ShortCode) .to_string(), - remarks: self.remarks.unwrap_or_else(|| "None"), + remarks: self.remarks.unwrap_or_else(|| stringify!(None)), initiator: self.initiator_name, - queue_time_out_url: self.queue_timeout_url, - result_url: self.result_url, + queue_time_out_url: self + .queue_timeout_url + .ok_or(MpesaError::Message("queue_timeout_url is required"))?, + result_url: self + .result_url + .ok_or(MpesaError::Message("result_url is required"))?, security_credential: &credentials, }; diff --git a/src/services/b2b.rs b/src/services/b2b.rs index f44b14ecd..08746109e 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -14,12 +14,12 @@ struct B2bPayload<'mpesa> { command_id: CommandId, #[serde(rename(serialize = "Amount"))] amount: f64, - #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] - party_a: Option<&'mpesa str>, + #[serde(rename(serialize = "PartyA"))] + party_a: &'mpesa str, #[serde(rename(serialize = "SenderIdentifierType"))] sender_identifier_type: &'mpesa str, - #[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")] - party_b: Option<&'mpesa str>, + #[serde(rename(serialize = "PartyB"))] + party_b: &'mpesa str, #[serde(rename(serialize = "RecieverIdentifierType"))] reciever_identifier_type: &'mpesa str, #[serde(rename(serialize = "Remarks"))] @@ -228,19 +228,24 @@ impl<'mpesa, Env: ApiEnvironment> B2bBuilder<'mpesa, Env> { command_id: self .command_id .unwrap_or_else(|| CommandId::BusinessToBusinessTransfer), - // TODO: Can this be improved? - amount: self.amount.unwrap_or_default(), - party_a: self.party_a, + amount: self + .amount + .ok_or(MpesaError::Message("amount is required"))?, + party_a: self + .party_a + .ok_or(MpesaError::Message("party_a is required"))?, sender_identifier_type: &self .sender_id .unwrap_or_else(|| IdentifierTypes::ShortCode) .to_string(), - party_b: self.party_b, + party_b: self + .party_b + .ok_or(MpesaError::Message("party_b is required"))?, reciever_identifier_type: &self .receiver_id .unwrap_or_else(|| IdentifierTypes::ShortCode) .to_string(), - remarks: self.remarks.unwrap_or_else(|| "None"), + remarks: self.remarks.unwrap_or_else(|| stringify!(None)), queue_time_out_url: self.queue_timeout_url, result_url: self.result_url, account_reference: self.account_ref, diff --git a/src/services/b2c.rs b/src/services/b2c.rs index 1109f2385..6ae46cfdc 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -14,27 +14,18 @@ struct B2cPayload<'mpesa> { command_id: CommandId, #[serde(rename(serialize = "Amount"))] amount: f64, - #[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")] - party_a: Option<&'mpesa str>, - #[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")] - party_b: Option<&'mpesa str>, + #[serde(rename(serialize = "PartyA"))] + party_a: &'mpesa str, + #[serde(rename(serialize = "PartyB"))] + party_b: &'mpesa str, #[serde(rename(serialize = "Remarks"))] remarks: &'mpesa str, - #[serde( - rename(serialize = "QueueTimeOutURL"), - skip_serializing_if = "Option::is_none" - )] - queue_time_out_url: Option<&'mpesa str>, - #[serde( - rename(serialize = "ResultURL"), - skip_serializing_if = "Option::is_none" - )] - result_url: Option<&'mpesa str>, - #[serde( - rename(serialize = "Occasion"), - skip_serializing_if = "Option::is_none" - )] - occasion: Option<&'mpesa str>, + #[serde(rename(serialize = "QueueTimeOutURL"))] + queue_time_out_url: &'mpesa str, + #[serde(rename(serialize = "ResultURL"))] + result_url: &'mpesa str, + #[serde(rename(serialize = "Occasion"))] + occasion: &'mpesa str, } #[derive(Debug, Deserialize, Clone)] @@ -205,13 +196,23 @@ impl<'mpesa, Env: ApiEnvironment> B2cBuilder<'mpesa, Env> { command_id: self .command_id .unwrap_or_else(|| CommandId::BusinessPayment), - amount: self.amount.unwrap_or_default(), - party_a: self.party_a, - party_b: self.party_b, - remarks: self.remarks.unwrap_or_else(|| "None"), - queue_time_out_url: self.queue_timeout_url, - result_url: self.result_url, - occasion: self.occasion, + amount: self + .amount + .ok_or(MpesaError::Message("amount is required"))?, + party_a: self + .party_a + .ok_or(MpesaError::Message("party_a is required"))?, + party_b: self + .party_b + .ok_or(MpesaError::Message("party_b is required"))?, + remarks: self.remarks.unwrap_or_else(|| stringify!(None)), + queue_time_out_url: self + .queue_timeout_url + .ok_or(MpesaError::Message("queue_timeout_url is required"))?, + result_url: self + .result_url + .ok_or(MpesaError::Message("result_url is required"))?, + occasion: self.occasion.unwrap_or_else(|| stringify!(None)), }; let response = self diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index 6839459a4..82635bcdf 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -87,7 +87,7 @@ impl<'mpesa, Env: ApiEnvironment> C2bRegisterBuilder<'mpesa, Env> { /// Adds `ShortCode` for the organization. This is a required field. /// /// # Error - /// If `ShortCode` is invalid + /// If `ShortCode` is invalid or not provided pub fn short_code(mut self, short_code: &'mpesa str) -> C2bRegisterBuilder<'mpesa, Env> { self.short_code = Some(short_code); self @@ -117,12 +117,18 @@ impl<'mpesa, Env: ApiEnvironment> C2bRegisterBuilder<'mpesa, Env> { ); let payload = C2bRegisterPayload { - validation_url: self.validation_url.unwrap_or_else(|| "None"), - confirmation_url: self.confirmation_url.unwrap_or_else(|| "None"), + validation_url: self + .validation_url + .ok_or(MpesaError::Message("validation_url is required"))?, + confirmation_url: self + .confirmation_url + .ok_or(MpesaError::Message("confirmation_url is required"))?, response_type: self .response_type .unwrap_or_else(|| ResponseType::Completed), - short_code: self.short_code.unwrap_or_else(|| "None"), + short_code: self + .short_code + .ok_or(MpesaError::Message("short_code is required"))?, }; let response = self diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index 8665c6c59..b9a41fdb6 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -12,18 +12,12 @@ struct C2bSimulatePayload<'mpesa> { command_id: CommandId, #[serde(rename(serialize = "Amount"))] amount: f64, - #[serde(rename(serialize = "Msisdn"), skip_serializing_if = "Option::is_none")] - msisdn: Option<&'mpesa str>, - #[serde( - rename(serialize = "BillRefNumber"), - skip_serializing_if = "Option::is_none" - )] - bill_ref_number: Option<&'mpesa str>, - #[serde( - rename(serialize = "ShortCode"), - skip_serializing_if = "Option::is_none" - )] - short_code: Option<&'mpesa str>, + #[serde(rename(serialize = "Msisdn"))] + msisdn: &'mpesa str, + #[serde(rename(serialize = "BillRefNumber"))] + bill_ref_number: &'mpesa str, + #[serde(rename(serialize = "ShortCode"))] + short_code: &'mpesa str, } #[derive(Debug, Clone, Deserialize)] @@ -74,7 +68,9 @@ impl<'mpesa, Env: ApiEnvironment> C2bSimulateBuilder<'mpesa, Env> { } /// Adds an `amount` to the request - /// This is a required field + /// + /// # Errors + /// If `Amount` is not provided pub fn amount>(mut self, amount: Number) -> C2bSimulateBuilder<'mpesa, Env> { self.amount = Some(amount.into()); self @@ -84,7 +80,7 @@ impl<'mpesa, Env: ApiEnvironment> C2bSimulateBuilder<'mpesa, Env> { /// This is a required field /// /// # Errors - /// If `MSISDN` is invalid + /// If `MSISDN` is invalid or not provided pub fn msisdn(mut self, msisdn: &'mpesa str) -> C2bSimulateBuilder<'mpesa, Env> { self.msisdn = Some(msisdn); self @@ -93,13 +89,16 @@ impl<'mpesa, Env: ApiEnvironment> C2bSimulateBuilder<'mpesa, Env> { /// Adds `ShortCode`; the 6 digit MPESA Till Number or PayBill Number /// /// # Errors - /// If Till or PayBill number is invalid + /// If Till or PayBill number is invalid or not provided pub fn short_code(mut self, short_code: &'mpesa str) -> C2bSimulateBuilder<'mpesa, Env> { self.short_code = Some(short_code); self } - /// Adds Bull reference number. This field is optional and will by default be `"None"`. + /// Adds Bill reference number. + /// + /// # Errors + /// If `BillRefNumber` is invalid or not provided pub fn bill_ref_number( mut self, bill_ref_number: &'mpesa str, @@ -130,10 +129,18 @@ impl<'mpesa, Env: ApiEnvironment> C2bSimulateBuilder<'mpesa, Env> { command_id: self .command_id .unwrap_or_else(|| CommandId::CustomerPayBillOnline), - amount: self.amount.unwrap_or_default(), - msisdn: self.msisdn, - bill_ref_number: self.bill_ref_number, - short_code: self.short_code, + amount: self + .amount + .ok_or(MpesaError::Message("amount is required"))?, + msisdn: self + .msisdn + .ok_or(MpesaError::Message("msisdn is required"))?, + bill_ref_number: self + .bill_ref_number + .ok_or(MpesaError::Message("bill_ref_number is required"))?, + short_code: self + .short_code + .ok_or(MpesaError::Message("short_code is required"))?, }; let response = self diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index 1e34758f1..cb152e066 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -21,15 +21,15 @@ pub struct TransactionReversalPayload<'mpesa> { #[serde(rename(serialize = "ReceiverParty"))] receiver_party: &'mpesa str, #[serde(rename(serialize = "RecieverIdentifierType"))] - receiver_identifier_type: Option, + receiver_identifier_type: IdentifierTypes, #[serde(rename(serialize = "ResultURL"))] - result_url: Option<&'mpesa str>, + result_url: &'mpesa str, #[serde(rename(serialize = "QueueTimeOutURL"))] - timeout_url: Option<&'mpesa str>, + timeout_url: &'mpesa str, #[serde(rename(serialize = "Remarks"))] - remarks: Option<&'mpesa str>, + remarks: &'mpesa str, #[serde(rename(serialize = "Occasion"))] - occasion: Option<&'mpesa str>, + occasion: &'mpesa str, #[serde(rename(serialize = "Amount"))] amount: f64, } @@ -107,7 +107,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { /// Type of organization receiving the transaction /// - /// This is required field + /// 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 @@ -133,14 +133,15 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { /// Comments that are sent along with the transaction. /// - /// This is required field + /// 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 + /// + /// This is an optional Parameter, defaults to "None" pub fn occasion(mut self, occasion: &'mpesa str) -> Self { self.occasion = Some(occasion); self @@ -187,18 +188,24 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { command_id: self.command_id.unwrap_or(CommandId::TransactionReversal), transaction_id: self .transaction_id - .ok_or(MpesaError::Message("transaction_id is required field"))?, + .ok_or(MpesaError::Message("transaction_id is required"))?, receiver_party: self .receiver_party - .ok_or(MpesaError::Message("receiver_party is required field"))?, - receiver_identifier_type: self.receiver_identifier_type, - result_url: self.result_url, - timeout_url: self.timeout_url, - remarks: self.remarks, - occasion: self.occasion, + .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_else(|| stringify!(None)), + occasion: self.occasion.unwrap_or_else(|| stringify!(None)), amount: self .amount - .ok_or(MpesaError::Message("amount is required field"))?, + .ok_or(MpesaError::Message("amount is required"))?, }; let response = self diff --git a/src/services/transaction_status.rs b/src/services/transaction_status.rs index 2b7eb72d5..143a5e890 100644 --- a/src/services/transaction_status.rs +++ b/src/services/transaction_status.rs @@ -19,17 +19,17 @@ pub struct TransactionStatusPayload<'mpesa> { #[serde(rename(serialize = "TransactionID"))] transaction_id: &'mpesa str, #[serde(rename = "PartyA")] - party_a: Option<&'mpesa str>, + party_a: &'mpesa str, #[serde(rename(serialize = "IdentifierType"))] - identifier_type: Option, + identifier_type: IdentifierTypes, #[serde(rename(serialize = "ResultURL"))] - result_url: Option<&'mpesa str>, + result_url: &'mpesa str, #[serde(rename(serialize = "QueueTimeOutURL"))] - timeout_url: Option<&'mpesa str>, + timeout_url: &'mpesa str, #[serde(rename(serialize = "Remarks"))] - remarks: Option<&'mpesa str>, + remarks: &'mpesa str, #[serde(rename(serialize = "Occasion"))] - occasion: Option<&'mpesa str>, + occasion: &'mpesa str, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -103,7 +103,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionStatusBuilder<'mpesa, Env> { /// Type of organization receiving the transaction /// - /// This is required field + /// This is an optional field, defaults to `IdentifierTypes::ShortCode` pub fn identifier_type(mut self, identifier_type: IdentifierTypes) -> Self { self.identifier_type = Some(identifier_type); self @@ -129,14 +129,15 @@ impl<'mpesa, Env: ApiEnvironment> TransactionStatusBuilder<'mpesa, Env> { /// Comments that are sent along with the transaction. /// - /// This is required field + /// This is an optional 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 + /// + /// This is an optional Parameter, defaults to "None" pub fn occasion(mut self, occasion: &'mpesa str) -> Self { self.occasion = Some(occasion); self @@ -173,12 +174,18 @@ impl<'mpesa, Env: ApiEnvironment> TransactionStatusBuilder<'mpesa, Env> { transaction_id: self .transaction_id .ok_or(MpesaError::Message("transaction_id is required field"))?, - party_a: self.party_a, - identifier_type: self.identifier_type, - result_url: self.result_url, - timeout_url: self.timeout_url, - remarks: self.remarks, - occasion: self.occasion, + party_a: self + .party_a + .ok_or(MpesaError::Message("party_a is required"))?, + identifier_type: self.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_else(|| stringify!(None)), + occasion: self.occasion.unwrap_or_else(|| stringify!(None)), }; let response = self diff --git a/tests/mpesa-rust/account_balance_test.rs b/tests/mpesa-rust/account_balance_test.rs index abcfd4715..ad9fc37cd 100644 --- a/tests/mpesa-rust/account_balance_test.rs +++ b/tests/mpesa-rust/account_balance_test.rs @@ -1,4 +1,5 @@ use crate::get_mpesa_client; +use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; @@ -34,3 +35,90 @@ async fn account_balance_success() { ); assert_eq!(response.response_code, "0"); } + +#[tokio::test] +async fn account_balance_fails_if_party_a_is_not_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.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/accountbalance/v1/query")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .account_balance("testapi496") + .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, "party_a is required") + } else { + panic!("Expected error"); + } +} + +#[tokio::test] +async fn account_balance_fails_if_result_url_is_not_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.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/accountbalance/v1/query")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .account_balance("testapi496") + .party_a("600496") + .timeout_url("https://testdomain.com/err") + .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 account_balance_fails_if_queue_timeout_url_is_not_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.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/accountbalance/v1/query")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .account_balance("testapi496") + .party_a("600496") + .result_url("https://testdomain.com/ok") + .send() + .await + { + let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + assert_eq!(msg, "queue_timeout_url is required") + } else { + panic!("Expected error"); + } +} diff --git a/tests/mpesa-rust/b2b_test.rs b/tests/mpesa-rust/b2b_test.rs index d86307fbe..03f183750 100644 --- a/tests/mpesa-rust/b2b_test.rs +++ b/tests/mpesa-rust/b2b_test.rs @@ -1,3 +1,4 @@ +use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; @@ -38,3 +39,99 @@ async fn b2b_success() { ); assert_eq!(response.response_code, "0"); } + +#[tokio::test] +async fn b2b_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.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/b2b/v1/paymentrequest")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .b2b("testapi496") + .party_a("600496") + .party_b("600000") + .result_url("https://testdomain.com/ok") + .timeout_url("https://testdomain.com/err") + .account_ref("254708374149") + .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 b2b_fails_if_no_party_a_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.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/b2b/v1/paymentrequest")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .b2b("testapi496") + .party_b("600000") + .amount(1000) + .result_url("https://testdomain.com/ok") + .timeout_url("https://testdomain.com/err") + .account_ref("254708374149") + .send() + .await + { + let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + assert_eq!(msg, "party_a is required"); + } else { + panic!("Expected error"); + } +} + +#[tokio::test] +async fn b2b_fails_if_no_party_b_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.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/b2b/v1/paymentrequest")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .b2b("testapi496") + .party_a("600496") + .amount(1000) + .result_url("https://testdomain.com/ok") + .timeout_url("https://testdomain.com/err") + .account_ref("254708374149") + .send() + .await + { + let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + assert_eq!(msg, "party_b is required"); + } else { + panic!("Expected error"); + } +} diff --git a/tests/mpesa-rust/b2c_test.rs b/tests/mpesa-rust/b2c_test.rs index c86baa622..28b0f2540 100644 --- a/tests/mpesa-rust/b2c_test.rs +++ b/tests/mpesa-rust/b2c_test.rs @@ -1,4 +1,5 @@ use crate::get_mpesa_client; +use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; @@ -36,3 +37,158 @@ async fn b2c_success() { ); assert_eq!(response.response_code, "0"); } + +#[tokio::test] +async fn b2c_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.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/b2c/v1/paymentrequest")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .b2c("testapi496") + .party_a("600496") + .party_b("254708374149") + .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, "amount is required"); + } else { + panic!("Expected error"); + } +} + +#[tokio::test] +async fn b2c_fails_if_no_party_a_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.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/b2c/v1/paymentrequest")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .b2c("testapi496") + .amount(1000) + .party_b("254708374149") + .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, "party_a is required"); + } else { + panic!("Expected error"); + } +} + +#[tokio::test] +async fn b2c_fails_if_no_party_b_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.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/b2c/v1/paymentrequest")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .b2c("testapi496") + .amount(1000) + .party_a("600496") + .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, "party_b is required"); + } else { + panic!("Expected error"); + } +} + +#[tokio::test] +async fn b2c_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.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/b2c/v1/paymentrequest")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .b2c("testapi496") + .amount(1000) + .party_a("600496") + .party_b("254708374149") + .timeout_url("https://testdomain.com/err") + .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 b2c_fails_if_no_queue_timeout_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.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/b2c/v1/paymentrequest")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .b2c("testapi496") + .amount(1000) + .party_a("600496") + .party_b("254708374149") + .result_url("https://testdomain.com/ok") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + assert_eq!(msg, "queue_timeout_url is required"); + } else { + panic!("Expected error"); + } +} diff --git a/tests/mpesa-rust/c2b_register_test.rs b/tests/mpesa-rust/c2b_register_test.rs index 754495044..190651c79 100644 --- a/tests/mpesa-rust/c2b_register_test.rs +++ b/tests/mpesa-rust/c2b_register_test.rs @@ -1,4 +1,5 @@ use crate::get_mpesa_client; +use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; @@ -33,3 +34,87 @@ async fn c2b_register_success() { assert_eq!(response.response_code, "0"); assert_eq!(response.conversation_id, None); } + +#[tokio::test] +async fn c2b_register_fails_if_short_code_is_not_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + let sample_response_body = json!({ + "OriginatorConverstionID": "29464-48063588-1", + "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/c2b/v1/registerurl")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .c2b_register() + .confirmation_url("https://testdomain.com/true") + .validation_url("https://testdomain.com/valid") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + assert_eq!(msg, "short_code is required"); + } else { + panic!("Expected error"); + } +} + +#[tokio::test] +async fn c2b_register_fails_if_confirmation_url_is_not_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + let sample_response_body = json!({ + "OriginatorConverstionID": "29464-48063588-1", + "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/c2b/v1/registerurl")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .c2b_register() + .short_code("600496") + .validation_url("https://testdomain.com/valid") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + assert_eq!(msg, "confirmation_url is required"); + } else { + panic!("Expected error"); + } +} + +#[tokio::test] +async fn c2b_register_fails_if_validation_url_is_not_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + let sample_response_body = json!({ + "OriginatorConverstionID": "29464-48063588-1", + "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/c2b/v1/registerurl")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .c2b_register() + .short_code("600496") + .confirmation_url("https://testdomain.com/true") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + assert_eq!(msg, "validation_url is required"); + } else { + panic!("Expected error"); + } +} diff --git a/tests/mpesa-rust/c2b_simulate_test.rs b/tests/mpesa-rust/c2b_simulate_test.rs index 6cd761176..eae5350ce 100644 --- a/tests/mpesa-rust/c2b_simulate_test.rs +++ b/tests/mpesa-rust/c2b_simulate_test.rs @@ -1,4 +1,5 @@ use crate::get_mpesa_client; +use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; @@ -19,9 +20,10 @@ async fn c2b_simulate_success() { .await; let response = client .c2b_simulate() - .short_code("600496") - .msisdn("254700000000") .amount(1000) + .bill_ref_number("2") + .msisdn("254700000000") + .short_code("600496") .send() .await .unwrap(); @@ -33,3 +35,119 @@ async fn c2b_simulate_success() { assert_eq!(response.response_code, "0"); assert_eq!(response.conversation_id, None); } + +#[tokio::test] +async fn c2b_simulate_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", + "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/c2b/v1/simulate")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .c2b_simulate() + .bill_ref_number("2") + .msisdn("254700000000") + .short_code("600496") + .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 c2b_simulate_fails_if_no_short_code_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + let sample_response_body = json!({ + "OriginatorConversationID": "29464-48063588-1", + "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/c2b/v1/simulate")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .c2b_simulate() + .amount(1000) + .bill_ref_number("2") + .msisdn("254700000000") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + assert_eq!(msg, "short_code is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn c2b_simulate_fails_if_no_bill_ref_number_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + let sample_response_body = json!({ + "OriginatorConversationID": "29464-48063588-1", + "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/c2b/v1/simulate")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .c2b_simulate() + .amount(1000) + .msisdn("254700000000") + .short_code("600496") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + assert_eq!(msg, "bill_ref_number is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn c2b_simulate_fails_if_no_msisdn_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + let sample_response_body = json!({ + "OriginatorConversationID": "29464-48063588-1", + "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0" + }); + Mock::given(method("POST")) + .and(path("/mpesa/c2b/v1/simulate")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .c2b_simulate() + .amount(1000) + .bill_ref_number("2") + .short_code("600496") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + assert_eq!(msg, "msisdn is required"); + } else { + panic!("Expected error") + } +} diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index 564a566d3..78f9e7ac4 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -28,10 +28,10 @@ impl ApiEnvironment for TestEnvironment { #[macro_export] macro_rules! get_mpesa_client { () => {{ - get_mpesa_client!(1) + get_mpesa_client!(expected_auth_requests = 1) }}; - ($expected_requests: expr) => {{ + (expected_auth_requests = $expected_requests: expr) => {{ use crate::helpers::TestEnvironment; use mpesa::Mpesa; use wiremock::{MockServer, Mock, ResponseTemplate}; diff --git a/tests/mpesa-rust/stk_push_test.rs b/tests/mpesa-rust/stk_push_test.rs index 80bacc409..486020436 100644 --- a/tests/mpesa-rust/stk_push_test.rs +++ b/tests/mpesa-rust/stk_push_test.rs @@ -42,7 +42,7 @@ async fn stk_push_success_success() { #[tokio::test] async fn stk_push_fails_if_no_amount_is_provided() { - let (client, server) = get_mpesa_client!(0); + 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", @@ -61,16 +61,18 @@ async fn stk_push_fails_if_no_amount_is_provided() { .phone_number("254708374149") .callback_url("https://test.example.com/api") .send() - .await { - if let MpesaError::Message(msg) = e { - assert_eq!(msg, "amount is required") - }; - } + .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() { - let (client, server) = get_mpesa_client!(0); + 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", @@ -89,17 +91,18 @@ async fn stk_push_fails_if_no_callback_url_is_provided() { .phone_number("254708374149") .amount(500) .send() - .await { - if let MpesaError::Message(msg) = e { - assert_eq!(msg, "callback_url is required") - }; - } + .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!(0); + 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", @@ -118,10 +121,11 @@ async fn stk_push_fails_if_no_phone_number_is_provided() { .amount(500) .callback_url("https://test.example.com/api") .send() - .await { - if let MpesaError::Message(msg) = e { - assert_eq!(msg, "phone_number is required") - }; - } + .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"); + } } - diff --git a/tests/mpesa-rust/transaction_reversal_test.rs b/tests/mpesa-rust/transaction_reversal_test.rs index 1b9d9757f..f502ff75d 100644 --- a/tests/mpesa-rust/transaction_reversal_test.rs +++ b/tests/mpesa-rust/transaction_reversal_test.rs @@ -1,5 +1,5 @@ use crate::get_mpesa_client; -use mpesa::IdentifierTypes; +use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; @@ -23,7 +23,6 @@ async fn transaction_reversal_success() { .result_url("https://testdomain.com/ok") .timeout_url("https://testdomain.com/err") .transaction_id("OEI2AK4Q16") - .receiver_identifier_type(IdentifierTypes::ShortCode) .amount(1.0) .receiver_party("600111") .remarks("wrong recipient") @@ -37,3 +36,153 @@ 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"); + } +} diff --git a/tests/mpesa-rust/transaction_status_test.rs b/tests/mpesa-rust/transaction_status_test.rs index 535042961..7195ec8e9 100644 --- a/tests/mpesa-rust/transaction_status_test.rs +++ b/tests/mpesa-rust/transaction_status_test.rs @@ -1,4 +1,4 @@ -use mpesa::IdentifierTypes; +use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; @@ -25,7 +25,6 @@ async fn transaction_status_success() { .result_url("https://testdomain.com/ok") .timeout_url("https://testdomain.com/err") .transaction_id("OEI2AK4Q16") - .identifier_type(IdentifierTypes::ShortCode) .party_a("600111") .remarks("status") .occasion("work") @@ -39,3 +38,119 @@ async fn transaction_status_success() { "Accept the service request successfully." ); } + +#[tokio::test] +async fn transaction_status_fails_if_transaction_id_is_not_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/transactionstatus/v1/query")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .transaction_status("testapi496") + .result_url("https://testdomain.com/ok") + .timeout_url("https://testdomain.com/err") + .party_a("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_status_fails_if_party_a_is_not_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/transactionstatus/v1/query")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .transaction_status("testapi496") + .result_url("https://testdomain.com/ok") + .timeout_url("https://testdomain.com/err") + .transaction_id("OEI2AK4Q16") + .send() + .await + { + let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + assert_eq!(msg, "party_a is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn transaction_status_fails_if_result_url_is_not_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/transactionstatus/v1/query")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .transaction_status("testapi496") + .timeout_url("https://testdomain.com/err") + .transaction_id("OEI2AK4Q16") + .party_a("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_status_fails_if_timeout_url_is_not_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/transactionstatus/v1/query")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .transaction_status("testapi496") + .result_url("https://testdomain.com/ok") + .transaction_id("OEI2AK4Q16") + .party_a("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") + } +} From 8a5c1ba893fed10166adc649b30c1608deb54501 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 10:31:35 +0300 Subject: [PATCH 102/140] Fix failing transaction status test --- src/services/transaction_status.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/transaction_status.rs b/src/services/transaction_status.rs index 143a5e890..731863e1f 100644 --- a/src/services/transaction_status.rs +++ b/src/services/transaction_status.rs @@ -173,7 +173,7 @@ impl<'mpesa, Env: ApiEnvironment> TransactionStatusBuilder<'mpesa, Env> { command_id: self.command_id.unwrap_or(CommandId::TransactionStatusQuery), transaction_id: self .transaction_id - .ok_or(MpesaError::Message("transaction_id is required field"))?, + .ok_or(MpesaError::Message("transaction_id is required"))?, party_a: self .party_a .ok_or(MpesaError::Message("party_a is required"))?, From 80fc8da2dfed02823e4ffa931c9685fa3dc1a59c Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 10:36:11 +0300 Subject: [PATCH 103/140] Update docs --- src/lib.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index fdbb2738f..069905659 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -305,6 +305,37 @@ //! assert!(response.is_ok()); //!} //! ``` +//! +//! * Transaction Status +//! +//! ```rust,norun +//! use mpesa::{Mpesa, Environment}; +//! use std::env; +//! use dotenv::dotenv; +//! +//! #[tokio::main] +//! async fn main() { +//! dotenv().ok(); +//! +//! let client = Mpesa::new( +//! env::var("CLIENT_KEY").unwrap(), +//! env::var("CLIENT_SECRET").unwrap(), +//! Environment::Sandbox +//! ); +//! +//! let response = client +//! .transaction_status("testapi496") +//! .result_url("https://testdomain.com/ok") +//! .timeout_url("https://testdomain.com/err") +//! .transaction_id("OEI2AK4Q16") +//! .party_a("600111") +//! .remarks("status") +//! .occasion("work") +//! .send() +//! .await; +//! assert(response.is_ok()); +//! } +//! ``` //! //! More will be added progressively, pull requests welcome //! From 29ae0b6dd59d3834bba5053b35bda6b4ec2219a2 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 10:36:30 +0300 Subject: [PATCH 104/140] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f582ae2fe..e2539103a 100644 --- a/README.md +++ b/README.md @@ -279,5 +279,5 @@ More will be added progressively, pull requests welcome Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/collinsmuriuki/mpesa-rust/issues). You can also take a look at the [contributing guide](CONTRIBUTING.md). -Copyright © 2022 [Collins Muriuki](https://github.com/collinsmuriuki).
+Copyright © 2023 [Collins Muriuki](https://github.com/collinsmuriuki).
This project is [MIT](LICENSE) licensed. From df1961abfb8cc5746ee3f18b694978e03012e2de Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 10:37:12 +0300 Subject: [PATCH 105/140] Fix failing doc test --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 069905659..b4653045d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -333,7 +333,7 @@ //! .occasion("work") //! .send() //! .await; -//! assert(response.is_ok()); +//! assert!(response.is_ok()); //! } //! ``` //! From 3806ce43544119760985785bf1d406f4ae4a5933 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 10:37:44 +0300 Subject: [PATCH 106/140] Fix failing doc test; again --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index b4653045d..d04ab59a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -308,7 +308,7 @@ //! //! * Transaction Status //! -//! ```rust,norun +//! ```rust,no_run //! use mpesa::{Mpesa, Environment}; //! use std::env; //! use dotenv::dotenv; From ec7f957ba2633360fb714d6952d462c29eddbc09 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 10:41:15 +0300 Subject: [PATCH 107/140] Fix formatting issues --- src/lib.rs | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d04ab59a8..a0057c4ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -305,35 +305,33 @@ //! assert!(response.is_ok()); //!} //! ``` -//! +//! //! * Transaction Status -//! +//! //! ```rust,no_run //! use mpesa::{Mpesa, Environment}; //! use std::env; //! use dotenv::dotenv; -//! +//! //! #[tokio::main] //! async fn main() { -//! dotenv().ok(); -//! -//! let client = Mpesa::new( +//! dotenv().ok(); +//! +//! let client = Mpesa::new( //! env::var("CLIENT_KEY").unwrap(), //! env::var("CLIENT_SECRET").unwrap(), //! Environment::Sandbox //! ); -//! -//! let response = client -//! .transaction_status("testapi496") -//! .result_url("https://testdomain.com/ok") -//! .timeout_url("https://testdomain.com/err") -//! .transaction_id("OEI2AK4Q16") -//! .party_a("600111") -//! .remarks("status") -//! .occasion("work") -//! .send() -//! .await; -//! assert!(response.is_ok()); +//! +//! let response = client +//! .transaction_status("testapi496") +//! .result_url("https://testdomain.com/ok") +//! .timeout_url("https://testdomain.com/err") +//! .transaction_id("OEI2AK4Q16") +//! .party_a("600111") +//! .send() +//! .await; +//! assert!(response.is_ok()); //! } //! ``` //! From 7955c570670896177fc864a703d0e2f9457e1146 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 10:44:34 +0300 Subject: [PATCH 108/140] Update cargo tarpaulin version --- .github/workflows/general.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 907658317..6c02444c5 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -71,6 +71,6 @@ jobs: override: true - name: Run cargo-tarpaulin - uses: actions-rs/tarpaulin@v0.1 + uses: actions-rs/tarpaulin@v0.22.0 with: args: '--ignore-tests --no-fail-fast' From 434d624a9e89dbacfd87765ee39bb14254c5b89b Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 10:46:53 +0300 Subject: [PATCH 109/140] Fix clippy warnings --- src/services/transaction_reversal.rs | 4 ++-- src/services/transaction_status.rs | 4 ++-- tests/mpesa-rust/helpers.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index cb152e066..5aec81853 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -201,8 +201,8 @@ impl<'mpesa, Env: ApiEnvironment> TransactionReversalBuilder<'mpesa, Env> { timeout_url: self .timeout_url .ok_or(MpesaError::Message("timeout_url is required"))?, - remarks: self.remarks.unwrap_or_else(|| stringify!(None)), - occasion: self.occasion.unwrap_or_else(|| stringify!(None)), + remarks: self.remarks.unwrap_or(stringify!(None)), + occasion: self.occasion.unwrap_or(stringify!(None)), amount: self .amount .ok_or(MpesaError::Message("amount is required"))?, diff --git a/src/services/transaction_status.rs b/src/services/transaction_status.rs index 731863e1f..eac9f5552 100644 --- a/src/services/transaction_status.rs +++ b/src/services/transaction_status.rs @@ -184,8 +184,8 @@ impl<'mpesa, Env: ApiEnvironment> TransactionStatusBuilder<'mpesa, Env> { timeout_url: self .timeout_url .ok_or(MpesaError::Message("timeout_url is required"))?, - remarks: self.remarks.unwrap_or_else(|| stringify!(None)), - occasion: self.occasion.unwrap_or_else(|| stringify!(None)), + remarks: self.remarks.unwrap_or(stringify!(None)), + occasion: self.occasion.unwrap_or(stringify!(None)), }; let response = self diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index 78f9e7ac4..5ff6ed93b 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -32,7 +32,7 @@ macro_rules! get_mpesa_client { }}; (expected_auth_requests = $expected_requests: expr) => {{ - use crate::helpers::TestEnvironment; + use $crate::helpers::TestEnvironment; use mpesa::Mpesa; use wiremock::{MockServer, Mock, ResponseTemplate}; use serde_json::json; From b776985b9212fa2adf1c1b35f2680972936a6214 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 10:48:48 +0300 Subject: [PATCH 110/140] Fix failing code coverage in ci --- .github/workflows/general.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 6c02444c5..a6db8343f 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -71,6 +71,6 @@ jobs: override: true - name: Run cargo-tarpaulin - uses: actions-rs/tarpaulin@v0.22.0 + uses: actions-rs/tarpaulin@master with: args: '--ignore-tests --no-fail-fast' From 349b63211d9c5a071b068be4318f5634e379994d Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 10:53:18 +0300 Subject: [PATCH 111/140] Fix failing clippy and tarpaulin --- .github/workflows/general.yml | 2 +- src/client.rs | 2 +- src/constants.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index a6db8343f..22a3b88ec 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -71,6 +71,6 @@ jobs: override: true - name: Run cargo-tarpaulin - uses: actions-rs/tarpaulin@master + uses: actions-rs/tarpaulin@v0.25.0 with: args: '--ignore-tests --no-fail-fast' diff --git a/src/client.rs b/src/client.rs index a69b7d3bc..20e7156c6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -42,7 +42,7 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { 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)) - .user_agent(format!("mpesa-rust@{}", CARGO_PACKAGE_VERSION)) + .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() diff --git a/src/constants.rs b/src/constants.rs index e08c0210f..82636188a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -22,7 +22,7 @@ pub enum CommandId { impl Display for CommandId { fn fmt(&self, f: &mut Formatter) -> FmtResult { - write!(f, "{:?}", self) + write!(f, "{self:?}") } } @@ -83,6 +83,6 @@ pub enum ResponseType { impl Display for ResponseType { fn fmt(&self, f: &mut Formatter) -> FmtResult { - write!(f, "{:?}", self) + write!(f, "{self:?}") } } From 62966cdca8c2d655a862924df2b1a94b94e8d837 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 7 Feb 2023 10:57:21 +0300 Subject: [PATCH 112/140] Fix failing cargo tarpaulin --- .github/workflows/general.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 22a3b88ec..500e1fdca 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -71,6 +71,7 @@ jobs: override: true - name: Run cargo-tarpaulin - uses: actions-rs/tarpaulin@v0.25.0 + uses: actions-rs/tarpaulin@v0.1 with: args: '--ignore-tests --no-fail-fast' + version: '0.22.0' From bfcff5a61909607d38b8d1f32c5be2173328f948 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Fri, 10 Feb 2023 09:58:03 +0300 Subject: [PATCH 113/140] Update docs; code clean up --- README.md | 21 +++++++++++---------- src/client.rs | 1 + tests/mpesa-rust/helpers.rs | 2 -- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e2539103a..53c365028 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Optionally, you can disable default-features, which is basically the entire suit - `c2b_simulate` - `express_request` - `transaction_reversal` +- `transaction_status` Example: @@ -253,16 +254,16 @@ assert!(response.is_ok()) ```rust let response = client - .transaction_status("testapi496") - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") - .transaction_id("OEI2AK4Q16") - .identifier_type(IdentifierTypes::ShortCode) - .party_a("600111") - .remarks("status") - .occasion("work") - .send() - .await; + .transaction_status("testapi496") + .result_url("https://testdomain.com/ok") + .timeout_url("https://testdomain.com/err") + .transaction_id("OEI2AK4Q16") + .identifier_type(IdentifierTypes::ShortCode) + .party_a("600111") + .remarks("status") + .occasion("work") + .send() + .await; assert!(response.is_ok()) ``` diff --git a/src/client.rs b/src/client.rs index 20e7156c6..f14f89eda 100644 --- a/src/client.rs +++ b/src/client.rs @@ -288,6 +288,7 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { ) -> TransactionReversalBuilder<'mpesa, Env> { TransactionReversalBuilder::new(self, initiator_name) } + ///**Transaction Status Builder** /// Queries the status of a B2B, B2C or C2B M-Pesa transaction. /// diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index 5ff6ed93b..c4b8dda23 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -6,7 +6,6 @@ pub struct TestEnvironment { } impl TestEnvironment { - #[allow(unused)] pub async fn new(server: &MockServer) -> Self { TestEnvironment { server_url: server.uri(), @@ -14,7 +13,6 @@ impl TestEnvironment { } } -// TODO: Implement mock server for testing impl ApiEnvironment for TestEnvironment { fn base_url(&self) -> &str { &self.server_url From 63d409c501f1c35cdd46c4fcace11390f1acb0c6 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Fri, 10 Feb 2023 10:26:49 +0300 Subject: [PATCH 114/140] Update release pipeline --- .github/workflows/release-core.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index 7ef844a85..01ca67a4b 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -3,7 +3,7 @@ name: publish to crates.io on: push: tags: - - '1.*' + - 'v*' env: CARGO_TERM_COLOR: always From 57f38233e4643203b8d969a6cbd065ac67439d36 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Sat, 18 Feb 2023 09:59:59 +0300 Subject: [PATCH 115/140] Conditional compiling for tests --- tests/mpesa-rust/main.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/mpesa-rust/main.rs b/tests/mpesa-rust/main.rs index 39e4de764..78824f33f 100644 --- a/tests/mpesa-rust/main.rs +++ b/tests/mpesa-rust/main.rs @@ -1,9 +1,18 @@ +#[cfg(test)] mod account_balance_test; +#[cfg(test)] mod b2b_test; +#[cfg(test)] mod b2c_test; +#[cfg(test)] mod c2b_register_test; +#[cfg(test)] mod c2b_simulate_test; +#[cfg(test)] mod helpers; +#[cfg(test)] mod stk_push_test; +#[cfg(test)] mod transaction_reversal_test; +#[cfg(test)] mod transaction_status_test; From 67f7175251be77e7856934fa4fd18d5b1cad63a6 Mon Sep 17 00:00:00 2001 From: Yasir Date: Sat, 18 Feb 2023 11:05:15 +0300 Subject: [PATCH 116/140] Remove base64 crate (#63) * Remove base64 crate * Fix audit error, enable only clock feature for chrono --- Cargo.toml | 17 ++++++++--------- src/client.rs | 3 ++- src/services/express_request.rs | 3 ++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a4f9e3022..b130bee58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,7 @@ readme = "./README.md" license = "MIT" [dependencies] -base64 = {version = "0.13", optional = true} -chrono = {version = "0.4", optional = true} +chrono = {version = "0.4", optional = true, default-features = false, features = ["clock"] } openssl = {version = "0.10", optional = true} reqwest = {version = "0.11", features = ["json"]} serde = {version="1.0", features= ["derive"]} @@ -28,7 +27,7 @@ wiremock = "0.5" [features] default = [ "account_balance", - "b2b", + "b2b", "b2c", "c2b_register", "c2b_simulate", @@ -36,11 +35,11 @@ default = [ "transaction_reversal", "transaction_status" ] -account_balance = ["dep:openssl", "dep:base64"] -b2b = ["dep:openssl", "dep:base64"] -b2c = ["dep:openssl", "dep:base64"] +account_balance = ["dep:openssl"] +b2b = ["dep:openssl"] +b2c = ["dep:openssl"] c2b_register = [] c2b_simulate = [] -express_request = ["dep:chrono", "dep:base64"] -transaction_reversal = ["dep:openssl", "dep:base64"] -transaction_status= ["dep:openssl", "dep:base64"] +express_request = ["dep:chrono"] +transaction_reversal = ["dep:openssl"] +transaction_status= ["dep:openssl"] diff --git a/src/client.rs b/src/client.rs index f14f89eda..3722fa06c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,6 +4,7 @@ use crate::services::{ MpesaExpressRequestBuilder, TransactionReversalBuilder, TransactionStatusBuilder, }; use crate::MpesaError; +use openssl::base64; use openssl::rsa::Padding; use openssl::x509::X509; use reqwest::Client as HttpClient; @@ -335,7 +336,7 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { &mut buffer, Padding::PKCS1, )?; - Ok(base64::encode(buffer)) + Ok(base64::encode_block(&buffer)) } } diff --git a/src/services/express_request.rs b/src/services/express_request.rs index 170ca0f85..a08ce2d16 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -3,6 +3,7 @@ use crate::constants::CommandId; use crate::environment::ApiEnvironment; use crate::errors::MpesaError; use chrono::prelude::Local; +use openssl::base64; use serde::{Deserialize, Serialize}; /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) @@ -99,7 +100,7 @@ impl<'mpesa, Env: ApiEnvironment> MpesaExpressRequestBuilder<'mpesa, Env> { /// Returns the encoded password and a timestamp string fn generate_password_and_timestamp(&self) -> (String, String) { let timestamp = Local::now().format("%Y%m%d%H%M%S").to_string(); - let encoded_password = base64::encode( + let encoded_password = base64::encode_block( format!( "{}{}{}", self.business_short_code(), From 3d78a3304cbfad0a1f7d9d1c88b9a96243e85ac6 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 27 Mar 2023 09:02:15 +0300 Subject: [PATCH 117/140] Derive clone for mpesa struct --- src/client.rs | 2 +- src/environment.rs | 2 +- tests/mpesa-rust/helpers.rs | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 3722fa06c..2d2dda27d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -20,7 +20,7 @@ static CARGO_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); pub type MpesaResult = Result; /// Mpesa client that will facilitate communication with the Safaricom API -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Mpesa { client_key: String, client_secret: String, diff --git a/src/environment.rs b/src/environment.rs index 2487120f9..eb6f1f579 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -13,7 +13,7 @@ use crate::MpesaError; use std::{convert::TryFrom, str::FromStr}; -#[derive(Debug)] +#[derive(Debug, Clone)] /// Enum to map to desired environment so as to access certificate /// and the base url /// Required to construct a new `Mpesa` struct diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index c4b8dda23..01dfcfaf5 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -1,6 +1,7 @@ use mpesa::ApiEnvironment; use wiremock::MockServer; +#[derive(Debug, Clone)] pub struct TestEnvironment { pub server_url: String, } From 3db4e1f88d69d7ec45b35d78dfb9664ca65d6518 Mon Sep 17 00:00:00 2001 From: dxphilo Date: Mon, 26 Jun 2023 12:57:39 +0300 Subject: [PATCH 118/140] fix: extract an api struct to store error details --- src/client.rs | 17 +++++++++++++---- src/errors.rs | 50 +++++++++++++++++++++++++++++++++++++++----------- src/lib.rs | 2 +- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/client.rs b/src/client.rs index 2d2dda27d..361ec6a5d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,7 +3,7 @@ use crate::services::{ AccountBalanceBuilder, B2bBuilder, B2cBuilder, C2bRegisterBuilder, C2bSimulateBuilder, MpesaExpressRequestBuilder, TransactionReversalBuilder, TransactionStatusBuilder, }; -use crate::MpesaError; +use crate::{MpesaError, ApiError}; use openssl::base64; use openssl::rsa::Padding; use openssl::x509::X509; @@ -114,16 +114,25 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { .await?; if response.status().is_success() { let value = response.json::().await?; + let error_message ="Failed to extract token from the response"; let access_token = value .get("access_token") - .ok_or_else(|| MpesaError::AuthenticationError(value.clone()))?; + .ok_or_else(|| MpesaError::AuthenticationError( + ApiError::new(value.to_string(),String::new(),error_message.to_string() + )))?; let access_token = access_token .as_str() - .ok_or_else(|| MpesaError::AuthenticationError(value.clone()))?; + .ok_or_else(|| MpesaError::AuthenticationError( + ApiError::new(value.to_string(), String::new(),error_message.to_string()) + ))?; return Ok(access_token.to_string()); } let value = response.json::().await?; - Err(MpesaError::AuthenticationError(value)) + let request_id = value.get("requestId").and_then(Value::as_str).unwrap_or_default().to_string(); + let error_code = value.get("errorCode").and_then(Value::as_str).unwrap_or_default().to_string(); + let error_message = value.get("errorMessage").and_then(Value::as_str).unwrap_or_default().to_string(); + let api_error = ApiError::new(request_id, error_code, error_message); + Err(MpesaError::AuthenticationError(api_error)) } /// **B2C Builder** diff --git a/src/errors.rs b/src/errors.rs index a3c39e46c..08760e922 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,26 +1,54 @@ -use std::env::VarError; +use std::{env::VarError, fmt}; +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ApiError { + pub request_id: String, + pub error_code: String, + pub error_message: String, +} + +impl fmt::Display for ApiError { + fn fmt(&self, f:&mut fmt::Formatter<'_>) -> fmt::Result{ + write!( + f, + "requestID: {}, errorCode:{}, errorMessage:{}",self.request_id, self.error_code, self.error_message + ) + } +} + +impl ApiError { + pub fn new(request_id: String, error_code: String, error_message: String) -> Self { + ApiError { + request_id, + error_code, + error_message, + } + } +} + /// Mpesa error stack #[derive(thiserror::Error, Debug)] pub enum MpesaError { - #[error("Authentication request failed: {0}")] - AuthenticationError(serde_json::Value), + #[error("{0}")] + AuthenticationError(ApiError), #[error("B2B request failed: {0}")] - B2bError(serde_json::Value), + B2bError(ApiError), #[error("B2C request failed: {0}")] - B2cError(serde_json::Value), + B2cError(ApiError), #[error("C2B register request failed: {0}")] - C2bRegisterError(serde_json::Value), + C2bRegisterError(ApiError), #[error("C2B simulate request failed: {0}")] - C2bSimulateError(serde_json::Value), + C2bSimulateError(ApiError), #[error("Account Balance request failed: {0}")] - AccountBalanceError(serde_json::Value), + AccountBalanceError(ApiError), #[error("Mpesa Express request/ STK push failed: {0}")] - MpesaExpressRequestError(serde_json::Value), + MpesaExpressRequestError(ApiError), #[error("Mpesa Transaction reversal failed: {0}")] - MpesaTransactionReversalError(serde_json::Value), + MpesaTransactionReversalError(ApiError), #[error("Mpesa Transaction status failed: {0}")] - MpesaTransactionStatusError(serde_json::Value), + MpesaTransactionStatusError(ApiError), #[error("An error has occured while performing the http request")] NetworkError(#[from] reqwest::Error), #[error("An error has occured while serializig/ deserializing")] diff --git a/src/lib.rs b/src/lib.rs index a0057c4ef..54c1a5369 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -357,4 +357,4 @@ pub use client::{Mpesa, MpesaResult}; pub use constants::{CommandId, IdentifierTypes, ResponseType}; pub use environment::ApiEnvironment; pub use environment::Environment::{self, Production, Sandbox}; -pub use errors::MpesaError; +pub use errors::{MpesaError, ApiError}; From 010a4ec41d7d24c1a37b9bd0b7956ea89e92e1f1 Mon Sep 17 00:00:00 2001 From: dxphilo Date: Mon, 24 Jul 2023 17:16:13 +0300 Subject: [PATCH 119/140] fix: review changes --- src/client.rs | 17 +++++----------- src/errors.rs | 54 +++++++++++++++++++++++++-------------------------- 2 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/client.rs b/src/client.rs index 361ec6a5d..aedc3d122 100644 --- a/src/client.rs +++ b/src/client.rs @@ -114,24 +114,17 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { .await?; if response.status().is_success() { let value = response.json::().await?; - let error_message ="Failed to extract token from the response"; let access_token = value .get("access_token") - .ok_or_else(|| MpesaError::AuthenticationError( - ApiError::new(value.to_string(),String::new(),error_message.to_string() - )))?; + .ok_or_else(|| String::from("Failed to extract token from the response")).unwrap(); let access_token = access_token .as_str() - .ok_or_else(|| MpesaError::AuthenticationError( - ApiError::new(value.to_string(), String::new(),error_message.to_string()) - ))?; + .ok_or_else(|| String::from("Error converting access token to string")).unwrap(); + return Ok(access_token.to_string()); } - let value = response.json::().await?; - let request_id = value.get("requestId").and_then(Value::as_str).unwrap_or_default().to_string(); - let error_code = value.get("errorCode").and_then(Value::as_str).unwrap_or_default().to_string(); - let error_message = value.get("errorMessage").and_then(Value::as_str).unwrap_or_default().to_string(); - let api_error = ApiError::new(request_id, error_code, error_message); + let error = response.json::().await?; + let api_error = ApiError::new(error.request_id, error.error_code, error.error_message); Err(MpesaError::AuthenticationError(api_error)) } diff --git a/src/errors.rs b/src/errors.rs index 08760e922..e048f000e 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,33 +1,6 @@ use std::{env::VarError, fmt}; use serde::{Serialize, Deserialize}; -#[derive(Debug, Serialize, Deserialize)] -pub struct ApiError { - pub request_id: String, - pub error_code: String, - pub error_message: String, -} - -impl fmt::Display for ApiError { - fn fmt(&self, f:&mut fmt::Formatter<'_>) -> fmt::Result{ - write!( - f, - "requestID: {}, errorCode:{}, errorMessage:{}",self.request_id, self.error_code, self.error_message - ) - } -} - -impl ApiError { - pub fn new(request_id: String, error_code: String, error_message: String) -> Self { - ApiError { - request_id, - error_code, - error_message, - } - } -} - - /// Mpesa error stack #[derive(thiserror::Error, Debug)] pub enum MpesaError { @@ -60,3 +33,30 @@ pub enum MpesaError { #[error("{0}")] Message(&'static str), } + + +#[derive(Debug, Serialize, Deserialize)] +pub struct ApiError { + pub request_id: String, + pub error_code: String, + pub error_message: String, +} + +impl fmt::Display for ApiError { + fn fmt(&self, f:&mut fmt::Formatter<'_>) -> fmt::Result{ + write!( + f, + "requestID: {}, errorCode:{}, errorMessage:{}",self.request_id, self.error_code, self.error_message + ) + } +} + +impl ApiError { + pub fn new(request_id: String, error_code: String, error_message: String) -> Self { + ApiError { + request_id, + error_code, + error_message, + } + } +} \ No newline at end of file From 0aaadf31612edf518510cb9912dda7de8d6ab40c Mon Sep 17 00:00:00 2001 From: dxphilo Date: Thu, 27 Jul 2023 00:08:40 +0300 Subject: [PATCH 120/140] fix: rustfmt ci errors --- src/client.rs | 10 ++++++---- src/errors.rs | 10 +++++----- src/lib.rs | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/client.rs b/src/client.rs index aedc3d122..7ee8fe030 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,7 +3,7 @@ use crate::services::{ AccountBalanceBuilder, B2bBuilder, B2cBuilder, C2bRegisterBuilder, C2bSimulateBuilder, MpesaExpressRequestBuilder, TransactionReversalBuilder, TransactionStatusBuilder, }; -use crate::{MpesaError, ApiError}; +use crate::{ApiError, MpesaError}; use openssl::base64; use openssl::rsa::Padding; use openssl::x509::X509; @@ -116,11 +116,13 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { let value = response.json::().await?; let access_token = value .get("access_token") - .ok_or_else(|| String::from("Failed to extract token from the response")).unwrap(); + .ok_or_else(|| String::from("Failed to extract token from the response")) + .unwrap(); let access_token = access_token .as_str() - .ok_or_else(|| String::from("Error converting access token to string")).unwrap(); - + .ok_or_else(|| String::from("Error converting access token to string")) + .unwrap(); + return Ok(access_token.to_string()); } let error = response.json::().await?; diff --git a/src/errors.rs b/src/errors.rs index e048f000e..051dcf6c5 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,5 +1,5 @@ +use serde::{Deserialize, Serialize}; use std::{env::VarError, fmt}; -use serde::{Serialize, Deserialize}; /// Mpesa error stack #[derive(thiserror::Error, Debug)] @@ -34,7 +34,6 @@ pub enum MpesaError { Message(&'static str), } - #[derive(Debug, Serialize, Deserialize)] pub struct ApiError { pub request_id: String, @@ -43,10 +42,11 @@ pub struct ApiError { } impl fmt::Display for ApiError { - fn fmt(&self, f:&mut fmt::Formatter<'_>) -> fmt::Result{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "requestID: {}, errorCode:{}, errorMessage:{}",self.request_id, self.error_code, self.error_message + "requestID: {}, errorCode:{}, errorMessage:{}", + self.request_id, self.error_code, self.error_message ) } } @@ -59,4 +59,4 @@ impl ApiError { error_message, } } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 54c1a5369..3b71c2df4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -357,4 +357,4 @@ pub use client::{Mpesa, MpesaResult}; pub use constants::{CommandId, IdentifierTypes, ResponseType}; pub use environment::ApiEnvironment; pub use environment::Environment::{self, Production, Sandbox}; -pub use errors::{MpesaError, ApiError}; +pub use errors::{ApiError, MpesaError}; From 0b20b19b0fc2487ace0be695e2c322956c0ecb2c Mon Sep 17 00:00:00 2001 From: John Philip Date: Tue, 15 Aug 2023 13:56:12 +0300 Subject: [PATCH 121/140] fix: review changes --- src/client.rs | 3 +-- src/errors.rs | 12 +----------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/client.rs b/src/client.rs index 7ee8fe030..b22b05c4b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -126,8 +126,7 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { return Ok(access_token.to_string()); } let error = response.json::().await?; - let api_error = ApiError::new(error.request_id, error.error_code, error.error_message); - Err(MpesaError::AuthenticationError(api_error)) + Err(MpesaError::AuthenticationError(error)) } /// **B2C Builder** diff --git a/src/errors.rs b/src/errors.rs index 051dcf6c5..25c0c13e3 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -49,14 +49,4 @@ impl fmt::Display for ApiError { self.request_id, self.error_code, self.error_message ) } -} - -impl ApiError { - pub fn new(request_id: String, error_code: String, error_message: String) -> Self { - ApiError { - request_id, - error_code, - error_message, - } - } -} +} \ No newline at end of file From 0bb0e90cfc67ef25a3f00098acb9eead1d1bf6e5 Mon Sep 17 00:00:00 2001 From: John Philip Date: Wed, 16 Aug 2023 10:57:14 +0300 Subject: [PATCH 122/140] fix: run rust fmt ci error --- src/errors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/errors.rs b/src/errors.rs index 25c0c13e3..f8b858732 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -49,4 +49,4 @@ impl fmt::Display for ApiError { self.request_id, self.error_code, self.error_message ) } -} \ No newline at end of file +} From 5ac17dafe12067a7627f9fa6f6fb662470a18a42 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Sat, 19 Aug 2023 15:45:08 +0300 Subject: [PATCH 123/140] Keep readme and rust docs in sync --- CONTRIBUTING.md | 11 +- README.md | 33 +++-- src/lib.rs | 349 +----------------------------------------------- 3 files changed, 21 insertions(+), 372 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb7f5e0c9..4107510fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,5 @@ -# TODO +# How to Contribute -- [x] Unify `mpesa_derive` and `mpesa_core` -- [ ] Add missing services: `transaction_status`, `bill_manager`, `dynamic_qr_code`, `c2b_simulate_v2` -- [x] Clean up `Cargo.toml`: Correctly use Cargo features and declare optional and dev dependencies -- [x] Convert library to async and update tests -- [x] Migrate to `thiserror` and remove `failure` -- [ ] Refine tests: test more edge cases \ No newline at end of file +- Create an issue or pick an already existing issue. Please avoid creating duplicate issues +- Fork the repo, add your changes and make a pull request +- Request for review from either me or other contributors \ No newline at end of file diff --git a/README.md b/README.md index 53c365028..8c05c151c 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ mpesa = { git = "https://github.com/collinsmuriuki/mpesa-rust", default_features In your lib or binary crate: -```rs +```rust,no_run use mpesa::Mpesa; ``` @@ -52,7 +52,7 @@ read the docs [here](https://developer.safaricom.co.ke/docs?javascript#going-liv These are the following ways you can instantiate `Mpesa`: -```rust +```rust,no_run use mpesa::{Mpesa, Environment}; let client = Mpesa::new( @@ -67,7 +67,7 @@ assert!(client.is_connected().await) Since the `Environment` enum implements `FromStr` and `TryFrom` for `String` and `&str` types, you can call `Environment::from_str` or `Environment::try_from` to create an `Environment` type. This is ideal if the environment values are stored in a `.env` or any other configuration file: -```rust +```rust,no_run use mpesa::{Mpesa, Environment}; use std::str::FromStr; use std::convert::TryFrom; @@ -77,13 +77,13 @@ let client0 = Mpesa::new( env!("CLIENT_SECRET"), Environment::from_str("sandbox")? // "Sandbox" and "SANDBOX" also valid ); +assert!(client0.is_connected().await) let client1 = Mpesa::new( env!("CLIENT_KEY"), env!("CLIENT_SECRET"), Environment::try_from("production")? // "Production" and "PRODUCTION" also valid ); -assert!(client0.is_connected().await) assert!(client1.is_connected().await) ``` @@ -101,7 +101,7 @@ This trait allows you to create your own type to pass to the `environment` param See the example below (and [here](./src/environment.rs) so see how the trait is implemented for the `Environment` enum): -```rust +```rust,no_run use mpesa::{Mpesa, ApiEnvironment}; use std::str::FromStr; use std::convert::TryFrom; @@ -132,7 +132,7 @@ let client: Mpesa = Mpesa::new( If you intend to use in production, you will need to call a the `set_initiator_password` method from `Mpesa` after initially creating the client. Here you provide your initiator password, which overrides the default password used in sandbox `"Safcom496!"`: -```rust +```rust,no_run use mpesa::Mpesa; let client = Mpesa::new( @@ -152,7 +152,7 @@ The following services are currently available from the `Mpesa` client as method - B2C -```rust +```rust,no_run let response = client .b2c("testapi496") .party_a("600496") @@ -167,7 +167,7 @@ assert!(response.is_ok()) - B2B -```rust +```rust,no_run let response = client .b2b("testapi496") .party_a("600496") @@ -183,7 +183,7 @@ assert!(response.is_ok()) - C2B Register -```rust +```rust,no_run let response = client .c2b_register() .short_code("600496") @@ -196,8 +196,7 @@ assert!(response.is_ok()) - C2B Simulate -```rust - +```rust,no_run let response = client .c2b_simulate() .short_code("600496") @@ -210,7 +209,7 @@ assert!(response.is_ok()) - Account Balance -```rust +```rust,no_run let response = client .account_balance("testapi496") .result_url("https://testdomain.com/ok") @@ -223,7 +222,7 @@ assert!(response.is_ok()) - Mpesa Express Request / STK push / Lipa na M-PESA online -```rust +```rust,no_run let response = client .express_request("174379") .phone_number("254708374149") @@ -236,7 +235,7 @@ assert!(response.is_ok()) - Transaction Reversal: -```rust +```rust,no_run let response = client .transaction_reversal("testapi496") .result_url("https://testdomain.com/ok") @@ -252,7 +251,7 @@ assert!(response.is_ok()) - Transaction Status -```rust +```rust,no_run let response = client .transaction_status("testapi496") .result_url("https://testdomain.com/ok") @@ -278,7 +277,7 @@ More will be added progressively, pull requests welcome ## Contributing -Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/collinsmuriuki/mpesa-rust/issues). You can also take a look at the [contributing guide](CONTRIBUTING.md). +Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/collinsmuriuki/mpesa-rust/issues). You can also take a look at the [contributing guide](https://raw.githubusercontent.com/collinsmuriuki/mpesa-rust/master/CONTRIBUTING.md). Copyright © 2023 [Collins Muriuki](https://github.com/collinsmuriuki).
-This project is [MIT](LICENSE) licensed. +This project is [MIT](https://raw.githubusercontent.com/collinsmuriuki/mpesa-rust/master/LICENSE) licensed. diff --git a/src/lib.rs b/src/lib.rs index 3b71c2df4..3b0c16ad0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,351 +1,4 @@ -//!## mpesa-rust -//! -//! An unofficial Rust wrapper around the [Safaricom API](https://developer.safaricom.co.ke/docs?shell#introduction) for accessing M-Pesa services. -//! -//!## Install -//! `Cargo.toml` -//! -//! ```toml -//! [dependencies] -//! mpesa = "0.4.2" -//! ``` -//! Optionally, you can disable default-features, which is basically the entire suite of MPESA APIs to conditionally select from either: -//! - `b2b` -//! - `b2c` -//! - `account_balance` -//! - `c2b_register` -//! - `c2b_simulate` -//! - `express_request` -//! - `transaction_reversal` -//! -//! Example: -//! -//! ```toml -//! [dependencies] -//! mpesa = { version = "0.4.2", default_features = false, features = ["b2b", "express_request"] } -//! ``` -//! -//! In your lib or binary crate: -//! ```rs -//! use mpesa::Mpesa; -//! ``` -//! -//!## Usage -//! -//!### Creating a `Mpesa` client -//! You will first need to create an instance of the `Mpesa` instance (the client). You are required to provide a **CLIENT_KEY** and -//! **CLIENT_SECRET**. [Here](https://developer.safaricom.co.ke/test_credentials) is how you can get these credentials for the Safaricom sandbox -//! environment. It's worth noting that these credentials are only valid in the sandbox environment. To go live and get production keys -//! read the docs [here](https://developer.safaricom.co.ke/docs?javascript#going-live). -//! -//! These are the following ways you can instantiate `Mpesa`: -//! -//! ```rust,no_run -//! use mpesa::{Mpesa, Environment}; -//! use std::env; -//! use dotenv::dotenv; -//! -//! #[tokio::main] -//! async fn main() { -//! dotenv().ok(); -//! -//! let client = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! Environment::Sandbox, -//! ); -//! assert!(client.is_connected().await) -//! } -//! ``` -//! -//! Since the `Environment` enum implements `FromStr` and `TryFrom` for `String` and `&str` types, you can call `Environment::from_str` or `Environment::try_from` to create an `Environment` type. This is ideal if the environment values are -//! stored in a `.env` or any other configuration file -//! -//! ```rust,no_run -//! use mpesa::{Mpesa, Environment}; -//! use std::env; -//! use dotenv::dotenv; -//! use std::str::FromStr; -//! -//! #[tokio::main] -//! async fn main() { -//! dotenv().ok(); -//! -//! let client = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! Environment::from_str("sandbox").unwrap() -//! ); -//! assert!(client.is_connected().await) -//! } -//! ``` -//! If you intend to use in production, you will need to call a the `set_initiator_password` method from `Mpesa` after initially -//! creating the client. Here you provide your initiator password, which overrides the default password used in sandbox `"Safcom496!"`: -//! -//! ```rust,no_run -//! use mpesa::{Mpesa, Environment}; -//! use std::env; -//! use dotenv::dotenv; -//! -//! #[tokio::main] -//! async fn main() { -//! dotenv().ok(); -//! -//! let client = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! Environment::Sandbox -//! ); -//! -//! client.set_initiator_password("new_password"); -//! -//! assert!(client.is_connected().await) -//! } -//! ``` -//! -//!### Services -//! The following services are currently available from the `Mpesa` client as methods that return builders: -//! * B2C -//! ```rust,no_run -//! use mpesa::{Mpesa, Environment}; -//! use std::env; -//! use dotenv::dotenv; -//! -//! #[tokio::main] -//! async fn main() { -//! dotenv().ok(); -//! -//! let client = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! Environment::Sandbox -//! ); -//! -//! let response = client -//! .b2c("testapi496") -//! .party_a("600496") -//! .party_b("254708374149") -//! .result_url("https://testdomain.com/ok") -//! .timeout_url("https://testdomain.com/err") -//! .amount(1000) -//! .send() -//! .await; -//! assert!(response.is_ok()) -//! } -//! ``` -//! -//! * B2B -//! ```rust,no_run -//! use mpesa::{Mpesa, Environment}; -//! use std::env; -//! use dotenv::dotenv; -//! -//! #[tokio::main] -//! async fn main() { -//! dotenv().ok(); -//! -//! let client = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! Environment::Sandbox -//! ); -//! -//! let response = client -//! .b2b("testapi496") -//! .party_a("600496") -//! .party_b("600000") -//! .result_url("https://testdomain.com/ok") -//! .timeout_url("https://testdomain.com/err") -//! .account_ref("254708374149") -//! .amount(1000) -//! .send() -//! .await; -//! assert!(response.is_ok()) -//! } -//! ``` -//! -//! * C2B Register -//! ```rust,no_run -//! use mpesa::{Mpesa, Environment}; -//! use serde_json::Value; -//! use std::env; -//! use dotenv::dotenv; -//! -//! #[tokio::main] -//! async fn main() { -//! dotenv().ok(); -//! -//! let client = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! Environment::Sandbox -//! ); -//! -//! let response = client -//! .c2b_register() -//! .short_code("600496") -//! .confirmation_url("https://testdomain.com/true") -//! .validation_url("https://testdomain.com/valid") -//! .send() -//! .await; -//! assert!(response.is_ok()) -//! } -//! ``` -//! -//! * C2B Simulate -//! ```rust,no_run -//! use mpesa::{Mpesa, Environment}; -//! use std::env; -//! use dotenv::dotenv; -//! -//! #[tokio::main] -//! async fn main() { -//! dotenv().ok(); -//! -//! let client = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! Environment::Sandbox -//! ); -//! -//! let response = client -//! .c2b_simulate() -//! .short_code("600496") -//! .msisdn("254700000000") -//! .amount(1000) -//! .send() -//! .await; -//! assert!(response.is_ok()) -//! } -//! ``` -//! -//! * Account Balance -//! -//! ```rust,no_run -//! use mpesa::{Mpesa, Environment}; -//! use std::env; -//! use dotenv::dotenv; -//! -//! #[tokio::main] -//! async fn main() { -//! dotenv().ok(); -//! -//! let client = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! Environment::Sandbox -//! ); -//! -//! let response = client -//! .account_balance("testapi496") -//! .result_url("https://testdomain.com/ok") -//! .timeout_url("https://testdomain.com/err") -//! .party_a("600496") -//! .send() -//! .await; -//! assert!(response.is_ok()) -//! } -//! ``` -//! -//! * Mpesa Express Request / STK push/ Lipa na M-PESA online -//! -//! ```rust,no_run -//! use mpesa::{Mpesa, Environment}; -//! use std::env; -//! use dotenv::dotenv; -//! -//! #[tokio::main] -//! async fn main() { -//! dotenv().ok(); -//! -//! let client = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! Environment::Sandbox -//! ); -//! -//! let response = client -//! .express_request("174379") -//! .phone_number("254708374149") -//! .amount(500) -//! .callback_url("https://testdomain.com/ok") -//! .send() -//! .await; -//! assert!(response.is_ok()) -//! } -//! ``` -//! -//! * Transaction Reversal -//! -//! ```rust,no_run -//! use mpesa::{Mpesa, Environment, IdentifierTypes}; -//! use std::env; -//! use dotenv::dotenv; -//! -//! #[tokio::main] -//! async fn main() { -//! dotenv().ok(); -//! -//! let client = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! Environment::Sandbox -//! ); -//! -//! let response = client -//! .transaction_reversal("testapi496") -//! .result_url("https://testdomain.com/ok") -//! .timeout_url("https://testdomain.com/err") -//! .transaction_id("OEI2AK4Q16") -//! .receiver_identifier_type(IdentifierTypes::ShortCode) -//! .amount(100.0) -//! .receiver_party("600111") -//! .send() -//! .await; -//! assert!(response.is_ok()); -//!} -//! ``` -//! -//! * Transaction Status -//! -//! ```rust,no_run -//! use mpesa::{Mpesa, Environment}; -//! use std::env; -//! use dotenv::dotenv; -//! -//! #[tokio::main] -//! async fn main() { -//! dotenv().ok(); -//! -//! let client = Mpesa::new( -//! env::var("CLIENT_KEY").unwrap(), -//! env::var("CLIENT_SECRET").unwrap(), -//! Environment::Sandbox -//! ); -//! -//! let response = client -//! .transaction_status("testapi496") -//! .result_url("https://testdomain.com/ok") -//! .timeout_url("https://testdomain.com/err") -//! .transaction_id("OEI2AK4Q16") -//! .party_a("600111") -//! .send() -//! .await; -//! assert!(response.is_ok()); -//! } -//! ``` -//! -//! More will be added progressively, pull requests welcome -//! -//!## Author -//! -//! **Collins Muriuki** -//! -//! * Twitter: [@collinsmuriuki_](https://twitter.com/collinsmuriuki_) -//! * Not affiliated with Safaricom. -//! -//!## License -//! This project is MIT licensed +#![doc = include_str!("../README.md")] mod client; mod constants; From 39a763edee98c60328fa5c9bea5328f9a6191f10 Mon Sep 17 00:00:00 2001 From: Crispin Koech Date: Sun, 20 Aug 2023 08:40:57 +0300 Subject: [PATCH 124/140] Implement bill manager api (#66) * Add bill manager onboarding api * Add bill manager onboard modify API * Add single invoicing API * Add single invoicing API tests * Clean up bill manager onboard modules and tests * Rename bill manager single invoice payload to just Payload<> * Remove unnecessary type annotations * Make 'due_date' for invoice type owned * Add bill manager bulk invoicing API * Add bill manager reconciliation API * Add cancel invoice API * Clean up bulk invoicing module to throw if invoices is None * Keep bill manager features under one 'bill_manager' feature flag * Update bill manager urls on our docs * Apply suggestions from code review Apply suggestions Co-authored-by: Collins Muriuki * Rename res_msg and res_code; tie chrono dependency under bill_manager * Add function to add single invoices to bulk invoice builder * Update README.md * Add function to CancelInvoiceBuilder --------- Co-authored-by: Collins Muriuki --- .cargo/config.toml | 4 +- Cargo.toml | 4 +- README.md | 104 +++++++ src/client.rs | 185 +++++++++++- src/constants.rs | 54 ++++ src/errors.rs | 12 + src/lib.rs | 4 +- src/services/bill_manager.rs | 13 + src/services/bill_manager/bulk_invoice.rs | 81 ++++++ src/services/bill_manager/cancel_invoice.rs | 93 ++++++ src/services/bill_manager/onboard.rs | 168 +++++++++++ src/services/bill_manager/onboard_modify.rs | 152 ++++++++++ src/services/bill_manager/reconciliation.rs | 176 ++++++++++++ src/services/bill_manager/single_invoice.rs | 181 ++++++++++++ src/services/mod.rs | 4 + tests/mpesa-rust/bill_manager_test.rs | 6 + .../bill_manager_test/bulk_invoice_test.rs | 65 +++++ .../bill_manager_test/cancel_invoice_test.rs | 34 +++ .../bill_manager_test/onboard_modify_test.rs | 33 +++ .../bill_manager_test/onboard_test.rs | 163 +++++++++++ .../bill_manager_test/reconciliation_test.rs | 264 +++++++++++++++++ .../bill_manager_test/single_invoice_test.rs | 270 ++++++++++++++++++ tests/mpesa-rust/main.rs | 2 + 23 files changed, 2066 insertions(+), 6 deletions(-) create mode 100644 src/services/bill_manager.rs create mode 100644 src/services/bill_manager/bulk_invoice.rs create mode 100644 src/services/bill_manager/cancel_invoice.rs create mode 100644 src/services/bill_manager/onboard.rs create mode 100644 src/services/bill_manager/onboard_modify.rs create mode 100644 src/services/bill_manager/reconciliation.rs create mode 100644 src/services/bill_manager/single_invoice.rs create mode 100644 tests/mpesa-rust/bill_manager_test.rs create mode 100644 tests/mpesa-rust/bill_manager_test/bulk_invoice_test.rs create mode 100644 tests/mpesa-rust/bill_manager_test/cancel_invoice_test.rs create mode 100644 tests/mpesa-rust/bill_manager_test/onboard_modify_test.rs create mode 100644 tests/mpesa-rust/bill_manager_test/onboard_test.rs create mode 100644 tests/mpesa-rust/bill_manager_test/reconciliation_test.rs create mode 100644 tests/mpesa-rust/bill_manager_test/single_invoice_test.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 81ae92510..d26687b8e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,5 @@ [build] -rustdoc = "rustdoc" +rustdoc = "rustdoc" [alias] -t = ["test", "--all-features", "--no-fail-fast"] \ No newline at end of file +t = ["test", "--all-features", "--no-fail-fast"] diff --git a/Cargo.toml b/Cargo.toml index b130bee58..aa9412f42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ readme = "./README.md" license = "MIT" [dependencies] -chrono = {version = "0.4", optional = true, default-features = false, features = ["clock"] } +chrono = {version = "0.4", optional = true, default-features = false, features = ["clock", "serde"] } openssl = {version = "0.10", optional = true} reqwest = {version = "0.11", features = ["json"]} serde = {version="1.0", features= ["derive"]} @@ -29,6 +29,7 @@ default = [ "account_balance", "b2b", "b2c", + "bill_manager", "c2b_register", "c2b_simulate", "express_request", @@ -38,6 +39,7 @@ default = [ account_balance = ["dep:openssl"] b2b = ["dep:openssl"] b2c = ["dep:openssl"] +bill_manager = ["dep:chrono"] c2b_register = [] c2b_simulate = [] express_request = ["dep:chrono"] diff --git a/README.md b/README.md index 8c05c151c..2cd3dce05 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,110 @@ let response = client assert!(response.is_ok()) ``` +- Bill Manager Onboard + +```rust,ignore +let response = client + .onboard() + .callback_url("https://testdomain.com/true") + .email("email@test.com") + .logo("https://file.domain/file.png") + .official_contact("0712345678") + .send_reminders(SendRemindersTypes::Enable) + .short_code("600496") + .send() + .await; +assert!(response.is_ok()) +``` + +- Bill Manager Onboard Modify + +```rust,ignore +let response = client + .onboard_modify() + .callback_url("https://testdomain.com/true") + .email("email@test.com") + .short_code("600496") + .send() + .await; +assert!(response.is_ok()) +``` + +- Bill Manager Bulk Invoice + +```rust,ignore +let response = client + .bulk_invoice() + .invoices(vec![ + Invoice { + amount: 1000.0, + account_reference: "John Doe", + billed_full_name: "John Doe", + billed_period: "August 2021", + billed_phone_number: "0712345678", + due_date: Utc::now(), + external_reference: "INV2345", + invoice_items: Some( + vec![InvoiceItem {amount: 1000.0, item_name: "An item"}] + ), + invoice_name: "Invoice 001" + } + ]) + .send() + .await; +assert!(response.is_ok()) +``` + +- Bill Manager Single Invoice + +```rust,ignore +let response = client + .single_invoice() + .amount(1000.0) + .account_reference("John Doe") + .billed_full_name("John Doe") + .billed_period("August 2021") + .billed_phone_number("0712345678") + .due_date(Utc::now()) + .external_reference("INV2345") + .invoice_items(vec![ + InvoiceItem {amount: 1000.0, item_name: "An item"} + ]) + .invoice_name("Invoice 001") + .send() + .await; +assert!(response.is_ok()) +``` + +- Bill Manager Reconciliation + +```rust,ignore +let response = client + .reconciliation() + .account_reference("John Doe") + .external_reference("INV2345") + .full_name("John Doe") + .invoice_name("Invoice 001") + .paid_amount(1000.0) + .payment_date(Utc::now()) + .phone_number("0712345678") + .transaction_id("TRANSACTION_ID") + .send() + .await; +assert!(response.is_ok()) +``` + +- Bill Manager Cancel Invoice + +```rust,ignore +let response = client + .cancel_invoice() + .external_references(vec!["9KLSS011"]) + .send() + .await; +assert!(response.is_ok()) +``` + More will be added progressively, pull requests welcome ## Author diff --git a/src/client.rs b/src/client.rs index b22b05c4b..833b99039 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,7 +1,9 @@ use crate::environment::ApiEnvironment; use crate::services::{ - AccountBalanceBuilder, B2bBuilder, B2cBuilder, C2bRegisterBuilder, C2bSimulateBuilder, - MpesaExpressRequestBuilder, TransactionReversalBuilder, TransactionStatusBuilder, + AccountBalanceBuilder, B2bBuilder, B2cBuilder, BulkInvoiceBuilder, C2bRegisterBuilder, + C2bSimulateBuilder, CancelInvoiceBuilder, MpesaExpressRequestBuilder, OnboardBuilder, + OnboardModifyBuilder, ReconciliationBuilder, SingleInvoiceBuilder, TransactionReversalBuilder, + TransactionStatusBuilder, }; use crate::{ApiError, MpesaError}; use openssl::base64; @@ -184,6 +186,185 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { B2bBuilder::new(self, initiator_name) } + /// **Bill Manager Onboard Builder** + /// + /// Creates a `OnboardBuilder` which allows you to opt in as a biller to the bill manager features. + /// + /// See more from the Safaricom API docs [here](https://developer.safaricom.co.ke/APIs/BillManager) + /// + /// # Example + /// ```ignore + /// let response = client + /// .onboard() + /// .callback_url("https://testdomain.com/true") + /// .email("email@test.com") + /// .logo("https://file.domain/file.png") + /// .official_contact("0712345678") + /// .send_reminders(SendRemindersTypes::Enable) + /// .short_code("600496") + /// .send() + /// .await; + /// ``` + #[cfg(feature = "bill_manager")] + pub fn onboard(&'mpesa self) -> OnboardBuilder<'mpesa, Env> { + OnboardBuilder::new(self) + } + + /// **Bill Manager Onboard Modify Builder** + /// + /// Creates a `OnboardModifyBuilder` which allows you to opt in as a biller to the bill manager features. + /// + /// See more from the Safaricom API docs [here](https://developer.safaricom.co.ke/APIs/BillManager) + /// + /// # Example + /// ```ignore + /// let response = client + /// .onboard_modify() + /// .callback_url("https://testdomain.com/true") + /// .email("email@test.com") + /// .logo("https://file.domain/file.png") + /// .official_contact("0712345678") + /// .send_reminders(SendRemindersTypes::Enable) + /// .short_code("600496") + /// .send() + /// .await; + /// ``` + #[cfg(feature = "bill_manager")] + pub fn onboard_modify(&'mpesa self) -> OnboardModifyBuilder<'mpesa, Env> { + OnboardModifyBuilder::new(self) + } + + /// **Bill Manager Bulk Invoice Builder** + /// + /// Creates a `BulkInvoiceBuilder` which allows you to send invoices to your customers in bulk. + /// See more from the Safaricom API docs [here](https://developer.safaricom.co.ke/APIs/BillManager) + /// + /// # Example + /// ```ignore + /// use chrone::prelude::Utc; + /// + /// let response = client + /// .bulk_invoice() + /// + /// // Add multiple invoices at once + /// .invoices(vec![ + /// Invoice { + /// amount: 1000.0, + /// account_reference: "John Doe", + /// billed_full_name: "John Doe", + /// billed_period: "August 2021", + /// billed_phone_number: "0712345678", + /// due_date: Utc::now(), + /// external_reference: "INV2345", + /// invoice_items: Some( + /// vec![InvoiceItem {amount: 1000.0, item_name: "An item"}] + /// ), + /// invoice_name: "Invoice 001" + /// } + /// ]) + /// + /// // Add a single invoice + /// .invoice( + /// Invoice { + /// amount: 1000.0, + /// account_reference: "John Doe", + /// billed_full_name: "John Doe", + /// billed_period: "August 2021", + /// billed_phone_number: "0712345678", + /// due_date: Utc::now(), + /// external_reference: "INV2345", + /// invoice_items: Some(vec![InvoiceItem { + /// amount: 1000.0, + /// item_name: "An item", + /// }]), + /// invoice_name: "Invoice 001", + /// } + /// ) + /// .send() + /// .await; + /// ``` + #[cfg(feature = "bill_manager")] + pub fn bulk_invoice(&'mpesa self) -> BulkInvoiceBuilder<'mpesa, Env> { + BulkInvoiceBuilder::new(self) + } + + /// **Bill Manager Single Invoice Builder** + /// + /// Creates a `SingleInvoiceBuilder` which allows you to create and send invoices to your customers. + /// See more from the Safaricom API docs [here](https://developer.safaricom.co.ke/APIs/BillManager) + /// + /// # Example + /// ```ignore + /// use chrono::prelude::Utc; + /// + /// let response = client + /// .single_invoice() + /// .amount(1000.0) + /// .account_reference("John Doe") + /// .billed_full_name("John Doe") + /// .billed_period("August 2021") + /// .billed_phone_number("0712345678") + /// .due_date(Utc::now()) + /// .external_reference("INV2345") + /// .invoice_items(vec![ + /// InvoiceItem {amount: 1000.0, item_name: "An item"} + /// ]) + /// .invoice_name("Invoice 001") + /// .send() + /// .await; + /// ``` + #[cfg(feature = "bill_manager")] + pub fn single_invoice(&'mpesa self) -> SingleInvoiceBuilder<'mpesa, Env> { + SingleInvoiceBuilder::new(self) + } + + /// **Bill Manager Reconciliation Builder** + /// + /// Creates a `ReconciliationBuilder` which enables your customers to receive e-receipts for payments made to your paybill account. + /// See more from the Safaricom API docs [here](https://developer.safaricom.co.ke/APIs/BillManager) + /// + /// # Example + /// ```ignore + /// use chrono::prelude::Utc; + /// + /// let response = client + /// .reconciliation() + /// .account_reference("John Doe") + /// .external_reference("INV2345") + /// .full_name("John Doe") + /// .invoice_name("Invoice 001") + /// .paid_amount(1000.0) + /// .payment_date(Utc::now()) + /// .phone_number("0712345678") + /// .transaction_id("TRANSACTION_ID") + /// .send() + /// .await; + /// ``` + #[cfg(feature = "bill_manager")] + pub fn reconciliation(&'mpesa self) -> ReconciliationBuilder<'mpesa, Env> { + ReconciliationBuilder::new(self) + } + + /// **Bill Manager Cancel Invoice Builder** + /// + /// Creates a `CancelInvoiceBuilder` which allows you to recall a sent invoice. + /// See more from the Safaricom API docs [here](https://developer.safaricom.co.ke/APIs/BillManager) + /// + /// # Example + /// ```ignore + /// use chrono::prelude::Utc; + /// + /// let response = client + /// .cancel_invoice() + /// .external_references(vec!["9KLSS011"]) + /// .send() + /// .await; + /// ``` + #[cfg(feature = "bill_manager")] + pub fn cancel_invoice(&'mpesa self) -> CancelInvoiceBuilder<'mpesa, Env> { + CancelInvoiceBuilder::new(self) + } + /// **C2B Register builder** /// /// Creates a `C2bRegisterBuilder` for registering URLs to the 3rd party shortcode. diff --git a/src/constants.rs b/src/constants.rs index 82636188a..418ad5a52 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,3 +1,4 @@ +use chrono::prelude::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::fmt::{Display, Formatter, Result as FmtResult}; @@ -86,3 +87,56 @@ impl Display for ResponseType { write!(f, "{self:?}") } } + +#[derive(Debug, Deserialize_repr, Serialize_repr, Copy, Clone)] +#[repr(u16)] +pub enum SendRemindersTypes { + Disable = 0, + Enable = 1, +} + +impl Display for SendRemindersTypes { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "{:?}", *self as u16) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Invoice<'i> { + pub amount: f64, + pub account_reference: &'i str, + pub billed_full_name: &'i str, + pub billed_period: &'i str, + pub billed_phone_number: &'i str, + pub due_date: DateTime, + pub external_reference: &'i str, + #[serde(skip_serializing_if = "Option::is_none")] + pub invoice_items: Option>>, + pub invoice_name: &'i str, +} + +impl<'i> Display for Invoice<'i> { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!( + f, + "amount: {}, account_reference: {}, due_date: {}, invoice_name: {}", + self.amount, + self.account_reference, + self.due_date.format("%Y-%m-%d"), + self.invoice_name, + ) + } +} + +#[derive(Debug, Serialize)] +pub struct InvoiceItem<'i> { + pub amount: f64, + pub item_name: &'i str, +} + +impl<'i> Display for InvoiceItem<'i> { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "amount: {}, item_name: {}", self.amount, self.item_name) + } +} diff --git a/src/errors.rs b/src/errors.rs index f8b858732..2adc20891 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -16,6 +16,18 @@ pub enum MpesaError { C2bSimulateError(ApiError), #[error("Account Balance request failed: {0}")] AccountBalanceError(ApiError), + #[error("Bill manager onboarding failed: {0}")] + OnboardError(ApiError), + #[error("Bill manager onboarding modify failed: {0}")] + OnboardModifyError(ApiError), + #[error("Bill manager bulk invoice failed: {0}")] + BulkInvoiceError(ApiError), + #[error("Bill manager reconciliation failed: {0}")] + ReconciliationError(ApiError), + #[error("Bill manager single invoice failed: {0}")] + SingleInvoiceError(ApiError), + #[error("Bill manager cancel invoice failed: {0}")] + CancelInvoiceError(ApiError), #[error("Mpesa Express request/ STK push failed: {0}")] MpesaExpressRequestError(ApiError), #[error("Mpesa Transaction reversal failed: {0}")] diff --git a/src/lib.rs b/src/lib.rs index 3b0c16ad0..7a9e84138 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,9 @@ mod errors; pub mod services; pub use client::{Mpesa, MpesaResult}; -pub use constants::{CommandId, IdentifierTypes, ResponseType}; +pub use constants::{ + CommandId, IdentifierTypes, Invoice, InvoiceItem, ResponseType, SendRemindersTypes, +}; pub use environment::ApiEnvironment; pub use environment::Environment::{self, Production, Sandbox}; pub use errors::{ApiError, MpesaError}; diff --git a/src/services/bill_manager.rs b/src/services/bill_manager.rs new file mode 100644 index 000000000..6394e53eb --- /dev/null +++ b/src/services/bill_manager.rs @@ -0,0 +1,13 @@ +mod bulk_invoice; +mod cancel_invoice; +mod onboard; +mod onboard_modify; +mod reconciliation; +mod single_invoice; + +pub use bulk_invoice::{BulkInvoiceBuilder, BulkInvoiceResponse}; +pub use cancel_invoice::{CancelInvoiceBuilder, CancelInvoiceResponse}; +pub use onboard::{OnboardBuilder, OnboardResponse}; +pub use onboard_modify::{OnboardModifyBuilder, OnboardModifyResponse}; +pub use reconciliation::{ReconciliationBuilder, ReconciliationResponse}; +pub use single_invoice::{SingleInvoiceBuilder, SingleInvoiceResponse}; diff --git a/src/services/bill_manager/bulk_invoice.rs b/src/services/bill_manager/bulk_invoice.rs new file mode 100644 index 000000000..15618b9b1 --- /dev/null +++ b/src/services/bill_manager/bulk_invoice.rs @@ -0,0 +1,81 @@ +use crate::client::{Mpesa, MpesaResult}; +use crate::constants::Invoice; +use crate::environment::ApiEnvironment; +use crate::errors::MpesaError; +use serde::Deserialize; + +#[derive(Clone, Debug, Deserialize)] +pub struct BulkInvoiceResponse { + #[serde(rename(deserialize = "rescode"))] + pub response_code: String, + #[serde(rename(deserialize = "resmsg"))] + pub response_message: String, + #[serde(rename(deserialize = "Status_Message"))] + pub status_message: String, +} + +#[derive(Debug)] +pub struct BulkInvoiceBuilder<'mpesa, Env: ApiEnvironment> { + client: &'mpesa Mpesa, + invoices: Vec>, +} + +impl<'mpesa, Env: ApiEnvironment> BulkInvoiceBuilder<'mpesa, Env> { + /// Creates a new Bill Manager Bulk Invoice builder + pub fn new(client: &'mpesa Mpesa) -> BulkInvoiceBuilder<'mpesa, Env> { + BulkInvoiceBuilder { + client, + invoices: vec![], + } + } + + /// Adds a single `invoice` + pub fn invoice(mut self, invoice: Invoice<'mpesa>) -> BulkInvoiceBuilder<'mpesa, Env> { + self.invoices.push(invoice); + self + } + + /// Adds multiple `invoices` + pub fn invoices( + mut self, + mut invoices: Vec>, + ) -> BulkInvoiceBuilder<'mpesa, Env> { + self.invoices.append(&mut invoices); + self + } + + /// Bill Manager Bulk Invoice API + /// + /// Sends invoices to your customers in bulk + /// + /// # Errors + /// Returns an `MpesaError` on failure. + #[allow(clippy::unnecessary_lazy_evaluations)] + pub async fn send(self) -> MpesaResult { + let url = format!( + "{}/v1/billmanager-invoice/bulk-invoicing", + self.client.environment.base_url() + ); + + if self.invoices.is_empty() { + return Err(MpesaError::Message("invoices cannot be empty")); + } + + let response = self + .client + .http_client + .post(&url) + .bearer_auth(self.client.auth().await?) + .json(&self.invoices) + .send() + .await?; + + if response.status().is_success() { + let value = response.json().await?; + return Ok(value); + } + + let value = response.json().await?; + Err(MpesaError::BulkInvoiceError(value)) + } +} diff --git a/src/services/bill_manager/cancel_invoice.rs b/src/services/bill_manager/cancel_invoice.rs new file mode 100644 index 000000000..aa159dd30 --- /dev/null +++ b/src/services/bill_manager/cancel_invoice.rs @@ -0,0 +1,93 @@ +use crate::client::{Mpesa, MpesaResult}; +use crate::environment::ApiEnvironment; +use crate::errors::MpesaError; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct CancelInvoicePayload<'mpesa> { + external_reference: &'mpesa str, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct CancelInvoiceResponse { + #[serde(rename(deserialize = "rescode"))] + pub response_code: String, + #[serde(rename(deserialize = "resmsg"))] + pub response_message: String, + #[serde(rename(deserialize = "Status_Message"))] + pub status_message: String, +} + +#[derive(Debug)] +pub struct CancelInvoiceBuilder<'mpesa, Env: ApiEnvironment> { + client: &'mpesa Mpesa, + external_references: Vec>, +} + +impl<'mpesa, Env: ApiEnvironment> CancelInvoiceBuilder<'mpesa, Env> { + /// Creates a new Bill Manager Cancel invoice builder + pub fn new(client: &'mpesa Mpesa) -> CancelInvoiceBuilder<'mpesa, Env> { + CancelInvoiceBuilder { + client, + external_references: vec![], + } + } + + /// Adds an `external_reference` + pub fn external_reference( + mut self, + external_reference: &'mpesa str, + ) -> CancelInvoiceBuilder<'mpesa, Env> { + self.external_references + .push(CancelInvoicePayload { external_reference }); + self + } + + /// Adds `external_references` + pub fn external_references( + mut self, + external_references: Vec<&'mpesa str>, + ) -> CancelInvoiceBuilder<'mpesa, Env> { + self.external_references.append( + &mut external_references + .into_iter() + .map(|external_reference| CancelInvoicePayload { external_reference }) + .collect(), + ); + self + } + + /// Bill Manager Cancel Invoice API + /// + /// Cancels a list of invoices by their `external_reference` + /// + /// A successful request returns a `CancelInvoiceResponse` type + /// + /// # Errors + /// Returns an `MpesaError` on failure + #[allow(clippy::unnecessary_lazy_evaluations)] + pub async fn send(self) -> MpesaResult { + let url = format!( + "{}/v1/billmanager-invoice/cancel-single-invoice", + self.client.environment.base_url() + ); + + let response = self + .client + .http_client + .post(&url) + .bearer_auth(self.client.auth().await?) + .json(&self.external_references) + .send() + .await?; + + if response.status().is_success() { + let value = response.json().await?; + return Ok(value); + } + + let value = response.json().await?; + Err(MpesaError::CancelInvoiceError(value)) + } +} diff --git a/src/services/bill_manager/onboard.rs b/src/services/bill_manager/onboard.rs new file mode 100644 index 000000000..9bb6f20dd --- /dev/null +++ b/src/services/bill_manager/onboard.rs @@ -0,0 +1,168 @@ +use crate::client::{Mpesa, MpesaResult}; +use crate::constants::SendRemindersTypes; +use crate::environment::ApiEnvironment; +use crate::errors::MpesaError; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +/// Payload to opt you in as a biller to the bill manager features. +struct OnboardPayload<'mpesa> { + #[serde(rename(serialize = "callbackUrl"))] + callback_url: &'mpesa str, + #[serde(rename(serialize = "email"))] + email: &'mpesa str, + #[serde(rename(serialize = "logo"))] + logo: &'mpesa str, + #[serde(rename(serialize = "officialContact"))] + official_contact: &'mpesa str, + #[serde(rename(serialize = "sendReminders"))] + send_reminders: SendRemindersTypes, + #[serde(rename(serialize = "shortcode"))] + short_code: &'mpesa str, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct OnboardResponse { + #[serde(rename(deserialize = "app_key"))] + pub app_key: String, + #[serde(rename(deserialize = "rescode"))] + pub response_code: String, + #[serde(rename(deserialize = "resmsg"))] + pub response_message: String, +} + +#[derive(Debug)] +pub struct OnboardBuilder<'mpesa, Env: ApiEnvironment> { + client: &'mpesa Mpesa, + callback_url: Option<&'mpesa str>, + email: Option<&'mpesa str>, + logo: Option<&'mpesa str>, + official_contact: Option<&'mpesa str>, + send_reminders: Option, + short_code: Option<&'mpesa str>, +} + +impl<'mpesa, Env: ApiEnvironment> OnboardBuilder<'mpesa, Env> { + /// Creates a new Bill Manager Onboard builder + pub fn new(client: &'mpesa Mpesa) -> OnboardBuilder<'mpesa, Env> { + OnboardBuilder { + client, + callback_url: None, + email: None, + logo: None, + official_contact: None, + send_reminders: None, + short_code: None, + } + } + + /// Adds `callbackUrl`. + /// + /// # Errors + /// If 'callbackUrl` is not provided. + pub fn callback_url(mut self, callback_url: &'mpesa str) -> OnboardBuilder<'mpesa, Env> { + self.callback_url = Some(callback_url); + self + } + + /// Adds an `email` address to the request. + /// + /// # Errors + /// If `email` is not provided. + pub fn email(mut self, email: &'mpesa str) -> OnboardBuilder<'mpesa, Env> { + self.email = Some(email); + self + } + + /// Adds `logo`; a file with your organizions's logo. + /// + /// # Errors + /// If `logo` is not provided. + pub fn logo(mut self, logo: &'mpesa str) -> OnboardBuilder<'mpesa, Env> { + self.logo = Some(logo); + self + } + + /// Adds `officialContact` to the request; must be in the format `07XXXXXXXX` + /// + /// # Errors + /// If `officialContact` is invalid or not provided. + pub fn official_contact( + mut self, + official_contact: &'mpesa str, + ) -> OnboardBuilder<'mpesa, Env> { + self.official_contact = Some(official_contact); + self + } + + /// Adds `sendReminders`. Defaults to `SendRemindersTypes::Disable` if no value is explicitely passed. + /// + /// # Errors + /// If `sendReminders` is not valid. + pub fn send_reminders( + mut self, + send_reminders: SendRemindersTypes, + ) -> OnboardBuilder<'mpesa, Env> { + self.send_reminders = Some(send_reminders); + self + } + + /// Adds `ShortCode`; the 6 digit MPESA Till Number or PayBill Number + /// + /// # Errors + /// If Till or PayBill number is invalid or not provided + pub fn short_code(mut self, short_code: &'mpesa str) -> OnboardBuilder<'mpesa, Env> { + self.short_code = Some(short_code); + self + } + + /// # Bill Manager Onboarding API + /// + /// Opt in as a biller to mpesa's bill manager features. + /// + /// A successful request returns a `OnboardResponse` type + /// + /// # Errors + /// Returns an `MpesaError` on failure + #[allow(clippy::unnecessary_lazy_evaluations)] + pub async fn send(self) -> MpesaResult { + let url = format!( + "{}/v1/billmanager-invoice/optin", + self.client.environment.base_url() + ); + + let payload = OnboardPayload { + callback_url: self + .callback_url + .ok_or(MpesaError::Message("callback_url is required"))?, + email: self.email.ok_or(MpesaError::Message("email is required"))?, + logo: self.logo.ok_or(MpesaError::Message("logo is required"))?, + official_contact: self + .official_contact + .ok_or(MpesaError::Message("official_contact is required"))?, + send_reminders: self + .send_reminders + .unwrap_or_else(|| SendRemindersTypes::Disable), + short_code: self + .short_code + .ok_or(MpesaError::Message("short_code is required"))?, + }; + + let response = self + .client + .http_client + .post(&url) + .bearer_auth(self.client.auth().await?) + .json(&payload) + .send() + .await?; + + if response.status().is_success() { + let value = response.json().await?; + return Ok(value); + } + + let value = response.json().await?; + Err(MpesaError::OnboardError(value)) + } +} diff --git a/src/services/bill_manager/onboard_modify.rs b/src/services/bill_manager/onboard_modify.rs new file mode 100644 index 000000000..36ff7e069 --- /dev/null +++ b/src/services/bill_manager/onboard_modify.rs @@ -0,0 +1,152 @@ +use crate::client::{Mpesa, MpesaResult}; +use crate::constants::SendRemindersTypes; +use crate::environment::ApiEnvironment; +use crate::errors::MpesaError; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +/// Payload to modify opt-in details to the bill manager api. +struct OnboardModifyPayload<'mpesa> { + #[serde( + rename(serialize = "callbackUrl"), + skip_serializing_if = "Option::is_none" + )] + callback_url: Option<&'mpesa str>, + #[serde(rename(serialize = "email"), skip_serializing_if = "Option::is_none")] + email: Option<&'mpesa str>, + #[serde(rename(serialize = "logo"), skip_serializing_if = "Option::is_none")] + logo: Option<&'mpesa str>, + #[serde( + rename(serialize = "officialContact"), + skip_serializing_if = "Option::is_none" + )] + official_contact: Option<&'mpesa str>, + #[serde( + rename(serialize = "sendReminders"), + skip_serializing_if = "Option::is_none" + )] + send_reminders: Option, + #[serde( + rename(serialize = "shortcode"), + skip_serializing_if = "Option::is_none" + )] + short_code: Option<&'mpesa str>, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct OnboardModifyResponse { + #[serde(rename(deserialize = "rescode"))] + pub response_code: String, + #[serde(rename(deserialize = "resmsg"))] + pub response_message: String, +} + +#[derive(Debug)] +pub struct OnboardModifyBuilder<'mpesa, Env: ApiEnvironment> { + client: &'mpesa Mpesa, + callback_url: Option<&'mpesa str>, + email: Option<&'mpesa str>, + logo: Option<&'mpesa str>, + official_contact: Option<&'mpesa str>, + send_reminders: Option, + short_code: Option<&'mpesa str>, +} + +impl<'mpesa, Env: ApiEnvironment> OnboardModifyBuilder<'mpesa, Env> { + /// Creates a new Bill Manager Onboard Modify builder + pub fn new(client: &'mpesa Mpesa) -> OnboardModifyBuilder<'mpesa, Env> { + OnboardModifyBuilder { + client, + callback_url: None, + email: None, + logo: None, + official_contact: None, + send_reminders: None, + short_code: None, + } + } + + /// Adds `callbackUrl`. + pub fn callback_url(mut self, callback_url: &'mpesa str) -> OnboardModifyBuilder<'mpesa, Env> { + self.callback_url = Some(callback_url); + self + } + + /// Adds an `email` address to the request. + pub fn email(mut self, email: &'mpesa str) -> OnboardModifyBuilder<'mpesa, Env> { + self.email = Some(email); + self + } + + /// Adds `logo`; a file with your organizions's logo. + pub fn logo(mut self, logo: &'mpesa str) -> OnboardModifyBuilder<'mpesa, Env> { + self.logo = Some(logo); + self + } + + /// Adds `officialContact` to the request; must be in the format `07XXXXXXXX` + pub fn official_contact( + mut self, + official_contact: &'mpesa str, + ) -> OnboardModifyBuilder<'mpesa, Env> { + self.official_contact = Some(official_contact); + self + } + + /// Adds `sendReminders`. + pub fn send_reminders( + mut self, + send_reminders: SendRemindersTypes, + ) -> OnboardModifyBuilder<'mpesa, Env> { + self.send_reminders = Some(send_reminders); + self + } + + /// Adds `ShortCode`; the 6 digit MPESA Till Number or PayBill Number + pub fn short_code(mut self, short_code: &'mpesa str) -> OnboardModifyBuilder<'mpesa, Env> { + self.short_code = Some(short_code); + self + } + + /// # Bill Manager Onboarding Modify API + /// + /// Modifies opt-in details to the bill manager api. + /// + /// A successful request returns a `OnboardModifyResponse` type + /// + /// # Errors + /// Returns an `MpesaError` on failure + #[allow(clippy::unnecessary_lazy_evaluations)] + pub async fn send(self) -> MpesaResult { + let url = format!( + "{}/v1/billmanager-invoice/change-optin-details", + self.client.environment.base_url() + ); + + let payload = OnboardModifyPayload { + callback_url: self.callback_url, + email: self.email, + logo: self.logo, + official_contact: self.official_contact, + send_reminders: self.send_reminders, + short_code: self.short_code, + }; + + let response = self + .client + .http_client + .post(&url) + .bearer_auth(self.client.auth().await?) + .json(&payload) + .send() + .await?; + + if response.status().is_success() { + let value = response.json().await?; + return Ok(value); + } + + let value = response.json().await?; + Err(MpesaError::OnboardModifyError(value)) + } +} diff --git a/src/services/bill_manager/reconciliation.rs b/src/services/bill_manager/reconciliation.rs new file mode 100644 index 000000000..887d677ce --- /dev/null +++ b/src/services/bill_manager/reconciliation.rs @@ -0,0 +1,176 @@ +use crate::client::{Mpesa, MpesaResult}; +use crate::environment::ApiEnvironment; +use crate::errors::MpesaError; +use chrono::prelude::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct ReconciliationPayload<'mpesa> { + account_reference: &'mpesa str, + external_reference: &'mpesa str, + full_name: &'mpesa str, + invoice_name: &'mpesa str, + paid_amount: f64, + payment_date: DateTime, + phone_number: &'mpesa str, + transaction_id: &'mpesa str, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct ReconciliationResponse { + #[serde(rename(deserialize = "rescode"))] + pub response_code: String, + #[serde(rename(deserialize = "resmsg"))] + pub response_message: String, +} + +#[derive(Debug)] +pub struct ReconciliationBuilder<'mpesa, Env: ApiEnvironment> { + client: &'mpesa Mpesa, + account_reference: Option<&'mpesa str>, + external_reference: Option<&'mpesa str>, + full_name: Option<&'mpesa str>, + invoice_name: Option<&'mpesa str>, + paid_amount: Option, + payment_date: Option>, + phone_number: Option<&'mpesa str>, + transaction_id: Option<&'mpesa str>, +} + +impl<'mpesa, Env: ApiEnvironment> ReconciliationBuilder<'mpesa, Env> { + /// Creates a new Bill Manager Reconciliation Builder + pub fn new(client: &'mpesa Mpesa) -> ReconciliationBuilder<'mpesa, Env> { + ReconciliationBuilder { + client, + account_reference: None, + external_reference: None, + full_name: None, + invoice_name: None, + paid_amount: None, + payment_date: None, + phone_number: None, + transaction_id: None, + } + } + + /// Adds `account_reference` + pub fn account_reference( + mut self, + account_reference: &'mpesa str, + ) -> ReconciliationBuilder<'mpesa, Env> { + self.account_reference = Some(account_reference); + self + } + + /// Adds `external_reference` + pub fn external_reference( + mut self, + external_reference: &'mpesa str, + ) -> ReconciliationBuilder<'mpesa, Env> { + self.external_reference = Some(external_reference); + self + } + + /// Adds `full_name` + pub fn full_name(mut self, full_name: &'mpesa str) -> ReconciliationBuilder<'mpesa, Env> { + self.full_name = Some(full_name); + self + } + + /// Adds `invoice_name` + pub fn invoice_name(mut self, invoice_name: &'mpesa str) -> ReconciliationBuilder<'mpesa, Env> { + self.invoice_name = Some(invoice_name); + self + } + + /// Adds `paid_amount` + pub fn paid_amount(mut self, paid_amount: f64) -> ReconciliationBuilder<'mpesa, Env> { + self.paid_amount = Some(paid_amount); + self + } + + /// Adds `payment_date` + pub fn payment_date( + mut self, + payment_date: DateTime, + ) -> ReconciliationBuilder<'mpesa, Env> { + self.payment_date = Some(payment_date); + self + } + + /// Adds `phone_number` + pub fn phone_number(mut self, phone_number: &'mpesa str) -> ReconciliationBuilder<'mpesa, Env> { + self.phone_number = Some(phone_number); + self + } + + /// Adds `transaction_id` + pub fn transaction_id( + mut self, + transaction_id: &'mpesa str, + ) -> ReconciliationBuilder<'mpesa, Env> { + self.transaction_id = Some(transaction_id); + self + } + + /// Bill Manager Reconciliation API + /// + /// Enables your customers to receive e-receipts for payments made to your paybill account + /// + /// A successful request returns a `ReconciliationResponse` type. + /// + /// # Errors + /// Returns an `MpesaError` on failure. + #[allow(clippy::unnecessary_lazy_evaluations)] + pub async fn send(self) -> MpesaResult { + let url = format!( + "{}/v1/billmanager-invoice/reconciliation", + self.client.environment.base_url() + ); + + let payload = ReconciliationPayload { + account_reference: self + .account_reference + .ok_or(MpesaError::Message("account_reference is required"))?, + external_reference: self + .external_reference + .ok_or(MpesaError::Message("external_reference is required"))?, + full_name: self + .full_name + .ok_or(MpesaError::Message("full_name is required"))?, + invoice_name: self + .invoice_name + .ok_or(MpesaError::Message("invoice_name is required"))?, + paid_amount: self + .paid_amount + .ok_or(MpesaError::Message("paid_amount is required"))?, + payment_date: self + .payment_date + .ok_or(MpesaError::Message("payment_date is required"))?, + phone_number: self + .phone_number + .ok_or(MpesaError::Message("phone_number is required"))?, + transaction_id: self + .transaction_id + .ok_or(MpesaError::Message("transaction_id is required"))?, + }; + + let response = self + .client + .http_client + .post(&url) + .bearer_auth(self.client.auth().await?) + .json(&payload) + .send() + .await?; + + if response.status().is_success() { + let value = response.json().await?; + return Ok(value); + } + + let value = response.json().await?; + Err(MpesaError::ReconciliationError(value)) + } +} diff --git a/src/services/bill_manager/single_invoice.rs b/src/services/bill_manager/single_invoice.rs new file mode 100644 index 000000000..45fa6fdb9 --- /dev/null +++ b/src/services/bill_manager/single_invoice.rs @@ -0,0 +1,181 @@ +use crate::client::{Mpesa, MpesaResult}; +use crate::constants::{Invoice, InvoiceItem}; +use crate::environment::ApiEnvironment; +use crate::errors::MpesaError; +use chrono::prelude::{DateTime, Utc}; +use serde::Deserialize; + +#[derive(Clone, Debug, Deserialize)] +pub struct SingleInvoiceResponse { + #[serde(rename(deserialize = "rescode"))] + pub response_code: String, + #[serde(rename(deserialize = "resmsg"))] + pub response_message: String, + #[serde(rename(deserialize = "Status_Message"))] + pub status_message: String, +} + +#[derive(Debug)] +pub struct SingleInvoiceBuilder<'mpesa, Env: ApiEnvironment> { + client: &'mpesa Mpesa, + amount: Option, + account_reference: Option<&'mpesa str>, + billed_full_name: Option<&'mpesa str>, + billed_period: Option<&'mpesa str>, + billed_phone_number: Option<&'mpesa str>, + due_date: Option>, + external_reference: Option<&'mpesa str>, + invoice_items: Option>>, + invoice_name: Option<&'mpesa str>, +} + +impl<'mpesa, Env: ApiEnvironment> SingleInvoiceBuilder<'mpesa, Env> { + /// Creates a new Bill Manager Single Invoice Builder + pub fn new(client: &'mpesa Mpesa) -> SingleInvoiceBuilder<'mpesa, Env> { + SingleInvoiceBuilder { + client, + amount: None, + account_reference: None, + billed_full_name: None, + billed_period: None, + billed_phone_number: None, + due_date: None, + external_reference: None, + invoice_items: None, + invoice_name: None, + } + } + + /// Adds `amount` + pub fn amount(mut self, amount: f64) -> SingleInvoiceBuilder<'mpesa, Env> { + self.amount = Some(amount); + self + } + + /// Adds `account_reference` + pub fn account_reference( + mut self, + account_refernce: &'mpesa str, + ) -> SingleInvoiceBuilder<'mpesa, Env> { + self.account_reference = Some(account_refernce); + self + } + + /// Adds `billed_full_name` + pub fn billed_full_name( + mut self, + billed_full_name: &'mpesa str, + ) -> SingleInvoiceBuilder<'mpesa, Env> { + self.billed_full_name = Some(billed_full_name); + self + } + + /// Adds `billed_period`; must be in the format `"Month Year"` e.g. `"March 2023"` + pub fn billed_period( + mut self, + billed_period: &'mpesa str, + ) -> SingleInvoiceBuilder<'mpesa, Env> { + self.billed_period = Some(billed_period); + self + } + + /// Adds `billed_phone_number`; must be in the format `0722XXXXXX` + pub fn billed_phone_number( + mut self, + billed_phone_number: &'mpesa str, + ) -> SingleInvoiceBuilder<'mpesa, Env> { + self.billed_phone_number = Some(billed_phone_number); + self + } + + /// Adds `due_date` + pub fn due_date(mut self, due_date: DateTime) -> SingleInvoiceBuilder<'mpesa, Env> { + self.due_date = Some(due_date); + self + } + + /// Adds `external_reference` + pub fn external_reference( + mut self, + external_reference: &'mpesa str, + ) -> SingleInvoiceBuilder<'mpesa, Env> { + self.external_reference = Some(external_reference); + self + } + + /// Adds `invoice_items` + pub fn invoice_items( + mut self, + invoice_items: Vec>, + ) -> SingleInvoiceBuilder<'mpesa, Env> { + self.invoice_items = Some(invoice_items); + self + } + + /// Adds `invoice_name` + pub fn invoice_name(mut self, invoice_name: &'mpesa str) -> SingleInvoiceBuilder<'mpesa, Env> { + self.invoice_name = Some(invoice_name); + self + } + + /// Bill Manager Single Invoice API + /// + /// Creates and sends invoices to your customers + /// + /// A successful request returns a `SingleInvoiceResponse` type + /// + /// # Errors + /// Returns an `MpesaError` on failure + #[allow(clippy::unnecessary_lazy_evaluations)] + pub async fn send(self) -> MpesaResult { + let url = format!( + "{}/v1/billmanager-invoice/single-invoicing", + self.client.environment.base_url() + ); + + let payload = Invoice { + amount: self + .amount + .ok_or(MpesaError::Message("amount is required"))?, + account_reference: self + .account_reference + .ok_or(MpesaError::Message("account_reference is required"))?, + billed_full_name: self + .billed_full_name + .ok_or(MpesaError::Message("billed_full_name is required"))?, + billed_period: self + .billed_period + .ok_or(MpesaError::Message("billed_period is required"))?, + billed_phone_number: self + .billed_phone_number + .ok_or(MpesaError::Message("billed_phone_number is required"))?, + due_date: self + .due_date + .ok_or(MpesaError::Message("due_date is required"))?, + external_reference: self + .external_reference + .ok_or(MpesaError::Message("external_reference is required"))?, + invoice_items: self.invoice_items, + invoice_name: self + .invoice_name + .ok_or(MpesaError::Message("invoice_name is required"))?, + }; + + let response = self + .client + .http_client + .post(&url) + .bearer_auth(self.client.auth().await?) + .json(&payload) + .send() + .await?; + + if response.status().is_success() { + let value = response.json().await?; + return Ok(value); + } + + let value = response.json().await?; + Err(MpesaError::SingleInvoiceError(value)) + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs index 3354f35ac..3ea340f88 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -12,10 +12,12 @@ //! 5. [C2B Simulate](https://developer.safaricom.co.ke/docs#account-balance-api) //! 6. [Mpesa Express/ STK Push](https://developer.safaricom.co.ke/docs#lipa-na-m-pesa-online-payment) //! 7. [Transaction Reversal](https://developer.safaricom.co.ke/docs#reversal) +//! 8. [Bill Manager](https://developer.safaricom.co.ke/APIs/BillManager) mod account_balance; mod b2b; mod b2c; +mod bill_manager; mod c2b_register; mod c2b_simulate; mod express_request; @@ -28,6 +30,8 @@ pub use account_balance::{AccountBalanceBuilder, AccountBalanceResponse}; pub use b2b::{B2bBuilder, B2bResponse}; #[cfg(feature = "b2c")] pub use b2c::{B2cBuilder, B2cResponse}; +#[cfg(feature = "bill_manager")] +pub use bill_manager::*; #[cfg(feature = "c2b_register")] pub use c2b_register::{C2bRegisterBuilder, C2bRegisterResponse}; #[cfg(feature = "c2b_simulate")] diff --git a/tests/mpesa-rust/bill_manager_test.rs b/tests/mpesa-rust/bill_manager_test.rs new file mode 100644 index 000000000..8bc018adf --- /dev/null +++ b/tests/mpesa-rust/bill_manager_test.rs @@ -0,0 +1,6 @@ +mod bulk_invoice_test; +mod cancel_invoice_test; +mod onboard_modify_test; +mod onboard_test; +mod reconciliation_test; +mod single_invoice_test; diff --git a/tests/mpesa-rust/bill_manager_test/bulk_invoice_test.rs b/tests/mpesa-rust/bill_manager_test/bulk_invoice_test.rs new file mode 100644 index 000000000..e80b1c21b --- /dev/null +++ b/tests/mpesa-rust/bill_manager_test/bulk_invoice_test.rs @@ -0,0 +1,65 @@ +use crate::get_mpesa_client; +use chrono::prelude::Utc; +use mpesa::{Invoice, InvoiceItem, MpesaError}; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; + +fn sample_response() -> ResponseTemplate { + let sample_response = json!({ + "rescode": "200", + "resmsg": "Success", + "Status_Message": "Invoice sent successfully" + }); + ResponseTemplate::new(200).set_body_json(sample_response) +} + +#[tokio::test] +async fn bulk_invoice_success() { + let (client, server) = get_mpesa_client!(); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/bulk-invoicing")) + .respond_with(sample_response()) + .expect(1) + .mount(&server) + .await; + let response = client + .bulk_invoice() + .invoices(vec![Invoice { + amount: 1000.0, + account_reference: "John Doe", + billed_full_name: "John Doe", + billed_period: "August 2021", + billed_phone_number: "0712345678", + due_date: Utc::now(), + external_reference: "INV2345", + invoice_items: Some(vec![InvoiceItem { + amount: 1000.0, + item_name: "An item", + }]), + invoice_name: "Invoice 001", + }]) + .send() + .await + .unwrap(); + assert_eq!(response.response_code, "200"); + assert_eq!(response.response_message, "Success"); + assert_eq!(response.status_message, "Invoice sent successfully"); +} + +#[tokio::test] +async fn bulk_invoice_fails_if_invoices_is_empty() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/bulk-invoicing")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client.bulk_invoice().send().await { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "invoices cannot be empty"); + } else { + panic!("Expected Error") + } +} diff --git a/tests/mpesa-rust/bill_manager_test/cancel_invoice_test.rs b/tests/mpesa-rust/bill_manager_test/cancel_invoice_test.rs new file mode 100644 index 000000000..64d361055 --- /dev/null +++ b/tests/mpesa-rust/bill_manager_test/cancel_invoice_test.rs @@ -0,0 +1,34 @@ +use crate::get_mpesa_client; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; + +fn sample_response() -> ResponseTemplate { + let sample_response = json!({ + "rescode": "200", + "resmsg": "Success", + "Status_Message": "Invoice cancelled successfully" + }); + ResponseTemplate::new(200).set_body_json(sample_response) +} + +#[tokio::test] +async fn cancel_invoice_success() { + let (client, server) = get_mpesa_client!(); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/cancel-single-invoice")) + .respond_with(sample_response()) + .expect(1) + .mount(&server) + .await; + let response = client + .cancel_invoice() + .external_references(vec!["9KLSS011"]) + .external_reference("87TH7JK1") + .send() + .await + .unwrap(); + assert_eq!(response.response_code, "200"); + assert_eq!(response.response_message, "Success"); + assert_eq!(response.status_message, "Invoice cancelled successfully"); +} diff --git a/tests/mpesa-rust/bill_manager_test/onboard_modify_test.rs b/tests/mpesa-rust/bill_manager_test/onboard_modify_test.rs new file mode 100644 index 000000000..dcf629dcc --- /dev/null +++ b/tests/mpesa-rust/bill_manager_test/onboard_modify_test.rs @@ -0,0 +1,33 @@ +use crate::get_mpesa_client; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; + +fn sample_response() -> ResponseTemplate { + let sample_response_body = json!({ + "rescode": "200", + "resmsg": "Biller updated successfully" + }); + ResponseTemplate::new(200).set_body_json(sample_response_body) +} + +#[tokio::test] +async fn onboard_modify_success() { + let (client, server) = get_mpesa_client!(); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/change-optin-details")) + .respond_with(sample_response()) + .expect(1) + .mount(&server) + .await; + let response = client + .onboard_modify() + .callback_url("https://testdomain.com/true") + .email("email@test.com") + .logo("https://file.domain/file.png") + .send() + .await + .unwrap(); + assert_eq!(response.response_code, "200"); + assert_eq!(response.response_message, "Biller updated successfully"); +} diff --git a/tests/mpesa-rust/bill_manager_test/onboard_test.rs b/tests/mpesa-rust/bill_manager_test/onboard_test.rs new file mode 100644 index 000000000..49684dd71 --- /dev/null +++ b/tests/mpesa-rust/bill_manager_test/onboard_test.rs @@ -0,0 +1,163 @@ +use crate::get_mpesa_client; +use mpesa::MpesaError; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; + +fn sample_response() -> ResponseTemplate { + let sample_response_body = json!({ + "app_key": "kfpB9X4o0H", + "rescode": "200", + "resmsg": "Success" + }); + ResponseTemplate::new(200).set_body_json(sample_response_body) +} + +#[tokio::test] +async fn onboard_success() { + let (client, server) = get_mpesa_client!(); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/optin")) + .respond_with(sample_response()) + .expect(1) + .mount(&server) + .await; + let response = client + .onboard() + .callback_url("https://testdomain.com/true") + .email("email@test.com") + .logo("https://file.domain/file.png") + .official_contact("0712345678") + .short_code("600496") + .send() + .await + .unwrap(); + assert_eq!(response.app_key, "kfpB9X4o0H"); + assert_eq!(response.response_code, "200"); + assert_eq!(response.response_message, "Success"); +} + +#[tokio::test] +async fn onboard_fails_if_no_callback_url_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/optin")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .onboard() + .email("email@test.com") + .logo("https://file.domain/file.png") + .official_contact("0712345678") + .short_code("600496") + .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 onboard_fails_if_no_email_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/optin")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .onboard() + .callback_url("https://testdomain.com/true") + .logo("https://file.domain/file.png") + .official_contact("0712345678") + .short_code("600496") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "email is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn onboard_fails_if_no_logo_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/optin")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .onboard() + .callback_url("https://testdomain.com/true") + .email("email@test.com") + .official_contact("0712345678") + .short_code("600496") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "logo is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn onboard_fails_if_no_official_contact_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/optin")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .onboard() + .callback_url("https://testdomain.com/true") + .email("email@test.com") + .logo("https://file.domain/file.png") + .short_code("600496") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "official_contact is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn onboard_fails_if_short_code_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/optin")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .onboard() + .callback_url("https://testdomain.com/true") + .email("email@test.com") + .logo("https://file.domain/file.png") + .official_contact("0712345678") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "short_code is required"); + } else { + panic!("Expected error") + } +} diff --git a/tests/mpesa-rust/bill_manager_test/reconciliation_test.rs b/tests/mpesa-rust/bill_manager_test/reconciliation_test.rs new file mode 100644 index 000000000..8c9afbb86 --- /dev/null +++ b/tests/mpesa-rust/bill_manager_test/reconciliation_test.rs @@ -0,0 +1,264 @@ +use crate::get_mpesa_client; +use chrono::prelude::Utc; +use mpesa::MpesaError; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; + +fn sample_response() -> ResponseTemplate { + let sample_response = json!({ + "rescode": "200", + "resmsg": "Success", + }); + ResponseTemplate::new(200).set_body_json(sample_response) +} + +#[tokio::test] +async fn reconciliation_success() { + let (client, server) = get_mpesa_client!(); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/reconciliation")) + .respond_with(sample_response()) + .expect(1) + .mount(&server) + .await; + let response = client + .reconciliation() + .account_reference("John Doe") + .external_reference("INV2345") + .full_name("John Doe") + .invoice_name("Invoice 001") + .paid_amount(1000.0) + .payment_date(Utc::now()) + .phone_number("0712345678") + .transaction_id("TRANSACTION_ID") + .send() + .await + .unwrap(); + assert_eq!(response.response_code, "200"); + assert_eq!(response.response_message, "Success"); +} + +#[tokio::test] +async fn reconciliation_fails_if_no_account_reference_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/reconciliation")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .reconciliation() + .external_reference("INV2345") + .full_name("John Doe") + .invoice_name("Invoice 001") + .paid_amount(1000.0) + .payment_date(Utc::now()) + .phone_number("0712345678") + .transaction_id("TRANSACTION_ID") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "account_reference is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn reconciliation_fails_if_no_external_reference_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/reconciliation")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .reconciliation() + .account_reference("John Doe") + .full_name("John Doe") + .invoice_name("Invoice 001") + .paid_amount(1000.0) + .payment_date(Utc::now()) + .phone_number("0712345678") + .transaction_id("TRANSACTION_ID") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "external_reference is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn reconciliation_fails_if_no_full_name_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/reconciliation")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .reconciliation() + .account_reference("John Doe") + .external_reference("INV2345") + .invoice_name("Invoice 001") + .paid_amount(1000.0) + .payment_date(Utc::now()) + .phone_number("0712345678") + .transaction_id("TRANSACTION_ID") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "full_name is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn reconciliation_fails_if_no_invoice_name_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/reconciliation")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .reconciliation() + .account_reference("John Doe") + .external_reference("INV2345") + .full_name("John Doe") + .paid_amount(1000.0) + .payment_date(Utc::now()) + .phone_number("0712345678") + .transaction_id("TRANSACTION_ID") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "invoice_name is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn reconciliation_fails_if_no_paid_amount_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/reconciliation")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .reconciliation() + .account_reference("John Doe") + .external_reference("INV2345") + .full_name("John Doe") + .invoice_name("Invoice 001") + .payment_date(Utc::now()) + .phone_number("0712345678") + .transaction_id("TRANSACTION_ID") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "paid_amount is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn reconciliation_fails_if_no_payment_date_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/reconciliation")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .reconciliation() + .account_reference("John Doe") + .external_reference("INV2345") + .full_name("John Doe") + .invoice_name("Invoice 001") + .paid_amount(1000.0) + .phone_number("0712345678") + .transaction_id("TRANSACTION_ID") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "payment_date is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn reconciliation_fails_if_no_phone_number_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/reconciliation")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .reconciliation() + .account_reference("John Doe") + .external_reference("INV2345") + .full_name("John Doe") + .invoice_name("Invoice 001") + .paid_amount(1000.0) + .payment_date(Utc::now()) + .transaction_id("TRANSACTION_ID") + .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") + } +} + +#[tokio::test] +async fn reconciliation_fails_if_no_transaction_id_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/reconciliation")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .reconciliation() + .account_reference("John Doe") + .external_reference("INV2345") + .full_name("John Doe") + .invoice_name("Invoice 001") + .paid_amount(1000.0) + .payment_date(Utc::now()) + .phone_number("0712345678") + .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") + } +} diff --git a/tests/mpesa-rust/bill_manager_test/single_invoice_test.rs b/tests/mpesa-rust/bill_manager_test/single_invoice_test.rs new file mode 100644 index 000000000..012c6020f --- /dev/null +++ b/tests/mpesa-rust/bill_manager_test/single_invoice_test.rs @@ -0,0 +1,270 @@ +use crate::get_mpesa_client; +use chrono::prelude::Utc; +use mpesa::{InvoiceItem, MpesaError}; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; + +fn sample_response() -> ResponseTemplate { + let sample_response = json!({ + "rescode": "200", + "resmsg": "Success", + "Status_Message": "Invoice sent successfully" + }); + ResponseTemplate::new(200).set_body_json(sample_response) +} + +#[tokio::test] +async fn single_invoice_success() { + let (client, server) = get_mpesa_client!(); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/single-invoicing")) + .respond_with(sample_response()) + .expect(1) + .mount(&server) + .await; + let response = client + .single_invoice() + .amount(1000.0) + .account_reference("John Doe") + .billed_full_name("John Doe") + .billed_period("August 2021") + .billed_phone_number("0712345678") + .due_date(Utc::now()) + .external_reference("INV2345") + .invoice_items(vec![InvoiceItem { + amount: 1000.0, + item_name: "An item", + }]) + .invoice_name("Invoice 001") + .send() + .await + .unwrap(); + assert_eq!(response.response_code, "200"); + assert_eq!(response.response_message, "Success"); + assert_eq!(response.status_message, "Invoice sent successfully"); +} + +#[tokio::test] +async fn single_invoice_fails_if_no_amount_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/single-invoicing")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .single_invoice() + .account_reference("John Doe") + .billed_full_name("John Doe") + .billed_period("August 2021") + .billed_phone_number("0712345678") + .due_date(Utc::now()) + .external_reference("INV2345") + .invoice_name("Invoice 001") + .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 single_invoice_fails_if_no_account_reference_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/single-invoicing")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .single_invoice() + .amount(1000.0) + .billed_full_name("John Doe") + .billed_period("August 2021") + .billed_phone_number("0712345678") + .due_date(Utc::now()) + .external_reference("INV2345") + .invoice_name("Invoice 001") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "account_reference is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn single_invoice_fails_if_no_billed_full_name_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/single-invoicing")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .single_invoice() + .amount(1000.0) + .account_reference("John Doe") + .billed_period("August 2021") + .billed_phone_number("0712345678") + .due_date(Utc::now()) + .external_reference("INV2345") + .invoice_name("Invoice 001") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "billed_full_name is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn single_invoice_fails_if_no_billed_period_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/single-invoicing")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .single_invoice() + .amount(1000.0) + .account_reference("John Doe") + .billed_full_name("John Doe") + .billed_phone_number("0712345678") + .due_date(Utc::now()) + .external_reference("INV2345") + .invoice_name("Invoice 001") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "billed_period is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn single_invoice_fails_if_no_billed_phone_number_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/single-invoicing")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .single_invoice() + .amount(1000.0) + .account_reference("John Doe") + .billed_full_name("John Doe") + .billed_period("August 2021") + .due_date(Utc::now()) + .external_reference("INV2345") + .invoice_name("Invoice 001") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "billed_phone_number is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn single_invoice_fails_if_no_due_date_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/single-invoicing")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .single_invoice() + .amount(1000.0) + .account_reference("John Doe") + .billed_full_name("John Doe") + .billed_period("August 2021") + .billed_phone_number("0712345678") + .external_reference("INV2345") + .invoice_name("Invoice 001") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "due_date is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn single_invoice_fails_if_no_external_reference_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/single-invoicing")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .single_invoice() + .amount(1000.0) + .account_reference("John Doe") + .billed_full_name("John Doe") + .billed_period("August 2021") + .billed_phone_number("0712345678") + .due_date(Utc::now()) + .invoice_name("Invoice 001") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "external_reference is required"); + } else { + panic!("Expected error") + } +} + +#[tokio::test] +async fn single_invoice_fails_if_no_invoice_name_is_provided() { + let (client, server) = get_mpesa_client!(expected_auth_requests = 0); + Mock::given(method("POST")) + .and(path("/v1/billmanager-invoice/single-invoicing")) + .respond_with(sample_response()) + .expect(0) + .mount(&server) + .await; + if let Err(e) = client + .single_invoice() + .amount(1000.0) + .account_reference("John Doe") + .billed_full_name("John Doe") + .billed_period("August 2021") + .billed_phone_number("0712345678") + .due_date(Utc::now()) + .external_reference("INV2345") + .send() + .await + { + let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + assert_eq!(msg, "invoice_name is required"); + } else { + panic!("Expected error") + } +} diff --git a/tests/mpesa-rust/main.rs b/tests/mpesa-rust/main.rs index 78824f33f..644d95585 100644 --- a/tests/mpesa-rust/main.rs +++ b/tests/mpesa-rust/main.rs @@ -5,6 +5,8 @@ mod b2b_test; #[cfg(test)] mod b2c_test; #[cfg(test)] +mod bill_manager_test; +#[cfg(test)] mod c2b_register_test; #[cfg(test)] mod c2b_simulate_test; From 71aa513637c09d35efc712ecee5233b04b3d7426 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Sun, 20 Aug 2023 09:40:25 +0300 Subject: [PATCH 125/140] Temporarily ignore readme code snippets from getting compiled (#72) --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2cd3dce05..0e1feb48d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ mpesa = { git = "https://github.com/collinsmuriuki/mpesa-rust", default_features In your lib or binary crate: -```rust,no_run +```rust,ignore use mpesa::Mpesa; ``` @@ -52,7 +52,7 @@ read the docs [here](https://developer.safaricom.co.ke/docs?javascript#going-liv These are the following ways you can instantiate `Mpesa`: -```rust,no_run +```rust,ignore use mpesa::{Mpesa, Environment}; let client = Mpesa::new( @@ -67,7 +67,7 @@ assert!(client.is_connected().await) Since the `Environment` enum implements `FromStr` and `TryFrom` for `String` and `&str` types, you can call `Environment::from_str` or `Environment::try_from` to create an `Environment` type. This is ideal if the environment values are stored in a `.env` or any other configuration file: -```rust,no_run +```rust,ignore use mpesa::{Mpesa, Environment}; use std::str::FromStr; use std::convert::TryFrom; @@ -101,7 +101,7 @@ This trait allows you to create your own type to pass to the `environment` param See the example below (and [here](./src/environment.rs) so see how the trait is implemented for the `Environment` enum): -```rust,no_run +```rust,ignore use mpesa::{Mpesa, ApiEnvironment}; use std::str::FromStr; use std::convert::TryFrom; @@ -132,7 +132,7 @@ let client: Mpesa = Mpesa::new( If you intend to use in production, you will need to call a the `set_initiator_password` method from `Mpesa` after initially creating the client. Here you provide your initiator password, which overrides the default password used in sandbox `"Safcom496!"`: -```rust,no_run +```rust,ignore use mpesa::Mpesa; let client = Mpesa::new( @@ -152,7 +152,7 @@ The following services are currently available from the `Mpesa` client as method - B2C -```rust,no_run +```rust,ignore let response = client .b2c("testapi496") .party_a("600496") @@ -167,7 +167,7 @@ assert!(response.is_ok()) - B2B -```rust,no_run +```rust,ignore let response = client .b2b("testapi496") .party_a("600496") @@ -183,7 +183,7 @@ assert!(response.is_ok()) - C2B Register -```rust,no_run +```rust,ignore let response = client .c2b_register() .short_code("600496") @@ -196,7 +196,7 @@ assert!(response.is_ok()) - C2B Simulate -```rust,no_run +```rust,ignore let response = client .c2b_simulate() .short_code("600496") @@ -209,7 +209,7 @@ assert!(response.is_ok()) - Account Balance -```rust,no_run +```rust,ignore let response = client .account_balance("testapi496") .result_url("https://testdomain.com/ok") @@ -222,7 +222,7 @@ assert!(response.is_ok()) - Mpesa Express Request / STK push / Lipa na M-PESA online -```rust,no_run +```rust,ignore let response = client .express_request("174379") .phone_number("254708374149") @@ -235,7 +235,7 @@ assert!(response.is_ok()) - Transaction Reversal: -```rust,no_run +```rust,ignore let response = client .transaction_reversal("testapi496") .result_url("https://testdomain.com/ok") @@ -251,7 +251,7 @@ assert!(response.is_ok()) - Transaction Status -```rust,no_run +```rust,ignore let response = client .transaction_status("testapi496") .result_url("https://testdomain.com/ok") From 41f5c47a6269e7b3cda0dba57b1e3516efa1cffc Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Sat, 26 Aug 2023 17:50:54 +0300 Subject: [PATCH 126/140] Make amount args generic; reformat tests --- README.md | 1 + src/client.rs | 2 +- src/services/bill_manager/reconciliation.rs | 7 ++-- src/services/bill_manager/single_invoice.rs | 7 ++-- tests/mpesa-rust/account_balance_test.rs | 12 +++++-- tests/mpesa-rust/b2b_test.rs | 12 +++++-- tests/mpesa-rust/b2c_test.rs | 20 +++++++++--- .../bill_manager_test/bulk_invoice_test.rs | 4 ++- .../bill_manager_test/onboard_test.rs | 20 +++++++++--- .../bill_manager_test/reconciliation_test.rs | 32 ++++++++++++++----- .../bill_manager_test/single_invoice_test.rs | 32 ++++++++++++++----- tests/mpesa-rust/c2b_register_test.rs | 12 +++++-- tests/mpesa-rust/c2b_simulate_test.rs | 16 +++++++--- tests/mpesa-rust/stk_push_test.rs | 12 +++++-- tests/mpesa-rust/transaction_reversal_test.rs | 20 +++++++++--- tests/mpesa-rust/transaction_status_test.rs | 16 +++++++--- 16 files changed, 168 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 0e1feb48d..f2eddf418 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Optionally, you can disable default-features, which is basically the entire suit - `express_request` - `transaction_reversal` - `transaction_status` +- `bill_manager` Example: diff --git a/src/client.rs b/src/client.rs index 833b99039..0e7b7c37a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -63,7 +63,7 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { /// If `None`, the default password is `"Safcom496!"` pub(crate) fn initiator_password(&'mpesa self) -> String { let Some(p) = &*self.initiator_password.borrow() else { - return DEFAULT_INITIATOR_PASSWORD.to_owned() + return DEFAULT_INITIATOR_PASSWORD.to_owned(); }; p.to_owned() } diff --git a/src/services/bill_manager/reconciliation.rs b/src/services/bill_manager/reconciliation.rs index 887d677ce..baf470b6f 100644 --- a/src/services/bill_manager/reconciliation.rs +++ b/src/services/bill_manager/reconciliation.rs @@ -85,8 +85,11 @@ impl<'mpesa, Env: ApiEnvironment> ReconciliationBuilder<'mpesa, Env> { } /// Adds `paid_amount` - pub fn paid_amount(mut self, paid_amount: f64) -> ReconciliationBuilder<'mpesa, Env> { - self.paid_amount = Some(paid_amount); + pub fn paid_amount>( + mut self, + paid_amount: Number, + ) -> ReconciliationBuilder<'mpesa, Env> { + self.paid_amount = Some(paid_amount.into()); self } diff --git a/src/services/bill_manager/single_invoice.rs b/src/services/bill_manager/single_invoice.rs index 45fa6fdb9..5ce438bc0 100644 --- a/src/services/bill_manager/single_invoice.rs +++ b/src/services/bill_manager/single_invoice.rs @@ -47,8 +47,11 @@ impl<'mpesa, Env: ApiEnvironment> SingleInvoiceBuilder<'mpesa, Env> { } /// Adds `amount` - pub fn amount(mut self, amount: f64) -> SingleInvoiceBuilder<'mpesa, Env> { - self.amount = Some(amount); + pub fn amount>( + mut self, + amount: Number, + ) -> SingleInvoiceBuilder<'mpesa, Env> { + self.amount = Some(amount.into()); self } diff --git a/tests/mpesa-rust/account_balance_test.rs b/tests/mpesa-rust/account_balance_test.rs index ad9fc37cd..034311f09 100644 --- a/tests/mpesa-rust/account_balance_test.rs +++ b/tests/mpesa-rust/account_balance_test.rs @@ -58,7 +58,9 @@ async fn account_balance_fails_if_party_a_is_not_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "party_a is required") } else { panic!("Expected error"); @@ -87,7 +89,9 @@ async fn account_balance_fails_if_result_url_is_not_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "result_url is required") } else { panic!("Expected error"); @@ -116,7 +120,9 @@ async fn account_balance_fails_if_queue_timeout_url_is_not_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "queue_timeout_url is required") } else { panic!("Expected error"); diff --git a/tests/mpesa-rust/b2b_test.rs b/tests/mpesa-rust/b2b_test.rs index 03f183750..efd0d8a51 100644 --- a/tests/mpesa-rust/b2b_test.rs +++ b/tests/mpesa-rust/b2b_test.rs @@ -65,7 +65,9 @@ async fn b2b_fails_if_no_amount_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "amount is required"); } else { panic!("Expected error"); @@ -97,7 +99,9 @@ async fn b2b_fails_if_no_party_a_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "party_a is required"); } else { panic!("Expected error"); @@ -129,7 +133,9 @@ async fn b2b_fails_if_no_party_b_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "party_b is required"); } else { panic!("Expected error"); diff --git a/tests/mpesa-rust/b2c_test.rs b/tests/mpesa-rust/b2c_test.rs index 28b0f2540..821a4a8da 100644 --- a/tests/mpesa-rust/b2c_test.rs +++ b/tests/mpesa-rust/b2c_test.rs @@ -62,7 +62,9 @@ async fn b2c_fails_if_no_amount_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "amount is required"); } else { panic!("Expected error"); @@ -93,7 +95,9 @@ async fn b2c_fails_if_no_party_a_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "party_a is required"); } else { panic!("Expected error"); @@ -124,7 +128,9 @@ async fn b2c_fails_if_no_party_b_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "party_b is required"); } else { panic!("Expected error"); @@ -155,7 +161,9 @@ async fn b2c_fails_if_no_result_url_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "result_url is required"); } else { panic!("Expected error"); @@ -186,7 +194,9 @@ async fn b2c_fails_if_no_queue_timeout_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "queue_timeout_url is required"); } else { panic!("Expected error"); diff --git a/tests/mpesa-rust/bill_manager_test/bulk_invoice_test.rs b/tests/mpesa-rust/bill_manager_test/bulk_invoice_test.rs index e80b1c21b..591a373b0 100644 --- a/tests/mpesa-rust/bill_manager_test/bulk_invoice_test.rs +++ b/tests/mpesa-rust/bill_manager_test/bulk_invoice_test.rs @@ -57,7 +57,9 @@ async fn bulk_invoice_fails_if_invoices_is_empty() { .mount(&server) .await; if let Err(e) = client.bulk_invoice().send().await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "invoices cannot be empty"); } else { panic!("Expected Error") diff --git a/tests/mpesa-rust/bill_manager_test/onboard_test.rs b/tests/mpesa-rust/bill_manager_test/onboard_test.rs index 49684dd71..2b2332468 100644 --- a/tests/mpesa-rust/bill_manager_test/onboard_test.rs +++ b/tests/mpesa-rust/bill_manager_test/onboard_test.rs @@ -55,7 +55,9 @@ async fn onboard_fails_if_no_callback_url_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "callback_url is required"); } else { panic!("Expected error") @@ -80,7 +82,9 @@ async fn onboard_fails_if_no_email_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "email is required"); } else { panic!("Expected error") @@ -105,7 +109,9 @@ async fn onboard_fails_if_no_logo_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "logo is required"); } else { panic!("Expected error") @@ -130,7 +136,9 @@ async fn onboard_fails_if_no_official_contact_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "official_contact is required"); } else { panic!("Expected error") @@ -155,7 +163,9 @@ async fn onboard_fails_if_short_code_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "short_code is required"); } else { panic!("Expected error") diff --git a/tests/mpesa-rust/bill_manager_test/reconciliation_test.rs b/tests/mpesa-rust/bill_manager_test/reconciliation_test.rs index 8c9afbb86..02489be15 100644 --- a/tests/mpesa-rust/bill_manager_test/reconciliation_test.rs +++ b/tests/mpesa-rust/bill_manager_test/reconciliation_test.rs @@ -60,7 +60,9 @@ async fn reconciliation_fails_if_no_account_reference_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "account_reference is required"); } else { panic!("Expected error") @@ -88,7 +90,9 @@ async fn reconciliation_fails_if_no_external_reference_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "external_reference is required"); } else { panic!("Expected error") @@ -116,7 +120,9 @@ async fn reconciliation_fails_if_no_full_name_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "full_name is required"); } else { panic!("Expected error") @@ -144,7 +150,9 @@ async fn reconciliation_fails_if_no_invoice_name_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "invoice_name is required"); } else { panic!("Expected error") @@ -172,7 +180,9 @@ async fn reconciliation_fails_if_no_paid_amount_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "paid_amount is required"); } else { panic!("Expected error") @@ -200,7 +210,9 @@ async fn reconciliation_fails_if_no_payment_date_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "payment_date is required"); } else { panic!("Expected error") @@ -228,7 +240,9 @@ async fn reconciliation_fails_if_no_phone_number_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "phone_number is required"); } else { panic!("Expected error") @@ -256,7 +270,9 @@ async fn reconciliation_fails_if_no_transaction_id_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "transaction_id is required"); } else { panic!("Expected error") diff --git a/tests/mpesa-rust/bill_manager_test/single_invoice_test.rs b/tests/mpesa-rust/bill_manager_test/single_invoice_test.rs index 012c6020f..a9622050b 100644 --- a/tests/mpesa-rust/bill_manager_test/single_invoice_test.rs +++ b/tests/mpesa-rust/bill_manager_test/single_invoice_test.rs @@ -66,7 +66,9 @@ async fn single_invoice_fails_if_no_amount_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "amount is required"); } else { panic!("Expected error") @@ -94,7 +96,9 @@ async fn single_invoice_fails_if_no_account_reference_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "account_reference is required"); } else { panic!("Expected error") @@ -122,7 +126,9 @@ async fn single_invoice_fails_if_no_billed_full_name_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "billed_full_name is required"); } else { panic!("Expected error") @@ -150,7 +156,9 @@ async fn single_invoice_fails_if_no_billed_period_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "billed_period is required"); } else { panic!("Expected error") @@ -178,7 +186,9 @@ async fn single_invoice_fails_if_no_billed_phone_number_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message but found {}", e); + }; assert_eq!(msg, "billed_phone_number is required"); } else { panic!("Expected error") @@ -206,7 +216,9 @@ async fn single_invoice_fails_if_no_due_date_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "due_date is required"); } else { panic!("Expected error") @@ -234,7 +246,9 @@ async fn single_invoice_fails_if_no_external_reference_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "external_reference is required"); } else { panic!("Expected error") @@ -262,7 +276,9 @@ async fn single_invoice_fails_if_no_invoice_name_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "invoice_name is required"); } else { panic!("Expected error") diff --git a/tests/mpesa-rust/c2b_register_test.rs b/tests/mpesa-rust/c2b_register_test.rs index 190651c79..030c5fb4c 100644 --- a/tests/mpesa-rust/c2b_register_test.rs +++ b/tests/mpesa-rust/c2b_register_test.rs @@ -56,7 +56,9 @@ async fn c2b_register_fails_if_short_code_is_not_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "short_code is required"); } else { panic!("Expected error"); @@ -84,7 +86,9 @@ async fn c2b_register_fails_if_confirmation_url_is_not_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "confirmation_url is required"); } else { panic!("Expected error"); @@ -112,7 +116,9 @@ async fn c2b_register_fails_if_validation_url_is_not_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "validation_url is required"); } else { panic!("Expected error"); diff --git a/tests/mpesa-rust/c2b_simulate_test.rs b/tests/mpesa-rust/c2b_simulate_test.rs index eae5350ce..3a70e23e8 100644 --- a/tests/mpesa-rust/c2b_simulate_test.rs +++ b/tests/mpesa-rust/c2b_simulate_test.rs @@ -58,7 +58,9 @@ async fn c2b_simulate_fails_if_no_amount_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "amount is required"); } else { panic!("Expected error") @@ -87,7 +89,9 @@ async fn c2b_simulate_fails_if_no_short_code_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "short_code is required"); } else { panic!("Expected error") @@ -116,7 +120,9 @@ async fn c2b_simulate_fails_if_no_bill_ref_number_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "bill_ref_number is required"); } else { panic!("Expected error") @@ -145,7 +151,9 @@ async fn c2b_simulate_fails_if_no_msisdn_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else {panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "msisdn is required"); } else { panic!("Expected error") diff --git a/tests/mpesa-rust/stk_push_test.rs b/tests/mpesa-rust/stk_push_test.rs index 486020436..19be8c512 100644 --- a/tests/mpesa-rust/stk_push_test.rs +++ b/tests/mpesa-rust/stk_push_test.rs @@ -63,7 +63,9 @@ async fn stk_push_fails_if_no_amount_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "amount is required") } else { panic!("Expected error"); @@ -93,7 +95,9 @@ async fn stk_push_fails_if_no_callback_url_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "callback_url is required") } else { panic!("Expected error"); @@ -123,7 +127,9 @@ async fn stk_push_fails_if_no_phone_number_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "phone_number is required") } else { panic!("Expected error"); diff --git a/tests/mpesa-rust/transaction_reversal_test.rs b/tests/mpesa-rust/transaction_reversal_test.rs index f502ff75d..a5b39ad9a 100644 --- a/tests/mpesa-rust/transaction_reversal_test.rs +++ b/tests/mpesa-rust/transaction_reversal_test.rs @@ -60,7 +60,9 @@ async fn transaction_reversal_fails_if_no_transaction_id_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "transaction_id is required"); } else { panic!("Expected error"); @@ -90,7 +92,9 @@ async fn transaction_reversal_fails_if_no_amount_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "amount is required") } else { panic!("Expected error"); @@ -120,7 +124,9 @@ async fn transaction_reversal_fails_if_no_result_url_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "timeout_url is required") } else { panic!("Expected error"); @@ -150,7 +156,9 @@ async fn transaction_reversal_fails_if_no_timeout_url_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "result_url is required") } else { panic!("Expected error"); @@ -180,7 +188,9 @@ async fn transaction_reversal_fails_if_no_receiver_party_is_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "receiver_party is required") } else { panic!("Expected error"); diff --git a/tests/mpesa-rust/transaction_status_test.rs b/tests/mpesa-rust/transaction_status_test.rs index 7195ec8e9..b0011c1a2 100644 --- a/tests/mpesa-rust/transaction_status_test.rs +++ b/tests/mpesa-rust/transaction_status_test.rs @@ -61,7 +61,9 @@ async fn transaction_status_fails_if_transaction_id_is_not_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "transaction_id is required"); } else { panic!("Expected error") @@ -90,7 +92,9 @@ async fn transaction_status_fails_if_party_a_is_not_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "party_a is required"); } else { panic!("Expected error") @@ -119,7 +123,9 @@ async fn transaction_status_fails_if_result_url_is_not_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "result_url is required"); } else { panic!("Expected error") @@ -148,7 +154,9 @@ async fn transaction_status_fails_if_timeout_url_is_not_provided() { .send() .await { - let MpesaError::Message(msg) = e else { panic!("Expected MpesaError::Message, but found {}", e)}; + let MpesaError::Message(msg) = e else { + panic!("Expected MpesaError::Message, but found {}", e); + }; assert_eq!(msg, "timeout_url is required"); } else { panic!("Expected error") From 0c93b977b9c243906cb5a17e289eb8d100f8e456 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Sat, 26 Aug 2023 18:06:18 +0300 Subject: [PATCH 127/140] v1.0.0 release --- Cargo.toml | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aa9412f42..400f2eccb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "mpesa" -version = "0.4.2" +version = "1.0.0" authors = ["Collins Muriuki "] -edition = "2018" +edition = "2021" description = "A wrapper around the M-PESA API in Rust." keywords = ["api", "mpesa", "mobile"] repository = "https://github.com/collinsmuriuki/mpesa-rust" diff --git a/README.md b/README.md index f2eddf418..de7c66298 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ An unofficial Rust wrapper around the [Safaricom API](https://developer.safarico ```toml [dependencies] -mpesa = { git = "https://github.com/collinsmuriuki/mpesa-rust" } +mpesa = { version = "1.0.0" } ``` Optionally, you can disable default-features, which is basically the entire suite of MPESA APIs to conditionally select from either: @@ -33,7 +33,7 @@ Example: ```toml [dependencies] -mpesa = { git = "https://github.com/collinsmuriuki/mpesa-rust", default_features = false, features = ["b2b", "express_request"] } +mpesa = { git = "1.0.0", default_features = false, features = ["b2b", "express_request"] } ``` In your lib or binary crate: From d8d18187ff3aefd663543d41320be58c8cbb26b2 Mon Sep 17 00:00:00 2001 From: "brian.orwe" Date: Wed, 4 Oct 2023 13:35:31 +0300 Subject: [PATCH 128/140] cleanup --- src/client.rs | 4 ++-- src/services/c2b_register.rs | 5 ----- tests/mpesa-rust/c2b_register_test.rs | 1 - tests/mpesa-rust/helpers.rs | 10 ---------- 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0e7b7c37a..65c87ee7b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -14,9 +14,9 @@ use serde_json::Value; use std::cell::RefCell; /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) -static DEFAULT_INITIATOR_PASSWORD: &str = "Safcom496!"; +const DEFAULT_INITIATOR_PASSWORD: &str = "Safcom496!"; /// Get current package version from metadata -static CARGO_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); +const CARGO_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); /// `Result` enum type alias pub type MpesaResult = Result; diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index 82635bcdf..fe214e7e6 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -19,11 +19,6 @@ struct C2bRegisterPayload<'mpesa> { #[derive(Debug, Deserialize, Clone)] pub struct C2bRegisterResponse { - #[serde( - rename(deserialize = "ConversationID"), - skip_serializing_if = "Option::is_none" - )] - pub conversation_id: Option, #[serde(rename(deserialize = "OriginatorConverstionID"))] pub originator_conversation_id: String, #[serde(rename(deserialize = "ResponseCode"))] diff --git a/tests/mpesa-rust/c2b_register_test.rs b/tests/mpesa-rust/c2b_register_test.rs index 030c5fb4c..c50815455 100644 --- a/tests/mpesa-rust/c2b_register_test.rs +++ b/tests/mpesa-rust/c2b_register_test.rs @@ -32,7 +32,6 @@ async fn c2b_register_success() { "Accept the service request successfully." ); assert_eq!(response.response_code, "0"); - assert_eq!(response.conversation_id, None); } #[tokio::test] diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index 01dfcfaf5..a6f13cf60 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -90,14 +90,4 @@ mod tests { ); assert!(!client.is_connected().await); } - - #[tokio::test] - async fn test_client_will_not_authenticate_with_sandbox_credentials_in_production() { - let client = get_mpesa_client!( - std::env::var("CLIENT_KEY").unwrap(), - std::env::var("CLIENT_SECRET").unwrap(), - Environment::from_str("production").unwrap() - ); - assert!(!client.is_connected().await); - } } From 9c0a66f4607b9ba5dc14568ee2cb6ee68a42d81a Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Thu, 9 Nov 2023 14:13:02 +0300 Subject: [PATCH 129/140] Simplify auth method --- src/client.rs | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/client.rs b/src/client.rs index 65c87ee7b..b20f966f0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,12 +5,11 @@ use crate::services::{ OnboardModifyBuilder, ReconciliationBuilder, SingleInvoiceBuilder, TransactionReversalBuilder, TransactionStatusBuilder, }; -use crate::{ApiError, MpesaError}; +use crate::MpesaError; use openssl::base64; use openssl::rsa::Padding; use openssl::x509::X509; use reqwest::Client as HttpClient; -use serde_json::Value; use std::cell::RefCell; /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) @@ -113,22 +112,11 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { .get(&url) .basic_auth(&self.client_key, Some(&self.client_secret)) .send() + .await? + .error_for_status()? + .json::() .await?; - if response.status().is_success() { - let value = response.json::().await?; - let access_token = value - .get("access_token") - .ok_or_else(|| String::from("Failed to extract token from the response")) - .unwrap(); - let access_token = access_token - .as_str() - .ok_or_else(|| String::from("Error converting access token to string")) - .unwrap(); - - return Ok(access_token.to_string()); - } - let error = response.json::().await?; - Err(MpesaError::AuthenticationError(error)) + Ok(response.access_token) } /// **B2C Builder** @@ -524,6 +512,11 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { } } +#[derive(Debug, serde::Deserialize)] +struct AuthResponse { + pub access_token: String, +} + #[cfg(test)] mod tests { use crate::Sandbox; From 624511af967c78f0b7e053d3cb3a4664ef980994 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Thu, 9 Nov 2023 14:16:34 +0300 Subject: [PATCH 130/140] Remove redundant clippy attribute --- src/client.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index b20f966f0..4d04a8a92 100644 --- a/src/client.rs +++ b/src/client.rs @@ -101,7 +101,6 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { /// /// # Errors /// Returns a `MpesaError` on failure - #[allow(clippy::single_char_pattern)] pub(crate) async fn auth(&self) -> MpesaResult { let url = format!( "{}/oauth/v1/generate?grant_type=client_credentials", From e590cb249c9a945867fbe82dd30162f2dd3ffc47 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Thu, 9 Nov 2023 14:19:45 +0300 Subject: [PATCH 131/140] Mask client secret in debug --- Cargo.toml | 13 ++----------- src/client.rs | 7 ++++--- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 400f2eccb..9b7707636 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ license = "MIT" chrono = {version = "0.4", optional = true, default-features = false, features = ["clock", "serde"] } openssl = {version = "0.10", optional = true} reqwest = {version = "0.11", features = ["json"]} +secrecy = "0.8.0" serde = {version="1.0", features= ["derive"]} serde_json = "1.0" serde_repr = "0.1" @@ -25,17 +26,7 @@ tokio = {version = "1", features = ["rt", "rt-multi-thread", "macros"]} wiremock = "0.5" [features] -default = [ - "account_balance", - "b2b", - "b2c", - "bill_manager", - "c2b_register", - "c2b_simulate", - "express_request", - "transaction_reversal", - "transaction_status" -] +default = ["account_balance", "b2b", "b2c", "bill_manager", "c2b_register", "c2b_simulate", "express_request", "transaction_reversal", "transaction_status"] account_balance = ["dep:openssl"] b2b = ["dep:openssl"] b2c = ["dep:openssl"] diff --git a/src/client.rs b/src/client.rs index 4d04a8a92..8a734677e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -10,6 +10,7 @@ use openssl::base64; use openssl::rsa::Padding; use openssl::x509::X509; use reqwest::Client as HttpClient; +use secrecy::{ExposeSecret, Secret}; use std::cell::RefCell; /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) @@ -24,7 +25,7 @@ pub type MpesaResult = Result; #[derive(Clone, Debug)] pub struct Mpesa { client_key: String, - client_secret: String, + client_secret: Secret, initiator_password: RefCell>, pub(crate) environment: Env, pub(crate) http_client: HttpClient, @@ -51,7 +52,7 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { .expect("Error building http client"); Self { client_key: client_key.into(), - client_secret: client_secret.into(), + client_secret: Secret::new(client_secret.into()), initiator_password: RefCell::new(None), environment, http_client, @@ -109,7 +110,7 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { let response = self .http_client .get(&url) - .basic_auth(&self.client_key, Some(&self.client_secret)) + .basic_auth(&self.client_key, Some(&self.client_secret.expose_secret())) .send() .await? .error_for_status()? From e62d0a997089ee324fe1190db694aa26b8dba557 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Thu, 9 Nov 2023 16:22:44 +0300 Subject: [PATCH 132/140] Additionally mask initiator password' --- src/client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8a734677e..a2ae488f5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -26,7 +26,7 @@ pub type MpesaResult = Result; pub struct Mpesa { client_key: String, client_secret: Secret, - initiator_password: RefCell>, + initiator_password: RefCell>>, pub(crate) environment: Env, pub(crate) http_client: HttpClient, } @@ -65,7 +65,7 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { let Some(p) = &*self.initiator_password.borrow() else { return DEFAULT_INITIATOR_PASSWORD.to_owned(); }; - p.to_owned() + p.expose_secret().into() } /// Optional in development but required for production, you will need to call this method and set your production initiator password. @@ -82,7 +82,7 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { /// client.set_initiator_password("your_initiator_password"); /// ``` pub fn set_initiator_password>(&self, initiator_password: S) { - *self.initiator_password.borrow_mut() = Some(initiator_password.into()); + *self.initiator_password.borrow_mut() = Some(Secret::new(initiator_password.into())); } /// Checks if the client can be authenticated From 57becb52718a0044fe42d6303f61e46fdf776768 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Thu, 9 Nov 2023 16:27:24 +0300 Subject: [PATCH 133/140] Add doc to new method --- src/client.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client.rs b/src/client.rs index a2ae488f5..35c2b918e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -42,6 +42,9 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { /// Environment::Sandbox, /// ); /// ``` + /// + /// # Panics + /// 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)) From 2ebf13ec7ad0f82b61c4bccb4bab035ab231a889 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Thu, 9 Nov 2023 17:51:00 +0300 Subject: [PATCH 134/140] Revert auth changes --- src/client.rs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/client.rs b/src/client.rs index 35c2b918e..3a9640e4b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,12 +5,13 @@ use crate::services::{ OnboardModifyBuilder, ReconciliationBuilder, SingleInvoiceBuilder, TransactionReversalBuilder, TransactionStatusBuilder, }; -use crate::MpesaError; +use crate::{ApiError, MpesaError}; use openssl::base64; use openssl::rsa::Padding; use openssl::x509::X509; use reqwest::Client as HttpClient; use secrecy::{ExposeSecret, Secret}; +use serde_json::Value; use std::cell::RefCell; /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) @@ -115,11 +116,22 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { .get(&url) .basic_auth(&self.client_key, Some(&self.client_secret.expose_secret())) .send() - .await? - .error_for_status()? - .json::() .await?; - Ok(response.access_token) + if response.status().is_success() { + let value = response.json::().await?; + let access_token = value + .get("access_token") + .ok_or_else(|| String::from("Failed to extract token from the response")) + .unwrap(); + let access_token = access_token + .as_str() + .ok_or_else(|| String::from("Error converting access token to string")) + .unwrap(); + + return Ok(access_token.to_string()); + } + let error = response.json::().await?; + Err(MpesaError::AuthenticationError(error)) } /// **B2C Builder** @@ -515,11 +527,6 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { } } -#[derive(Debug, serde::Deserialize)] -struct AuthResponse { - pub access_token: String, -} - #[cfg(test)] mod tests { use crate::Sandbox; From b50641dcff214e6ae302bcbf122d47acb1c827e1 Mon Sep 17 00:00:00 2001 From: Yasir Date: Tue, 14 Nov 2023 16:13:55 +0300 Subject: [PATCH 135/140] chore: Implement auth caching (#78) * Initial implementation of auth caching * Add auth mod * Fix tests * Move Result to err mod * fix: refactor codes --- .vscode/settings.json | 5 + Cargo.toml | 30 +++-- src/auth.rs | 115 ++++++++++++++++++ src/client.rs | 77 ++++++------ src/constants.rs | 3 +- src/environment.rs | 4 +- src/errors.rs | 7 +- src/lib.rs | 5 +- src/services/account_balance.rs | 6 +- src/services/b2b.rs | 7 +- src/services/b2c.rs | 11 +- src/services/bill_manager/bulk_invoice.rs | 7 +- src/services/bill_manager/cancel_invoice.rs | 7 +- src/services/bill_manager/onboard.rs | 7 +- src/services/bill_manager/onboard_modify.rs | 7 +- src/services/bill_manager/reconciliation.rs | 7 +- src/services/bill_manager/single_invoice.rs | 9 +- src/services/c2b_register.rs | 7 +- src/services/c2b_simulate.rs | 7 +- src/services/express_request.rs | 9 +- src/services/transaction_reversal.rs | 12 +- src/services/transaction_status.rs | 12 +- tests/mpesa-rust/account_balance_test.rs | 3 +- tests/mpesa-rust/b2c_test.rs | 3 +- .../bill_manager_test/bulk_invoice_test.rs | 3 +- .../bill_manager_test/cancel_invoice_test.rs | 3 +- .../bill_manager_test/onboard_modify_test.rs | 3 +- .../bill_manager_test/onboard_test.rs | 3 +- .../bill_manager_test/reconciliation_test.rs | 3 +- .../bill_manager_test/single_invoice_test.rs | 3 +- tests/mpesa-rust/c2b_register_test.rs | 3 +- tests/mpesa-rust/c2b_simulate_test.rs | 3 +- tests/mpesa-rust/helpers.rs | 29 ++++- tests/mpesa-rust/stk_push_test.rs | 3 +- tests/mpesa-rust/transaction_reversal_test.rs | 3 +- tests/mpesa-rust/transaction_status_test.rs | 1 - 36 files changed, 302 insertions(+), 125 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/auth.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..a03a9d2e6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "Mpesa" + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 9b7707636..341af1203 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,23 +10,37 @@ readme = "./README.md" license = "MIT" [dependencies] -chrono = {version = "0.4", optional = true, default-features = false, features = ["clock", "serde"] } -openssl = {version = "0.10", optional = true} -reqwest = {version = "0.11", features = ["json"]} -secrecy = "0.8.0" -serde = {version="1.0", features= ["derive"]} +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"] } +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" [dev-dependencies] dotenv = "0.15" -tokio = {version = "1", features = ["rt", "rt-multi-thread", "macros"]} +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } wiremock = "0.5" [features] -default = ["account_balance", "b2b", "b2c", "bill_manager", "c2b_register", "c2b_simulate", "express_request", "transaction_reversal", "transaction_status"] +default = [ + "account_balance", + "b2b", + "b2c", + "bill_manager", + "c2b_register", + "c2b_simulate", + "express_request", + "transaction_reversal", + "transaction_status", +] account_balance = ["dep:openssl"] b2b = ["dep:openssl"] b2c = ["dep:openssl"] @@ -35,4 +49,4 @@ c2b_register = [] c2b_simulate = [] express_request = ["dep:chrono"] transaction_reversal = ["dep:openssl"] -transaction_status= ["dep:openssl"] +transaction_status = ["dep:openssl"] diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 000000000..dcaffa944 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,115 @@ +use cached::proc_macro::cached; +use serde::{Deserialize, Serialize}; + +use crate::{ApiEnvironment, ApiError, Mpesa, MpesaError, MpesaResult}; + +const AUTHENTICATION_URL: &str = "/oauth/v1/generate?grant_type=client_credentials"; + +#[cached( + size = 1, + time = 3600, + key = "String", + result = true, + convert = r#"{ format!("{}", client.client_key()) }"# +)] +pub(crate) async fn auth(client: &Mpesa) -> MpesaResult { + let url = format!("{}{}", client.environment.base_url(), AUTHENTICATION_URL); + + let response = client + .http_client + .get(&url) + .basic_auth(client.client_key(), Some(&client.client_secret())) + .send() + .await?; + + if response.status().is_success() { + let value = response.json::().await?; + let access_token = value.access_token; + + return Ok(access_token); + } + + let error = response.json::().await?; + Err(MpesaError::AuthenticationError(error)) +} + +/// Response returned from the authentication function +#[derive(Debug, Serialize, Deserialize)] +pub struct AuthenticationResponse { + /// Access token which is used as the Bearer-Auth-Token + pub access_token: String, + /// Expiry time in seconds + pub expiry_in: u64, +} + +impl std::fmt::Display for AuthenticationResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "token :{} expires in: {}", + self.access_token, self.expiry_in + ) + } +} + +#[cfg(test)] +mod tests { + use wiremock::{Mock, MockServer}; + + use super::*; + + #[derive(Debug, Clone)] + pub struct TestEnvironment { + pub server_url: String, + } + + impl TestEnvironment { + pub async fn new(server: &MockServer) -> Self { + TestEnvironment { + server_url: server.uri(), + } + } + } + + impl ApiEnvironment for TestEnvironment { + fn base_url(&self) -> &str { + &self.server_url + } + + fn get_certificate(&self) -> &str { + include_str!("../src/certificates/sandbox") + } + } + + #[tokio::test] + async fn test_cached_auth() { + use cached::Cached; + + use crate::Mpesa; + + let server = MockServer::start().await; + + let env = TestEnvironment::new(&server).await; + + let client = Mpesa::new("test_api_key", "test_public_key", env); + + Mock::given(wiremock::matchers::method("GET")) + .respond_with(wiremock::ResponseTemplate::new(200).set_body_json( + AuthenticationResponse { + access_token: "test_token".to_string(), + expiry_in: 3600, + }, + )) + .expect(1) + .mount(&server) + .await; + + auth_prime_cache(&client).await.unwrap(); + + let mut cache = AUTH.lock().await; + + assert!(cache.cache_get(&client.client_key().to_string()).is_some()); + assert_eq!(cache.cache_hits().unwrap(), 1); + assert_eq!(cache.cache_capacity().unwrap(), 1); + } +} diff --git a/src/client.rs b/src/client.rs index 3a9640e4b..b4918556c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,3 +1,12 @@ +use std::cell::RefCell; + +use cached::Cached; +use openssl::base64; +use openssl::rsa::Padding; +use openssl::x509::X509; +use reqwest::Client as HttpClient; + +use crate::auth::AUTH; use crate::environment::ApiEnvironment; use crate::services::{ AccountBalanceBuilder, B2bBuilder, B2cBuilder, BulkInvoiceBuilder, C2bRegisterBuilder, @@ -5,23 +14,14 @@ use crate::services::{ OnboardModifyBuilder, ReconciliationBuilder, SingleInvoiceBuilder, TransactionReversalBuilder, TransactionStatusBuilder, }; -use crate::{ApiError, MpesaError}; -use openssl::base64; -use openssl::rsa::Padding; -use openssl::x509::X509; -use reqwest::Client as HttpClient; +use crate::{auth, MpesaResult}; use secrecy::{ExposeSecret, Secret}; -use serde_json::Value; -use std::cell::RefCell; /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) const DEFAULT_INITIATOR_PASSWORD: &str = "Safcom496!"; /// Get current package version from metadata const CARGO_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); -/// `Result` enum type alias -pub type MpesaResult = Result; - /// Mpesa client that will facilitate communication with the Safaricom API #[derive(Clone, Debug)] pub struct Mpesa { @@ -72,6 +72,16 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { p.expose_secret().into() } + /// Get the client key + pub(crate) fn client_key(&self) -> &str { + &self.client_key + } + + /// Get the client secret + pub(crate) fn client_secret(&self) -> &str { + self.client_secret.expose_secret() + } + /// Optional in development but required for production, you will need to call this method and set your production initiator password. /// If in development, default initiator password is already pre-set /// ```ignore @@ -107,31 +117,27 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { /// # Errors /// Returns a `MpesaError` on failure pub(crate) async fn auth(&self) -> MpesaResult { - let url = format!( - "{}/oauth/v1/generate?grant_type=client_credentials", - self.environment.base_url() - ); - let response = self - .http_client - .get(&url) - .basic_auth(&self.client_key, Some(&self.client_secret.expose_secret())) - .send() - .await?; - if response.status().is_success() { - let value = response.json::().await?; - let access_token = value - .get("access_token") - .ok_or_else(|| String::from("Failed to extract token from the response")) - .unwrap(); - let access_token = access_token - .as_str() - .ok_or_else(|| String::from("Error converting access token to string")) - .unwrap(); - - return Ok(access_token.to_string()); + if let Some(token) = AUTH.lock().await.cache_get(&self.client_key) { + return Ok(token.to_owned()); } - let error = response.json::().await?; - Err(MpesaError::AuthenticationError(error)) + + // Generate a new access token + let new_token = match auth::auth_prime_cache(self).await { + Ok(token) => token, + Err(e) => return Err(e), + }; + + // Double-check if the access token is cached by another thread + if let Some(token) = AUTH.lock().await.cache_get(&self.client_key) { + return Ok(token.to_owned()); + } + + // Cache the new token + AUTH.lock() + .await + .cache_set(self.client_key.clone(), new_token.to_owned()); + + Ok(new_token) } /// **B2C Builder** @@ -529,9 +535,8 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { #[cfg(test)] mod tests { - use crate::Sandbox; - use super::*; + use crate::Sandbox; #[test] fn test_setting_initator_password() { diff --git a/src/constants.rs b/src/constants.rs index 418ad5a52..0bdd9fa1b 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,7 +1,8 @@ +use std::fmt::{Display, Formatter, Result as FmtResult}; + use chrono::prelude::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use std::fmt::{Display, Formatter, Result as FmtResult}; /// Mpesa command ids #[derive(Debug, Serialize, Deserialize)] diff --git a/src/environment.rs b/src/environment.rs index eb6f1f579..0000cb959 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -10,8 +10,10 @@ //! and the `public key` an X509 certificate used for encrypting initiator passwords. You can read more about that from //! the Safaricom API [docs](https://developer.safaricom.co.ke/docs?javascript#security-credentials). +use std::convert::TryFrom; +use std::str::FromStr; + use crate::MpesaError; -use std::{convert::TryFrom, str::FromStr}; #[derive(Debug, Clone)] /// Enum to map to desired environment so as to access certificate diff --git a/src/errors.rs b/src/errors.rs index 2adc20891..60f191ff3 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,5 +1,7 @@ +use std::env::VarError; +use std::fmt; + use serde::{Deserialize, Serialize}; -use std::{env::VarError, fmt}; /// Mpesa error stack #[derive(thiserror::Error, Debug)] @@ -46,6 +48,9 @@ pub enum MpesaError { Message(&'static str), } +/// `Result` enum type alias +pub type MpesaResult = Result; + #[derive(Debug, Serialize, Deserialize)] pub struct ApiError { pub request_id: String, diff --git a/src/lib.rs b/src/lib.rs index 7a9e84138..03f490d81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,16 @@ #![doc = include_str!("../README.md")] +mod auth; mod client; mod constants; pub mod environment; mod errors; pub mod services; -pub use client::{Mpesa, MpesaResult}; +pub use client::Mpesa; pub use constants::{ CommandId, IdentifierTypes, Invoice, InvoiceItem, ResponseType, SendRemindersTypes, }; pub use environment::ApiEnvironment; pub use environment::Environment::{self, Production, Sandbox}; -pub use errors::{ApiError, MpesaError}; +pub use errors::{ApiError, MpesaError, MpesaResult}; diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index b3d7ba4d4..6ed46db88 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -1,8 +1,8 @@ -use crate::client::MpesaResult; +use serde::{Deserialize, Serialize}; + use crate::constants::{CommandId, IdentifierTypes}; use crate::environment::ApiEnvironment; -use crate::{Mpesa, MpesaError}; -use serde::{Deserialize, Serialize}; +use crate::{Mpesa, MpesaError, MpesaResult}; #[derive(Debug, Serialize)] /// Account Balance payload diff --git a/src/services/b2b.rs b/src/services/b2b.rs index 08746109e..431e07a70 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -1,8 +1,9 @@ -use crate::client::{Mpesa, MpesaResult}; +use serde::{Deserialize, Serialize}; + +use crate::client::Mpesa; use crate::constants::{CommandId, IdentifierTypes}; use crate::environment::ApiEnvironment; -use crate::errors::MpesaError; -use serde::{Deserialize, Serialize}; +use crate::errors::{MpesaError, MpesaResult}; #[derive(Debug, Serialize)] struct B2bPayload<'mpesa> { diff --git a/src/services/b2c.rs b/src/services/b2c.rs index 6ae46cfdc..d2dc96059 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -1,8 +1,8 @@ -use crate::client::MpesaResult; -use crate::environment::ApiEnvironment; -use crate::{CommandId, Mpesa, MpesaError}; use serde::{Deserialize, Serialize}; +use crate::environment::ApiEnvironment; +use crate::{CommandId, Mpesa, MpesaError, MpesaResult}; + #[derive(Debug, Serialize)] /// Payload to allow for b2c transactions: struct B2cPayload<'mpesa> { @@ -182,7 +182,6 @@ impl<'mpesa, Env: ApiEnvironment> B2cBuilder<'mpesa, Env> { /// /// # Errors /// Returns a `MpesaError` on failure. - #[allow(clippy::unnecessary_lazy_evaluations)] pub async fn send(self) -> MpesaResult { let url = format!( "{}/mpesa/b2c/v1/paymentrequest", @@ -193,9 +192,7 @@ impl<'mpesa, Env: ApiEnvironment> B2cBuilder<'mpesa, Env> { let payload = B2cPayload { initiator_name: self.initiator_name, security_credential: &credentials, - command_id: self - .command_id - .unwrap_or_else(|| CommandId::BusinessPayment), + command_id: self.command_id.unwrap_or(CommandId::BusinessPayment), amount: self .amount .ok_or(MpesaError::Message("amount is required"))?, diff --git a/src/services/bill_manager/bulk_invoice.rs b/src/services/bill_manager/bulk_invoice.rs index 15618b9b1..5004076c2 100644 --- a/src/services/bill_manager/bulk_invoice.rs +++ b/src/services/bill_manager/bulk_invoice.rs @@ -1,8 +1,9 @@ -use crate::client::{Mpesa, MpesaResult}; +use serde::Deserialize; + +use crate::client::Mpesa; use crate::constants::Invoice; use crate::environment::ApiEnvironment; -use crate::errors::MpesaError; -use serde::Deserialize; +use crate::errors::{MpesaError, MpesaResult}; #[derive(Clone, Debug, Deserialize)] pub struct BulkInvoiceResponse { diff --git a/src/services/bill_manager/cancel_invoice.rs b/src/services/bill_manager/cancel_invoice.rs index aa159dd30..0b12f4f18 100644 --- a/src/services/bill_manager/cancel_invoice.rs +++ b/src/services/bill_manager/cancel_invoice.rs @@ -1,8 +1,9 @@ -use crate::client::{Mpesa, MpesaResult}; -use crate::environment::ApiEnvironment; -use crate::errors::MpesaError; use serde::{Deserialize, Serialize}; +use crate::client::Mpesa; +use crate::environment::ApiEnvironment; +use crate::errors::{MpesaError, MpesaResult}; + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct CancelInvoicePayload<'mpesa> { diff --git a/src/services/bill_manager/onboard.rs b/src/services/bill_manager/onboard.rs index 9bb6f20dd..79e23619b 100644 --- a/src/services/bill_manager/onboard.rs +++ b/src/services/bill_manager/onboard.rs @@ -1,8 +1,9 @@ -use crate::client::{Mpesa, MpesaResult}; +use serde::{Deserialize, Serialize}; + +use crate::client::Mpesa; use crate::constants::SendRemindersTypes; use crate::environment::ApiEnvironment; -use crate::errors::MpesaError; -use serde::{Deserialize, Serialize}; +use crate::errors::{MpesaError, MpesaResult}; #[derive(Debug, Serialize)] /// Payload to opt you in as a biller to the bill manager features. diff --git a/src/services/bill_manager/onboard_modify.rs b/src/services/bill_manager/onboard_modify.rs index 36ff7e069..bf990a58a 100644 --- a/src/services/bill_manager/onboard_modify.rs +++ b/src/services/bill_manager/onboard_modify.rs @@ -1,8 +1,9 @@ -use crate::client::{Mpesa, MpesaResult}; +use serde::{Deserialize, Serialize}; + +use crate::client::Mpesa; use crate::constants::SendRemindersTypes; use crate::environment::ApiEnvironment; -use crate::errors::MpesaError; -use serde::{Deserialize, Serialize}; +use crate::errors::{MpesaError, MpesaResult}; #[derive(Debug, Serialize)] /// Payload to modify opt-in details to the bill manager api. diff --git a/src/services/bill_manager/reconciliation.rs b/src/services/bill_manager/reconciliation.rs index baf470b6f..0cbc75f2f 100644 --- a/src/services/bill_manager/reconciliation.rs +++ b/src/services/bill_manager/reconciliation.rs @@ -1,9 +1,10 @@ -use crate::client::{Mpesa, MpesaResult}; -use crate::environment::ApiEnvironment; -use crate::errors::MpesaError; use chrono::prelude::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use crate::client::Mpesa; +use crate::environment::ApiEnvironment; +use crate::errors::{MpesaError, MpesaResult}; + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct ReconciliationPayload<'mpesa> { diff --git a/src/services/bill_manager/single_invoice.rs b/src/services/bill_manager/single_invoice.rs index 5ce438bc0..a62f0b7b5 100644 --- a/src/services/bill_manager/single_invoice.rs +++ b/src/services/bill_manager/single_invoice.rs @@ -1,10 +1,11 @@ -use crate::client::{Mpesa, MpesaResult}; -use crate::constants::{Invoice, InvoiceItem}; -use crate::environment::ApiEnvironment; -use crate::errors::MpesaError; use chrono::prelude::{DateTime, Utc}; use serde::Deserialize; +use crate::client::Mpesa; +use crate::constants::{Invoice, InvoiceItem}; +use crate::environment::ApiEnvironment; +use crate::errors::{MpesaError, MpesaResult}; + #[derive(Clone, Debug, Deserialize)] pub struct SingleInvoiceResponse { #[serde(rename(deserialize = "rescode"))] diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index fe214e7e6..0e0d844c8 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -1,8 +1,9 @@ -use crate::client::{Mpesa, MpesaResult}; +use serde::{Deserialize, Serialize}; + +use crate::client::Mpesa; use crate::constants::ResponseType; use crate::environment::ApiEnvironment; -use crate::errors::MpesaError; -use serde::{Deserialize, Serialize}; +use crate::errors::{MpesaError, MpesaResult}; #[derive(Debug, Serialize)] /// Payload to register the 3rd party’s confirmation and validation URLs to M-Pesa diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index b9a41fdb6..dc7c78b6d 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -1,8 +1,9 @@ -use crate::client::{Mpesa, MpesaResult}; +use serde::{Deserialize, Serialize}; + +use crate::client::Mpesa; use crate::constants::CommandId; use crate::environment::ApiEnvironment; -use crate::errors::MpesaError; -use serde::{Deserialize, Serialize}; +use crate::errors::{MpesaError, MpesaResult}; #[derive(Debug, Serialize)] /// Payload to make payment requests from C2B. diff --git a/src/services/express_request.rs b/src/services/express_request.rs index a08ce2d16..d6340a78b 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -1,11 +1,12 @@ -use crate::client::{Mpesa, MpesaResult}; -use crate::constants::CommandId; -use crate::environment::ApiEnvironment; -use crate::errors::MpesaError; use chrono::prelude::Local; use openssl::base64; use serde::{Deserialize, Serialize}; +use crate::client::Mpesa; +use crate::constants::CommandId; +use crate::environment::ApiEnvironment; +use crate::errors::{MpesaError, MpesaResult}; + /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) static DEFAULT_PASSKEY: &str = "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919"; diff --git a/src/services/transaction_reversal.rs b/src/services/transaction_reversal.rs index 5aec81853..0080485ce 100644 --- a/src/services/transaction_reversal.rs +++ b/src/services/transaction_reversal.rs @@ -1,12 +1,6 @@ -use serde::Deserialize; -use serde::Serialize; - -use crate::ApiEnvironment; -use crate::CommandId; -use crate::IdentifierTypes; -use crate::Mpesa; -use crate::MpesaError; -use crate::MpesaResult; +use serde::{Deserialize, Serialize}; + +use crate::{ApiEnvironment, CommandId, IdentifierTypes, Mpesa, MpesaError, MpesaResult}; #[derive(Debug, Serialize)] pub struct TransactionReversalPayload<'mpesa> { diff --git a/src/services/transaction_status.rs b/src/services/transaction_status.rs index eac9f5552..5a7a50b99 100644 --- a/src/services/transaction_status.rs +++ b/src/services/transaction_status.rs @@ -1,12 +1,6 @@ -use serde::Deserialize; -use serde::Serialize; - -use crate::ApiEnvironment; -use crate::CommandId; -use crate::IdentifierTypes; -use crate::Mpesa; -use crate::MpesaError; -use crate::MpesaResult; +use serde::{Deserialize, Serialize}; + +use crate::{ApiEnvironment, CommandId, IdentifierTypes, Mpesa, MpesaError, MpesaResult}; #[derive(Debug, Serialize)] pub struct TransactionStatusPayload<'mpesa> { diff --git a/tests/mpesa-rust/account_balance_test.rs b/tests/mpesa-rust/account_balance_test.rs index 034311f09..ebbc65a9d 100644 --- a/tests/mpesa-rust/account_balance_test.rs +++ b/tests/mpesa-rust/account_balance_test.rs @@ -1,9 +1,10 @@ -use crate::get_mpesa_client; use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; +use crate::get_mpesa_client; + #[tokio::test] async fn account_balance_success() { let (client, server) = get_mpesa_client!(); diff --git a/tests/mpesa-rust/b2c_test.rs b/tests/mpesa-rust/b2c_test.rs index 821a4a8da..39dec8ed4 100644 --- a/tests/mpesa-rust/b2c_test.rs +++ b/tests/mpesa-rust/b2c_test.rs @@ -1,9 +1,10 @@ -use crate::get_mpesa_client; use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; +use crate::get_mpesa_client; + #[tokio::test] async fn b2c_success() { let (client, server) = get_mpesa_client!(); diff --git a/tests/mpesa-rust/bill_manager_test/bulk_invoice_test.rs b/tests/mpesa-rust/bill_manager_test/bulk_invoice_test.rs index 591a373b0..6e05928e9 100644 --- a/tests/mpesa-rust/bill_manager_test/bulk_invoice_test.rs +++ b/tests/mpesa-rust/bill_manager_test/bulk_invoice_test.rs @@ -1,10 +1,11 @@ -use crate::get_mpesa_client; use chrono::prelude::Utc; use mpesa::{Invoice, InvoiceItem, MpesaError}; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; +use crate::get_mpesa_client; + fn sample_response() -> ResponseTemplate { let sample_response = json!({ "rescode": "200", diff --git a/tests/mpesa-rust/bill_manager_test/cancel_invoice_test.rs b/tests/mpesa-rust/bill_manager_test/cancel_invoice_test.rs index 64d361055..201bbce3c 100644 --- a/tests/mpesa-rust/bill_manager_test/cancel_invoice_test.rs +++ b/tests/mpesa-rust/bill_manager_test/cancel_invoice_test.rs @@ -1,8 +1,9 @@ -use crate::get_mpesa_client; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; +use crate::get_mpesa_client; + fn sample_response() -> ResponseTemplate { let sample_response = json!({ "rescode": "200", diff --git a/tests/mpesa-rust/bill_manager_test/onboard_modify_test.rs b/tests/mpesa-rust/bill_manager_test/onboard_modify_test.rs index dcf629dcc..3e0376940 100644 --- a/tests/mpesa-rust/bill_manager_test/onboard_modify_test.rs +++ b/tests/mpesa-rust/bill_manager_test/onboard_modify_test.rs @@ -1,8 +1,9 @@ -use crate::get_mpesa_client; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; +use crate::get_mpesa_client; + fn sample_response() -> ResponseTemplate { let sample_response_body = json!({ "rescode": "200", diff --git a/tests/mpesa-rust/bill_manager_test/onboard_test.rs b/tests/mpesa-rust/bill_manager_test/onboard_test.rs index 2b2332468..b9a813112 100644 --- a/tests/mpesa-rust/bill_manager_test/onboard_test.rs +++ b/tests/mpesa-rust/bill_manager_test/onboard_test.rs @@ -1,9 +1,10 @@ -use crate::get_mpesa_client; use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; +use crate::get_mpesa_client; + fn sample_response() -> ResponseTemplate { let sample_response_body = json!({ "app_key": "kfpB9X4o0H", diff --git a/tests/mpesa-rust/bill_manager_test/reconciliation_test.rs b/tests/mpesa-rust/bill_manager_test/reconciliation_test.rs index 02489be15..c555c8b1c 100644 --- a/tests/mpesa-rust/bill_manager_test/reconciliation_test.rs +++ b/tests/mpesa-rust/bill_manager_test/reconciliation_test.rs @@ -1,10 +1,11 @@ -use crate::get_mpesa_client; use chrono::prelude::Utc; use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; +use crate::get_mpesa_client; + fn sample_response() -> ResponseTemplate { let sample_response = json!({ "rescode": "200", diff --git a/tests/mpesa-rust/bill_manager_test/single_invoice_test.rs b/tests/mpesa-rust/bill_manager_test/single_invoice_test.rs index a9622050b..b876de4f9 100644 --- a/tests/mpesa-rust/bill_manager_test/single_invoice_test.rs +++ b/tests/mpesa-rust/bill_manager_test/single_invoice_test.rs @@ -1,10 +1,11 @@ -use crate::get_mpesa_client; use chrono::prelude::Utc; use mpesa::{InvoiceItem, MpesaError}; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; +use crate::get_mpesa_client; + fn sample_response() -> ResponseTemplate { let sample_response = json!({ "rescode": "200", diff --git a/tests/mpesa-rust/c2b_register_test.rs b/tests/mpesa-rust/c2b_register_test.rs index c50815455..18bcee648 100644 --- a/tests/mpesa-rust/c2b_register_test.rs +++ b/tests/mpesa-rust/c2b_register_test.rs @@ -1,9 +1,10 @@ -use crate::get_mpesa_client; use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; +use crate::get_mpesa_client; + #[tokio::test] async fn c2b_register_success() { let (client, server) = get_mpesa_client!(); diff --git a/tests/mpesa-rust/c2b_simulate_test.rs b/tests/mpesa-rust/c2b_simulate_test.rs index 3a70e23e8..0ff085188 100644 --- a/tests/mpesa-rust/c2b_simulate_test.rs +++ b/tests/mpesa-rust/c2b_simulate_test.rs @@ -1,9 +1,10 @@ -use crate::get_mpesa_client; use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; +use crate::get_mpesa_client; + #[tokio::test] async fn c2b_simulate_success() { let (client, server) = get_mpesa_client!(); diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index a6f13cf60..cc3a589be 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -27,7 +27,30 @@ impl ApiEnvironment for TestEnvironment { #[macro_export] macro_rules! get_mpesa_client { () => {{ - get_mpesa_client!(expected_auth_requests = 1) + use $crate::helpers::TestEnvironment; + use mpesa::Mpesa; + use wiremock::{MockServer, Mock, ResponseTemplate}; + use serde_json::json; + use wiremock::matchers::{path, query_param, method}; + + dotenv::dotenv().ok(); + let server = MockServer::start().await; + let test_environment = TestEnvironment::new(&server).await; + let client = Mpesa::new( + std::env::var("CLIENT_KEY").unwrap(), + std::env::var("CLIENT_SECRET").unwrap(), + test_environment, + ); + Mock::given(method("GET")) + .and(path("/oauth/v1/generate")) + .and(query_param("grant_type", "client_credentials")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "access_token": "dummy_access_token", + "expiry_in": 3600 + }))) + .mount(&server) + .await; + (client, server) }}; (expected_auth_requests = $expected_requests: expr) => {{ @@ -49,9 +72,9 @@ macro_rules! get_mpesa_client { .and(path("/oauth/v1/generate")) .and(query_param("grant_type", "client_credentials")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "access_token": "dummy_access_token" + "access_token": "dummy_access_token", + "expiry_in": 3600 }))) - .expect($expected_requests) .mount(&server) .await; (client, server) diff --git a/tests/mpesa-rust/stk_push_test.rs b/tests/mpesa-rust/stk_push_test.rs index 19be8c512..dec5815da 100644 --- a/tests/mpesa-rust/stk_push_test.rs +++ b/tests/mpesa-rust/stk_push_test.rs @@ -1,9 +1,10 @@ -use crate::get_mpesa_client; use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; +use crate::get_mpesa_client; + #[tokio::test] async fn stk_push_success_success() { let (client, server) = get_mpesa_client!(); diff --git a/tests/mpesa-rust/transaction_reversal_test.rs b/tests/mpesa-rust/transaction_reversal_test.rs index a5b39ad9a..017ac4583 100644 --- a/tests/mpesa-rust/transaction_reversal_test.rs +++ b/tests/mpesa-rust/transaction_reversal_test.rs @@ -1,9 +1,10 @@ -use crate::get_mpesa_client; use mpesa::MpesaError; 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!(); diff --git a/tests/mpesa-rust/transaction_status_test.rs b/tests/mpesa-rust/transaction_status_test.rs index b0011c1a2..46f0aa008 100644 --- a/tests/mpesa-rust/transaction_status_test.rs +++ b/tests/mpesa-rust/transaction_status_test.rs @@ -1,5 +1,4 @@ use mpesa::MpesaError; - use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; From 175a93f357753aa85cf293b6ff840a1fc2025deb Mon Sep 17 00:00:00 2001 From: Yasir Date: Tue, 14 Nov 2023 16:30:51 +0300 Subject: [PATCH 136/140] feat: add dynamic qr code (#80) * feat: add dynamic qr code * chore: add builder errors * Fix doc * Update src/services/dynamic_qr.rs Co-authored-by: Collins Muriuki * chore: add from_request fn * fix: merge conflicts --------- Co-authored-by: Collins Muriuki --- Cargo.toml | 3 + README.md | 20 ++++ src/client.rs | 38 ++++++- src/constants.rs | 37 +++++++ src/environment.rs | 2 +- src/errors.rs | 27 ++++- src/lib.rs | 1 + src/services/dynamic_qr.rs | 158 +++++++++++++++++++++++++++ src/services/mod.rs | 5 + tests/mpesa-rust/dynamic_qr_tests.rs | 82 ++++++++++++++ tests/mpesa-rust/main.rs | 3 +- 11 files changed, 369 insertions(+), 7 deletions(-) create mode 100644 src/services/dynamic_qr.rs create mode 100644 tests/mpesa-rust/dynamic_qr_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 341af1203..461944b2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ chrono = { version = "0.4", optional = true, default-features = false, features ] } 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" @@ -40,7 +41,9 @@ default = [ "express_request", "transaction_reversal", "transaction_status", + "dynamic_qr" ] +dynamic_qr = [] account_balance = ["dep:openssl"] b2b = ["dep:openssl"] b2c = ["dep:openssl"] diff --git a/README.md b/README.md index de7c66298..7a48132cb 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Optionally, you can disable default-features, which is basically the entire suit - `transaction_reversal` - `transaction_status` - `bill_manager` +- `dynamic_qr` Example: @@ -371,6 +372,25 @@ let response = client assert!(response.is_ok()) ``` +- Dynamic QR + +```rust,ignore +let response = client + .dynamic_qr_code() + .amount(1000) + .ref_no("John Doe") + .size("300") + .merchant_name("John Doe") + .credit_party_identifier("600496") + .try_transaction_type("bg") + .unwrap() + .build() + .unwrap() + .send() + .await; +assert!(response.is_ok()) +``` + More will be added progressively, pull requests welcome ## Author diff --git a/src/client.rs b/src/client.rs index b4918556c..d52e5cb53 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,17 +5,17 @@ use openssl::base64; use openssl::rsa::Padding; use openssl::x509::X509; use reqwest::Client as HttpClient; +use secrecy::{ExposeSecret, Secret}; use crate::auth::AUTH; use crate::environment::ApiEnvironment; use crate::services::{ AccountBalanceBuilder, B2bBuilder, B2cBuilder, BulkInvoiceBuilder, C2bRegisterBuilder, - C2bSimulateBuilder, CancelInvoiceBuilder, MpesaExpressRequestBuilder, OnboardBuilder, - OnboardModifyBuilder, ReconciliationBuilder, SingleInvoiceBuilder, TransactionReversalBuilder, - TransactionStatusBuilder, + C2bSimulateBuilder, CancelInvoiceBuilder, DynamicQR, DynamicQRBuilder, + MpesaExpressRequestBuilder, OnboardBuilder, OnboardModifyBuilder, ReconciliationBuilder, + SingleInvoiceBuilder, TransactionReversalBuilder, TransactionStatusBuilder, }; use crate::{auth, MpesaResult}; -use secrecy::{ExposeSecret, Secret}; /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) const DEFAULT_INITIATOR_PASSWORD: &str = "Safcom496!"; @@ -507,6 +507,35 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { TransactionStatusBuilder::new(self, initiator_name) } + /// ** Dynamic QR Code Builder ** + /// + /// Generates a QR code that can be scanned by a M-Pesa customer to make + /// payments. + /// + /// See more from the Safaricom API docs [here](https://developer.safaricom. + /// co.ke/APIs/DynamicQRCode) + /// + /// # Example + /// ```ignore + /// let response = client + /// .dynamic_qr_code() + /// .amount(1000) + /// .ref_no("John Doe") + /// .size("300") + /// .merchant_name("John Doe") + /// .credit_party_identifier("600496") + /// .try_transaction_type("bg") + /// .unwrap() + /// .build() + /// .unwrap() + /// .send() + /// .await; + /// ``` + /// + #[cfg(feature = "dynamic_qr")] + pub fn dynamic_qr(&'mpesa self) -> DynamicQRBuilder<'mpesa, Env> { + DynamicQR::builder(self) + } /// Generates 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. @@ -546,6 +575,7 @@ mod tests { assert_eq!(client.initiator_password(), "foo_bar".to_string()); } + #[derive(Clone)] struct TestEnvironment; impl ApiEnvironment for TestEnvironment { diff --git a/src/constants.rs b/src/constants.rs index 0bdd9fa1b..756fdc996 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -4,6 +4,8 @@ use chrono::prelude::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use crate::MpesaError; + /// Mpesa command ids #[derive(Debug, Serialize, Deserialize)] pub enum CommandId { @@ -141,3 +143,38 @@ impl<'i> Display for InvoiceItem<'i> { write!(f, "amount: {}, item_name: {}", self.amount, self.item_name) } } + +#[derive(Debug, Clone, Copy, Serialize)] +pub enum TransactionType { + /// Send Money(Mobile number). + SendMoney, + /// Withdraw Cash at Agent Till + Withdraw, + /// Pay Merchant (Buy Goods) + BG, + /// Paybill or Business number + PayBill, + /// Sent to Business. Business number CPI in MSISDN format. + SendBusiness, +} + +impl Display for TransactionType { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "{self:?}") + } +} + +impl TryFrom<&str> for TransactionType { + type Error = MpesaError; + + fn try_from(value: &str) -> Result { + match value.to_lowercase().as_str() { + "bg" => Ok(TransactionType::BG), + "wa" => Ok(TransactionType::Withdraw), + "pb" => Ok(TransactionType::PayBill), + "sm" => Ok(TransactionType::SendMoney), + "sb" => Ok(TransactionType::SendBusiness), + _ => Err(MpesaError::Message("Invalid transaction type")), + } + } +} diff --git a/src/environment.rs b/src/environment.rs index 0000cb959..84ee51c86 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -28,7 +28,7 @@ pub enum Environment { /// Expected behavior of an `Mpesa` client environment /// This abstraction exists to make it possible to mock the MPESA api server for tests -pub trait ApiEnvironment { +pub trait ApiEnvironment: Clone { fn base_url(&self) -> &str; fn get_certificate(&self) -> &str; } diff --git a/src/errors.rs b/src/errors.rs index 60f191ff3..cb3bed48a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -2,9 +2,10 @@ use std::env::VarError; use std::fmt; use serde::{Deserialize, Serialize}; +use thiserror::Error; /// Mpesa error stack -#[derive(thiserror::Error, Debug)] +#[derive(Error, Debug)] pub enum MpesaError { #[error("{0}")] AuthenticationError(ApiError), @@ -36,6 +37,8 @@ pub enum MpesaError { MpesaTransactionReversalError(ApiError), #[error("Mpesa Transaction status failed: {0}")] MpesaTransactionStatusError(ApiError), + #[error("Mpesa Dynamic QR failed: {0}")] + MpesaDynamicQrError(ApiError), #[error("An error has occured while performing the http request")] NetworkError(#[from] reqwest::Error), #[error("An error has occured while serializig/ deserializing")] @@ -46,6 +49,8 @@ pub enum MpesaError { EncryptionError(#[from] openssl::error::ErrorStack), #[error("{0}")] Message(&'static str), + #[error("An error has occurred while building the request: {0}")] + BuilderError(BuilderError), } /// `Result` enum type alias @@ -67,3 +72,23 @@ impl fmt::Display for ApiError { ) } } + +#[derive(Debug, Error)] +pub enum BuilderError { + #[error("Field [{0}] is required")] + UninitializedField(&'static str), + #[error("Field [{0}] is invalid")] + ValidationError(String), +} + +impl From for BuilderError { + fn from(s: String) -> Self { + Self::ValidationError(s) + } +} + +impl From for MpesaError { + fn from(e: derive_builder::UninitializedFieldError) -> Self { + Self::BuilderError(BuilderError::UninitializedField(e.field_name())) + } +} diff --git a/src/lib.rs b/src/lib.rs index 03f490d81..240eb0a5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub mod services; pub use client::Mpesa; pub use constants::{ CommandId, IdentifierTypes, Invoice, InvoiceItem, ResponseType, SendRemindersTypes, + TransactionType, }; pub use environment::ApiEnvironment; pub use environment::Environment::{self, Production, Sandbox}; diff --git a/src/services/dynamic_qr.rs b/src/services/dynamic_qr.rs new file mode 100644 index 000000000..25d2ce9ee --- /dev/null +++ b/src/services/dynamic_qr.rs @@ -0,0 +1,158 @@ +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::client::Mpesa; +use crate::constants::TransactionType; +use crate::environment::ApiEnvironment; +use crate::errors::{MpesaError, MpesaResult}; + +const DYNAMIC_QR_URL: &str = "/mpesa/qrcode/v1/generate"; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct DynamicQRRequest<'mpesa> { + /// Name of the Company/M-Pesa Merchant Name + pub merchant_name: &'mpesa str, + /// Transaction Reference Number + pub ref_no: &'mpesa str, + /// The total amount of the transaction + pub amount: f64, + #[serde(rename = "TrxCode")] + /// Transaction Type + /// + /// This can be a `TransactionType` or a `&str` + /// The `&str` must be one of the following: + /// - `BG` for Buy Goods + /// - `PB` for Pay Bill + /// - `WA` Withdraw Cash + /// - `SM` Send Money (Mobile Number) + /// - `SB` Sent to Business. Business number CPI in MSISDN format. + pub transaction_type: TransactionType, + ///Credit Party Identifier. + /// + /// Can be a Mobile Number, Business Number, Agent + /// Till, Paybill or Business number, or Merchant Buy Goods. + #[serde(rename = "CPI")] + pub credit_party_identifier: &'mpesa str, + /// Size of the QR code image in pixels. + /// + /// QR code image will always be a square image. + pub size: &'mpesa str, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct DynamicQRResponse { + #[serde(rename(deserialize = "QRCode"))] + pub qr_code: String, + pub response_code: String, + pub response_description: String, +} + +/// Dynamic QR builder struct +#[derive(Builder, Debug, Clone)] +#[builder(build_fn(error = "MpesaError"))] +pub struct DynamicQR<'mpesa, Env: ApiEnvironment> { + #[builder(pattern = "immutable")] + client: &'mpesa Mpesa, + /// Name of the Company/M-Pesa Merchant Name + #[builder(setter(into))] + merchant_name: &'mpesa str, + /// Transaction Reference Number + #[builder(setter(into))] + amount: f64, + /// The total amount of the transaction + ref_no: &'mpesa str, + /// Transaction Type + /// + /// This can be a `TransactionType` or a `&str` + /// The `&str` must be one of the following: + /// - `BG` for Buy Goods + /// - `PB` for Pay Bill + /// - `WA` Withdraw Cash + /// - `SM` Send Money (Mobile Number) + /// - `SB` Sent to Business. Business number CPI in MSISDN format. + #[builder(try_setter, setter(into))] + transaction_type: TransactionType, + /// Credit Party Identifier. + /// Can be a Mobile Number, Business Number, Agent + /// Till, Paybill or Business number, or Merchant Buy Goods. + #[builder(setter(into))] + credit_party_identifier: &'mpesa str, + /// Size of the QR code image in pixels. + /// + /// QR code image will always be a square image. + #[builder(setter(into))] + size: &'mpesa str, +} + +impl<'mpesa, Env: ApiEnvironment> From> for DynamicQRRequest<'mpesa> { + fn from(express: DynamicQR<'mpesa, Env>) -> DynamicQRRequest<'mpesa> { + DynamicQRRequest { + merchant_name: express.merchant_name, + ref_no: express.ref_no, + amount: express.amount, + transaction_type: express.transaction_type, + credit_party_identifier: express.credit_party_identifier, + size: express.size, + } + } +} + +impl<'mpesa, Env: ApiEnvironment> DynamicQR<'mpesa, Env> { + pub(crate) fn builder(client: &'mpesa Mpesa) -> DynamicQRBuilder<'mpesa, Env> { + DynamicQRBuilder::default().client(client) + } + + /// # Build Dynamic QR + /// + /// Returns a `DynamicQR` which can be used to send a request + pub fn from_request( + client: &'mpesa Mpesa, + request: DynamicQRRequest<'mpesa>, + ) -> DynamicQR<'mpesa, Env> { + DynamicQR { + client, + merchant_name: request.merchant_name, + ref_no: request.ref_no, + amount: request.amount, + transaction_type: request.transaction_type, + credit_party_identifier: request.credit_party_identifier, + size: request.size, + } + } + + /// # Generate a Dynamic QR + /// + /// This enables Safaricom M-PESA customers who + /// have My Safaricom App or M-PESA app, to scan a QR (Quick Response) + /// code, to capture till number and amount then authorize to pay for goods + /// and services at select LIPA NA M-PESA (LNM) merchant outlets. + /// + /// # Response + /// A successful request returns a `DynamicQRResponse` type + /// which contains the QR code + /// + /// # Errors + /// Returns a `MpesaError` on failure + pub async fn send(self) -> MpesaResult { + let url = format!("{}{}", self.client.environment.base_url(), DYNAMIC_QR_URL); + + let response = self + .client + .http_client + .post(&url) + .bearer_auth(self.client.auth().await?) + .json::(&self.into()) + .send() + .await?; + + if response.status().is_success() { + let value = response.json::<_>().await?; + return Ok(value); + } + + let value = response.json().await?; + Err(MpesaError::MpesaDynamicQrError(value)) + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs index 3ea340f88..b688f819f 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -13,6 +13,8 @@ //! 6. [Mpesa Express/ STK Push](https://developer.safaricom.co.ke/docs#lipa-na-m-pesa-online-payment) //! 7. [Transaction Reversal](https://developer.safaricom.co.ke/docs#reversal) //! 8. [Bill Manager](https://developer.safaricom.co.ke/APIs/BillManager) +//! 9. [Transaction Status](https://developer.safaricom.co.ke/docs#transaction-status) +//! 10. [Dynamic QR](https://developer.safaricom.co.ke/APIs/DynamicQRCode) mod account_balance; mod b2b; @@ -20,6 +22,7 @@ mod b2c; mod bill_manager; mod c2b_register; mod c2b_simulate; +mod dynamic_qr; mod express_request; mod transaction_reversal; mod transaction_status; @@ -36,6 +39,8 @@ pub use bill_manager::*; pub use c2b_register::{C2bRegisterBuilder, C2bRegisterResponse}; #[cfg(feature = "c2b_simulate")] 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::{MpesaExpressRequestBuilder, MpesaExpressRequestResponse}; #[cfg(feature = "transaction_reversal")] diff --git a/tests/mpesa-rust/dynamic_qr_tests.rs b/tests/mpesa-rust/dynamic_qr_tests.rs new file mode 100644 index 000000000..710fbe5de --- /dev/null +++ b/tests/mpesa-rust/dynamic_qr_tests.rs @@ -0,0 +1,82 @@ +use mpesa::services::{DynamicQR, DynamicQRRequest}; +use serde_json::json; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, ResponseTemplate}; + +use crate::get_mpesa_client; + +#[tokio::test] +async fn dynamic_qr_code_test_using_builder_pattern() { + let (client, server) = get_mpesa_client!(); + + let sample_response_body = json!({ + "QRCode": "A3F7B1H", + "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0" + }); + + Mock::given(method("POST")) + .and(path("/mpesa/qrcode/v1/generate")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(1) + .mount(&server) + .await; + + let response = client + .dynamic_qr() + .amount(2000) + .credit_party_identifier("17408") + .merchant_name("SafaricomLTD") + .ref_no("rf38f04") + .size("300") + .try_transaction_type("bg") + //.transaction_type(TransactionType::BuyGoods) // This is the same as the above + .unwrap() + .build() + .unwrap() + .send() + .await + .unwrap(); + + assert_eq!( + response.response_description, + "Accept the service request successfully." + ); + assert_eq!(response.response_code, "0"); +} + +#[tokio::test] +async fn dynamic_qr_code_test_using_struct_initialization() { + let (client, server) = get_mpesa_client!(); + + let sample_response_body = json!({ + "QRCode": "A3F7B1H", + "ResponseDescription": "Accept the service request successfully.", + "ResponseCode": "0" + }); + + let request = DynamicQRRequest { + amount: 2000.0, + credit_party_identifier: "17408", + merchant_name: "SafaricomLTD", + ref_no: "rf38f04", + size: "300", + transaction_type: "bg".try_into().unwrap(), + }; + + Mock::given(method("POST")) + .and(path("/mpesa/qrcode/v1/generate")) + .respond_with(ResponseTemplate::new(200).set_body_json(sample_response_body)) + .expect(1) + .mount(&server) + .await; + + let response = DynamicQR::from_request(&client, request); + let response = response.send().await.unwrap(); + + assert_eq!( + response.response_description, + "Accept the service request successfully." + ); + assert_eq!(response.response_code, "0"); +} diff --git a/tests/mpesa-rust/main.rs b/tests/mpesa-rust/main.rs index 644d95585..ed859273a 100644 --- a/tests/mpesa-rust/main.rs +++ b/tests/mpesa-rust/main.rs @@ -10,7 +10,8 @@ mod bill_manager_test; mod c2b_register_test; #[cfg(test)] mod c2b_simulate_test; -#[cfg(test)] + +mod dynamic_qr_tests; mod helpers; #[cfg(test)] mod stk_push_test; From 06ecc388f3aefc784223c40cf3f14d0ca8884ff6 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 14 Nov 2023 17:52:55 +0300 Subject: [PATCH 137/140] Fix auth response deserializion errors --- Cargo.toml | 1 + src/auth.rs | 8 +++++--- tests/mpesa-rust/helpers.rs | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 461944b2a..d437f958a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ serde_repr = "0.1" thiserror = "1.0.37" wiremock = "0.5" secrecy = "0.8.0" +serde-aux = "4.2.0" [dev-dependencies] dotenv = "0.15" diff --git a/src/auth.rs b/src/auth.rs index dcaffa944..4392d3073 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,5 +1,6 @@ use cached::proc_macro::cached; use serde::{Deserialize, Serialize}; +use serde_aux::field_attributes::deserialize_number_from_string; use crate::{ApiEnvironment, ApiError, Mpesa, MpesaError, MpesaResult}; @@ -39,7 +40,8 @@ pub struct AuthenticationResponse { /// Access token which is used as the Bearer-Auth-Token pub access_token: String, /// Expiry time in seconds - pub expiry_in: u64, + #[serde(deserialize_with = "deserialize_number_from_string")] + pub expires_in: u64, } impl std::fmt::Display for AuthenticationResponse { @@ -47,7 +49,7 @@ impl std::fmt::Display for AuthenticationResponse { write!( f, "token :{} expires in: {}", - self.access_token, self.expiry_in + self.access_token, self.expires_in ) } } @@ -97,7 +99,7 @@ mod tests { .respond_with(wiremock::ResponseTemplate::new(200).set_body_json( AuthenticationResponse { access_token: "test_token".to_string(), - expiry_in: 3600, + expires_in: 3600, }, )) .expect(1) diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index cc3a589be..cc332888a 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -46,7 +46,7 @@ macro_rules! get_mpesa_client { .and(query_param("grant_type", "client_credentials")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "access_token": "dummy_access_token", - "expiry_in": 3600 + "expires_in": 3600 }))) .mount(&server) .await; @@ -73,7 +73,7 @@ macro_rules! get_mpesa_client { .and(query_param("grant_type", "client_credentials")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "access_token": "dummy_access_token", - "expiry_in": 3600 + "expires_in": "3600" }))) .mount(&server) .await; From abed4e858682dd76420705dfb1c38cccd2f3c3a2 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 14 Nov 2023 18:15:21 +0300 Subject: [PATCH 138/140] Add missing serde rename attribute --- src/errors.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/errors.rs b/src/errors.rs index cb3bed48a..6b2489d73 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -57,6 +57,7 @@ pub enum MpesaError { pub type MpesaResult = Result; #[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] pub struct ApiError { pub request_id: String, pub error_code: String, From 84448a4221d35773f114ff63af123ed52dd509f8 Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 14 Nov 2023 18:18:05 +0300 Subject: [PATCH 139/140] Maintain consistency with auth response in helpers --- tests/mpesa-rust/helpers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mpesa-rust/helpers.rs b/tests/mpesa-rust/helpers.rs index cc332888a..8626b03f3 100644 --- a/tests/mpesa-rust/helpers.rs +++ b/tests/mpesa-rust/helpers.rs @@ -46,7 +46,7 @@ macro_rules! get_mpesa_client { .and(query_param("grant_type", "client_credentials")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "access_token": "dummy_access_token", - "expires_in": 3600 + "expires_in": "3600" }))) .mount(&server) .await; From effc098ba8f8e8a1c3e4a56b21d09cdbd83fde7b Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Tue, 14 Nov 2023 18:38:46 +0300 Subject: [PATCH 140/140] Bump crate version --- Cargo.toml | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d437f958a..4986fbe8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mpesa" -version = "1.0.0" +version = "1.1.0" authors = ["Collins Muriuki "] edition = "2021" description = "A wrapper around the M-PESA API in Rust." diff --git a/README.md b/README.md index 7a48132cb..1e5cb9c61 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ An unofficial Rust wrapper around the [Safaricom API](https://developer.safarico ```toml [dependencies] -mpesa = { version = "1.0.0" } +mpesa = { version = "1.1.0" } ``` Optionally, you can disable default-features, which is basically the entire suite of MPESA APIs to conditionally select from either: @@ -34,7 +34,7 @@ Example: ```toml [dependencies] -mpesa = { git = "1.0.0", default_features = false, features = ["b2b", "express_request"] } +mpesa = { git = "1.1.0", default_features = false, features = ["b2b", "express_request"] } ``` In your lib or binary crate: