From 63904a785bd90686ec0c4f7618eb1eeca5bed8c3 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Mon, 8 Jul 2024 18:29:24 +0300 Subject: [PATCH] feat(service): Migrate b2c, b2b to the builder pattern --- src/client.rs | 14 +- src/services/b2b.rs | 268 ++++++++++------------------------- src/services/b2c.rs | 224 ++++++++--------------------- src/services/mod.rs | 16 +-- tests/mpesa-rust/b2b_test.rs | 63 ++++---- tests/mpesa-rust/b2c_test.rs | 93 ++++++------ 6 files changed, 225 insertions(+), 453 deletions(-) diff --git a/src/client.rs b/src/client.rs index 5b804b635..07edcb83b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -13,9 +13,9 @@ use serde::Serialize; use crate::auth::AUTH; use crate::environment::ApiEnvironment; use crate::services::{ - AccountBalanceBuilder, B2bBuilder, B2cBuilder, BulkInvoiceBuilder, C2bRegisterBuilder, - C2bSimulateBuilder, CancelInvoiceBuilder, DynamicQR, DynamicQRBuilder, MpesaExpress, - MpesaExpressBuilder, MpesaExpressQuery, MpesaExpressQueryBuilder, OnboardBuilder, + AccountBalanceBuilder, B2b, B2bBuilder, B2c, B2cBuilder, BulkInvoiceBuilder, + C2bRegisterBuilder, C2bSimulateBuilder, CancelInvoiceBuilder, DynamicQR, DynamicQRBuilder, + MpesaExpress, MpesaExpressBuilder, MpesaExpressQuery, MpesaExpressQueryBuilder, OnboardBuilder, OnboardModifyBuilder, ReconciliationBuilder, SingleInvoiceBuilder, TransactionReversal, TransactionReversalBuilder, TransactionStatusBuilder, }; @@ -172,14 +172,14 @@ impl Mpesa { #[cfg(feature = "b2c")] #[doc = include_str!("../docs/client/b2c.md")] - pub fn b2c<'a>(&'a self, initiator_name: &'a str) -> B2cBuilder { - B2cBuilder::new(self, initiator_name) + pub fn b2c(&self) -> B2cBuilder { + B2c::builder(self) } #[cfg(feature = "b2b")] #[doc = include_str!("../docs/client/b2b.md")] - pub fn b2b<'a>(&'a self, initiator_name: &'a str) -> B2bBuilder { - B2bBuilder::new(self, initiator_name) + pub fn b2b(&self) -> B2bBuilder { + B2b::builder(self) } #[cfg(feature = "bill_manager")] diff --git a/src/services/b2b.rs b/src/services/b2b.rs index 3a1f4621d..8d15b9b56 100644 --- a/src/services/b2b.rs +++ b/src/services/b2b.rs @@ -1,6 +1,6 @@ -#![doc = include_str!("../../docs/client/b2b.md")] - +use derive_builder::Builder; use serde::{Deserialize, Serialize}; +use url::Url; use crate::client::Mpesa; use crate::constants::{CommandId, IdentifierTypes}; @@ -9,192 +9,83 @@ use crate::errors::{MpesaError, MpesaResult}; const B2B_URL: &str = "mpesa/b2b/v1/paymentrequest"; #[derive(Debug, Serialize)] -struct B2bPayload<'mpesa> { - #[serde(rename(serialize = "Initiator"))] - initiator: &'mpesa str, - #[serde(rename(serialize = "SecurityCredential"))] - security_credential: &'mpesa str, - #[serde(rename(serialize = "CommandID"))] - command_id: CommandId, - #[serde(rename(serialize = "Amount"))] - amount: f64, - #[serde(rename(serialize = "PartyA"))] - party_a: &'mpesa str, - #[serde(rename(serialize = "SenderIdentifierType"))] - sender_identifier_type: &'mpesa str, - #[serde(rename(serialize = "PartyB"))] - party_b: &'mpesa str, +#[serde(rename_all = "PascalCase")] +pub struct B2bRequest { + /// The credential/ username used to authenticate the transaction request + pub initiator: String, + pub security_credential: String, + pub command_id: CommandId, + pub amount: f64, + pub party_a: String, + pub sender_identifier_type: IdentifierTypes, + pub party_b: String, #[serde(rename(serialize = "RecieverIdentifierType"))] - reciever_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 = "AccountReference"), - skip_serializing_if = "Option::is_none" - )] - account_reference: Option<&'mpesa str>, + pub reciever_identifier_type: IdentifierTypes, + pub remarks: Option, + #[serde(rename = "QueueTimeOutURL")] + pub queue_time_out_url: Url, + #[serde(rename = "ResultURL")] + pub result_url: Url, + #[serde(rename = "AccountReference")] + pub account_reference: String, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "PascalCase")] pub struct B2bResponse { #[serde(rename(deserialize = "ConversationID"))] pub conversation_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, } -#[derive(Debug)] /// B2B transaction builder struct -pub struct B2bBuilder<'mpesa> { - initiator_name: &'mpesa str, +#[derive(Builder, Debug, Clone)] +#[builder(build_fn(error = "MpesaError"))] +pub struct B2b<'mpesa> { + #[builder(pattern = "immutable")] client: &'mpesa Mpesa, - command_id: Option, - amount: Option, - party_a: Option<&'mpesa str>, - sender_id: Option, - party_b: Option<&'mpesa str>, - receiver_id: Option, - remarks: Option<&'mpesa str>, - queue_timeout_url: Option<&'mpesa str>, - result_url: Option<&'mpesa str>, - account_ref: Option<&'mpesa str>, + /// The credential/ username used to authenticate the transaction request + #[builder(setter(into))] + initiator_name: String, + /// The amount being transacted + #[builder(setter(into))] + amount: f64, + /// Organization's shortcode initiating the transaction + #[builder(setter(into))] + party_a: String, + /// Organization's shortcode receiving the funds + #[builder(setter(into))] + party_b: String, + /// The path that stores information of time out transaction + #[builder(try_setter, setter(into))] + queue_timeout_url: Url, + /// The path that stores information of transaction + #[builder(try_setter, setter(into))] + result_url: Url, + /// Unique identifier for the transaction + #[builder(setter(into))] + account_ref: String, + /// Type of organization sending the transaction + #[builder(default = "IdentifierTypes::ShortCode")] + sender_id: IdentifierTypes, + /// Type of organization receiving the funds + #[builder(default = "IdentifierTypes::ShortCode")] + receiver_id: IdentifierTypes, + /// Comments that are sent along with the transaction + #[builder(setter(into), default = "None")] + remarks: Option, + /// The type of operation + #[builder(default = "CommandId::BusinessToBusinessTransfer")] + command_id: CommandId, } -impl<'mpesa> B2bBuilder<'mpesa> { +impl<'mpesa> B2b<'mpesa> { /// Creates a new B2B builder - /// Requires an `initiator_name`, the credential/ username used to authenticate the transaction request - pub fn new(client: &'mpesa Mpesa, initiator_name: &'mpesa str) -> B2bBuilder<'mpesa> { - B2bBuilder { - client, - initiator_name, - amount: None, - party_a: None, - sender_id: None, - party_b: None, - receiver_id: None, - remarks: None, - queue_timeout_url: None, - result_url: None, - command_id: None, - account_ref: None, - } - } - - /// Adds the `CommandId`. Defaults to `CommandId::BusinessToBusinessTransfer` if not explicitly provided. - /// - /// # Errors - /// If invalid `CommandId` is provided - pub fn command_id(mut self, command_id: CommandId) -> B2bBuilder<'mpesa> { - self.command_id = Some(command_id); - self - } - - /// Adds `Party A` which is a required field - /// `Party A` should be a paybill number. - /// - /// # Errors - /// If `Party A` is invalid or not provided - pub fn party_a(mut self, party_a: &'mpesa str) -> B2bBuilder<'mpesa> { - self.party_a = Some(party_a); - self - } - - /// Adds `Party B` which is a required field - /// `Party B` should be a mobile number. - /// - /// # Errors - /// If `Party B` is invalid or not provided - pub fn party_b(mut self, party_b: &'mpesa str) -> B2bBuilder<'mpesa> { - self.party_b = Some(party_b); - self - } - - /// Adds `Party A` and `Party B`. Both are required fields - /// `Party A` should be a paybill number while `Party B` should be a mobile number. - /// - /// # Errors - /// If either `Party A` or `Party B` is invalid or not provided - #[deprecated] - pub fn parties(mut self, party_a: &'mpesa str, party_b: &'mpesa str) -> B2bBuilder<'mpesa> { - self.party_a = Some(party_a); - self.party_b = Some(party_b); - self - } - - // Adds `QueueTimeoutUrl` This is a required field - /// - /// # Error - /// If `QueueTimeoutUrl` is invalid or not provided - pub fn timeout_url(mut self, timeout_url: &'mpesa str) -> B2bBuilder<'mpesa> { - self.queue_timeout_url = Some(timeout_url); - 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) -> B2bBuilder<'mpesa> { - 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 - #[deprecated] - pub fn urls(mut self, timeout_url: &'mpesa str, result_url: &'mpesa str) -> B2bBuilder<'mpesa> { - // TODO: validate urls - self.queue_timeout_url = Some(timeout_url); - self.result_url = Some(result_url); - self - } - - /// Adds `sender_id`. Will default to `IdentifierTypes::ShortCode` if not explicitly provided - pub fn sender_id(mut self, sender_id: IdentifierTypes) -> B2bBuilder<'mpesa> { - 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<'mpesa> { - self.receiver_id = Some(receiver_id); - self - } - - /// Adds `account_ref`. This field is required - pub fn account_ref(mut self, account_ref: &'mpesa str) -> B2bBuilder<'mpesa> { - // TODO: add validation - self.account_ref = Some(account_ref); - self - } - - /// Adds an `amount` to the request - /// This is a required field - pub fn amount>(mut self, amount: Number) -> B2bBuilder<'mpesa> { - 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: &'mpesa str) -> B2bBuilder<'mpesa> { - self.remarks = Some(remarks); - self + pub(crate) fn builder(client: &'mpesa Mpesa) -> B2bBuilder<'mpesa> { + B2bBuilder::default().client(client) } /// # B2B API @@ -203,8 +94,7 @@ impl<'mpesa> B2bBuilder<'mpesa> { /// /// This API enables Business to Business (B2B) transactions between a business and another /// business. Use of this API requires a valid and verified B2B M-Pesa short code for the - /// business initiating the transaction and the both businesses involved in the transaction - /// See more [here](https://developer.safaricom.co.ke/docs?shell#b2b-api) + /// business initiating the transaction and the both businesses involved in the transaction. /// /// A successful request returns a `B2bResponse` type /// @@ -213,30 +103,16 @@ impl<'mpesa> B2bBuilder<'mpesa> { pub async fn send(self) -> MpesaResult { let credentials = self.client.gen_security_credentials()?; - let payload = B2bPayload { + let payload = B2bRequest { initiator: self.initiator_name, - security_credential: &credentials, - command_id: self - .command_id - .unwrap_or(CommandId::BusinessToBusinessTransfer), - 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(IdentifierTypes::ShortCode) - .to_string(), - party_b: self - .party_b - .ok_or(MpesaError::Message("party_b is required"))?, - reciever_identifier_type: &self - .receiver_id - .unwrap_or(IdentifierTypes::ShortCode) - .to_string(), - remarks: self.remarks.unwrap_or_else(|| stringify!(None)), + security_credential: credentials, + command_id: self.command_id, + amount: self.amount, + party_a: self.party_a, + sender_identifier_type: self.sender_id, + party_b: self.party_b, + reciever_identifier_type: self.receiver_id, + remarks: self.remarks, 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 87201b084..1f30ac393 100644 --- a/src/services/b2c.rs +++ b/src/services/b2c.rs @@ -1,167 +1,80 @@ #![doc = include_str!("../../docs/client/b2c.md")] +use derive_builder::Builder; use serde::{Deserialize, Serialize}; +use url::Url; use crate::{CommandId, Mpesa, MpesaError, MpesaResult}; const B2C_URL: &str = "mpesa/b2c/v1/paymentrequest"; #[derive(Debug, Serialize)] -/// Payload to allow for b2c transactions: -struct B2cPayload<'mpesa> { - #[serde(rename(serialize = "InitiatorName"))] - initiator_name: &'mpesa str, - #[serde(rename(serialize = "SecurityCredential"))] - security_credential: &'mpesa str, - #[serde(rename(serialize = "CommandID"))] - command_id: CommandId, - #[serde(rename(serialize = "Amount"))] - amount: f64, - #[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"))] - queue_time_out_url: &'mpesa str, - #[serde(rename(serialize = "ResultURL"))] - result_url: &'mpesa str, - #[serde(rename(serialize = "Occasion"))] - occasion: &'mpesa str, +#[serde(rename_all = "PascalCase")] +pub struct B2cRequest { + pub initiator_name: String, + pub security_credential: String, + pub command_id: CommandId, + pub amount: f64, + pub party_a: String, + pub party_b: String, + pub remarks: Option, + #[serde(rename = "QueueTimeOutURL")] + pub queue_time_out_url: Url, + #[serde(rename = "ResultURL")] + pub result_url: Url, + pub occasion: Option, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "PascalCase")] pub struct B2cResponse { #[serde(rename(deserialize = "ConversationID"))] pub conversation_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, } -#[derive(Debug)] /// B2C transaction builder struct -pub struct B2cBuilder<'mpesa> { - initiator_name: &'mpesa str, +#[derive(Builder, Debug, Clone)] +#[builder(build_fn(error = "MpesaError"))] +pub struct B2c<'mpesa> { + #[builder(pattern = "immutable")] client: &'mpesa Mpesa, - command_id: Option, - amount: Option, - 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>, + /// The credential/ username used to authenticate the transaction request + #[builder(setter(into))] + initiator_name: String, + /// The amount being transacted + #[builder(setter(into))] + amount: f64, + /// Organization's shortcode initiating the transaction + #[builder(setter(into))] + party_a: String, + /// Phone number receiving the transaction + #[builder(setter(into))] + party_b: String, + /// The path that stores information of time out transaction + #[builder(try_setter, setter(into))] + queue_timeout_url: Url, + /// The path that stores information of transaction + #[builder(try_setter, setter(into))] + result_url: Url, + /// Comments that are sent along with the transaction + #[builder(setter(into), default = "None")] + remarks: Option, + /// Optional parameter + #[builder(setter(into), default = "None")] + occasion: Option, + /// The type of operation + #[builder(default = "CommandId::BusinessPayment")] + command_id: CommandId, } -impl<'mpesa> B2cBuilder<'mpesa> { - /// Create a new B2C builder. - /// Requires an `initiator_name`, the credential/ username used to authenticate the transaction request - pub fn new(client: &'mpesa Mpesa, initiator_name: &'mpesa str) -> B2cBuilder<'mpesa> { - B2cBuilder { - client, - initiator_name, - amount: None, - party_a: None, - party_b: None, - remarks: None, - queue_timeout_url: None, - result_url: None, - occasion: None, - command_id: None, - } - } - - /// Adds the `CommandId`. Defaults to `CommandId::BusinessPayment` if not explicitly provided. - pub fn command_id(mut self, command_id: CommandId) -> B2cBuilder<'mpesa> { - self.command_id = Some(command_id); - self - } - - /// Adds `Party A` which is a required field - /// `Party A` should be a paybill number. - /// - /// # Errors - /// If `Party A` is invalid or not provided - pub fn party_a(mut self, party_a: &'mpesa str) -> B2cBuilder<'mpesa> { - self.party_a = Some(party_a); - self - } - - /// Adds `Party B` which is a required field - /// `Party B` should be a mobile number. - /// - /// # Errors - /// If `Party B` is invalid or not provided - pub fn party_b(mut self, party_b: &'mpesa str) -> B2cBuilder<'mpesa> { - self.party_b = Some(party_b); - self - } - - /// Adds `Party A` and `Party B`. Both are required fields - /// `Party A` should be a paybill number while `Party B` should be a mobile number. - /// - /// # Errors - /// If either `Party A` or `Party B` is invalid or not provided - #[deprecated] - pub fn parties(mut self, party_a: &'mpesa str, party_b: &'mpesa str) -> B2cBuilder<'mpesa> { - // TODO: add validation - self.party_a = Some(party_a); - self.party_b = Some(party_b); - self - } - - /// Adds `Remarks`. This is an optional field, will default to "None" if not explicitly provided - pub fn remarks(mut self, remarks: &'mpesa str) -> B2cBuilder<'mpesa> { - self.remarks = Some(remarks); - self - } - - /// Adds `Occasion`. This is an optional field, will default to an empty string - pub fn occasion(mut self, occasion: &'mpesa str) -> B2cBuilder<'mpesa> { - self.occasion = Some(occasion); - self - } - - /// Adds an `amount` to the request - /// This is a required field - pub fn amount>(mut self, amount: Number) -> B2cBuilder<'mpesa> { - self.amount = Some(amount.into()); - self - } - - // Adds `QueueTimeoutUrl` This is a required field - /// - /// # Error - /// If `QueueTimeoutUrl` is invalid or not provided - pub fn timeout_url(mut self, timeout_url: &'mpesa str) -> B2cBuilder<'mpesa> { - self.queue_timeout_url = Some(timeout_url); - 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) -> B2cBuilder<'mpesa> { - 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 - #[deprecated] - pub fn urls(mut self, timeout_url: &'mpesa str, result_url: &'mpesa str) -> B2cBuilder<'mpesa> { - // TODO: validate urls; will probably return a `Result` from this - self.queue_timeout_url = Some(timeout_url); - self.result_url = Some(result_url); - self +impl<'mpesa> B2c<'mpesa> { + /// Creates a new B2C builder + pub fn builder(client: &'mpesa Mpesa) -> B2cBuilder<'mpesa> { + B2cBuilder::default().client(client) } /// # B2C API @@ -171,7 +84,6 @@ impl<'mpesa> B2cBuilder<'mpesa> { /// This API enables Business to Customer (B2C) transactions between a company and /// customers who are the end-users of its products or services. Use of this API requires 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 `B2cResponse` type /// @@ -180,27 +92,17 @@ impl<'mpesa> B2cBuilder<'mpesa> { pub async fn send(self) -> MpesaResult { let credentials = self.client.gen_security_credentials()?; - let payload = B2cPayload { + let payload = B2cRequest { initiator_name: self.initiator_name, - security_credential: &credentials, - command_id: self.command_id.unwrap_or(CommandId::BusinessPayment), - 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)), + security_credential: credentials, + command_id: self.command_id, + amount: self.amount, + party_a: self.party_a, + party_b: self.party_b, + remarks: self.remarks, + queue_time_out_url: self.queue_timeout_url, + result_url: self.result_url, + occasion: self.occasion, }; self.client diff --git a/src/services/mod.rs b/src/services/mod.rs index cb7d579eb..6d36dcd63 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -31,9 +31,9 @@ mod transaction_status; #[cfg(feature = "account_balance")] pub use account_balance::{AccountBalanceBuilder, AccountBalanceResponse}; #[cfg(feature = "b2b")] -pub use b2b::{B2bBuilder, B2bResponse}; +pub use b2b::*; #[cfg(feature = "b2c")] -pub use b2c::{B2cBuilder, B2cResponse}; +pub use b2c::*; #[cfg(feature = "bill_manager")] pub use bill_manager::*; #[cfg(feature = "c2b_register")] @@ -43,14 +43,8 @@ pub use c2b_simulate::{C2bSimulateBuilder, C2bSimulateResponse}; #[cfg(feature = "dynamic_qr")] pub use dynamic_qr::{DynamicQR, DynamicQRBuilder, DynamicQRRequest, DynamicQRResponse}; #[cfg(feature = "express")] -pub use express::{ - MpesaExpress, MpesaExpressBuilder, MpesaExpressQuery, MpesaExpressQueryBuilder, - MpesaExpressQueryResponse, MpesaExpressRequest, MpesaExpressResponse, -}; +pub use express::*; #[cfg(feature = "transaction_reversal")] -pub use transaction_reversal::{ - TransactionReversal, TransactionReversalBuilder, TransactionReversalRequest, - TransactionReversalResponse, -}; +pub use transaction_reversal::*; #[cfg(feature = "transaction_status")] -pub use transaction_status::{TransactionStatusBuilder, TransactionStatusResponse}; +pub use transaction_status::*; diff --git a/tests/mpesa-rust/b2b_test.rs b/tests/mpesa-rust/b2b_test.rs index efd0d8a51..9548d78bc 100644 --- a/tests/mpesa-rust/b2b_test.rs +++ b/tests/mpesa-rust/b2b_test.rs @@ -1,4 +1,3 @@ -use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; @@ -21,13 +20,18 @@ async fn b2b_success() { .mount(&server) .await; let response = client - .b2b("testapi496") + .b2b() + .initiator_name("testapi496") .party_a("600496") .party_b("600000") - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") + .try_result_url("https://testdomain.com/ok") + .unwrap() + .try_queue_timeout_url("https://testdomain.com/err") + .unwrap() .account_ref("254708374149") .amount(1000) + .build() + .unwrap() .send() .await .unwrap(); @@ -56,19 +60,18 @@ async fn b2b_fails_if_no_amount_is_provided() { .mount(&server) .await; if let Err(e) = client - .b2b("testapi496") + .b2b() + .initiator_name("testapi496") .party_a("600496") .party_b("600000") - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") + .try_result_url("https://testdomain.com/ok") + .unwrap() + .try_queue_timeout_url("https://testdomain.com/err") + .unwrap() .account_ref("254708374149") - .send() - .await + .build() { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "amount is required"); + assert!(e.to_string().contains("Field [amount] is required")) } else { panic!("Expected error"); } @@ -90,19 +93,18 @@ async fn b2b_fails_if_no_party_a_is_provided() { .mount(&server) .await; if let Err(e) = client - .b2b("testapi496") + .b2b() + .initiator_name("testapi496") .party_b("600000") .amount(1000) - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") + .try_result_url("https://testdomain.com/ok") + .unwrap() + .try_queue_timeout_url("https://testdomain.com/err") + .unwrap() .account_ref("254708374149") - .send() - .await + .build() { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "party_a is required"); + assert!(e.to_string().contains("Field [party_a] is required"),) } else { panic!("Expected error"); } @@ -124,19 +126,18 @@ async fn b2b_fails_if_no_party_b_is_provided() { .mount(&server) .await; if let Err(e) = client - .b2b("testapi496") + .b2b() + .initiator_name("testapi496") .party_a("600496") .amount(1000) - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") + .try_result_url("https://testdomain.com/ok") + .unwrap() + .try_queue_timeout_url("https://testdomain.com/err") + .unwrap() .account_ref("254708374149") - .send() - .await + .build() { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "party_b is required"); + assert!(e.to_string().contains("Field [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 39dec8ed4..fd53bc967 100644 --- a/tests/mpesa-rust/b2c_test.rs +++ b/tests/mpesa-rust/b2c_test.rs @@ -1,4 +1,3 @@ -use mpesa::MpesaError; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, ResponseTemplate}; @@ -20,16 +19,23 @@ async fn b2c_success() { .expect(1) .mount(&server) .await; + let response = client - .b2c("testapi496") + .b2c() + .initiator_name("testapi496") .party_a("600496") .party_b("254708374149") - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") + .try_result_url("https://testdomain.com/ok") + .unwrap() + .try_queue_timeout_url("https://testdomain.com/err") + .unwrap() .amount(1000) + .build() + .unwrap() .send() .await .unwrap(); + assert_eq!(response.originator_conversation_id, "29464-48063588-1"); assert_eq!(response.conversation_id, "AG_20230206_201056794190723278ff"); assert_eq!( @@ -55,18 +61,17 @@ async fn b2c_fails_if_no_amount_is_provided() { .mount(&server) .await; if let Err(e) = client - .b2c("testapi496") + .b2c() + .initiator_name("testapi496") .party_a("600496") .party_b("254708374149") - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") - .send() - .await + .try_result_url("https://testdomain.com/ok") + .unwrap() + .try_queue_timeout_url("https://testdomain.com/err") + .unwrap() + .build() { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "amount is required"); + assert!(e.to_string().contains("Field [amount] is required")) } else { panic!("Expected error"); } @@ -88,18 +93,17 @@ async fn b2c_fails_if_no_party_a_is_provided() { .mount(&server) .await; if let Err(e) = client - .b2c("testapi496") + .b2c() + .initiator_name("testapi496") .amount(1000) .party_b("254708374149") - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") - .send() - .await + .try_result_url("https://testdomain.com/ok") + .unwrap() + .try_queue_timeout_url("https://testdomain.com/err") + .unwrap() + .build() { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "party_a is required"); + assert!(e.to_string().contains("Field [part_a] is required")) } else { panic!("Expected error"); } @@ -121,18 +125,17 @@ async fn b2c_fails_if_no_party_b_is_provided() { .mount(&server) .await; if let Err(e) = client - .b2c("testapi496") + .b2c() + .initiator_name("testapi496") .amount(1000) .party_a("600496") - .result_url("https://testdomain.com/ok") - .timeout_url("https://testdomain.com/err") - .send() - .await + .try_result_url("https://testdomain.com/ok") + .unwrap() + .try_queue_timeout_url("https://testdomain.com/err") + .unwrap() + .build() { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "party_b is required"); + assert!(e.to_string().contains("Field [part_b] is required")) } else { panic!("Expected error"); } @@ -154,18 +157,16 @@ async fn b2c_fails_if_no_result_url_is_provided() { .mount(&server) .await; if let Err(e) = client - .b2c("testapi496") + .b2c() + .initiator_name("testapi496") .amount(1000) .party_a("600496") .party_b("254708374149") - .timeout_url("https://testdomain.com/err") - .send() - .await + .try_queue_timeout_url("https://testdomain.com/err") + .unwrap() + .build() { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "result_url is required"); + assert!(e.to_string().contains("Field [result_url] is required")) } else { panic!("Expected error"); } @@ -187,18 +188,16 @@ async fn b2c_fails_if_no_queue_timeout_is_provided() { .mount(&server) .await; if let Err(e) = client - .b2c("testapi496") + .b2c() + .initiator_name("testapi496") .amount(1000) .party_a("600496") .party_b("254708374149") - .result_url("https://testdomain.com/ok") - .send() - .await + .try_result_url("https://testdomain.com/ok") + .unwrap() + .build() { - let MpesaError::Message(msg) = e else { - panic!("Expected MpesaError::Message, but found {}", e); - }; - assert_eq!(msg, "queue_timeout_url is required"); + assert!(e.to_string().contains("Field [amount] is required")) } else { panic!("Expected error"); }