Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add dynamic qr code #80

Merged
merged 7 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
31 changes: 29 additions & 2 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::env::VarError;
use std::fmt;

use serde::{Deserialize, Serialize};
use std::{env::VarError, fmt};
use thiserror::Error;

/// Mpesa error stack
#[derive(thiserror::Error, Debug)]
#[derive(Error, Debug)]
pub enum MpesaError {
#[error("{0}")]
AuthenticationError(ApiError),
Expand Down Expand Up @@ -34,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")]
Expand All @@ -44,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),
}

#[derive(Debug, Serialize, Deserialize)]
Expand All @@ -62,3 +69,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<String> for BuilderError {
fn from(s: String) -> Self {
Self::ValidationError(s)
}
}

impl From<derive_builder::UninitializedFieldError> for MpesaError {
fn from(e: derive_builder::UninitializedFieldError) -> Self {
Self::BuilderError(BuilderError::UninitializedField(e.field_name()))
}
}
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
Loading
Loading