Skip to content

Commit

Permalink
Implemented the Transaction Split API
Browse files Browse the repository at this point in the history
  • Loading branch information
morukele committed Jul 15, 2023
1 parent 2a18d1e commit e016176
Show file tree
Hide file tree
Showing 11 changed files with 752 additions and 47 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "paystack-rs"
version = "0.1.5"
version = "0.1.6" # updated version number already
description = "A unofficial client library for the Paystack API"
authors = ["Oghenemarho Orukele <orukele.dev@gmail.com>"]
edition = "2021"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Convenient **Async** rust bindings and types for the Paystack HTTP API aiming to
The client current covers the follow section of the API:

- Transactions
- Transaction Splits

## Documentation

Expand Down
248 changes: 236 additions & 12 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use serde::Serialize;

use crate::{
Charge, Currency, ExportTransactionResponse, PartialDebitTransaction, PaystackError,
PaystackResult, RequestNotSuccessful, Status, Transaction, TransactionResponse,
PaystackResult, RequestNotSuccessful, ResponseWithoutData, Status, Subaccount, Transaction,
TransactionResponse, TransactionSplit, TransactionSplitListResponse, TransactionSplitResponse,
TransactionStatus, TransactionStatusList, TransactionTimeline, TransactionTotalsResponse,
};

Expand All @@ -38,7 +39,34 @@ impl PaystackClient {
}
}

/// A function for sending post requests to a specified url
/// A function for sending GET request to a specified url
/// with optional query parameters using reqwest client.
async fn get_request(
&self,
url: &String,
query: Option<Vec<(&str, String)>>,
) -> PaystackResult<Response> {
let response = self
.client
.get(url)
.query(&query)
.bearer_auth(&self.api_key)
.header("Content-Type", "application/json")
.send()
.await;

match response {
Ok(response) => match response.status() {
StatusCode::OK => Ok(response),
_ => {
Err(RequestNotSuccessful::new(response.status(), response.text().await?).into())
}
},
Err(err) => Err(PaystackError::Generic(err.to_string())),
}
}

/// A function for sending POST requests to a specified url
/// using the reqwest client.
async fn post_request<T>(&self, url: &String, body: T) -> PaystackResult<Response>
where
Expand All @@ -64,19 +92,44 @@ impl PaystackClient {
}
}

/// A function for sending get request to a specified url
/// with optional query parameters using reqwest client.
async fn get_request(
&self,
url: &String,
query: Option<Vec<(&str, String)>>,
) -> PaystackResult<Response> {
/// A function for sending PUT requests to a specified url
/// using the reqwest client.
async fn put_request<T>(&self, url: &String, body: T) -> PaystackResult<Response>
where
T: Debug + Serialize,
{
let response = self
.client
.get(url)
.query(&query)
.put(url)
.bearer_auth(&self.api_key)
.header("Content-Type", "application/json")
.json(&body)
.send()
.await;

match response {
Ok(response) => match response.status() {
StatusCode::OK => Ok(response),
_ => {
Err(RequestNotSuccessful::new(response.status(), response.text().await?).into())
}
},
Err(err) => Err(PaystackError::Generic(err.to_string())),
}
}

/// A function for sending DELETE requests to a specified url
/// using the reqwest client.
async fn _delete_request<T>(&self, url: &String, body: T) -> PaystackResult<Response>
where
T: Debug + Serialize,
{
let response = self
.client
.delete(url)
.bearer_auth(&self.api_key)
.header("Content-Type", "application/json")
.json(&body)
.send()
.await;

Expand Down Expand Up @@ -211,7 +264,7 @@ impl PaystackClient {

/// View the timeline of a transaction
///
/// This function takes in the Transaction id or reference as a parameter
/// This method takes in the Transaction id or reference as a parameter
pub async fn view_transaction_timeline(
&self,
id: Option<u32>,
Expand Down Expand Up @@ -336,4 +389,175 @@ impl PaystackClient {
Err(err) => Err(err),
}
}

/// Create a split payment on your integration.
///
/// This method takes a TransactionSplit object as a parameter.
pub async fn create_transaction_split(
&self,
split_body: TransactionSplit,
) -> PaystackResult<TransactionSplitResponse> {
let url = format!("{}/split", BASE_URL);

match self.post_request(&url, split_body).await {
Ok(response) => match response.status() {
reqwest::StatusCode::OK => {
match response.json::<TransactionSplitResponse>().await {
Ok(content) => Ok(content),
Err(err) => Err(PaystackError::TransactionSplit(err.to_string())),
}
}
_ => {
Err(RequestNotSuccessful::new(response.status(), response.text().await?).into())
}
},
Err(err) => Err(err),
}
}

/// List the transaction splits available on your integration
///
/// Takes in the following parameters:
/// - `split_name`: (Optional) name of the split to retrieve.
/// - `split_active`: (Optional) status of the split to retrieve.
pub async fn list_transaction_splits(
&self,
split_name: Option<String>,
split_active: Option<bool>,
) -> PaystackResult<TransactionSplitListResponse> {
let url = format!("{}/split", BASE_URL);

// Specify a default option for active splits
let split_active = match split_active {
Some(active) => active.to_string(),
None => String::from(""),
};

let query = vec![
("name", split_name.unwrap_or("".to_string())),
("active", split_active),
];

match self.get_request(&url, Some(query)).await {
Ok(response) => match response.status() {
reqwest::StatusCode::OK => {
match response.json::<TransactionSplitListResponse>().await {
Ok(content) => Ok(content),
Err(err) => Err(PaystackError::TransactionSplit(err.to_string())),
}
}
_ => {
Err(RequestNotSuccessful::new(response.status(), response.text().await?).into())
}
},
Err(err) => Err(err),
}
}

/// Get details of a split on your integration.
///
/// Takes in the following parameter:
/// - `split_id`: Id of the transaction split.
pub async fn fetch_transaction_split(
&self,
split_id: String,
) -> PaystackResult<TransactionSplitResponse> {
let url = format!("{}/split{}", BASE_URL, split_id);

match self.get_request(&url, None).await {
Ok(response) => match response.status() {
reqwest::StatusCode::OK => {
match response.json::<TransactionSplitResponse>().await {
Ok(content) => Ok(content),
Err(err) => Err(PaystackError::TransactionSplit(err.to_string())),
}
}
_ => {
Err(RequestNotSuccessful::new(response.status(), response.text().await?).into())
}
},
Err(err) => Err(err),
}
}

/// Update a transaction split details on your integration.
///
/// Takes in the following parameters:
/// - `split_id`: Id of the transaction split.
/// - `split_name`: updated name for the split.
/// - `split_status`: updated states for the split.
/// - `bearer_type`: (Optional) updated bearer type for the split.
/// - `bearer_subaccount`: (Optional) updated bearer subaccount for the split
pub async fn update_transaction_split(
&self,
split_id: String,
body: TransactionSplit,
) -> PaystackResult<TransactionSplitResponse> {
let url = format!("{}/split/{}", BASE_URL, split_id);

match self.put_request(&url, body).await {
Ok(respone) => match respone.status() {
reqwest::StatusCode::OK => match respone.json::<TransactionSplitResponse>().await {
Ok(content) => Ok(content),
Err(err) => Err(PaystackError::TransactionSplit(err.to_string())),
},
_ => Err(RequestNotSuccessful::new(respone.status(), respone.text().await?).into()),
},
Err(err) => Err(err),
}
}

/// Add a Subaccount to a Transaction Split, or update the share of an existing Subaccount in a Transaction Split
///
/// Takes in the following parameters:
/// - `split_id`: Id of the transaction split to update.
/// - `body`: Subacount to add to the transaction split.
pub async fn add_or_update_subaccount_split(
&self,
split_id: String,
body: Subaccount,
) -> PaystackResult<TransactionSplitResponse> {
let url = format!("{}/split/{}/subaccount/add", BASE_URL, split_id);

match self.post_request(&url, body).await {
Ok(response) => match response.status() {
reqwest::StatusCode::OK => {
match response.json::<TransactionSplitResponse>().await {
Ok(content) => Ok(content),
Err(err) => Err(PaystackError::TransactionSplit(err.to_string())),
}
}
_ => {
Err(RequestNotSuccessful::new(response.status(), response.text().await?).into())
}
},
Err(err) => Err(err),
}
}

/// Remove a subaccount from a transaction split.
///
/// Takes in the following parameters
/// - split_id: Id of the transaction split
/// - subaccount: subaccount code to remove
pub async fn remove_subaccount_from_transaction_split(
&self,
split_id: String,
subaccount: String,
) -> PaystackResult<ResponseWithoutData> {
let url = format!("{}/split/{}/subaccount/remove", BASE_URL, split_id);

match self.post_request(&url, subaccount).await {
Ok(response) => match response.status() {
reqwest::StatusCode::OK => match response.json::<ResponseWithoutData>().await {
Ok(content) => Ok(content),
Err(err) => Err(PaystackError::TransactionSplit(err.to_string())),
},
_ => {
Err(RequestNotSuccessful::new(response.status(), response.text().await?).into())
}
},
Err(err) => Err(err),
}
}
}
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ pub enum PaystackError {
#[error("Charge Error: {0}")]
Charge(String),

/// Error associated with Transaction Split
#[error("Transaction Split Error: {0}")]
TransactionSplit(String),

/// Error for unsuccessful request to the Paystack API
#[error("Request failed: `{0}`")]
RequestNotSuccessful(#[from] RequestNotSuccessful),
Expand Down
27 changes: 25 additions & 2 deletions src/resources/charge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! create charges usingt the Paystack API.
use crate::{error, Channel, Currency, PaystackResult};
use serde::Serialize;

/// This struct is used to create a charge body for creating a Charge Authorization using the Paystack API.
///
Expand All @@ -24,7 +25,7 @@ use crate::{error, Channel, Currency, PaystackResult};
/// Ideally, you will need to use this if you are splitting in flat rates
/// (since subaccount creation only allows for percentage split). e.g. 7000 for a 70 naira
#[derive(serde::Serialize, Debug)]
#[derive(Serialize, Debug)]
pub struct Charge {
email: String,
amount: String,
Expand All @@ -35,7 +36,29 @@ pub struct Charge {
transaction_charge: Option<u32>,
}

/// Builder for the Charge object
/// The `ChargeBuilder` struct provides a convenient way to construct a charge object
/// with optional fields. Each field can be set individually using the builder's methods.
/// Once all the desired fields are set, the `build` method can be called to create
/// an instance of the `Charge` struct.
///
/// # Errors
///
/// Returns a `PaystackResult` with an `Err` variant if any required fields are missing,
/// including email, amount, and authorization code. The error indicates which field is missing.
///
/// # Examples
///
/// ```rust
/// use paystack::{Currency, Channel, ChargeBuilder};
///
/// let charge = ChargeBuilder::new()
/// .email("user@example.com")
/// .amount("10000")
/// .authorization_code("AUTH_CODE")
/// .currency(Currency::USD)
/// .channel(vec![Channel::Card, Channel::Bank])
/// .build();
/// ```
#[derive(Default, Clone)]
pub struct ChargeBuilder {
email: Option<String>,
Expand Down
Loading

0 comments on commit e016176

Please sign in to comment.