"]
+edition = "2021"
+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]
+cached = { version = "0.46", features = ["wasm", "async", "proc_macro"] }
+chrono = { version = "0.4", optional = true, default-features = false, features = [
+ "clock",
+ "serde",
+] }
+openssl = { version = "0.10", optional = true }
+reqwest = { version = "0.11", features = ["json"] }
+derive_builder = "0.12"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+serde_repr = "0.1"
+thiserror = "1.0.37"
+wiremock = "0.5"
+secrecy = "0.8.0"
+serde-aux = "4.2.0"
+
+[dev-dependencies]
+dotenv = "0.15"
+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",
+ "dynamic_qr"
+]
+dynamic_qr = []
+account_balance = ["dep:openssl"]
+b2b = ["dep:openssl"]
+b2c = ["dep:openssl"]
+bill_manager = ["dep:chrono"]
+c2b_register = []
+c2b_simulate = []
+express_request = ["dep:chrono"]
+transaction_reversal = ["dep:openssl"]
+transaction_status = ["dep:openssl"]
diff --git a/README.md b/README.md
index d6bea7953..1e5cb9c61 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,8 @@
# mpesa-rust
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+[![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
@@ -25,20 +14,32 @@ An unofficial Rust wrapper around the [Safaricom API](https://developer.safarico
```toml
[dependencies]
-mpesa = "0.4.2"
+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 `["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`
+- `transaction_reversal`
+- `transaction_status`
+- `bill_manager`
+- `dynamic_qr`
+
Example:
```toml
[dependencies]
-mpesa = { version = "0.4.2", 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:
-```rs
+```rust,ignore
use mpesa::Mpesa;
```
@@ -51,65 +52,109 @@ 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
+```rust,ignore
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,
);
-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()`
-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,ignore
+use mpesa::{Mpesa, Environment};
+use std::str::FromStr;
+use std::convert::TryFrom;
+
+let client0 = Mpesa::new(
+ env!("CLIENT_KEY"),
+ 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!(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
-use mpesa::Mpesa;
-use std::env;
+pub trait ApiEnvironment {
+ fn base_url(&self) -> &str;
+ fn get_certificate(&self) -> &str;
+}
+```
-let client = Mpesa::new(
- env::var("CLIENT_KEY")?,
- env::var("CLIENT_SECRET")?,
- "sandbox".parse()?, // "production"
+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 (and [here](./src/environment.rs) so see how the trait is implemented for the `Environment` enum):
+
+```rust,ignore
+use mpesa::{Mpesa, ApiEnvironment};
+use std::str::FromStr;
+use std::convert::TryFrom;
+
+pub struct MyCustomEnvironment;
+
+impl ApiEnvironment for MyCustomEnvironment {
+ fn base_url(&self) -> &str {
+ // your base url here
+ "https://your_base_url.com"
+ }
+
+ fn get_certificate(&self) -> &str {
+ // your certificate here
+ r#"..."#
+ }
+}
+
+let client: Mpesa = Mpesa::new(
+ env!("CLIENT_KEY"),
+ env!("CLIENT_SECRET"),
+ MyCustomEnvironment // ✔ valid
);
-assert!(client.is_connected())
+
+//...
```
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,ignore
use mpesa::Mpesa;
-use std::env;
let client = Mpesa::new(
- env::var("CLIENT_KEY")?,
- env::var("CLIENT_SECRET")?,
- "production".parse()?,
+ env!("CLIENT_KEY"),
+ env!("CLIENT_SECRET"),
+ Environment::Sandbox,
);
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
+- B2C
-```rust
+```rust,ignore
let response = client
.b2c("testapi496")
.party_a("600496")
@@ -117,13 +162,14 @@ let response = client
.result_url("https://testdomain.com/ok")
.timeout_url("https://testdomain.com/err")
.amount(1000)
- .send();
+ .send()
+ .await;
assert!(response.is_ok())
```
-- B2B
+- B2B
-```rust
+```rust,ignore
let response = client
.b2b("testapi496")
.party_a("600496")
@@ -132,56 +178,216 @@ let response = client
.timeout_url("https://testdomain.com/err")
.account_ref("254708374149")
.amount(1000)
- .send();
+ .send()
+ .await;
assert!(response.is_ok())
```
-- C2B Register
+- C2B Register
-```rust
+```rust,ignore
let response = client
.c2b_register()
.short_code("600496")
.confirmation_url("https://testdomain.com/true")
.validation_url("https://testdomain.com/valid")
- .send();
+ .send()
+ .await;
assert!(response.is_ok())
```
-- C2B Simulate
-
-```rust
+- C2B Simulate
+```rust,ignore
let response = client
.c2b_simulate()
.short_code("600496")
.msisdn("254700000000")
.amount(1000)
- .send();
+ .send()
+ .await;
assert!(response.is_ok())
```
-- Account Balance
+- Account Balance
-```rust
+```rust,ignore
let response = client
.account_balance("testapi496")
.result_url("https://testdomain.com/ok")
.timeout_url("https://testdomain.com/err")
.party_a("600496")
- .send();
+ .send()
+ .await;
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
+```rust,ignore
let response = client
.express_request("174379")
.phone_number("254708374149")
.amount(500)
.callback_url("https://test.example.com/api")
- .send();
+ .send()
+ .await;
+assert!(response.is_ok())
+```
+
+- Transaction Reversal:
+
+```rust,ignore
+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,ignore
+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())
+```
+
+- 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())
+```
+
+- 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())
```
@@ -191,12 +397,12 @@ More will be added progressively, pull requests welcome
**Collins Muriuki**
-- Twitter: [@collinsmuriuki\_](https://twitter.com/collinsmuriuki_)
-- Not affiliated with Safaricom.
+- Twitter: [@collinsmuriuki\_](https://twitter.com/collinsmuriuki_)
+- Not affiliated with Safaricom.
## 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 © 2021 [Collins Muriuki](https://github.com/collinsmuriuki).
-This project is [MIT](LICENSE) licensed.
+Copyright © 2023 [Collins Muriuki](https://github.com/collinsmuriuki).
+This project is [MIT](https://raw.githubusercontent.com/collinsmuriuki/mpesa-rust/master/LICENSE) licensed.
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/client.rs b/mpesa_core/src/client.rs
deleted file mode 100644
index 94bcb3d7b..000000000
--- a/mpesa_core/src/client.rs
+++ /dev/null
@@ -1,269 +0,0 @@
-use super::environment::Environment;
-use super::services::{
- AccountBalanceBuilder, B2bBuilder, B2cBuilder, C2bRegisterBuilder, C2bSimulateBuilder,
- MpesaExpressRequestBuilder,
-};
-use crate::MpesaSecurity;
-use mpesa_derive::*;
-use reqwest::blocking::Client;
-use serde_json::Value;
-use std::cell::RefCell;
-
-/// `Result` enum type alias
-pub type MpesaResult = Result;
-
-/// Mpesa client that will facilitate communication with the Safaricom API
-#[derive(Debug, MpesaSecurity)]
-pub struct Mpesa {
- client_key: String,
- client_secret: String,
- initiator_password: RefCell