Skip to content

Commit

Permalink
feat: add dynamic qr code
Browse files Browse the repository at this point in the history
  • Loading branch information
itsyaasir committed Nov 10, 2023
1 parent e373905 commit 7b4853a
Show file tree
Hide file tree
Showing 38 changed files with 376 additions and 70 deletions.
31 changes: 24 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,40 @@ 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"]}
chrono = { version = "0.4", optional = true, default-features = false, features = [
"clock",
"serde",
] }
derive_builder = "0.12.0"
openssl = { version = "0.10", optional = true }
reqwest = { version = "0.11", features = ["json"] }
secrecy = "0.8.0"
serde = {version="1.0", features= ["derive"]}
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"
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",
"dynamic_qr",
]

dynamic_qr = []
account_balance = ["dep:openssl"]
b2b = ["dep:openssl"]
b2c = ["dep:openssl"]
Expand All @@ -35,4 +52,4 @@ c2b_register = []
c2b_simulate = []
express_request = ["dep:chrono"]
transaction_reversal = ["dep:openssl"]
transaction_status= ["dep:openssl"]
transaction_status = ["dep:openssl"]
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down
53 changes: 42 additions & 11 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
use crate::environment::ApiEnvironment;
use crate::services::{
AccountBalanceBuilder, B2bBuilder, B2cBuilder, BulkInvoiceBuilder, C2bRegisterBuilder,
C2bSimulateBuilder, CancelInvoiceBuilder, MpesaExpressRequestBuilder, OnboardBuilder,
OnboardModifyBuilder, ReconciliationBuilder, SingleInvoiceBuilder, TransactionReversalBuilder,
TransactionStatusBuilder,
};
use crate::{ApiError, MpesaError};
use std::cell::RefCell;

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;

use crate::environment::ApiEnvironment;
use crate::services::{
AccountBalanceBuilder, B2bBuilder, B2cBuilder, BulkInvoiceBuilder, C2bRegisterBuilder,
C2bSimulateBuilder, CancelInvoiceBuilder, DynamicQR, DynamicQRBuilder,
MpesaExpressRequestBuilder, OnboardBuilder, OnboardModifyBuilder, ReconciliationBuilder,
SingleInvoiceBuilder, TransactionReversalBuilder, TransactionStatusBuilder,
};
use crate::{ApiError, MpesaError};

/// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials)
const DEFAULT_INITIATOR_PASSWORD: &str = "Safcom496!";
Expand Down Expand Up @@ -501,6 +503,35 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa<Env> {
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.
Expand Down Expand Up @@ -529,9 +560,8 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa<Env> {

#[cfg(test)]
mod tests {
use crate::Sandbox;

use super::*;
use crate::Sandbox;

#[test]
fn test_setting_initator_password() {
Expand All @@ -541,6 +571,7 @@ mod tests {
assert_eq!(client.initiator_password(), "foo_bar".to_string());
}

#[derive(Clone)]
struct TestEnvironment;

impl ApiEnvironment for TestEnvironment {
Expand Down
40 changes: 39 additions & 1 deletion src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
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};

use crate::MpesaError;

/// Mpesa command ids
#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -140,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<Self, Self::Error> {
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")),
}
}
}
6 changes: 4 additions & 2 deletions src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,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;
}
Expand Down
6 changes: 5 additions & 1 deletion src/errors.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -34,6 +36,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")]
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod services;
pub use client::{Mpesa, MpesaResult};
pub use constants::{
CommandId, IdentifierTypes, Invoice, InvoiceItem, ResponseType, SendRemindersTypes,
TransactionType,
};
pub use environment::ApiEnvironment;
pub use environment::Environment::{self, Production, Sandbox};
Expand Down
3 changes: 2 additions & 1 deletion src/services/account_balance.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};

use crate::client::MpesaResult;
use crate::constants::{CommandId, IdentifierTypes};
use crate::environment::ApiEnvironment;
use crate::{Mpesa, MpesaError};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
/// Account Balance payload
Expand Down
3 changes: 2 additions & 1 deletion src/services/b2b.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};

use crate::client::{Mpesa, MpesaResult};
use crate::constants::{CommandId, IdentifierTypes};
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
struct B2bPayload<'mpesa> {
Expand Down
3 changes: 2 additions & 1 deletion src/services/b2c.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};

use crate::client::MpesaResult;
use crate::environment::ApiEnvironment;
use crate::{CommandId, Mpesa, MpesaError};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
/// Payload to allow for b2c transactions:
Expand Down
3 changes: 2 additions & 1 deletion src/services/bill_manager/bulk_invoice.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::Deserialize;

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 {
Expand Down
3 changes: 2 additions & 1 deletion src/services/bill_manager/cancel_invoice.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};

use crate::client::{Mpesa, MpesaResult};
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
Expand Down
3 changes: 2 additions & 1 deletion src/services/bill_manager/onboard.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};

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.
Expand Down
3 changes: 2 additions & 1 deletion src/services/bill_manager/onboard_modify.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};

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.
Expand Down
5 changes: 3 additions & 2 deletions src/services/bill_manager/reconciliation.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use chrono::prelude::{DateTime, Utc};
use serde::{Deserialize, Serialize};

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")]
Expand Down
5 changes: 3 additions & 2 deletions src/services/bill_manager/single_invoice.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use chrono::prelude::{DateTime, Utc};
use serde::Deserialize;

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 {
Expand Down
3 changes: 2 additions & 1 deletion src/services/c2b_register.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};

use crate::client::{Mpesa, MpesaResult};
use crate::constants::ResponseType;
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
/// Payload to register the 3rd party’s confirmation and validation URLs to M-Pesa
Expand Down
3 changes: 2 additions & 1 deletion src/services/c2b_simulate.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};

use crate::client::{Mpesa, MpesaResult};
use crate::constants::CommandId;
use crate::environment::ApiEnvironment;
use crate::errors::MpesaError;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
/// Payload to make payment requests from C2B.
Expand Down
Loading

0 comments on commit 7b4853a

Please sign in to comment.