Skip to content

feat(service): Migrate b2c, b2b to the builder pattern #112

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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: 7 additions & 7 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
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,
};
Expand Down Expand Up @@ -172,14 +172,14 @@

#[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")]
Expand All @@ -195,20 +195,20 @@
}

#[cfg(feature = "bill_manager")]
#[doc = include_str!("../docs/client/bill_manager/bulk_invoice.md")]

Check failure on line 198 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 198 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

this method takes 0 arguments but 1 argument was supplied
pub fn bulk_invoice(&self) -> BulkInvoiceBuilder {
BulkInvoiceBuilder::new(self)
}

Check failure on line 201 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 201 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 202 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

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

Check failure on line 202 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

no method named `timeout_url` found for mutable reference `&mut B2cBuilder<'_>` in the current scope
#[cfg(feature = "bill_manager")]
#[doc = include_str!("../docs/client/bill_manager/single_invoice.md")]
pub fn single_invoice(&self) -> SingleInvoiceBuilder {
SingleInvoiceBuilder::new(self)
}

Check failure on line 207 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 207 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

this method takes 0 arguments but 1 argument was supplied

#[cfg(feature = "bill_manager")]
#[doc = include_str!("../docs/client/bill_manager/reconciliation.md")]

Check failure on line 210 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 210 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

the trait bound `url::Url: From<&str>` is not satisfied
pub fn reconciliation(&self) -> ReconciliationBuilder {

Check failure on line 211 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

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

Check failure on line 211 in src/client.rs

View workflow job for this annotation

GitHub Actions / Test

no method named `timeout_url` found for mutable reference `&mut B2bBuilder<'_>` in the current scope
ReconciliationBuilder::new(self)
}

Expand Down
268 changes: 72 additions & 196 deletions src/services/b2b.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this inner rename if he outer struct has the rename_all="PascalCase" attribute?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny things, there is a typo in the receiver so I am renaming the single field and same fields can't parse the way it the API expects like ResultURL

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hehehe yeah, always has to be safaricom with the inconsistent API/docs.

#[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<String>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following from the comment above, I guess we can then rename this struct field to have the correct english naming like so:

Suggested change
pub reciever_identifier_type: IdentifierTypes,
pub receiver_identifier_type: IdentifierTypes,

Copy link
Collaborator

@crispinkoech crispinkoech Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...but it might be desirable to keep the same as the typo that mpesa api uses. Up to you

#[serde(rename = "QueueTimeOutURL")]
pub queue_time_out_url: Url,
#[serde(rename = "ResultURL")]
pub result_url: Url,
#[serde(rename = "AccountReference")]
pub account_reference: String,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the AccountReference...

}

#[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<CommandId>,
amount: Option<f64>,
party_a: Option<&'mpesa str>,
sender_id: Option<IdentifierTypes>,
party_b: Option<&'mpesa str>,
receiver_id: Option<IdentifierTypes>,
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<String>,
/// 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<Number: Into<f64>>(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
Expand All @@ -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
///
Expand All @@ -213,30 +103,16 @@ impl<'mpesa> B2bBuilder<'mpesa> {
pub async fn send(self) -> MpesaResult<B2bResponse> {
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,
Expand Down
Loading
Loading