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!: Migrate to derive builder #107

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
14 changes: 4 additions & 10 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,7 @@

use crate::auth::AUTH;
use crate::environment::ApiEnvironment;
use crate::services::{
AccountBalanceBuilder, B2bBuilder, B2cBuilder, BulkInvoiceBuilder, C2bRegisterBuilder,
C2bSimulateBuilder, CancelInvoiceBuilder, DynamicQR, DynamicQRBuilder, MpesaExpress,
MpesaExpressBuilder, OnboardBuilder, OnboardModifyBuilder, ReconciliationBuilder,
SingleInvoiceBuilder, TransactionReversal, TransactionReversalBuilder,
TransactionStatusBuilder,
};
use crate::services::*;
use crate::{auth, MpesaError, MpesaResult, ResponseError};

/// Source: [test credentials](https://developer.safaricom.co.ke/test_credentials)
Expand Down Expand Up @@ -232,8 +226,8 @@

#[cfg(feature = "account_balance")]
#[doc = include_str!("../docs/client/account_balance.md")]
pub fn account_balance<'a>(&'a self, initiator_name: &'a str) -> AccountBalanceBuilder {
AccountBalanceBuilder::new(self, initiator_name)
pub fn account_balance(&self) -> AccountBalanceBuilder {
AccountBalance::builder(self)
}

#[cfg(feature = "express_request")]
Expand All @@ -257,9 +251,9 @@
#[cfg(feature = "dynamic_qr")]
#[doc = include_str!("../docs/client/dynamic_qr.md")]
pub fn dynamic_qr(&self) -> DynamicQRBuilder {
DynamicQR::builder(self)

Check failure on line 254 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

this method takes 0 arguments but 1 argument was supplied

Check failure on line 254 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

this method takes 0 arguments but 1 argument was supplied
}

Check failure on line 255 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

the trait bound `url::Url: From<&str>` is not satisfied

Check failure on line 255 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

the trait bound `url::Url: From<&str>` is not satisfied

Check failure on line 256 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

no method named `timeout_url` found for mutable reference `&mut AccountBalanceBuilder<'_>` in the current scope

Check failure on line 256 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

no method named `timeout_url` found for mutable reference `&mut AccountBalanceBuilder<'_>` in the current scope
/// 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 All @@ -267,7 +261,7 @@
///
/// # Errors
/// Returns `EncryptionError` variant of `MpesaError`
pub(crate) fn gen_security_credentials(&self) -> MpesaResult<String> {
pub fn gen_security_credentials(&self) -> MpesaResult<String> {
let pem = self.certificate.as_bytes();
let cert = X509::from_pem(pem)?;
// getting the public and rsa keys
Expand Down
2 changes: 1 addition & 1 deletion src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl Display for CommandId {
/// Identifier types - both sender and receiver - identify an M-Pesa transaction’s sending and receiving party as
/// either a shortcode, a till number or a MSISDN (phone number).
/// There are three identifier types that can be used with M-Pesa APIs.
#[derive(Debug, Serialize_repr, Deserialize_repr, Copy, Clone)]
#[derive(Debug, Serialize_repr, Deserialize_repr, Copy, Clone, PartialEq, Eq)]
#[repr(u16)]
pub enum IdentifierTypes {
MSISDN = 1,
Expand Down
193 changes: 71 additions & 122 deletions src/services/account_balance.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#![doc = include_str!("../../docs/client/account_balance.md")]

use derive_builder::Builder;
use reqwest::Url;
use serde::{Deserialize, Serialize};

use crate::constants::{CommandId, IdentifierTypes};
Expand All @@ -8,28 +10,23 @@
const ACCOUNT_BALANCE_URL: &str = "mpesa/accountbalance/v1/query";

#[derive(Debug, Serialize)]
/// Account Balance payload
struct AccountBalancePayload<'mpesa> {
#[serde(rename(serialize = "Initiator"))]
initiator: &'mpesa str,
#[serde(rename(serialize = "SecurityCredential"))]
security_credential: &'mpesa str,
#[serde(rename_all = "PascalCase")]
pub struct AccountBalanceRequest<'mpesa> {
pub initiator: &'mpesa str,
pub security_credential: String,
#[serde(rename(serialize = "CommandID"))]
command_id: CommandId,
#[serde(rename(serialize = "PartyA"))]
party_a: &'mpesa str,
#[serde(rename(serialize = "IdentifierType"))]
identifier_type: &'mpesa str,
#[serde(rename(serialize = "Remarks"))]
remarks: &'mpesa str,
pub command_id: CommandId,
pub party_a: &'mpesa str,
pub identifier_type: IdentifierTypes,
pub remarks: &'mpesa str,
#[serde(rename(serialize = "QueueTimeOutURL"))]
queue_time_out_url: &'mpesa str,
pub queue_time_out_url: Url,
#[serde(rename(serialize = "ResultURL"))]
result_url: &'mpesa str,
pub result_url: Url,
}

Check failure on line 27 in src/services/account_balance.rs

View workflow job for this annotation

GitHub Actions / Test

this method takes 0 arguments but 1 argument was supplied

Check failure on line 27 in src/services/account_balance.rs

View workflow job for this annotation

GitHub Actions / Test

this method takes 0 arguments but 1 argument was supplied
#[derive(Debug, Deserialize, Clone)]

Check failure on line 28 in src/services/account_balance.rs

View workflow job for this annotation

GitHub Actions / Test

the trait bound `url::Url: From<&str>` is not satisfied

Check failure on line 28 in src/services/account_balance.rs

View workflow job for this annotation

GitHub Actions / Test

the trait bound `url::Url: From<&str>` is not satisfied
pub struct AccountBalanceResponse {

Check failure on line 29 in src/services/account_balance.rs

View workflow job for this annotation

GitHub Actions / Test

no method named `timeout_url` found for mutable reference `&mut AccountBalanceBuilder<'_>` in the current scope

Check failure on line 29 in src/services/account_balance.rs

View workflow job for this annotation

GitHub Actions / Test

no method named `timeout_url` found for mutable reference `&mut AccountBalanceBuilder<'_>` in the current scope
#[serde(rename(deserialize = "ConversationID"))]
pub conversation_id: String,
#[serde(rename(deserialize = "OriginatorConversationID"))]
Expand All @@ -39,146 +36,98 @@
#[serde(rename(deserialize = "ResponseDescription"))]
pub response_description: String,
}
#[derive(Debug)]
pub struct AccountBalanceBuilder<'mpesa> {
initiator_name: &'mpesa str,
client: &'mpesa Mpesa,
command_id: Option<CommandId>,
party_a: Option<&'mpesa str>,
identifier_type: Option<IdentifierTypes>,
remarks: Option<&'mpesa str>,
queue_timeout_url: Option<&'mpesa str>,
result_url: Option<&'mpesa str>,
}

impl<'mpesa> AccountBalanceBuilder<'mpesa> {
/// Creates a new `AccountBalanceBuilder`.
/// Requires an `initiator_name`, the credential/ username used to authenticate the transaction request
pub fn new(
client: &'mpesa Mpesa,
initiator_name: &'mpesa str,
) -> AccountBalanceBuilder<'mpesa> {
AccountBalanceBuilder {
initiator_name,
client,
command_id: None,
party_a: None,
identifier_type: None,
remarks: None,
queue_timeout_url: None,
result_url: None,
}
}

#[derive(Builder, Debug, Clone)]
#[builder(build_fn(error = "MpesaError"))]
pub struct AccountBalance<'mpesa> {
#[builder(pattern = "immutable", private)]
client: &'mpesa Mpesa,
#[builder(setter(into))]
/// The credential/ username used to authenticate the transaction request
initiator_name: &'mpesa str,
/// Adds a `CommandId`, the unique command passed to the MPESA system.
/// Defaults to `CommandId::AccountBalance` if not passed explicitly.
///
/// # Errors
/// If `CommandId` is invalid
pub fn command_id(mut self, command_id: CommandId) -> AccountBalanceBuilder<'mpesa> {
self.command_id = Some(command_id);
self
}

#[builder(default = "crate::CommandId::AccountBalance")]
command_id: CommandId,
/// Adds `PartyA`, the shortcode of the organization receiving the transaction.
/// This is a required field.
///
/// # Errors
/// If `Party A` is not provided or invalid
pub fn party_a(mut self, party_a: &'mpesa str) -> AccountBalanceBuilder<'mpesa> {
self.party_a = Some(party_a);
self
}

/// Adds the `ReceiverIdentifierType`, the type of organization receiving the transaction.
party_a: &'mpesa str,
// Adds the `ReceiverIdentifierType`, the type of organization receiving the transaction.
/// Defaults to `IdentifierTypes::ShortCode` if not passed explicitly
///
/// # Errors
/// If invalid `ReceiverIdentifierType` is provided
pub fn identifier_type(
mut self,
identifier_type: IdentifierTypes,
) -> AccountBalanceBuilder<'mpesa> {
self.identifier_type = Some(identifier_type);
self
}

#[builder(default = "crate::IdentifierTypes::ShortCode")]
identifier_type: IdentifierTypes,
/// Adds `Remarks`, a comment sent along transaction.
/// Optional field that defaults to `"None"` if no value is provided
pub fn remarks(mut self, remarks: &'mpesa str) -> AccountBalanceBuilder<'mpesa> {
self.remarks = Some(remarks);
self
}

#[builder(setter(into, strip_option), default = "Some(\"None\")")]
remarks: Option<&'mpesa str>,
// 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) -> AccountBalanceBuilder<'mpesa> {
self.queue_timeout_url = Some(timeout_url);
self
}

#[builder(try_setter, setter(into))]
queue_timeout_url: Url,
// 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) -> AccountBalanceBuilder<'mpesa> {
self.result_url = Some(result_url);
self
}
#[builder(try_setter, setter(into))]
result_url: Url,
}

/// 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,
) -> AccountBalanceBuilder<'mpesa> {
self.queue_timeout_url = Some(timeout_url);
self.result_url = Some(result_url);
self
impl<'mpesa> TryFrom<AccountBalance<'mpesa>> for AccountBalanceRequest<'mpesa> {
type Error = MpesaError;

fn try_from(value: AccountBalance<'mpesa>) -> MpesaResult<AccountBalanceRequest> {
Ok(AccountBalanceRequest {
command_id: value.command_id,
identifier_type: value.identifier_type,
initiator: value.initiator_name,
party_a: value.party_a,
queue_time_out_url: value.queue_timeout_url,
remarks: value.remarks.unwrap_or_default(),
result_url: value.result_url,
security_credential: value.client.gen_security_credentials()?,
})
}
}

/// # AccountBalance API
///
/// Enquire the balance on an M-Pesa BuyGoods (Till Number).
/// A successful request returns a `C2bRegisterResponse` type.
/// See more [here](https://developer.safaricom.co.ke/docs#account-balance-api)
///
/// # Errors
/// Returns a `MpesaError` on failure
pub async fn send(self) -> MpesaResult<AccountBalanceResponse> {
let credentials = self.client.gen_security_credentials()?;
impl<'mpesa> AccountBalance<'mpesa> {
/// Creates a new `AccountBalanceBuilder`
pub(crate) fn builder(client: &'mpesa Mpesa) -> AccountBalanceBuilder<'mpesa> {
AccountBalanceBuilder::default().client(client)
}

let payload = AccountBalancePayload {
command_id: self.command_id.unwrap_or(CommandId::AccountBalance),
party_a: self
.party_a
.ok_or(MpesaError::Message("party_a is required"))?,
identifier_type: &self
.identifier_type
.unwrap_or(IdentifierTypes::ShortCode)
.to_string(),
remarks: self.remarks.unwrap_or_else(|| stringify!(None)),
initiator: self.initiator_name,
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"))?,
security_credential: &credentials,
};
pub fn from_request(
client: &'mpesa Mpesa,
request: AccountBalanceRequest<'mpesa>,
) -> AccountBalance<'mpesa> {
AccountBalance {
client,
command_id: request.command_id,
identifier_type: request.identifier_type,
initiator_name: request.initiator,
party_a: request.party_a,
queue_timeout_url: request.queue_time_out_url,
remarks: Some(request.remarks),
result_url: request.result_url,
}
}

pub async fn send(self) -> MpesaResult<AccountBalanceResponse> {
self.client
.send(crate::client::Request {
.send::<AccountBalanceRequest, _>(crate::client::Request {
method: reqwest::Method::POST,
path: ACCOUNT_BALANCE_URL,
body: payload,
body: self.try_into()?,
})
.await
}
Expand Down
4 changes: 3 additions & 1 deletion src/services/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ mod transaction_reversal;
mod transaction_status;

#[cfg(feature = "account_balance")]
pub use account_balance::{AccountBalanceBuilder, AccountBalanceResponse};
pub use account_balance::{
AccountBalance, AccountBalanceBuilder, AccountBalanceRequest, AccountBalanceResponse,
};
#[cfg(feature = "b2b")]
pub use b2b::{B2bBuilder, B2bResponse};
#[cfg(feature = "b2c")]
Expand Down
Loading
Loading