From 7b4853a1552832fa6c75c894bcb33e7bc01b71a9 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Fri, 10 Nov 2023 08:52:10 +0300 Subject: [PATCH] feat: add dynamic qr code --- Cargo.toml | 31 ++++- README.md | 20 +++ src/client.rs | 53 +++++-- src/constants.rs | 40 +++++- src/environment.rs | 6 +- src/errors.rs | 6 +- src/lib.rs | 1 + src/services/account_balance.rs | 3 +- src/services/b2b.rs | 3 +- src/services/b2c.rs | 3 +- src/services/bill_manager/bulk_invoice.rs | 3 +- src/services/bill_manager/cancel_invoice.rs | 3 +- src/services/bill_manager/onboard.rs | 3 +- src/services/bill_manager/onboard_modify.rs | 3 +- src/services/bill_manager/reconciliation.rs | 5 +- src/services/bill_manager/single_invoice.rs | 5 +- src/services/c2b_register.rs | 3 +- src/services/c2b_simulate.rs | 3 +- src/services/dynamic_qr.rs | 131 ++++++++++++++++++ src/services/express_request.rs | 7 +- src/services/mod.rs | 5 + 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/dynamic_qr_tests.rs | 45 ++++++ tests/mpesa-rust/main.rs | 3 +- tests/mpesa-rust/stk_push_test.rs | 3 +- tests/mpesa-rust/transaction_reversal_test.rs | 3 +- tests/mpesa-rust/transaction_status_test.rs | 1 - 38 files changed, 376 insertions(+), 70 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 9b7707636..0ad127531 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,15 @@ 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" @@ -22,11 +26,24 @@ 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"] @@ -35,4 +52,4 @@ c2b_register = [] c2b_simulate = [] express_request = ["dep:chrono"] transaction_reversal = ["dep:openssl"] -transaction_status= ["dep:openssl"] +transaction_status = ["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 3a9640e4b..be66d366e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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!"; @@ -501,6 +503,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. @@ -529,9 +560,8 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa { #[cfg(test)] mod tests { - use crate::Sandbox; - use super::*; + use crate::Sandbox; #[test] fn test_setting_initator_password() { @@ -541,6 +571,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 418ad5a52..756fdc996 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -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)] @@ -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 { + 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 eb6f1f579..84ee51c86 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 @@ -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; } diff --git a/src/errors.rs b/src/errors.rs index 2adc20891..8ca6edcdc 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)] @@ -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")] diff --git a/src/lib.rs b/src/lib.rs index 7a9e84138..3443785fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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}; diff --git a/src/services/account_balance.rs b/src/services/account_balance.rs index b3d7ba4d4..b59bf8b4f 100644 --- a/src/services/account_balance.rs +++ b/src/services/account_balance.rs @@ -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 diff --git a/src/services/b2b.rs b/src/services/b2b.rs index 08746109e..aaaab1341 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -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> { diff --git a/src/services/b2c.rs b/src/services/b2c.rs index 6ae46cfdc..6489ef329 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -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: diff --git a/src/services/bill_manager/bulk_invoice.rs b/src/services/bill_manager/bulk_invoice.rs index 15618b9b1..6eeee012c 100644 --- a/src/services/bill_manager/bulk_invoice.rs +++ b/src/services/bill_manager/bulk_invoice.rs @@ -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 { diff --git a/src/services/bill_manager/cancel_invoice.rs b/src/services/bill_manager/cancel_invoice.rs index aa159dd30..2b6c23641 100644 --- a/src/services/bill_manager/cancel_invoice.rs +++ b/src/services/bill_manager/cancel_invoice.rs @@ -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")] diff --git a/src/services/bill_manager/onboard.rs b/src/services/bill_manager/onboard.rs index 9bb6f20dd..a6456c9d3 100644 --- a/src/services/bill_manager/onboard.rs +++ b/src/services/bill_manager/onboard.rs @@ -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. diff --git a/src/services/bill_manager/onboard_modify.rs b/src/services/bill_manager/onboard_modify.rs index 36ff7e069..fecb33b03 100644 --- a/src/services/bill_manager/onboard_modify.rs +++ b/src/services/bill_manager/onboard_modify.rs @@ -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. diff --git a/src/services/bill_manager/reconciliation.rs b/src/services/bill_manager/reconciliation.rs index baf470b6f..59a7d6842 100644 --- a/src/services/bill_manager/reconciliation.rs +++ b/src/services/bill_manager/reconciliation.rs @@ -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")] diff --git a/src/services/bill_manager/single_invoice.rs b/src/services/bill_manager/single_invoice.rs index 5ce438bc0..a04c774d3 100644 --- a/src/services/bill_manager/single_invoice.rs +++ b/src/services/bill_manager/single_invoice.rs @@ -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 { diff --git a/src/services/c2b_register.rs b/src/services/c2b_register.rs index fe214e7e6..3e4ad85a7 100644 --- a/src/services/c2b_register.rs +++ b/src/services/c2b_register.rs @@ -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 diff --git a/src/services/c2b_simulate.rs b/src/services/c2b_simulate.rs index b9a41fdb6..157642ecc 100644 --- a/src/services/c2b_simulate.rs +++ b/src/services/c2b_simulate.rs @@ -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. diff --git a/src/services/dynamic_qr.rs b/src/services/dynamic_qr.rs new file mode 100644 index 000000000..ac82123b6 --- /dev/null +++ b/src/services/dynamic_qr.rs @@ -0,0 +1,131 @@ +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::client::{Mpesa, MpesaResult}; +use crate::constants::TransactionType; +use crate::environment::ApiEnvironment; +use crate::errors::MpesaError; + +const DYNAMIC_QR_URL: &str = "/mpesa/qrcode/v1/generate"; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "PascalCase")] +struct DynamicQRRequest<'mpesa> { + /// Name of the Company/M-Pesa Merchant Name + merchant_name: &'mpesa str, + /// Transaction Reference Number + ref_no: &'mpesa str, + /// The total amount of the transaction + amount: f64, + #[serde(rename = "TrxCode")] + /// Transaction Type + transaction_type: TransactionType, + #[serde(rename = "CPI")] + + ///Credit Party Identifier. + /// + /// Can be a Mobile Number, Business Number, Agent + /// Till, Paybill or Business number, or Merchant Buy Goods. + credit_party_identifier: &'mpesa str, + /// Size of the QR code image in pixels. + /// + /// QR code image will always be a square image. + 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)] +pub struct DynamicQR<'mpesa, Env: ApiEnvironment> { + #[builder(pattern = "owned")] + client: &'mpesa Mpesa, + #[builder(setter(into))] + /// Name of the Company/M-Pesa Merchant Name + merchant_name: &'mpesa str, + #[builder(setter(into))] + /// Transaction Reference Number + amount: f64, + /// The total amount of the transaction + ref_no: &'mpesa str, + #[builder(try_setter, setter(into))] + /// 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. + transaction_type: TransactionType, + /// Credit Party Identifier. + /// Can be a Mobile Number, Business Number, Agent + /// Till, Paybill or Business number, or Merchant Buy Goods. + 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) + } + + /// # 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/express_request.rs b/src/services/express_request.rs index a08ce2d16..74749bbdb 100644 --- a/src/services/express_request.rs +++ b/src/services/express_request.rs @@ -1,10 +1,11 @@ +use chrono::prelude::Local; +use openssl::base64; +use serde::{Deserialize, Serialize}; + 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}; /// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials) static DEFAULT_PASSKEY: &str = "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919"; diff --git a/src/services/mod.rs b/src/services/mod.rs index 3ea340f88..f6b3e922b 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, DynamicQRResponse}; #[cfg(feature = "express_request")] pub use express_request::{MpesaExpressRequestBuilder, MpesaExpressRequestResponse}; #[cfg(feature = "transaction_reversal")] 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/dynamic_qr_tests.rs b/tests/mpesa-rust/dynamic_qr_tests.rs new file mode 100644 index 000000000..cd02a0321 --- /dev/null +++ b/tests/mpesa-rust/dynamic_qr_tests.rs @@ -0,0 +1,45 @@ +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() { + 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") + // .try_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"); +} 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; 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};