Skip to content

Commit

Permalink
Fix error handling in API calls
Browse files Browse the repository at this point in the history
  • Loading branch information
itsyaasir committed Nov 18, 2023
1 parent 8bf5eaa commit 2e9e38d
Show file tree
Hide file tree
Showing 18 changed files with 150 additions and 362 deletions.
6 changes: 3 additions & 3 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use cached::proc_macro::cached;
use serde::{Deserialize, Serialize};
use serde_aux::field_attributes::deserialize_number_from_string;

use crate::{ApiEnvironment, ApiError, Mpesa, MpesaError, MpesaResult};
use crate::{ApiEnvironment, Mpesa, MpesaError, MpesaResult, ResponseError};

const AUTHENTICATION_URL: &str = "/oauth/v1/generate?grant_type=client_credentials";

Expand Down Expand Up @@ -30,8 +30,8 @@ pub(crate) async fn auth(client: &Mpesa<impl ApiEnvironment>) -> MpesaResult<Str
return Ok(access_token);
}

let error = response.json::<ApiError>().await?;
Err(MpesaError::AuthenticationError(error))
let error = response.json::<ResponseError>().await?;
Err(MpesaError::Service(error))
}

/// Response returned from the authentication function
Expand Down
4 changes: 2 additions & 2 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -587,9 +587,9 @@ impl<'mpesa, Env: ApiEnvironment> Mpesa<Env> {

Ok(body)
} else {
let err = res.json::<crate::ApiError>().await?;
let err = res.json::<crate::ResponseError>().await?;

Err(crate::MpesaError::ApiError(err))
Err(crate::MpesaError::Service(err))
}
}
}
Expand Down
40 changes: 4 additions & 36 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,8 @@ use thiserror::Error;
/// Mpesa error stack
#[derive(Error, Debug)]
pub enum MpesaError {
#[error("Api error: {0}")]
ApiError(ApiError),
#[error("{0}")]
AuthenticationError(ApiError),
#[error("B2B request failed: {0}")]
B2bError(ApiError),
#[error("B2C request failed: {0}")]
B2cError(ApiError),
#[error("C2B register request failed: {0}")]
C2bRegisterError(ApiError),
#[error("C2B simulate request failed: {0}")]
C2bSimulateError(ApiError),
#[error("Account Balance request failed: {0}")]
AccountBalanceError(ApiError),
#[error("Bill manager onboarding failed: {0}")]
OnboardError(ApiError),
#[error("Bill manager onboarding modify failed: {0}")]
OnboardModifyError(ApiError),
#[error("Bill manager bulk invoice failed: {0}")]
BulkInvoiceError(ApiError),
#[error("Bill manager reconciliation failed: {0}")]
ReconciliationError(ApiError),
#[error("Bill manager single invoice failed: {0}")]
SingleInvoiceError(ApiError),
#[error("Bill manager cancel invoice failed: {0}")]
CancelInvoiceError(ApiError),
#[error("Mpesa Express request/ STK push failed: {0}")]
MpesaExpressRequestError(ApiError),
#[error("Mpesa Transaction reversal failed: {0}")]
MpesaTransactionReversalError(ApiError),
#[error("Mpesa Transaction status failed: {0}")]
MpesaTransactionStatusError(ApiError),
#[error("Mpesa Dynamic QR failed: {0}")]
MpesaDynamicQrError(ApiError),
#[error("Service error: {0}")]
Service(ResponseError),
#[error("An error has occured while performing the http request")]
NetworkError(#[from] reqwest::Error),
#[error("An error has occured while serializig/ deserializing")]
Expand All @@ -60,13 +28,13 @@ pub type MpesaResult<T> = Result<T, MpesaError>;

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct ApiError {
pub struct ResponseError {
pub request_id: String,
pub error_code: String,
pub error_message: String,
}

impl fmt::Display for ApiError {
impl fmt::Display for ResponseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ pub use constants::{
};
pub use environment::ApiEnvironment;
pub use environment::Environment::{self, Production, Sandbox};
pub use errors::{ApiError, MpesaError, MpesaResult};
pub use errors::{MpesaError, MpesaResult, ResponseError};
35 changes: 11 additions & 24 deletions src/services/account_balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use crate::constants::{CommandId, IdentifierTypes};
use crate::environment::ApiEnvironment;
use crate::{Mpesa, MpesaError, MpesaResult};

const ACCOUNT_BALANCE_URL: &str = "mpesa/accountbalance/v1/query";

#[derive(Debug, Serialize)]
/// Account Balance payload
struct AccountBalancePayload<'mpesa> {
Expand Down Expand Up @@ -148,23 +150,17 @@ impl<'mpesa, Env: ApiEnvironment> AccountBalanceBuilder<'mpesa, Env> {
///
/// # Errors
/// Returns a `MpesaError` on failure
#[allow(clippy::unnecessary_lazy_evaluations)]
pub async fn send(self) -> MpesaResult<AccountBalanceResponse> {
let url = format!(
"{}/mpesa/accountbalance/v1/query",
self.client.environment.base_url()
);

let credentials = self.client.gen_security_credentials()?;

let payload = AccountBalancePayload {
command_id: self.command_id.unwrap_or_else(|| CommandId::AccountBalance),
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_else(|| IdentifierTypes::ShortCode)
.unwrap_or(IdentifierTypes::ShortCode)
.to_string(),
remarks: self.remarks.unwrap_or_else(|| stringify!(None)),
initiator: self.initiator_name,
Expand All @@ -177,21 +173,12 @@ impl<'mpesa, Env: ApiEnvironment> AccountBalanceBuilder<'mpesa, Env> {
security_credential: &credentials,
};

let response = self
.client
.http_client
.post(&url)
.bearer_auth(self.client.auth().await?)
.json(&payload)
.send()
.await?;

if response.status().is_success() {
let value = response.json::<_>().await?;
return Ok(value);
}

let value = response.json().await?;
Err(MpesaError::AccountBalanceError(value))
self.client
.send(crate::client::Request {
method: reqwest::Method::POST,
path: ACCOUNT_BALANCE_URL,
body: payload,
})
.await
}
}
36 changes: 12 additions & 24 deletions src/services/b2b.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use crate::constants::{CommandId, IdentifierTypes};
use crate::environment::ApiEnvironment;
use crate::errors::{MpesaError, MpesaResult};

const B2B_URL: &str = "mpesa/b2b/v1/paymentrequest";

#[derive(Debug, Serialize)]
struct B2bPayload<'mpesa> {
#[serde(rename(serialize = "Initiator"))]
Expand Down Expand Up @@ -215,20 +217,15 @@ impl<'mpesa, Env: ApiEnvironment> B2bBuilder<'mpesa, Env> {
///
/// # Errors
/// Returns a `MpesaError` on failure
#[allow(clippy::unnecessary_lazy_evaluations)]
pub async fn send(self) -> MpesaResult<B2bResponse> {
let url = format!(
"{}/mpesa/b2b/v1/paymentrequest",
self.client.environment.base_url()
);
let credentials = self.client.gen_security_credentials()?;

let payload = B2bPayload {
initiator: self.initiator_name,
security_credential: &credentials,
command_id: self
.command_id
.unwrap_or_else(|| CommandId::BusinessToBusinessTransfer),
.unwrap_or(CommandId::BusinessToBusinessTransfer),
amount: self
.amount
.ok_or(MpesaError::Message("amount is required"))?,
Expand All @@ -237,36 +234,27 @@ impl<'mpesa, Env: ApiEnvironment> B2bBuilder<'mpesa, Env> {
.ok_or(MpesaError::Message("party_a is required"))?,
sender_identifier_type: &self
.sender_id
.unwrap_or_else(|| IdentifierTypes::ShortCode)
.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_else(|| IdentifierTypes::ShortCode)
.unwrap_or(IdentifierTypes::ShortCode)
.to_string(),
remarks: self.remarks.unwrap_or_else(|| stringify!(None)),
queue_time_out_url: self.queue_timeout_url,
result_url: self.result_url,
account_reference: self.account_ref,
};

let response = self
.client
.http_client
.post(&url)
.bearer_auth(self.client.auth().await?)
.json(&payload)
.send()
.await?;

if response.status().is_success() {
let value = response.json::<_>().await?;
return Ok(value);
}

let value = response.json().await?;
Err(MpesaError::B2bError(value))
self.client
.send(crate::client::Request {
method: reqwest::Method::POST,
path: B2B_URL,
body: payload,
})
.await
}
}
27 changes: 10 additions & 17 deletions src/services/b2c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
use crate::environment::ApiEnvironment;
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> {
Expand Down Expand Up @@ -183,7 +185,7 @@ impl<'mpesa, Env: ApiEnvironment> B2cBuilder<'mpesa, Env> {
/// # Errors
/// Returns a `MpesaError` on failure.
pub async fn send(self) -> MpesaResult<B2cResponse> {
let url = format!(
let _url = format!(
"{}/mpesa/b2c/v1/paymentrequest",
self.client.environment.base_url()
);
Expand Down Expand Up @@ -212,21 +214,12 @@ impl<'mpesa, Env: ApiEnvironment> B2cBuilder<'mpesa, Env> {
occasion: self.occasion.unwrap_or_else(|| stringify!(None)),
};

let response = self
.client
.http_client
.post(&url)
.bearer_auth(self.client.auth().await?)
.json(&payload)
.send()
.await?;

if response.status().is_success() {
let value = response.json::<_>().await?;
return Ok(value);
}

let value = response.json().await?;
Err(MpesaError::B2cError(value))
self.client
.send(crate::client::Request {
method: reqwest::Method::POST,
path: B2C_URL,
body: payload,
})
.await
}
}
31 changes: 9 additions & 22 deletions src/services/bill_manager/bulk_invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use crate::constants::Invoice;
use crate::environment::ApiEnvironment;
use crate::errors::{MpesaError, MpesaResult};

const BILL_MANAGER_BULK_INVOICE_API_URL: &str = "v1/billmanager-invoice/bulk-invoicing";

#[derive(Clone, Debug, Deserialize)]
pub struct BulkInvoiceResponse {
#[serde(rename(deserialize = "rescode"))]
Expand Down Expand Up @@ -51,32 +53,17 @@ impl<'mpesa, Env: ApiEnvironment> BulkInvoiceBuilder<'mpesa, Env> {
///
/// # Errors
/// Returns an `MpesaError` on failure.
#[allow(clippy::unnecessary_lazy_evaluations)]
pub async fn send(self) -> MpesaResult<BulkInvoiceResponse> {
let url = format!(
"{}/v1/billmanager-invoice/bulk-invoicing",
self.client.environment.base_url()
);

if self.invoices.is_empty() {
return Err(MpesaError::Message("invoices cannot be empty"));
}

let response = self
.client
.http_client
.post(&url)
.bearer_auth(self.client.auth().await?)
.json(&self.invoices)
.send()
.await?;

if response.status().is_success() {
let value = response.json().await?;
return Ok(value);
}

let value = response.json().await?;
Err(MpesaError::BulkInvoiceError(value))
self.client
.send(crate::client::Request {
method: reqwest::Method::POST,
path: BILL_MANAGER_BULK_INVOICE_API_URL,
body: self.invoices,
})
.await
}
}
34 changes: 11 additions & 23 deletions src/services/bill_manager/cancel_invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use serde::{Deserialize, Serialize};

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

const BILL_MANAGER_CANCEL_INVOICE_API_URL: &str = "v1/billmanager-invoice/cancel-single-invoice";

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -67,28 +69,14 @@ impl<'mpesa, Env: ApiEnvironment> CancelInvoiceBuilder<'mpesa, Env> {
///
/// # Errors
/// Returns an `MpesaError` on failure
#[allow(clippy::unnecessary_lazy_evaluations)]
pub async fn send(self) -> MpesaResult<CancelInvoiceResponse> {
let url = format!(
"{}/v1/billmanager-invoice/cancel-single-invoice",
self.client.environment.base_url()
);

let response = self
.client
.http_client
.post(&url)
.bearer_auth(self.client.auth().await?)
.json(&self.external_references)
.send()
.await?;
if response.status().is_success() {
let value = response.json().await?;
return Ok(value);
}

let value = response.json().await?;
Err(MpesaError::CancelInvoiceError(value))
pub async fn send(self) -> MpesaResult<CancelInvoiceResponse> {
self.client
.send(crate::client::Request {
method: reqwest::Method::POST,
path: BILL_MANAGER_CANCEL_INVOICE_API_URL,
body: self.external_references,
})
.await
}
}
Loading

0 comments on commit 2e9e38d

Please sign in to comment.